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