@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.
- package/README.md +425 -0
- package/assets/Logo_o2vend.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-white.png +0 -0
- package/bin/o2vend +42 -0
- package/config/widget-map.json +50 -0
- package/lib/commands/check.js +201 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/optimize.js +216 -0
- package/lib/commands/package.js +208 -0
- package/lib/commands/serve.js +105 -0
- package/lib/commands/validate.js +191 -0
- package/lib/lib/api-client.js +357 -0
- package/lib/lib/dev-server.js +2618 -0
- package/lib/lib/file-watcher.js +80 -0
- package/lib/lib/hot-reload.js +106 -0
- package/lib/lib/liquid-engine.js +822 -0
- package/lib/lib/liquid-filters.js +671 -0
- package/lib/lib/mock-api-server.js +989 -0
- package/lib/lib/mock-data.js +1468 -0
- package/lib/lib/widget-service.js +321 -0
- package/package.json +70 -0
- package/test-theme/README.md +27 -0
- package/test-theme/assets/async-sections.js +446 -0
- package/test-theme/assets/cart-drawer.js +463 -0
- package/test-theme/assets/cart-manager.js +223 -0
- package/test-theme/assets/checkout-price-handler.js +368 -0
- package/test-theme/assets/components.css +4629 -0
- package/test-theme/assets/delivery-zone.css +299 -0
- package/test-theme/assets/delivery-zone.js +396 -0
- package/test-theme/assets/logo.png +0 -0
- package/test-theme/assets/sections.css +48 -0
- package/test-theme/assets/theme.css +3500 -0
- package/test-theme/assets/theme.js +3745 -0
- package/test-theme/config/settings_data.json +292 -0
- package/test-theme/config/settings_schema.json +1050 -0
- package/test-theme/layout/theme.liquid +195 -0
- package/test-theme/locales/en.default.json +260 -0
- package/test-theme/sections/content-fallback.liquid +53 -0
- package/test-theme/sections/content.liquid +57 -0
- package/test-theme/sections/footer-fallback.liquid +328 -0
- package/test-theme/sections/footer.liquid +278 -0
- package/test-theme/sections/header-fallback.liquid +1805 -0
- package/test-theme/sections/header.liquid +1145 -0
- package/test-theme/sections/hero-fallback.liquid +212 -0
- package/test-theme/sections/hero.liquid +136 -0
- package/test-theme/snippets/account-sidebar.liquid +200 -0
- package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
- package/test-theme/snippets/breadcrumbs.liquid +134 -0
- package/test-theme/snippets/cart-drawer.liquid +467 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
- package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
- package/test-theme/snippets/delivery-zone-search.liquid +78 -0
- package/test-theme/snippets/icon.liquid +105 -0
- package/test-theme/snippets/login-modal.liquid +346 -0
- package/test-theme/snippets/mega-menu.liquid +812 -0
- package/test-theme/snippets/news-thumbnail.liquid +187 -0
- package/test-theme/snippets/pagination.liquid +120 -0
- package/test-theme/snippets/price.liquid +92 -0
- package/test-theme/snippets/product-card-related.liquid +78 -0
- package/test-theme/snippets/product-card-simple.liquid +41 -0
- package/test-theme/snippets/product-card.liquid +697 -0
- package/test-theme/snippets/rating.liquid +85 -0
- package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
- package/test-theme/snippets/skeleton-product-card.liquid +124 -0
- package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
- package/test-theme/snippets/social-sharing.liquid +185 -0
- package/test-theme/templates/account/dashboard.liquid +401 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
- package/test-theme/templates/account/loyalty.liquid +588 -0
- package/test-theme/templates/account/order-detail.liquid +230 -0
- package/test-theme/templates/account/orders.liquid +349 -0
- package/test-theme/templates/account/profile.liquid +758 -0
- package/test-theme/templates/account/register.liquid +232 -0
- package/test-theme/templates/account/return-orders.liquid +348 -0
- package/test-theme/templates/account/store-credit.liquid +464 -0
- package/test-theme/templates/account/subscriptions.liquid +601 -0
- package/test-theme/templates/account/wishlist.liquid +419 -0
- package/test-theme/templates/address-book.liquid +1092 -0
- package/test-theme/templates/categories.liquid +452 -0
- package/test-theme/templates/checkout.liquid +4511 -0
- package/test-theme/templates/error.liquid +384 -0
- package/test-theme/templates/index.liquid +11 -0
- package/test-theme/templates/login.liquid +185 -0
- package/test-theme/templates/order-confirmation.liquid +720 -0
- package/test-theme/templates/page.liquid +297 -0
- package/test-theme/templates/product-detail.liquid +4363 -0
- package/test-theme/templates/products.liquid +518 -0
- package/test-theme/templates/search.liquid +922 -0
- package/test-theme/theme.json.example +19 -0
- package/test-theme/widgets/brand-carousel.liquid +676 -0
- package/test-theme/widgets/brand.liquid +245 -0
- package/test-theme/widgets/carousel.liquid +843 -0
- package/test-theme/widgets/category-list-carousel.liquid +656 -0
- package/test-theme/widgets/category-list.liquid +340 -0
- package/test-theme/widgets/category.liquid +475 -0
- package/test-theme/widgets/discount-time.liquid +176 -0
- package/test-theme/widgets/footer-menu.liquid +695 -0
- package/test-theme/widgets/footer.liquid +179 -0
- package/test-theme/widgets/gallery.liquid +271 -0
- package/test-theme/widgets/header-menu.liquid +932 -0
- package/test-theme/widgets/header.liquid +159 -0
- package/test-theme/widgets/html.liquid +214 -0
- package/test-theme/widgets/news.liquid +217 -0
- package/test-theme/widgets/product-canvas.liquid +235 -0
- package/test-theme/widgets/product-carousel.liquid +502 -0
- package/test-theme/widgets/product.liquid +45 -0
- package/test-theme/widgets/recently-viewed.liquid +26 -0
- package/test-theme/widgets/shared/product-grid.liquid +339 -0
- package/test-theme/widgets/simple-product.liquid +42 -0
- package/test-theme/widgets/single-product.liquid +610 -0
- package/test-theme/widgets/spacebar-carousel.liquid +663 -0
- package/test-theme/widgets/spacebar.liquid +279 -0
- package/test-theme/widgets/splash.liquid +378 -0
- package/test-theme/widgets/testimonial-carousel.liquid +709 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
{% liquid
|
|
2
|
+
assign widget_settings = widget.settings
|
|
3
|
+
assign widget_data = widget.data
|
|
4
|
+
|
|
5
|
+
comment
|
|
6
|
+
Dynamic Widget: Categories come from widget_data.categories (enriched by WidgetService)
|
|
7
|
+
Settings come from widget_settings
|
|
8
|
+
endcomment
|
|
9
|
+
assign categories = widget_data.categories | default: widget_data.Categories
|
|
10
|
+
assign heading = widget_settings.title
|
|
11
|
+
assign subtitle = widget_settings.subtitle
|
|
12
|
+
assign show_widget_title_raw = widget_settings.showWidgetTitle | default: 'Yes'
|
|
13
|
+
if show_widget_title_raw == null or show_widget_title_raw == blank or show_widget_title_raw == 'null'
|
|
14
|
+
assign show_widget_title = true
|
|
15
|
+
else
|
|
16
|
+
if show_widget_title_raw == 'Yes'
|
|
17
|
+
assign show_widget_title = true
|
|
18
|
+
elsif show_widget_title_raw == true
|
|
19
|
+
assign show_widget_title = true
|
|
20
|
+
else
|
|
21
|
+
assign show_widget_title = false
|
|
22
|
+
endif
|
|
23
|
+
endif
|
|
24
|
+
assign widget_title_alignment_raw = widget_settings.widgetTitleAlignment | default: 'center'
|
|
25
|
+
assign widget_title_alignment = widget_title_alignment_raw | downcase
|
|
26
|
+
if widget_title_alignment != 'left' and widget_title_alignment != 'right' and widget_title_alignment != 'center'
|
|
27
|
+
assign widget_title_alignment = 'center'
|
|
28
|
+
endif
|
|
29
|
+
assign show_counts = widget_settings.showProductCount | default: false
|
|
30
|
+
assign background_color = widget_settings.backgroundColor
|
|
31
|
+
assign text_color = widget_settings.textColor
|
|
32
|
+
%}
|
|
33
|
+
|
|
34
|
+
<section class="widget widget-category-carousel" data-widget-id="{{ widget.id }}" data-category-carousel{% if background_color and background_color != blank and background_color != 'null' %} style="background-color: {{ background_color }};"{% endif %}>
|
|
35
|
+
<div class="category-carousel-container">
|
|
36
|
+
{% if show_widget_title %}
|
|
37
|
+
{% if heading or subtitle %}
|
|
38
|
+
<header class="category-carousel-header">
|
|
39
|
+
<div class="category-carousel-header__text" style="text-align: {{ widget_title_alignment }};">
|
|
40
|
+
{% if heading %}
|
|
41
|
+
<h2 class="category-carousel-title"{% if text_color and text_color != 'null' %} style="color: {{ text_color }};"{% endif %}>{{ heading }}</h2>
|
|
42
|
+
{% endif %}
|
|
43
|
+
{% if subtitle %}
|
|
44
|
+
<p class="category-carousel-subtitle"{% if text_color and text_color != 'null' %} style="color: {{ text_color }};"{% endif %}>{{ subtitle }}</p>
|
|
45
|
+
{% endif %}
|
|
46
|
+
</div>
|
|
47
|
+
{% if categories and categories.size > 0 %}
|
|
48
|
+
<div class="category-carousel-nav">
|
|
49
|
+
<button type="button" class="category-carousel-arrow category-carousel-arrow--prev" data-carousel-prev aria-label="Previous">
|
|
50
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
51
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
52
|
+
</svg>
|
|
53
|
+
</button>
|
|
54
|
+
<button type="button" class="category-carousel-arrow category-carousel-arrow--next" data-carousel-next aria-label="Next">
|
|
55
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
56
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
57
|
+
</svg>
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
{% endif %}
|
|
61
|
+
</header>
|
|
62
|
+
{% endif %}
|
|
63
|
+
{% endif %}
|
|
64
|
+
|
|
65
|
+
{% if categories and categories.size > 0 %}
|
|
66
|
+
<div class="category-carousel-wrapper">
|
|
67
|
+
<!-- Side Navigation Arrows (Desktop) -->
|
|
68
|
+
<button type="button" class="category-carousel-side-arrow category-carousel-side-arrow--prev" data-carousel-prev aria-label="Previous">
|
|
69
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
70
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
71
|
+
</svg>
|
|
72
|
+
</button>
|
|
73
|
+
|
|
74
|
+
<div class="category-carousel-track-wrapper" data-carousel-viewport>
|
|
75
|
+
<div class="category-carousel-track" data-carousel-track>
|
|
76
|
+
{% for category in categories %}
|
|
77
|
+
{% comment %} Extract category data with fallbacks {% endcomment %}
|
|
78
|
+
{% assign cat_name = category.name | default: category.Name | default: category.title | default: category.Title %}
|
|
79
|
+
{% assign cat_slug = category.slug | default: category.Slug | default: category.handle | default: category.Handle %}
|
|
80
|
+
{% assign cat_id = category.id | default: category.Id | default: category.categoryId | default: category.CategoryId %}
|
|
81
|
+
{% assign cat_product_count = category.productCount | default: category.ProductCount %}
|
|
82
|
+
|
|
83
|
+
{% comment %} Build category URL - slug is already a complete URL {% endcomment %}
|
|
84
|
+
{% assign cat_url = category.url | default: category.Url | default: category.link | default: category.Link %}
|
|
85
|
+
{% if cat_url == blank %}
|
|
86
|
+
{% if cat_slug and cat_slug != blank %}
|
|
87
|
+
{% assign cat_url = cat_slug %}
|
|
88
|
+
{% else %}
|
|
89
|
+
{% assign cat_url = '#' %}
|
|
90
|
+
{% endif %}
|
|
91
|
+
{% endif %}
|
|
92
|
+
|
|
93
|
+
{% comment %} Get category image with fallbacks {% endcomment %}
|
|
94
|
+
{% assign cat_image = nil %}
|
|
95
|
+
{% if category.thumbnailImage and category.thumbnailImage.url %}
|
|
96
|
+
{% assign cat_image = category.thumbnailImage.url %}
|
|
97
|
+
{% elsif category.ThumbnailImage and category.ThumbnailImage.Url %}
|
|
98
|
+
{% assign cat_image = category.ThumbnailImage.Url %}
|
|
99
|
+
{% elsif category.bannerImage and category.bannerImage.url %}
|
|
100
|
+
{% assign cat_image = category.bannerImage.url %}
|
|
101
|
+
{% elsif category.BannerImage and category.BannerImage.Url %}
|
|
102
|
+
{% assign cat_image = category.BannerImage.Url %}
|
|
103
|
+
{% elsif category.thumbnailImage and category.thumbnailImage != blank %}
|
|
104
|
+
{% assign cat_image = category.thumbnailImage %}
|
|
105
|
+
{% elsif category.bannerImage and category.bannerImage != blank %}
|
|
106
|
+
{% assign cat_image = category.bannerImage %}
|
|
107
|
+
{% elsif category.image %}
|
|
108
|
+
{% assign cat_image = category.image %}
|
|
109
|
+
{% elsif category.Image %}
|
|
110
|
+
{% assign cat_image = category.Image %}
|
|
111
|
+
{% endif %}
|
|
112
|
+
|
|
113
|
+
<div class="category-carousel-slide" data-slide-index="{{ forloop.index0 }}">
|
|
114
|
+
<a href="{{ cat_url }}" class="category-carousel-item">
|
|
115
|
+
<div class="category-carousel-item__media">
|
|
116
|
+
{% if cat_image != blank and cat_image != nil and cat_image != 'null' %}
|
|
117
|
+
<img src="{{ cat_image }}" alt="{{ cat_name }}" width="300" height="300" loading="lazy" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
|
118
|
+
<span class="category-carousel-item__placeholder category-carousel-item__placeholder--fallback" style="display: none;">
|
|
119
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
120
|
+
<path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z"/>
|
|
121
|
+
</svg>
|
|
122
|
+
</span>
|
|
123
|
+
{% else %}
|
|
124
|
+
<span class="category-carousel-item__placeholder">
|
|
125
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
126
|
+
<path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z"/>
|
|
127
|
+
</svg>
|
|
128
|
+
</span>
|
|
129
|
+
{% endif %}
|
|
130
|
+
</div>
|
|
131
|
+
<div class="category-carousel-item__content">
|
|
132
|
+
<h3>{{ cat_name }}</h3>
|
|
133
|
+
{% if show_counts and cat_product_count %}
|
|
134
|
+
<p>{{ cat_product_count }} products</p>
|
|
135
|
+
{% endif %}
|
|
136
|
+
</div>
|
|
137
|
+
</a>
|
|
138
|
+
</div>
|
|
139
|
+
{% endfor %}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<button type="button" class="category-carousel-side-arrow category-carousel-side-arrow--next" data-carousel-next aria-label="Next">
|
|
144
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
145
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
146
|
+
</svg>
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Dot Indicators -->
|
|
151
|
+
<div class="category-carousel-dots" data-carousel-dots></div>
|
|
152
|
+
{% else %}
|
|
153
|
+
<div class="widget-empty">
|
|
154
|
+
<p>{{ widget_settings.empty_state | default: 'Categories will appear here soon.' }}</p>
|
|
155
|
+
</div>
|
|
156
|
+
{% endif %}
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<style>
|
|
160
|
+
/* Category Carousel - True Carousel Behavior */
|
|
161
|
+
.widget-category-carousel {
|
|
162
|
+
padding: 48px 0;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.widget-category-carousel .category-carousel-container {
|
|
167
|
+
max-width: 1400px;
|
|
168
|
+
margin: 0 auto;
|
|
169
|
+
padding: 0 24px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Header */
|
|
173
|
+
.widget-category-carousel .category-carousel-header {
|
|
174
|
+
display: flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
justify-content: space-between;
|
|
177
|
+
margin-bottom: 28px;
|
|
178
|
+
gap: 16px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.widget-category-carousel .category-carousel-header__text {
|
|
182
|
+
flex: 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.widget-category-carousel .category-carousel-header {
|
|
186
|
+
position: static;
|
|
187
|
+
z-index: auto;
|
|
188
|
+
}
|
|
189
|
+
.widget-category-carousel .category-carousel-title {
|
|
190
|
+
font-size: 1.4rem;
|
|
191
|
+
font-weight: 500;
|
|
192
|
+
line-height: 1.3;
|
|
193
|
+
color: #111;
|
|
194
|
+
margin: 0;
|
|
195
|
+
letter-spacing: -0.01em;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.widget-category-carousel .category-carousel-subtitle {
|
|
199
|
+
font-size: 14px;
|
|
200
|
+
color: #6b7280;
|
|
201
|
+
margin: 6px 0 0 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Header Nav Arrows */
|
|
205
|
+
.widget-category-carousel .category-carousel-nav {
|
|
206
|
+
display: flex;
|
|
207
|
+
gap: 8px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.widget-category-carousel .category-carousel-arrow {
|
|
211
|
+
width: 40px;
|
|
212
|
+
height: 40px;
|
|
213
|
+
border-radius: 50%;
|
|
214
|
+
border: 1px solid #e0e0e0;
|
|
215
|
+
background: #fff;
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
justify-content: center;
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
color: #333;
|
|
221
|
+
transition: all 0.2s ease;
|
|
222
|
+
flex-shrink: 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.widget-category-carousel .category-carousel-arrow:hover:not(:disabled) {
|
|
226
|
+
background: #111;
|
|
227
|
+
color: #fff;
|
|
228
|
+
border-color: #111;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.widget-category-carousel .category-carousel-arrow:disabled {
|
|
232
|
+
opacity: 0.3;
|
|
233
|
+
cursor: not-allowed;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* Carousel Wrapper with Side Arrows */
|
|
237
|
+
.widget-category-carousel .category-carousel-wrapper {
|
|
238
|
+
position: relative;
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
gap: 12px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Side Navigation Arrows */
|
|
245
|
+
.widget-category-carousel .category-carousel-side-arrow {
|
|
246
|
+
display: none;
|
|
247
|
+
width: 44px;
|
|
248
|
+
height: 44px;
|
|
249
|
+
border-radius: 50%;
|
|
250
|
+
border: none;
|
|
251
|
+
background: #fff;
|
|
252
|
+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
|
|
253
|
+
align-items: center;
|
|
254
|
+
justify-content: center;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
color: #333;
|
|
257
|
+
transition: all 0.2s ease;
|
|
258
|
+
flex-shrink: 0;
|
|
259
|
+
z-index: 10;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.widget-category-carousel .category-carousel-side-arrow:hover:not(:disabled) {
|
|
263
|
+
background: #111;
|
|
264
|
+
color: #fff;
|
|
265
|
+
transform: scale(1.05);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.widget-category-carousel .category-carousel-side-arrow:disabled {
|
|
269
|
+
opacity: 0;
|
|
270
|
+
pointer-events: none;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Track Wrapper */
|
|
274
|
+
.widget-category-carousel .category-carousel-track-wrapper {
|
|
275
|
+
flex: 1;
|
|
276
|
+
overflow: hidden;
|
|
277
|
+
position: relative;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* The Scrolling Track */
|
|
281
|
+
.widget-category-carousel .category-carousel-track {
|
|
282
|
+
display: flex !important;
|
|
283
|
+
gap: 20px;
|
|
284
|
+
overflow-x: auto !important;
|
|
285
|
+
overflow-y: hidden !important;
|
|
286
|
+
scroll-snap-type: x mandatory;
|
|
287
|
+
scroll-behavior: smooth;
|
|
288
|
+
-webkit-overflow-scrolling: touch;
|
|
289
|
+
scrollbar-width: none;
|
|
290
|
+
-ms-overflow-style: none;
|
|
291
|
+
padding: 4px 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.widget-category-carousel .category-carousel-track::-webkit-scrollbar {
|
|
295
|
+
display: none;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* Individual Slides - Desktop: 4 items */
|
|
299
|
+
.widget-category-carousel .category-carousel-slide {
|
|
300
|
+
flex: 0 0 calc(25% - 15px);
|
|
301
|
+
min-width: 0;
|
|
302
|
+
max-width: calc(25% - 15px);
|
|
303
|
+
scroll-snap-align: start;
|
|
304
|
+
scroll-snap-stop: normal;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* Category Item Card */
|
|
308
|
+
.widget-category-carousel .category-carousel-item {
|
|
309
|
+
display: block;
|
|
310
|
+
background: #fff;
|
|
311
|
+
border-radius: 12px;
|
|
312
|
+
overflow: hidden;
|
|
313
|
+
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.08);
|
|
314
|
+
text-decoration: none;
|
|
315
|
+
color: inherit;
|
|
316
|
+
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.widget-category-carousel .category-carousel-item:hover {
|
|
320
|
+
transform: translateY(-4px);
|
|
321
|
+
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.12);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.widget-category-carousel .category-carousel-item__media {
|
|
325
|
+
aspect-ratio: 4 / 3;
|
|
326
|
+
overflow: hidden;
|
|
327
|
+
position: relative;
|
|
328
|
+
background: #f5f5f5;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.widget-category-carousel .category-carousel-item__media img {
|
|
332
|
+
width: 100%;
|
|
333
|
+
height: 100%;
|
|
334
|
+
object-fit: cover;
|
|
335
|
+
transition: transform 0.3s ease;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.widget-category-carousel .category-carousel-item:hover .category-carousel-item__media img {
|
|
339
|
+
transform: scale(1.05);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.widget-category-carousel .category-carousel-item__placeholder {
|
|
343
|
+
width: 100%;
|
|
344
|
+
height: 100%;
|
|
345
|
+
display: flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
justify-content: center;
|
|
348
|
+
background: linear-gradient(135deg, #f0f0f0 0%, #e8e8e8 100%);
|
|
349
|
+
color: #9ca3af;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.widget-category-carousel .category-carousel-item__placeholder--fallback {
|
|
353
|
+
position: absolute;
|
|
354
|
+
top: 0;
|
|
355
|
+
left: 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.widget-category-carousel .category-carousel-item__content {
|
|
359
|
+
padding: 16px;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.widget-category-carousel .category-carousel-item__content h3 {
|
|
363
|
+
margin: 0;
|
|
364
|
+
font-size: 16px;
|
|
365
|
+
font-weight: 600;
|
|
366
|
+
color: #111;
|
|
367
|
+
line-height: 1.3;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.widget-category-carousel .category-carousel-item__content p {
|
|
371
|
+
margin: 6px 0 0;
|
|
372
|
+
color: #6b7280;
|
|
373
|
+
font-size: 13px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Dot Indicators */
|
|
377
|
+
.widget-category-carousel .category-carousel-dots {
|
|
378
|
+
display: flex;
|
|
379
|
+
justify-content: center;
|
|
380
|
+
gap: 8px;
|
|
381
|
+
margin-top: 20px;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.widget-category-carousel .category-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-category-carousel .category-carousel-dot:hover {
|
|
396
|
+
background: #9ca3af;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.widget-category-carousel .category-carousel-dot.active {
|
|
400
|
+
background: #111;
|
|
401
|
+
width: 24px;
|
|
402
|
+
border-radius: 4px;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.widget-category-carousel .widget-empty {
|
|
406
|
+
text-align: center;
|
|
407
|
+
padding: 60px 24px;
|
|
408
|
+
color: #6b7280;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* Desktop: Show side arrows, hide header arrows */
|
|
412
|
+
@media (min-width: 1025px) {
|
|
413
|
+
.widget-category-carousel .category-carousel-side-arrow {
|
|
414
|
+
display: flex;
|
|
415
|
+
}
|
|
416
|
+
.widget-category-carousel .category-carousel-nav {
|
|
417
|
+
display: none;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Tablet: 3 items */
|
|
422
|
+
@media (max-width: 1024px) {
|
|
423
|
+
.widget-category-carousel .category-carousel-slide {
|
|
424
|
+
flex: 0 0 calc(33.333% - 14px);
|
|
425
|
+
max-width: calc(33.333% - 14px);
|
|
426
|
+
}
|
|
427
|
+
.widget-category-carousel .category-carousel-track {
|
|
428
|
+
gap: 20px;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* Small Tablet: 2.5 items */
|
|
433
|
+
@media (max-width: 768px) {
|
|
434
|
+
.widget-category-carousel {
|
|
435
|
+
padding: 36px 0;
|
|
436
|
+
}
|
|
437
|
+
.widget-category-carousel .category-carousel-container {
|
|
438
|
+
padding: 0 16px;
|
|
439
|
+
}
|
|
440
|
+
.widget-category-carousel .category-carousel-title {
|
|
441
|
+
font-size: 1.3rem;
|
|
442
|
+
}
|
|
443
|
+
.widget-category-carousel .category-carousel-track {
|
|
444
|
+
gap: 16px;
|
|
445
|
+
}
|
|
446
|
+
.widget-category-carousel .category-carousel-slide {
|
|
447
|
+
flex: 0 0 calc(50% - 8px);
|
|
448
|
+
max-width: calc(50% - 8px);
|
|
449
|
+
}
|
|
450
|
+
.widget-category-carousel .category-carousel-arrow {
|
|
451
|
+
width: 36px;
|
|
452
|
+
height: 36px;
|
|
453
|
+
}
|
|
454
|
+
.widget-category-carousel .category-carousel-dots {
|
|
455
|
+
margin-top: 16px;
|
|
456
|
+
}
|
|
457
|
+
.widget-category-carousel .category-carousel-item__content {
|
|
458
|
+
padding: 12px;
|
|
459
|
+
}
|
|
460
|
+
.widget-category-carousel .category-carousel-item__content h3 {
|
|
461
|
+
font-size: 14px;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/* Mobile: 2 items */
|
|
466
|
+
@media (max-width: 480px) {
|
|
467
|
+
.widget-category-carousel .category-carousel-container {
|
|
468
|
+
padding: 0 12px;
|
|
469
|
+
}
|
|
470
|
+
.widget-category-carousel .category-carousel-track {
|
|
471
|
+
gap: 12px;
|
|
472
|
+
}
|
|
473
|
+
.widget-category-carousel .category-carousel-slide {
|
|
474
|
+
flex: 0 0 calc(50% - 6px);
|
|
475
|
+
max-width: calc(50% - 6px);
|
|
476
|
+
}
|
|
477
|
+
.widget-category-carousel .category-carousel-title {
|
|
478
|
+
font-size: 1.2rem;
|
|
479
|
+
}
|
|
480
|
+
.widget-category-carousel .category-carousel-item {
|
|
481
|
+
border-radius: 8px;
|
|
482
|
+
}
|
|
483
|
+
.widget-category-carousel .category-carousel-item__content {
|
|
484
|
+
padding: 10px;
|
|
485
|
+
}
|
|
486
|
+
.widget-category-carousel .category-carousel-item__content h3 {
|
|
487
|
+
font-size: 13px;
|
|
488
|
+
}
|
|
489
|
+
.widget-category-carousel .category-carousel-item__content p {
|
|
490
|
+
font-size: 12px;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* Extra small */
|
|
495
|
+
@media (max-width: 360px) {
|
|
496
|
+
.widget-category-carousel .category-carousel-track {
|
|
497
|
+
gap: 8px;
|
|
498
|
+
}
|
|
499
|
+
.widget-category-carousel .category-carousel-slide {
|
|
500
|
+
flex: 0 0 calc(50% - 4px);
|
|
501
|
+
max-width: calc(50% - 4px);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
</style>
|
|
505
|
+
|
|
506
|
+
<script>
|
|
507
|
+
(function() {
|
|
508
|
+
// Wait for DOM
|
|
509
|
+
if (document.readyState === 'loading') {
|
|
510
|
+
document.addEventListener('DOMContentLoaded', initCategoryCarousel);
|
|
511
|
+
} else {
|
|
512
|
+
initCategoryCarousel();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function initCategoryCarousel() {
|
|
516
|
+
const widget = document.querySelector('[data-widget-id="{{ widget.id }}"][data-category-carousel]');
|
|
517
|
+
if (!widget) return;
|
|
518
|
+
|
|
519
|
+
const track = widget.querySelector('[data-carousel-track]');
|
|
520
|
+
const viewport = widget.querySelector('[data-carousel-viewport]');
|
|
521
|
+
const prevBtns = widget.querySelectorAll('[data-carousel-prev]');
|
|
522
|
+
const nextBtns = widget.querySelectorAll('[data-carousel-next]');
|
|
523
|
+
const dotsContainer = widget.querySelector('[data-carousel-dots]');
|
|
524
|
+
const slides = widget.querySelectorAll('.category-carousel-slide');
|
|
525
|
+
|
|
526
|
+
if (!track || slides.length === 0) return;
|
|
527
|
+
|
|
528
|
+
// Calculate items per view based on screen width
|
|
529
|
+
function getItemsPerView() {
|
|
530
|
+
const width = window.innerWidth;
|
|
531
|
+
if (width <= 360) return 2;
|
|
532
|
+
if (width <= 480) return 2;
|
|
533
|
+
if (width <= 768) return 2;
|
|
534
|
+
if (width <= 1024) return 3;
|
|
535
|
+
return 4;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Calculate number of pages
|
|
539
|
+
function getPageCount() {
|
|
540
|
+
const itemsPerView = getItemsPerView();
|
|
541
|
+
return Math.ceil(slides.length / itemsPerView);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Get current page based on scroll position
|
|
545
|
+
function getCurrentPage() {
|
|
546
|
+
if (!track.scrollWidth || track.scrollWidth <= viewport.clientWidth) return 0;
|
|
547
|
+
const scrollRatio = track.scrollLeft / (track.scrollWidth - viewport.clientWidth);
|
|
548
|
+
return Math.round(scrollRatio * (getPageCount() - 1));
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Create dot indicators
|
|
552
|
+
function createDots() {
|
|
553
|
+
if (!dotsContainer) return;
|
|
554
|
+
dotsContainer.innerHTML = '';
|
|
555
|
+
const pageCount = getPageCount();
|
|
556
|
+
if (pageCount <= 1) return;
|
|
557
|
+
|
|
558
|
+
for (let i = 0; i < pageCount; i++) {
|
|
559
|
+
const dot = document.createElement('button');
|
|
560
|
+
dot.className = 'category-carousel-dot' + (i === 0 ? ' active' : '');
|
|
561
|
+
dot.setAttribute('aria-label', 'Go to page ' + (i + 1));
|
|
562
|
+
dot.addEventListener('click', () => goToPage(i));
|
|
563
|
+
dotsContainer.appendChild(dot);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Update active dot
|
|
568
|
+
function updateDots() {
|
|
569
|
+
if (!dotsContainer) return;
|
|
570
|
+
const dots = dotsContainer.querySelectorAll('.category-carousel-dot');
|
|
571
|
+
const currentPage = getCurrentPage();
|
|
572
|
+
dots.forEach((dot, index) => {
|
|
573
|
+
dot.classList.toggle('active', index === currentPage);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Update arrow states
|
|
578
|
+
function updateArrows() {
|
|
579
|
+
const atStart = track.scrollLeft <= 5;
|
|
580
|
+
const atEnd = track.scrollLeft >= track.scrollWidth - viewport.clientWidth - 5;
|
|
581
|
+
|
|
582
|
+
prevBtns.forEach(btn => {
|
|
583
|
+
btn.disabled = atStart;
|
|
584
|
+
});
|
|
585
|
+
nextBtns.forEach(btn => {
|
|
586
|
+
btn.disabled = atEnd;
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Navigate to specific page
|
|
591
|
+
function goToPage(pageIndex) {
|
|
592
|
+
const itemsPerView = getItemsPerView();
|
|
593
|
+
const slide = slides[0];
|
|
594
|
+
if (!slide) return;
|
|
595
|
+
|
|
596
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 20;
|
|
597
|
+
const slideWidth = slide.offsetWidth + gap;
|
|
598
|
+
const scrollTo = pageIndex * itemsPerView * slideWidth;
|
|
599
|
+
|
|
600
|
+
track.scrollTo({ left: scrollTo, behavior: 'smooth' });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Scroll by items
|
|
604
|
+
function scrollByItems(direction) {
|
|
605
|
+
const itemsPerView = getItemsPerView();
|
|
606
|
+
const slide = slides[0];
|
|
607
|
+
if (!slide) return;
|
|
608
|
+
|
|
609
|
+
const gap = parseFloat(getComputedStyle(track).gap) || 20;
|
|
610
|
+
const scrollAmount = (slide.offsetWidth + gap) * Math.max(1, itemsPerView - 1);
|
|
611
|
+
|
|
612
|
+
track.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Event listeners
|
|
616
|
+
prevBtns.forEach(btn => {
|
|
617
|
+
btn.addEventListener('click', (e) => {
|
|
618
|
+
e.preventDefault();
|
|
619
|
+
scrollByItems(-1);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
nextBtns.forEach(btn => {
|
|
624
|
+
btn.addEventListener('click', (e) => {
|
|
625
|
+
e.preventDefault();
|
|
626
|
+
scrollByItems(1);
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Track scroll events
|
|
631
|
+
let scrollTimeout;
|
|
632
|
+
track.addEventListener('scroll', () => {
|
|
633
|
+
clearTimeout(scrollTimeout);
|
|
634
|
+
scrollTimeout = setTimeout(() => {
|
|
635
|
+
updateDots();
|
|
636
|
+
updateArrows();
|
|
637
|
+
}, 50);
|
|
638
|
+
}, { passive: true });
|
|
639
|
+
|
|
640
|
+
// Handle resize
|
|
641
|
+
let resizeTimeout;
|
|
642
|
+
window.addEventListener('resize', () => {
|
|
643
|
+
clearTimeout(resizeTimeout);
|
|
644
|
+
resizeTimeout = setTimeout(() => {
|
|
645
|
+
createDots();
|
|
646
|
+
updateArrows();
|
|
647
|
+
}, 200);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Initialize
|
|
651
|
+
createDots();
|
|
652
|
+
updateArrows();
|
|
653
|
+
}
|
|
654
|
+
})();
|
|
655
|
+
</script>
|
|
656
|
+
</section>
|