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