@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,19 @@
1
+ {
2
+ "id": "test-theme",
3
+ "name": "Test Theme",
4
+ "version": "1.0.0",
5
+ "author": "Theme Developer",
6
+ "description": "A beautiful O2VEND theme: test-theme",
7
+ "migration": {
8
+ "files": {
9
+ "modified": [],
10
+ "added": [],
11
+ "deleted": []
12
+ },
13
+ "script": null
14
+ },
15
+ "compatibility": {
16
+ "minO2VENDVersion": "1.0.0",
17
+ "dependencies": []
18
+ }
19
+ }
@@ -0,0 +1,676 @@
1
+ {% liquid
2
+ assign widget_settings = widget.settings
3
+ assign widget_data = widget.data
4
+
5
+ comment
6
+ Dynamic Widget: Brands come from widget_data.brands (enriched by WidgetService)
7
+ Settings come from widget_settings
8
+ endcomment
9
+ assign brands = widget_data.Brands | default: widget_data.brands
10
+ assign heading = widget_settings.title | default: widget.Title | default: widget.title
11
+ assign subtitle = widget_settings.subtitle
12
+ assign background_color = widget_settings.backgroundColor
13
+ assign text_color = widget_settings.textColor
14
+ assign show_container = widget_settings.showContainer
15
+ assign show_bottom_margin = widget_settings.showWidgetBottomMargin
16
+ assign show_widget_title_raw = widget_settings.showWidgetTitle | default: 'Yes'
17
+ if show_widget_title_raw == null or show_widget_title_raw == blank or show_widget_title_raw == 'null'
18
+ assign show_widget_title = true
19
+ else
20
+ if show_widget_title_raw == 'Yes'
21
+ assign show_widget_title = true
22
+ elsif show_widget_title_raw == true
23
+ assign show_widget_title = true
24
+ else
25
+ assign show_widget_title = false
26
+ endif
27
+ endif
28
+ assign widget_title_alignment_raw = widget_settings.widgetTitleAlignment | default: 'center'
29
+ assign widget_title_alignment = widget_title_alignment_raw | downcase
30
+ if widget_title_alignment != 'left' and widget_title_alignment != 'right' and widget_title_alignment != 'center'
31
+ assign widget_title_alignment = 'center'
32
+ endif
33
+ %}
34
+
35
+ <section class="widget widget-brand-carousel" data-widget-id="{{ widget.id }}" data-brand-carousel{% if background_color and background_color != blank and background_color != 'null' %} style="background-color: {{ background_color }};"{% endif %}>
36
+ <div class="brand-carousel-container">
37
+ {% if show_widget_title %}
38
+ {% if heading or subtitle %}
39
+ <header class="brand-carousel-header">
40
+ <div class="brand-carousel-header__text" style="text-align: {{ widget_title_alignment }};">
41
+ {% if heading %}
42
+ <h2 class="brand-carousel-title"{% if text_color and text_color != 'null' %} style="color: {{ text_color }};"{% endif %}>{{ heading }}</h2>
43
+ {% endif %}
44
+ {% if subtitle %}
45
+ <p class="brand-carousel-subtitle"{% if text_color and text_color != 'null' %} style="color: {{ text_color }};"{% endif %}>{{ subtitle }}</p>
46
+ {% endif %}
47
+ </div>
48
+ {% if brands and brands.size > 0 %}
49
+ <div class="brand-carousel-nav">
50
+ <button type="button" class="brand-carousel-arrow brand-carousel-arrow--prev" data-carousel-prev aria-label="Previous">
51
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
52
+ <polyline points="15 18 9 12 15 6"></polyline>
53
+ </svg>
54
+ </button>
55
+ <button type="button" class="brand-carousel-arrow brand-carousel-arrow--next" data-carousel-next aria-label="Next">
56
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
57
+ <polyline points="9 18 15 12 9 6"></polyline>
58
+ </svg>
59
+ </button>
60
+ </div>
61
+ {% endif %}
62
+ </header>
63
+ {% endif %}
64
+ {% endif %}
65
+
66
+ {% if brands and brands.size > 0 %}
67
+ <div class="brand-carousel-wrapper">
68
+ <!-- Side Navigation Arrows (Desktop) -->
69
+ <button type="button" class="brand-carousel-side-arrow brand-carousel-side-arrow--prev" data-carousel-prev aria-label="Previous">
70
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
71
+ <polyline points="15 18 9 12 15 6"></polyline>
72
+ </svg>
73
+ </button>
74
+
75
+ <div class="brand-carousel-track-wrapper" data-carousel-viewport>
76
+ <div class="brand-carousel-track" data-carousel-track>
77
+ {% for brand in brands %}
78
+ {% assign brand_id = brand.Id | default: brand.id | default: brand.brandId %}
79
+ {% assign brand_name = brand.Name | default: brand.name %}
80
+ {% assign brand_slug = brand.Slug | default: brand.slug %}
81
+ {% assign brand_url = brand.Url | default: brand.url | default: brand.link %}
82
+ {% if brand_url == blank %}
83
+ {% if brand_slug and brand_slug != blank %}
84
+ {% assign brand_url = brand_slug %}
85
+ {% else %}
86
+ {% assign brand_url = '#' %}
87
+ {% endif %}
88
+ {% endif %}
89
+
90
+ {% comment %} Image fallback: thumbnailImage.url > Logo > logo > image {% endcomment %}
91
+ {% assign brand_image = nil %}
92
+ {% if brand.thumbnailImage and brand.thumbnailImage.url %}
93
+ {% assign brand_image = brand.thumbnailImage.url %}
94
+ {% elsif brand.ThumbnailImage and brand.ThumbnailImage.Url %}
95
+ {% assign brand_image = brand.ThumbnailImage.Url %}
96
+ {% elsif brand.Logo %}
97
+ {% assign brand_image = brand.Logo %}
98
+ {% elsif brand.logo %}
99
+ {% assign brand_image = brand.logo %}
100
+ {% elsif brand.image %}
101
+ {% assign brand_image = brand.image %}
102
+ {% elsif brand.Image %}
103
+ {% assign brand_image = brand.Image %}
104
+ {% endif %}
105
+
106
+ <div class="brand-carousel-slide" data-slide-index="{{ forloop.index0 }}">
107
+ <a href="{{ brand_url }}" class="brand-carousel-item" aria-label="{{ brand_name }}">
108
+ {% if brand_image != blank and brand_image != '' and brand_image != 'null' %}
109
+ <div class="brand-carousel-item__image">
110
+ <img src="{{ brand_image }}" alt="{{ brand_name }}" width="200" height="60" loading="lazy" onerror="this.style.display='none'; this.parentElement.nextElementSibling.style.display='flex';">
111
+ </div>
112
+ <span class="brand-carousel-item__name brand-carousel-item__name--fallback" style="display: none;">{{ brand_name }}</span>
113
+ {% else %}
114
+ <span class="brand-carousel-item__name">{{ brand_name }}</span>
115
+ {% endif %}
116
+ </a>
117
+ </div>
118
+ {% endfor %}
119
+ </div>
120
+ </div>
121
+
122
+ <button type="button" class="brand-carousel-side-arrow brand-carousel-side-arrow--next" data-carousel-next aria-label="Next">
123
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
124
+ <polyline points="9 18 15 12 9 6"></polyline>
125
+ </svg>
126
+ </button>
127
+ </div>
128
+
129
+ <!-- Dot Indicators -->
130
+ <div class="brand-carousel-dots" data-carousel-dots></div>
131
+ {% else %}
132
+ <div class="widget-empty">
133
+ <p>{{ widget_settings.empty_state | default: 'No brands to display yet.' }}</p>
134
+ </div>
135
+ {% endif %}
136
+ </div>
137
+
138
+ <style>
139
+ .widget-brand-carousel {
140
+ {% if show_bottom_margin == 'Yes' %}
141
+ padding: 2.5rem 0;
142
+ {% elsif show_bottom_margin == 'No' %}
143
+ padding: 2.5rem 0 0;
144
+ {% else %}
145
+ padding: 2.5rem 0;
146
+ {% endif %}
147
+ background: {{ background_color | default: widget_settings.background_color | default: 'transparent' }};
148
+ }
149
+ .brand-carousel-container {
150
+ {% if show_container == 'Yes' %}
151
+ max-width: 1400px;
152
+ margin-left: auto;
153
+ margin-right: auto;
154
+ {% endif %}
155
+ padding-left: 24px;
156
+ padding-right: 24px;
157
+ }
158
+ .brand-carousel-slide {
159
+ position: relative;
160
+ border-radius: 12px;
161
+ background: #fff;
162
+ min-height: 100px;
163
+ text-decoration: none;
164
+ color: inherit;
165
+ overflow: hidden;
166
+
167
+ box-shadow:
168
+ 0 0 8px rgba(15, 23, 42, 0.06),
169
+ 0 0 16px rgba(15, 23, 42, 0.04);
170
+
171
+ transition:
172
+ transform 0.25s ease,
173
+ box-shadow 0.25s ease;
174
+ }
175
+
176
+ /* Hover / Focus */
177
+ .brand-carousel-slide:hover,
178
+ .brand-carousel-slide:focus-visible {
179
+ transform: translateY(-2px);
180
+
181
+ box-shadow:
182
+ 0 4px 12px rgba(15, 23, 42, 0.10),
183
+ 0 8px 24px rgba(15, 23, 42, 0.06);
184
+ }
185
+
186
+ /* Brand Carousel - True Carousel Behavior */
187
+ .widget-brand-carousel {
188
+ padding: 48px 0;
189
+ overflow: hidden;
190
+ }
191
+
192
+ .widget-brand-carousel .brand-carousel-container {
193
+ max-width: 1400px;
194
+ margin: 0 auto;
195
+ padding: 0 24px;
196
+ }
197
+
198
+ /* Header */
199
+ .widget-brand-carousel .brand-carousel-header {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: space-between;
203
+ margin-bottom: 28px;
204
+ gap: 16px;
205
+ }
206
+
207
+ .widget-brand-carousel .brand-carousel-header__text {
208
+ flex: 1;
209
+ }
210
+
211
+ .widget-brand-carousel .brand-carousel-header {
212
+ position: static;
213
+ z-index: auto;
214
+ }
215
+ .widget-brand-carousel .brand-carousel-title {
216
+ font-size: 1.4rem;
217
+ font-weight: 500;
218
+ line-height: 1.3;
219
+ color: #111;
220
+ margin: 0;
221
+ letter-spacing: -0.01em;
222
+ }
223
+
224
+ .widget-brand-carousel .brand-carousel-subtitle {
225
+ font-size: 14px;
226
+ color: #6b7280;
227
+ margin: 6px 0 0 0;
228
+ }
229
+
230
+ /* Header Nav Arrows */
231
+ .widget-brand-carousel .brand-carousel-nav {
232
+ display: flex;
233
+ gap: 8px;
234
+ }
235
+
236
+ .widget-brand-carousel .brand-carousel-arrow {
237
+ width: 40px;
238
+ height: 40px;
239
+ border-radius: 50%;
240
+ border: 1px solid #e0e0e0;
241
+ background: #fff;
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: center;
245
+ cursor: pointer;
246
+ color: #333;
247
+ transition: all 0.2s ease;
248
+ flex-shrink: 0;
249
+ }
250
+
251
+ .widget-brand-carousel .brand-carousel-arrow:hover:not(:disabled) {
252
+ background: #111;
253
+ color: #fff;
254
+ border-color: #111;
255
+ }
256
+
257
+ .widget-brand-carousel .brand-carousel-arrow:disabled {
258
+ opacity: 0.3;
259
+ cursor: not-allowed;
260
+ }
261
+
262
+ /* Carousel Wrapper with Side Arrows */
263
+ .widget-brand-carousel .brand-carousel-wrapper {
264
+ position: relative;
265
+ display: flex;
266
+ align-items: center;
267
+ gap: 12px;
268
+ }
269
+
270
+ /* Side Navigation Arrows */
271
+ .widget-brand-carousel .brand-carousel-side-arrow {
272
+ display: none; /* Hidden by default, shown on desktop */
273
+ width: 30px;
274
+ height: 30px;
275
+ border-radius: 50%;
276
+ border: none;
277
+ background: #fff;
278
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
279
+ align-items: center;
280
+ justify-content: center;
281
+ cursor: pointer;
282
+ color: #333;
283
+ transition: all 0.2s ease;
284
+ flex-shrink: 0;
285
+ z-index: 10;
286
+ }
287
+
288
+ .widget-brand-carousel .brand-carousel-side-arrow:hover:not(:disabled) {
289
+ background: #111;
290
+ color: #fff;
291
+ transform: scale(1.05);
292
+ }
293
+
294
+ .widget-brand-carousel .brand-carousel-side-arrow:disabled {
295
+ opacity: 0;
296
+ pointer-events: none;
297
+ }
298
+
299
+ /* Track Wrapper */
300
+ .widget-brand-carousel .brand-carousel-track-wrapper {
301
+ flex: 1;
302
+ overflow: hidden;
303
+ position: relative;
304
+ }
305
+
306
+ /* The Scrolling Track */
307
+ .widget-brand-carousel .brand-carousel-track {
308
+ display: flex !important;
309
+ gap: 20px;
310
+ overflow-x: auto !important;
311
+ overflow-y: hidden !important;
312
+ scroll-snap-type: x mandatory;
313
+ scroll-behavior: smooth;
314
+ -webkit-overflow-scrolling: touch;
315
+ scrollbar-width: none;
316
+ -ms-overflow-style: none;
317
+ padding: 4px 0;
318
+ }
319
+
320
+ .widget-brand-carousel .brand-carousel-track::-webkit-scrollbar {
321
+ display: none;
322
+ }
323
+
324
+ /* Individual Slides - Desktop: 6 items */
325
+ .widget-brand-carousel .brand-carousel-slide {
326
+ flex: 0 0 calc(16.666% - 17px);
327
+ min-width: 0;
328
+ max-width: calc(16.666% - 17px);
329
+ scroll-snap-align: start;
330
+ scroll-snap-stop: normal;
331
+ }
332
+
333
+ /* Brand Item Card */
334
+ .widget-brand-carousel .brand-carousel-item {
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ padding: 20px;
339
+ background: #fff;
340
+ min-height: 100px;
341
+ text-decoration: none;
342
+ color: inherit;
343
+ overflow: hidden;
344
+ position: relative;
345
+ }
346
+
347
+
348
+ .widget-brand-carousel .brand-carousel-item__image {
349
+ display: flex;
350
+ align-items: center;
351
+ justify-content: center;
352
+ width: 100%;
353
+ height: 60px;
354
+ }
355
+
356
+ .widget-brand-carousel .brand-carousel-item__image img {
357
+ max-width: 100%;
358
+ max-height: 60px;
359
+ width: auto;
360
+ height: auto;
361
+ object-fit: contain;
362
+ }
363
+
364
+ .widget-brand-carousel .brand-carousel-item__name {
365
+ font-size: 14px;
366
+ font-weight: 600;
367
+ color: #111;
368
+ text-align: center;
369
+ line-height: 1.3;
370
+ word-break: break-word;
371
+ letter-spacing: -0.01em;
372
+ }
373
+
374
+ .widget-brand-carousel .brand-carousel-item__name--fallback {
375
+ position: absolute;
376
+ top: 50%;
377
+ left: 50%;
378
+ transform: translate(-50%, -50%);
379
+ width: 100%;
380
+ padding: 0 8px;
381
+ box-sizing: border-box;
382
+ display: none;
383
+ align-items: center;
384
+ justify-content: center;
385
+ }
386
+
387
+ /* Dot Indicators */
388
+ .widget-brand-carousel .brand-carousel-dots {
389
+ display: flex;
390
+ justify-content: center;
391
+ gap: 8px;
392
+ margin-top: 20px;
393
+ }
394
+
395
+ .widget-brand-carousel .brand-carousel-dot {
396
+ width: 8px;
397
+ height: 8px;
398
+ border-radius: 50%;
399
+ background: #d1d5db;
400
+ border: none;
401
+ padding: 0;
402
+ cursor: pointer;
403
+ transition: all 0.2s ease;
404
+ }
405
+
406
+ .widget-brand-carousel .brand-carousel-dot:hover {
407
+ background: #9ca3af;
408
+ }
409
+
410
+ .widget-brand-carousel .brand-carousel-dot.active {
411
+ background: #111;
412
+ width: 24px;
413
+ border-radius: 4px;
414
+ }
415
+
416
+ /* Desktop: Show side arrows, hide header arrows */
417
+ @media (min-width: 1025px) {
418
+ .widget-brand-carousel .brand-carousel-side-arrow {
419
+ display: flex;
420
+ }
421
+ .widget-brand-carousel .brand-carousel-nav {
422
+ display: none;
423
+ }
424
+ }
425
+
426
+ /* Large Tablet: 5 items */
427
+ @media (max-width: 1200px) {
428
+ .widget-brand-carousel .brand-carousel-slide {
429
+ flex: 0 0 calc(20% - 16px);
430
+ max-width: calc(20% - 16px);
431
+ }
432
+ }
433
+
434
+ /* Tablet: 4 items */
435
+ @media (max-width: 1024px) {
436
+ .widget-brand-carousel .brand-carousel-slide {
437
+ flex: 0 0 calc(25% - 15px);
438
+ max-width: calc(25% - 15px);
439
+ }
440
+ .widget-brand-carousel .brand-carousel-track {
441
+ gap: 20px;
442
+ }
443
+ }
444
+
445
+ /* Small Tablet: 3 items */
446
+ @media (max-width: 768px) {
447
+ .widget-brand-carousel {
448
+ padding: 36px 0;
449
+ }
450
+ .widget-brand-carousel .brand-carousel-container {
451
+ padding: 0 16px;
452
+ }
453
+ .widget-brand-carousel .brand-carousel-title {
454
+ font-size: 1.3rem;
455
+ }
456
+ .widget-brand-carousel .brand-carousel-track {
457
+ gap: 12px;
458
+ }
459
+ .widget-brand-carousel .brand-carousel-slide {
460
+ flex: 0 0 calc(33.333% - 8px);
461
+ max-width: calc(33.333% - 8px);
462
+ }
463
+ .widget-brand-carousel .brand-carousel-item {
464
+ padding: 16px;
465
+ min-height: 80px;
466
+ }
467
+ .widget-brand-carousel .brand-carousel-item__image {
468
+ height: 45px;
469
+ }
470
+ .widget-brand-carousel .brand-carousel-item__image img {
471
+ max-height: 45px;
472
+ }
473
+ .widget-brand-carousel .brand-carousel-arrow {
474
+ width: 36px;
475
+ height: 36px;
476
+ }
477
+ .widget-brand-carousel .brand-carousel-dots {
478
+ margin-top: 16px;
479
+ }
480
+ }
481
+
482
+ /* Mobile: 2.5 items (partial view for hint) */
483
+ @media (max-width: 480px) {
484
+ .widget-brand-carousel .brand-carousel-container {
485
+ padding: 0 12px;
486
+ }
487
+ .widget-brand-carousel .brand-carousel-track {
488
+ gap: 10px;
489
+ }
490
+ .widget-brand-carousel .brand-carousel-slide {
491
+ flex: 0 0 calc(40% - 6px);
492
+ max-width: calc(40% - 6px);
493
+ }
494
+ .widget-brand-carousel .brand-carousel-item {
495
+ padding: 12px;
496
+ min-height: 70px;
497
+ border-radius: 8px;
498
+ }
499
+ .widget-brand-carousel .brand-carousel-item__image {
500
+ height: 40px;
501
+ }
502
+ .widget-brand-carousel .brand-carousel-item__image img {
503
+ max-height: 40px;
504
+ }
505
+ .widget-brand-carousel .brand-carousel-title {
506
+ font-size: 1.2rem;
507
+ }
508
+ .widget-brand-carousel .brand-carousel-item__name {
509
+ font-size: 12px;
510
+ }
511
+ }
512
+
513
+ /* Extra Small: 2 items */
514
+ @media (max-width: 360px) {
515
+ .widget-brand-carousel .brand-carousel-slide {
516
+ flex: 0 0 calc(50% - 5px);
517
+ max-width: calc(50% - 5px);
518
+ }
519
+ .widget-brand-carousel .brand-carousel-track {
520
+ gap: 8px;
521
+ }
522
+ }
523
+ </style>
524
+
525
+ <script>
526
+ (function() {
527
+ // Wait for DOM
528
+ if (document.readyState === 'loading') {
529
+ document.addEventListener('DOMContentLoaded', initBrandCarousel);
530
+ } else {
531
+ initBrandCarousel();
532
+ }
533
+
534
+ function initBrandCarousel() {
535
+ const widget = document.querySelector('[data-widget-id="{{ widget.id }}"][data-brand-carousel]');
536
+ if (!widget) return;
537
+
538
+ const track = widget.querySelector('[data-carousel-track]');
539
+ const viewport = widget.querySelector('[data-carousel-viewport]');
540
+ const prevBtns = widget.querySelectorAll('[data-carousel-prev]');
541
+ const nextBtns = widget.querySelectorAll('[data-carousel-next]');
542
+ const dotsContainer = widget.querySelector('[data-carousel-dots]');
543
+ const slides = widget.querySelectorAll('.brand-carousel-slide');
544
+
545
+ if (!track || slides.length === 0) return;
546
+
547
+ // Calculate items per view based on screen width
548
+ function getItemsPerView() {
549
+ const width = window.innerWidth;
550
+ if (width <= 360) return 2;
551
+ if (width <= 480) return 2.5;
552
+ if (width <= 768) return 3;
553
+ if (width <= 1024) return 4;
554
+ if (width <= 1200) return 5;
555
+ return 6;
556
+ }
557
+
558
+ // Calculate number of pages
559
+ function getPageCount() {
560
+ const itemsPerView = Math.floor(getItemsPerView());
561
+ return Math.ceil(slides.length / itemsPerView);
562
+ }
563
+
564
+ // Get current page based on scroll position
565
+ function getCurrentPage() {
566
+ if (!track.scrollWidth || track.scrollWidth <= viewport.clientWidth) return 0;
567
+ const scrollRatio = track.scrollLeft / (track.scrollWidth - viewport.clientWidth);
568
+ return Math.round(scrollRatio * (getPageCount() - 1));
569
+ }
570
+
571
+ // Create dot indicators
572
+ function createDots() {
573
+ if (!dotsContainer) return;
574
+ dotsContainer.innerHTML = '';
575
+ const pageCount = getPageCount();
576
+ if (pageCount <= 1) return;
577
+
578
+ for (let i = 0; i < pageCount; i++) {
579
+ const dot = document.createElement('button');
580
+ dot.className = 'brand-carousel-dot' + (i === 0 ? ' active' : '');
581
+ dot.setAttribute('aria-label', 'Go to page ' + (i + 1));
582
+ dot.addEventListener('click', () => goToPage(i));
583
+ dotsContainer.appendChild(dot);
584
+ }
585
+ }
586
+
587
+ // Update active dot
588
+ function updateDots() {
589
+ if (!dotsContainer) return;
590
+ const dots = dotsContainer.querySelectorAll('.brand-carousel-dot');
591
+ const currentPage = getCurrentPage();
592
+ dots.forEach((dot, index) => {
593
+ dot.classList.toggle('active', index === currentPage);
594
+ });
595
+ }
596
+
597
+ // Update arrow states
598
+ function updateArrows() {
599
+ const atStart = track.scrollLeft <= 5;
600
+ const atEnd = track.scrollLeft >= track.scrollWidth - viewport.clientWidth - 5;
601
+
602
+ prevBtns.forEach(btn => {
603
+ btn.disabled = atStart;
604
+ });
605
+ nextBtns.forEach(btn => {
606
+ btn.disabled = atEnd;
607
+ });
608
+ }
609
+
610
+ // Navigate to specific page
611
+ function goToPage(pageIndex) {
612
+ const itemsPerView = Math.floor(getItemsPerView());
613
+ const slide = slides[0];
614
+ if (!slide) return;
615
+
616
+ const gap = parseFloat(getComputedStyle(track).gap) || 20;
617
+ const slideWidth = slide.offsetWidth + gap;
618
+ const scrollTo = pageIndex * itemsPerView * slideWidth;
619
+
620
+ track.scrollTo({ left: scrollTo, behavior: 'smooth' });
621
+ }
622
+
623
+ // Scroll by items
624
+ function scrollByItems(direction) {
625
+ const itemsPerView = Math.floor(getItemsPerView());
626
+ const slide = slides[0];
627
+ if (!slide) return;
628
+
629
+ const gap = parseFloat(getComputedStyle(track).gap) || 20;
630
+ const scrollAmount = (slide.offsetWidth + gap) * Math.max(1, itemsPerView - 1);
631
+
632
+ track.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
633
+ }
634
+
635
+ // Event listeners
636
+ prevBtns.forEach(btn => {
637
+ btn.addEventListener('click', (e) => {
638
+ e.preventDefault();
639
+ scrollByItems(-1);
640
+ });
641
+ });
642
+
643
+ nextBtns.forEach(btn => {
644
+ btn.addEventListener('click', (e) => {
645
+ e.preventDefault();
646
+ scrollByItems(1);
647
+ });
648
+ });
649
+
650
+ // Track scroll events
651
+ let scrollTimeout;
652
+ track.addEventListener('scroll', () => {
653
+ clearTimeout(scrollTimeout);
654
+ scrollTimeout = setTimeout(() => {
655
+ updateDots();
656
+ updateArrows();
657
+ }, 50);
658
+ }, { passive: true });
659
+
660
+ // Handle resize
661
+ let resizeTimeout;
662
+ window.addEventListener('resize', () => {
663
+ clearTimeout(resizeTimeout);
664
+ resizeTimeout = setTimeout(() => {
665
+ createDots();
666
+ updateArrows();
667
+ }, 200);
668
+ });
669
+
670
+ // Initialize
671
+ createDots();
672
+ updateArrows();
673
+ }
674
+ })();
675
+ </script>
676
+ </section>