@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,843 @@
1
+ {% liquid
2
+ assign widget_settings = widget.settings
3
+ assign widget_data = widget.data
4
+
5
+ comment
6
+ Static Widget: Content comes from widget_data.content (Items array)
7
+ Settings come from widget_settings
8
+ endcomment
9
+
10
+ assign content_data = widget_data.content | default: widget.content
11
+ assign items = content_data.Items | default: content_data.items | default: widget_data.items | default: widget_data.Items | default: widget_data.slides
12
+
13
+ assign show_container = widget_settings.showContainer | default: 'Yes'
14
+ assign show_bottom_margin = widget_settings.showWidgetBottomMargin | default: 'Yes'
15
+ assign background_color = widget_settings.backgroundColor
16
+ assign text_color = widget_settings.textColor
17
+ assign image_height = widget_settings.imageHeight
18
+ assign image_width = widget_settings.imageWidth
19
+ assign hide_dot = widget_settings.hideDot | default: false
20
+ assign hide_arrow = widget_settings.hideArrow | default: false
21
+ assign autoplay = widget_settings.autoplay | default: true
22
+ assign interval = widget_settings.interval | default: 5000
23
+ %}
24
+
25
+ <section class="modern-carousel"
26
+ data-widget-id="{{ widget.id }}"
27
+ data-autoplay="{{ autoplay }}"
28
+ data-interval="{{ interval }}"
29
+ {% if background_color and background_color != 'null' %}style="background-color: {{ background_color }};"{% endif %}>
30
+ {% if items and items.size > 0 %}
31
+ <div class="modern-carousel__container{% if show_container == 'Yes' %} modern-carousel__container--contained{% endif %}">
32
+ <div class="modern-carousel__viewport" data-carousel-viewport>
33
+ <div class="modern-carousel__track" data-carousel-track>
34
+ {% for item in items %}
35
+ <article class="modern-carousel__slide{% if forloop.first %} is-active{% endif %}" data-slide="{{ forloop.index0 }}">
36
+ {% assign image_url = item.imageUrl | default: item.ImageUrl | default: item.image | default: item.Image %}
37
+
38
+ {% if image_url and image_url != 'null' %}
39
+ {% if image_url contains 'http://' or image_url contains 'https://' %}
40
+ {% assign image_src = image_url %}
41
+ {% else %}
42
+ {% assign image_src = image_url | asset_url %}
43
+ {% endif %}
44
+
45
+ <div class="modern-carousel__image-wrapper">
46
+ <img src="{{ image_src }}"
47
+ alt="{{ item.caption | default: item.Caption | default: item.title | default: 'Carousel image' }}"
48
+ class="modern-carousel__image"
49
+ loading="{% if forloop.first %}eager{% else %}lazy{% endif %}"
50
+ {% if image_height %}style="height: {{ image_height }}px;"{% endif %}>
51
+ <div class="modern-carousel__overlay"></div>
52
+ </div>
53
+ {% endif %}
54
+
55
+ {% assign caption = item.caption | default: item.Caption %}
56
+ {% assign sub_caption = item.subCaption | default: item.SubCaption | default: item.sub_caption | default: item.subtitle %}
57
+ {% assign link_text = item.linkText | default: item.LinkText | default: item.link_text | default: item.cta_label %}
58
+ {% assign target_url = item.targetUrl | default: item.TargetUrl | default: item.target_url | default: item.cta_link %}
59
+
60
+ {% if caption or sub_caption or link_text %}
61
+ <div class="modern-carousel__content" {% if text_color and text_color != 'null' %}style="color: {{ text_color }};"{% endif %}>
62
+ <div class="modern-carousel__content-inner">
63
+ {% if caption %}
64
+ <h2 class="modern-carousel__title animate-fade-up">{{ caption }}</h2>
65
+ {% endif %}
66
+ {% if sub_caption %}
67
+ <p class="modern-carousel__subtitle animate-fade-up animate-delay-1">{{ sub_caption }}</p>
68
+ {% endif %}
69
+ {% if link_text and link_text != '' and target_url and target_url != 'null' %}
70
+ <a href="{{ target_url }}" class="modern-carousel__cta animate-fade-up animate-delay-2">
71
+ <span>{{ link_text }}</span>
72
+ <svg class="modern-carousel__cta-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
73
+ <path d="M5 12h14M12 5l7 7-7 7"/>
74
+ </svg>
75
+ </a>
76
+ {% endif %}
77
+ </div>
78
+ </div>
79
+ {% endif %}
80
+ </article>
81
+ {% endfor %}
82
+ </div>
83
+ </div>
84
+
85
+ {% unless hide_arrow == true %}
86
+ <div class="modern-carousel__controls">
87
+ <button class="modern-carousel__arrow modern-carousel__arrow--prev"
88
+ data-carousel-prev
89
+ aria-label="Previous slide">
90
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
91
+ <path d="M15 18l-6-6 6-6"/>
92
+ </svg>
93
+ </button>
94
+ <button class="modern-carousel__arrow modern-carousel__arrow--next"
95
+ data-carousel-next
96
+ aria-label="Next slide">
97
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
98
+ <path d="M9 18l6-6-6-6"/>
99
+ </svg>
100
+ </button>
101
+ </div>
102
+ {% endunless %}
103
+
104
+ {% unless hide_dot == true %}
105
+ <div class="modern-carousel__pagination" data-carousel-dots>
106
+ {% for item in items %}
107
+ <button class="modern-carousel__dot{% if forloop.first %} is-active{% endif %}"
108
+ data-index="{{ forloop.index0 }}"
109
+ aria-label="Go to slide {{ forloop.index }}">
110
+ <span class="modern-carousel__dot-inner"></span>
111
+ </button>
112
+ {% endfor %}
113
+ </div>
114
+ {% endunless %}
115
+
116
+ <!-- Progress Bar -->
117
+ {% if autoplay %}
118
+ <div class="modern-carousel__progress">
119
+ <div class="modern-carousel__progress-bar" data-progress-bar></div>
120
+ </div>
121
+ {% endif %}
122
+ </div>
123
+ {% else %}
124
+ <div class="modern-carousel__empty">
125
+ <svg class="modern-carousel__empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
126
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
127
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
128
+ <polyline points="21 15 16 10 5 21"></polyline>
129
+ </svg>
130
+ <p>{{ widget_settings.empty_state | default: 'Add slides to display your carousel.' }}</p>
131
+ </div>
132
+ {% endif %}
133
+ </section>
134
+
135
+ <style>
136
+ /* ============================================
137
+ MODERN CAROUSEL - ROOT VARIABLES
138
+ ============================================ */
139
+
140
+ :root {
141
+ --carousel-height-desktop: 600px;
142
+ --carousel-height-tablet: 500px;
143
+ --carousel-height-mobile: 400px;
144
+ --carousel-radius: 20px;
145
+ --carousel-padding: 60px;
146
+ --carousel-gap: 24px;
147
+ --overlay-gradient: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.7) 100%);
148
+ --transition-smooth: cubic-bezier(0.4, 0, 0.2, 1);
149
+ --color-white: #ffffff;
150
+ --color-black: #000000;
151
+ --color-primary: {{ settings.color_primary | default: '#3b82f6' }};
152
+ }
153
+
154
+ /* ============================================
155
+ CAROUSEL CONTAINER
156
+ ============================================ */
157
+
158
+ .modern-carousel {
159
+ position: relative;
160
+ width: 100%;
161
+ {% if show_bottom_margin == 'Yes' %}
162
+ margin-bottom: 60px;
163
+ {% endif %}
164
+ overflow: hidden;
165
+ }
166
+
167
+ .modern-carousel__container {
168
+ position: relative;
169
+ width: 100%;
170
+ margin: 0 auto;
171
+ }
172
+
173
+ .modern-carousel__container--contained {
174
+ max-width: {{ settings.container_width | default: 1200 }}px;
175
+ padding: 0 20px;
176
+ }
177
+
178
+ /* ============================================
179
+ VIEWPORT & TRACK
180
+ ============================================ */
181
+
182
+ .modern-carousel__viewport {
183
+ position: relative;
184
+ width: 100%;
185
+ overflow: hidden;
186
+ {% if show_container == 'Yes' %}
187
+ border-radius: var(--carousel-radius);
188
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
189
+ {% endif %}
190
+ }
191
+
192
+ .modern-carousel__track {
193
+ display: flex;
194
+ transition: transform 0.7s var(--transition-smooth);
195
+ width: 100%;
196
+ }
197
+
198
+ /* ============================================
199
+ SLIDES
200
+ ============================================ */
201
+
202
+ .modern-carousel__slide {
203
+ position: relative;
204
+ flex: 0 0 100%;
205
+ min-width: 100%;
206
+ height: var(--carousel-height-desktop);
207
+ display: flex;
208
+ align-items: center;
209
+ justify-content: center;
210
+ overflow: hidden;
211
+ border-radius: var(--border-radius-medium);
212
+ }
213
+
214
+ .modern-carousel__image-wrapper {
215
+ position: absolute;
216
+ top: 0;
217
+ left: 0;
218
+ width: 100%;
219
+ height: 100%;
220
+ z-index: 1;
221
+ }
222
+
223
+ .modern-carousel__image {
224
+ width: 100%;
225
+ height: 100%;
226
+ {% comment %} object-fit: cover; {% endcomment %}
227
+ transform: scale(1);
228
+ transition: transform 8s var(--transition-smooth);
229
+ }
230
+
231
+ .modern-carousel__slide.is-active .modern-carousel__image {
232
+ transform: scale(1.08);
233
+ }
234
+
235
+ .modern-carousel__overlay {
236
+ position: absolute;
237
+ top: 0;
238
+ left: 0;
239
+ right: 0;
240
+ bottom: 0;
241
+ background: var(--overlay-gradient);
242
+ z-index: 2;
243
+ }
244
+
245
+ /* ============================================
246
+ CONTENT
247
+ ============================================ */
248
+
249
+ .modern-carousel__content {
250
+ position: absolute;
251
+ top: 0;
252
+ left: 0;
253
+ width: 100%;
254
+ height: 100%;
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ z-index: 3;
259
+ padding: var(--carousel-padding);
260
+ }
261
+
262
+ .modern-carousel__content-inner {
263
+ max-width: 800px;
264
+ text-align: center;
265
+ }
266
+
267
+ .modern-carousel__title {
268
+ margin: 0 0 20px;
269
+ font-size: clamp(2rem, 5vw, 4.5rem);
270
+ font-weight: 800;
271
+ line-height: 1.1;
272
+ color: var(--color-white);
273
+ text-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
274
+ letter-spacing: -0.03em;
275
+ }
276
+
277
+ .modern-carousel__subtitle {
278
+ margin: 0 0 32px;
279
+ font-size: clamp(1rem, 2vw, 1.5rem);
280
+ font-weight: 400;
281
+ line-height: 1.6;
282
+ color: var(--color-white);
283
+ opacity: 0.95;
284
+ text-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
285
+ max-width: 600px;
286
+ margin-left: auto;
287
+ margin-right: auto;
288
+ }
289
+
290
+ .modern-carousel__cta {
291
+ display: inline-flex;
292
+ align-items: center;
293
+ gap: 12px;
294
+ padding: 18px 40px;
295
+ background: var(--color-white);
296
+ color: var(--color-black);
297
+ text-decoration: none;
298
+ font-weight: 700;
299
+ font-size: 16px;
300
+ letter-spacing: 0.5px;
301
+ border-radius: 50px;
302
+ transition: all 0.4s var(--transition-smooth);
303
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
304
+ text-transform: uppercase;
305
+ position: relative;
306
+ overflow: hidden;
307
+ }
308
+
309
+ .modern-carousel__cta::before {
310
+ content: '';
311
+ position: absolute;
312
+ top: 0;
313
+ left: -100%;
314
+ width: 100%;
315
+ height: 100%;
316
+ background: var(--color-primary);
317
+ transition: left 0.4s var(--transition-smooth);
318
+ z-index: -1;
319
+ }
320
+
321
+ .modern-carousel__cta:hover {
322
+ transform: translateY(-4px);
323
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
324
+ color: var(--color-white);
325
+ }
326
+
327
+ .modern-carousel__cta:hover::before {
328
+ left: 0;
329
+ }
330
+
331
+ .modern-carousel__cta-icon {
332
+ width: 20px;
333
+ height: 20px;
334
+ transition: transform 0.4s var(--transition-smooth);
335
+ }
336
+
337
+ .modern-carousel__cta:hover .modern-carousel__cta-icon {
338
+ transform: translateX(4px);
339
+ }
340
+
341
+ /* ============================================
342
+ ANIMATIONS
343
+ ============================================ */
344
+
345
+ @keyframes fadeUp {
346
+ from {
347
+ opacity: 0;
348
+ transform: translateY(30px);
349
+ }
350
+ to {
351
+ opacity: 1;
352
+ transform: translateY(0);
353
+ }
354
+ }
355
+
356
+ .modern-carousel__slide.is-active .animate-fade-up {
357
+ animation: fadeUp 0.8s var(--transition-smooth) forwards;
358
+ }
359
+
360
+ .modern-carousel__slide.is-active .animate-delay-1 {
361
+ animation-delay: 0.2s;
362
+ opacity: 0;
363
+ }
364
+
365
+ .modern-carousel__slide.is-active .animate-delay-2 {
366
+ animation-delay: 0.4s;
367
+ opacity: 0;
368
+ }
369
+
370
+ /* ============================================
371
+ CONTROLS (ARROWS)
372
+ ============================================ */
373
+
374
+ .modern-carousel__controls {
375
+ position: absolute;
376
+ top: 50%;
377
+ left: 0;
378
+ right: 0;
379
+ transform: translateY(-50%);
380
+ display: flex;
381
+ justify-content: space-between;
382
+ padding: 0 30px;
383
+ pointer-events: none;
384
+ z-index: 10;
385
+ }
386
+
387
+ .modern-carousel__arrow {
388
+ width: 56px;
389
+ height: 56px;
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: center;
393
+ background: rgba(255, 255, 255, 0.95);
394
+ backdrop-filter: blur(12px);
395
+ border: none;
396
+ border-radius: 50%;
397
+ cursor: pointer;
398
+ pointer-events: all;
399
+ transition: all 0.3s var(--transition-smooth);
400
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
401
+ }
402
+
403
+ .modern-carousel__arrow svg {
404
+ width: 24px;
405
+ height: 24px;
406
+ color: var(--color-black);
407
+ transition: transform 0.3s var(--transition-smooth);
408
+ }
409
+
410
+ .modern-carousel__arrow:hover {
411
+ background: var(--color-white);
412
+ transform: scale(1.1);
413
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
414
+ }
415
+
416
+ .modern-carousel__arrow--prev:hover svg {
417
+ transform: translateX(-3px);
418
+ }
419
+
420
+ .modern-carousel__arrow--next:hover svg {
421
+ transform: translateX(3px);
422
+ }
423
+
424
+ .modern-carousel__arrow:active {
425
+ transform: scale(0.95);
426
+ }
427
+
428
+ /* ============================================
429
+ PAGINATION (DOTS)
430
+ ============================================ */
431
+
432
+ .modern-carousel__pagination {
433
+ position: absolute;
434
+ bottom: 30px;
435
+ left: 50%;
436
+ transform: translateX(-50%);
437
+ display: flex;
438
+ gap: 12px;
439
+ z-index: 10;
440
+ padding: 12px 20px;
441
+ background: rgba(0, 0, 0, 0.3);
442
+ backdrop-filter: blur(12px);
443
+ border-radius: 50px;
444
+ }
445
+
446
+ .modern-carousel__dot {
447
+ width: 44px;
448
+ height: 8px;
449
+ padding: 0;
450
+ border: none;
451
+ background: transparent;
452
+ cursor: pointer;
453
+ position: relative;
454
+ overflow: hidden;
455
+ border-radius: 4px;
456
+ }
457
+
458
+ .modern-carousel__dot-inner {
459
+ position: absolute;
460
+ top: 0;
461
+ left: 0;
462
+ width: 100%;
463
+ height: 100%;
464
+ background: rgba(255, 255, 255, 0.4);
465
+ transition: all 0.4s var(--transition-smooth);
466
+ border-radius: 4px;
467
+ }
468
+
469
+ .modern-carousel__dot:hover .modern-carousel__dot-inner {
470
+ background: rgba(255, 255, 255, 0.7);
471
+ }
472
+
473
+ .modern-carousel__dot.is-active .modern-carousel__dot-inner {
474
+ background: var(--color-white);
475
+ }
476
+
477
+ /* ============================================
478
+ PROGRESS BAR
479
+ ============================================ */
480
+
481
+ .modern-carousel__progress {
482
+ position: absolute;
483
+ bottom: 0;
484
+ left: 0;
485
+ width: 100%;
486
+ height: 4px;
487
+ background: rgba(255, 255, 255, 0.2);
488
+ z-index: 10;
489
+ overflow: hidden;
490
+ }
491
+
492
+ .modern-carousel__progress-bar {
493
+ height: 100%;
494
+ background: var(--color-white);
495
+ width: 0%;
496
+ transition: width 0.1s linear;
497
+ }
498
+
499
+ /* ============================================
500
+ EMPTY STATE
501
+ ============================================ */
502
+
503
+ .modern-carousel__empty {
504
+ padding: 100px 40px;
505
+ text-align: center;
506
+ color: #9ca3af;
507
+ }
508
+
509
+ .modern-carousel__empty-icon {
510
+ width: 80px;
511
+ height: 80px;
512
+ margin: 0 auto 24px;
513
+ opacity: 0.5;
514
+ }
515
+
516
+ .modern-carousel__empty p {
517
+ margin: 0;
518
+ font-size: 18px;
519
+ font-weight: 500;
520
+ }
521
+
522
+ /* ============================================
523
+ TABLET RESPONSIVE (768px - 1024px)
524
+ ============================================ */
525
+
526
+ @media screen and (max-width: 1024px) and (min-width: 769px) {
527
+ .modern-carousel__slide {
528
+ height: var(--carousel-height-tablet);
529
+ }
530
+
531
+ .modern-carousel__content {
532
+ padding: 50px 40px;
533
+ }
534
+
535
+ .modern-carousel__title {
536
+ font-size: clamp(1.75rem, 4vw, 3.5rem);
537
+ margin-bottom: 16px;
538
+ }
539
+
540
+ .modern-carousel__subtitle {
541
+ font-size: clamp(0.9rem, 1.8vw, 1.25rem);
542
+ margin-bottom: 28px;
543
+ }
544
+
545
+ .modern-carousel__cta {
546
+ padding: 16px 36px;
547
+ font-size: 15px;
548
+ }
549
+
550
+ .modern-carousel__arrow {
551
+ width: 50px;
552
+ height: 50px;
553
+ }
554
+
555
+ .modern-carousel__controls {
556
+ padding: 0 24px;
557
+ }
558
+ }
559
+
560
+ /* ============================================
561
+ MOBILE RESPONSIVE (max-width: 768px)
562
+ ============================================ */
563
+
564
+ @media screen and (max-width: 768px) {
565
+ :root {
566
+ --carousel-padding: 30px 20px;
567
+ --carousel-radius: 16px;
568
+ }
569
+
570
+ .modern-carousel__slide {
571
+ height: var(--carousel-height-mobile);
572
+ }
573
+
574
+ .modern-carousel__content {
575
+ padding: var(--carousel-padding);
576
+ }
577
+
578
+ .modern-carousel__content-inner {
579
+ max-width: 100%;
580
+ }
581
+
582
+ .modern-carousel__title {
583
+ font-size: clamp(1.5rem, 6vw, 2.5rem);
584
+ margin-bottom: 12px;
585
+ }
586
+
587
+ .modern-carousel__subtitle {
588
+ font-size: clamp(0.875rem, 3vw, 1.125rem);
589
+ margin-bottom: 20px;
590
+ }
591
+
592
+ .modern-carousel__cta {
593
+ padding: 14px 28px;
594
+ font-size: 14px;
595
+ gap: 8px;
596
+ }
597
+
598
+ .modern-carousel__cta-icon {
599
+ width: 16px;
600
+ height: 16px;
601
+ }
602
+
603
+ .modern-carousel__controls {
604
+ padding: 0 16px;
605
+ }
606
+
607
+ .modern-carousel__arrow {
608
+ width: 44px;
609
+ height: 44px;
610
+ }
611
+
612
+ .modern-carousel__arrow svg {
613
+ width: 20px;
614
+ height: 20px;
615
+ }
616
+
617
+ .modern-carousel__pagination {
618
+ bottom: 20px;
619
+ padding: 10px 16px;
620
+ gap: 10px;
621
+ }
622
+
623
+ .modern-carousel__dot {
624
+ width: 36px;
625
+ height: 6px;
626
+ }
627
+
628
+ .modern-carousel__empty {
629
+ padding: 60px 24px;
630
+ }
631
+
632
+ .modern-carousel__empty-icon {
633
+ width: 60px;
634
+ height: 60px;
635
+ }
636
+ }
637
+
638
+ /* ============================================
639
+ SMALL MOBILE (max-width: 480px)
640
+ ============================================ */
641
+
642
+ @media screen and (max-width: 480px) {
643
+ .modern-carousel__slide {
644
+ height: 350px;
645
+ }
646
+
647
+ .modern-carousel__title {
648
+ font-size: clamp(1.25rem, 7vw, 2rem);
649
+ }
650
+
651
+ .modern-carousel__subtitle {
652
+ font-size: 0.875rem;
653
+ margin-bottom: 16px;
654
+ }
655
+
656
+ .modern-carousel__cta {
657
+ padding: 12px 24px;
658
+ font-size: 13px;
659
+ }
660
+
661
+ .modern-carousel__arrow {
662
+ width: 40px;
663
+ height: 40px;
664
+ }
665
+
666
+ .modern-carousel__arrow svg {
667
+ width: 18px;
668
+ height: 18px;
669
+ }
670
+
671
+ .modern-carousel__controls {
672
+ padding: 0 12px;
673
+ }
674
+
675
+ .modern-carousel__pagination {
676
+ bottom: 16px;
677
+ padding: 8px 12px;
678
+ gap: 8px;
679
+ }
680
+
681
+ .modern-carousel__dot {
682
+ width: 28px;
683
+ }
684
+ }
685
+ </style>
686
+
687
+ <script>
688
+ document.addEventListener('DOMContentLoaded', function() {
689
+ const carouselEl = document.querySelector('[data-widget-id="{{ widget.id }}"]');
690
+ if (!carouselEl) return;
691
+
692
+ const track = carouselEl.querySelector('[data-carousel-track]');
693
+ const dots = carouselEl.querySelectorAll('.modern-carousel__dot');
694
+ const prevBtn = carouselEl.querySelector('[data-carousel-prev]');
695
+ const nextBtn = carouselEl.querySelector('[data-carousel-next]');
696
+ const progressBar = carouselEl.querySelector('[data-progress-bar]');
697
+ const autoplay = carouselEl.dataset.autoplay === 'true';
698
+ const interval = parseInt(carouselEl.dataset.interval || '5000', 10);
699
+
700
+ if (!track || dots.length === 0) return;
701
+
702
+ let currentIndex = 0;
703
+ const totalSlides = dots.length;
704
+ let autoplayTimer = null;
705
+ let progressTimer = null;
706
+ let progressWidth = 0;
707
+
708
+ // Go to specific slide
709
+ function goToSlide(index) {
710
+ // Remove active class from all slides and dots
711
+ const slides = carouselEl.querySelectorAll('.modern-carousel__slide');
712
+ slides.forEach(slide => slide.classList.remove('is-active'));
713
+ dots.forEach(dot => dot.classList.remove('is-active'));
714
+
715
+ // Set new index
716
+ currentIndex = (index + totalSlides) % totalSlides;
717
+
718
+ // Update transform
719
+ track.style.transform = `translateX(-${currentIndex * 100}%)`;
720
+
721
+ // Add active class
722
+ slides[currentIndex].classList.add('is-active');
723
+ dots[currentIndex].classList.add('is-active');
724
+
725
+ // Reset progress
726
+ if (progressBar) {
727
+ progressWidth = 0;
728
+ progressBar.style.width = '0%';
729
+ }
730
+
731
+ // Restart autoplay
732
+ if (autoplay) {
733
+ startAutoplay();
734
+ }
735
+ }
736
+
737
+ // Next slide
738
+ function nextSlide() {
739
+ goToSlide(currentIndex + 1);
740
+ }
741
+
742
+ // Previous slide
743
+ function prevSlide() {
744
+ goToSlide(currentIndex - 1);
745
+ }
746
+
747
+ // Start autoplay with progress
748
+ function startAutoplay() {
749
+ stopAutoplay();
750
+
751
+ if (progressBar) {
752
+ // Update progress bar smoothly
753
+ const progressInterval = 50; // Update every 50ms
754
+ const progressIncrement = (progressInterval / interval) * 100;
755
+
756
+ progressTimer = setInterval(() => {
757
+ progressWidth += progressIncrement;
758
+ if (progressWidth >= 100) {
759
+ progressWidth = 100;
760
+ progressBar.style.width = '100%';
761
+ clearInterval(progressTimer);
762
+ } else {
763
+ progressBar.style.width = progressWidth + '%';
764
+ }
765
+ }, progressInterval);
766
+ }
767
+
768
+ autoplayTimer = setTimeout(nextSlide, interval);
769
+ }
770
+
771
+ // Stop autoplay
772
+ function stopAutoplay() {
773
+ if (autoplayTimer) {
774
+ clearTimeout(autoplayTimer);
775
+ autoplayTimer = null;
776
+ }
777
+ if (progressTimer) {
778
+ clearInterval(progressTimer);
779
+ progressTimer = null;
780
+ }
781
+ }
782
+
783
+ // Event listeners for arrows
784
+ if (prevBtn) {
785
+ prevBtn.addEventListener('click', prevSlide);
786
+ }
787
+
788
+ if (nextBtn) {
789
+ nextBtn.addEventListener('click', nextSlide);
790
+ }
791
+
792
+ // Event listeners for dots
793
+ dots.forEach((dot, index) => {
794
+ dot.addEventListener('click', () => goToSlide(index));
795
+ });
796
+
797
+ // Pause on hover
798
+ if (autoplay) {
799
+ carouselEl.addEventListener('mouseenter', stopAutoplay);
800
+ carouselEl.addEventListener('mouseleave', startAutoplay);
801
+
802
+ // Start initial autoplay
803
+ startAutoplay();
804
+ }
805
+
806
+ // Keyboard navigation
807
+ document.addEventListener('keydown', (e) => {
808
+ if (e.key === 'ArrowLeft') {
809
+ prevSlide();
810
+ } else if (e.key === 'ArrowRight') {
811
+ nextSlide();
812
+ }
813
+ });
814
+
815
+ // Touch/swipe support
816
+ let touchStartX = 0;
817
+ let touchEndX = 0;
818
+
819
+ carouselEl.addEventListener('touchstart', (e) => {
820
+ touchStartX = e.changedTouches[0].screenX;
821
+ }, { passive: true });
822
+
823
+ carouselEl.addEventListener('touchend', (e) => {
824
+ touchEndX = e.changedTouches[0].screenX;
825
+ handleSwipe();
826
+ }, { passive: true });
827
+
828
+ function handleSwipe() {
829
+ const swipeThreshold = 50;
830
+ const diff = touchStartX - touchEndX;
831
+
832
+ if (Math.abs(diff) > swipeThreshold) {
833
+ if (diff > 0) {
834
+ // Swipe left - next slide
835
+ nextSlide();
836
+ } else {
837
+ // Swipe right - previous slide
838
+ prevSlide();
839
+ }
840
+ }
841
+ }
842
+ });
843
+ </script>