@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,812 @@
1
+ {% comment %}
2
+ Mega Menu Snippet
3
+ Displays a full-width mega menu with support for nested items
4
+ Parameters:
5
+ - menu: The menu object with items array (required)
6
+ Usage:
7
+ {% render 'snippets/mega-menu', menu: mainMenu %}
8
+ {% render 'snippets/mega-menu', menu: footerMenu %}
9
+ {% endcomment %}
10
+ {% comment %}
11
+ Header Menu Widget - FINAL FIX
12
+ Prevents duplicate menus and works on all pages
13
+ {% endcomment %}
14
+
15
+
16
+ {% comment %}Desktop Navigation Menu{% endcomment %}
17
+ <nav class="header-menu-nav" data-header-menu-nav role="navigation" aria-label="Main navigation"
18
+ {% if menu and menu.items and menu.items.size > 0 %}data-server-menu="true"{% endif %}>
19
+ <div class="header-menu-nav__loading" data-menu-loading style="display: none;">
20
+ <span>Loading menu...</span>
21
+ </div>
22
+ <div class="header-menu-nav__error" data-menu-error style="display: none;">
23
+ <span>Menu items unavailable</span>
24
+ </div>
25
+ <ul class="header-menu-nav__list" data-menu-list>
26
+ {% if menu and menu.items and menu.items.size > 0 %}
27
+ {% for item in menu.items %}
28
+ <li class="header-menu-nav__item {% if item.childItems and item.childItems.size > 0 %}has-children{% endif %}">
29
+ <a href="{{ item.link | default: '#' }}" class="header-menu-nav__link" aria-label="{{ item.name | default: item.title }}">{{ item.name | default: item.title }}</a>
30
+ {% if item.childItems and item.childItems.size > 0 %}
31
+ <ul class="header-menu-nav__submenu">
32
+ {% for child in item.childItems %}
33
+ <li class="header-menu-nav__submenu-item"><a href="{{ child.link | default: '#' }}" class="header-menu-nav__submenu-link">{{ child.name | default: child.title }}</a></li>
34
+ {% endfor %}
35
+ </ul>
36
+ {% endif %}
37
+ </li>
38
+ {% endfor %}
39
+ {% endif %}
40
+ </ul>
41
+ </nav>
42
+
43
+ {% comment %}Mobile Navigation Drawer{% endcomment %}
44
+ <div class="header-menu-nav__mobile-drawer" id="header-menu-mobile-drawer" aria-hidden="true">
45
+ <div class="header-menu-nav__mobile-drawer-content">
46
+ <div class="header-menu-nav__mobile-drawer-header">
47
+ <h2 class="header-menu-nav__mobile-drawer-title">Menu</h2>
48
+ <button type="button" class="header-menu-nav__mobile-drawer-close" aria-label="Close menu" data-header-menu-close>
49
+ <svg class="header-menu-nav__drawer-close-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
50
+ <line x1="18" y1="6" x2="6" y2="18"></line>
51
+ <line x1="6" y1="6" x2="18" y2="18"></line>
52
+ </svg>
53
+ </button>
54
+ </div>
55
+ <nav class="header-menu-nav__mobile-drawer-nav" role="navigation" aria-label="Mobile navigation">
56
+ <ul class="header-menu-nav__mobile-drawer-list" data-mobile-menu-list>
57
+ {% if menu and menu.items and menu.items.size > 0 %}
58
+ {% for item in menu.items %}
59
+ <li class="header-menu-nav__mobile-drawer-item {% if item.childItems and item.childItems.size > 0 %}has-children{% endif %}">
60
+ <a href="{{ item.link | default: '#' }}" class="header-menu-nav__mobile-drawer-link">{{ item.name | default: item.title }}</a>
61
+ {% if item.childItems and item.childItems.size > 0 %}
62
+ <ul class="header-menu-nav__mobile-drawer-sublist">
63
+ {% for child in item.childItems %}
64
+ <li class="header-menu-nav__mobile-drawer-subitem"><a href="{{ child.link | default: '#' }}" class="header-menu-nav__mobile-drawer-sublink">{{ child.name | default: child.title }}</a></li>
65
+ {% endfor %}
66
+ </ul>
67
+ {% endif %}
68
+ </li>
69
+ {% endfor %}
70
+ {% endif %}
71
+ </ul>
72
+ </nav>
73
+ </div>
74
+ </div>
75
+
76
+ <style>
77
+ /* Menu Navigation Styles */
78
+ .header-menu-nav {
79
+ flex: 1 1 auto;
80
+ display: flex;
81
+ justify-content: flex-start;
82
+ align-items: center;
83
+ min-width: 0;
84
+ width: 100%;
85
+ }
86
+
87
+ @media (max-width: 768px) {
88
+ .header-menu-nav {
89
+ display: none !important;
90
+ }
91
+ }
92
+
93
+ .header-menu-nav__loading,
94
+ .header-menu-nav__error {
95
+ padding: 0.5rem 0;
96
+ color: #000;
97
+ font-size: 1rem;
98
+ opacity: 0.6;
99
+ }
100
+
101
+ .header-menu-nav__list {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 1.5rem;
105
+ list-style: none;
106
+ margin: 0;
107
+ padding: 0;
108
+ }
109
+
110
+ .header-menu-nav__item {
111
+ position: relative;
112
+ display: flex;
113
+ align-items: center;
114
+ }
115
+
116
+ .header-menu-nav__link {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: var(--space-1, 0.25rem);
120
+ color: var(--menu-text, var(--header-text, #000));
121
+ text-decoration: none;
122
+ font-size: var(--text-base, 1rem);
123
+ font-weight: var(--font-weight-medium, 500);
124
+ line-height: var(--leading-normal, 1.5);
125
+ padding: var(--space-2, 0.5rem) 0;
126
+ transition: color var(--transition-fast, 150ms ease);
127
+ position: relative;
128
+ white-space: nowrap;
129
+ }
130
+
131
+ .header-menu-nav__link:hover {
132
+ color: #333;
133
+ }
134
+
135
+ .header-menu-nav__item.has-children .header-menu-nav__link::after {
136
+ content: '';
137
+ display: inline-block;
138
+ width: 0;
139
+ height: 0;
140
+ border-left: 0.25rem solid transparent;
141
+ border-right: 0.25rem solid transparent;
142
+ border-top: 0.3125rem solid #000;
143
+ margin-left: 0.25rem;
144
+ }
145
+
146
+ .header-menu-nav__submenu {
147
+ position: absolute;
148
+ top: calc(100% + 0.5rem);
149
+ left: 0;
150
+ display: none;
151
+ flex-direction: column;
152
+ background: #fff;
153
+ padding: 0.5rem 0;
154
+ border-radius: 8px;
155
+ box-shadow: 0 1rem 1.5rem rgba(0, 0, 0, 0.1);
156
+ min-width: 12.5rem;
157
+ list-style: none;
158
+ margin: 0;
159
+ z-index: 100;
160
+ border: 0.125rem solid rgba(0, 0, 0, 0.08);
161
+ }
162
+
163
+ .header-menu-nav__item.has-children:hover .header-menu-nav__submenu {
164
+ display: flex;
165
+ }
166
+
167
+ .header-menu-nav__submenu-link {
168
+ display: block;
169
+ padding: 0.625rem 1.25rem;
170
+ color: #000;
171
+ text-decoration: none;
172
+ font-size: 0.875rem;
173
+ transition: background-color 150ms ease;
174
+ }
175
+
176
+ .header-menu-nav__submenu-link:hover {
177
+ background-color: rgba(0, 0, 0, 0.04);
178
+ }
179
+
180
+ /* Mobile Drawer - Only visible on mobile */
181
+ @media (max-width: 768px) {
182
+ /* Ensure drawer is visible even if parent is hidden - use fixed positioning to escape parent */
183
+ .site-header__bottom-row .header-menu-nav__mobile-drawer,
184
+ .header-menu-nav__mobile-drawer {
185
+ position: fixed !important;
186
+ top: 0 !important;
187
+ left: 0 !important;
188
+ right: 0 !important;
189
+ bottom: 0 !important;
190
+ width: 100% !important;
191
+ height: 100vh !important;
192
+ background-color: rgba(0, 0, 0, 0.5);
193
+ backdrop-filter: blur(2px);
194
+ z-index: 9999 !important;
195
+ pointer-events: none;
196
+ display: block !important;
197
+ opacity: 0;
198
+ visibility: hidden;
199
+ transition: opacity 300ms ease, visibility 300ms ease;
200
+ margin: 0 !important;
201
+ padding: 0 !important;
202
+ }
203
+
204
+ .site-header__bottom-row .header-menu-nav__mobile-drawer.active,
205
+ .header-menu-nav__mobile-drawer.active {
206
+ pointer-events: auto !important;
207
+ opacity: 1 !important;
208
+ visibility: visible !important;
209
+ }
210
+
211
+ .header-menu-nav__mobile-drawer.active {
212
+ pointer-events: auto;
213
+ opacity: 1 !important;
214
+ visibility: visible !important;
215
+ }
216
+
217
+ .header-menu-nav__mobile-drawer-content {
218
+ position: absolute !important;
219
+ top: 0 !important;
220
+ left: 0 !important;
221
+ width: 280px !important;
222
+ max-width: 85vw !important;
223
+ height: 100% !important;
224
+ background-color: #fff !important;
225
+ padding: 1.5rem !important;
226
+ overflow-y: auto !important;
227
+ transform: translateX(-100%);
228
+ transition: transform 300ms ease;
229
+ box-shadow: 0.125rem 0 0.5rem rgba(0, 0, 0, 0.15);
230
+ display: flex !important;
231
+ flex-direction: column !important;
232
+ margin: 0 !important;
233
+ }
234
+
235
+ .header-menu-nav__mobile-drawer.active .header-menu-nav__mobile-drawer-content {
236
+ transform: translateX(0) !important;
237
+ }
238
+
239
+ .header-menu-nav__mobile-drawer-header {
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: space-between;
243
+ margin-bottom: 1.5rem;
244
+ padding-bottom: 0.75rem;
245
+ border-bottom: 0.125rem solid rgba(0, 0, 0, 0.08);
246
+ }
247
+
248
+ .header-menu-nav__mobile-drawer-title {
249
+ font-size: 1.125rem;
250
+ font-weight: 600;
251
+ margin: 0;
252
+ }
253
+
254
+ .header-menu-nav__mobile-drawer-close {
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ width: 2.5rem;
259
+ height: 2.5rem;
260
+ background: none;
261
+ border: none;
262
+ cursor: pointer;
263
+ border-radius: 0.25rem;
264
+ }
265
+
266
+ .header-menu-nav__mobile-drawer-close:hover {
267
+ background-color: rgba(0, 0, 0, 0.04);
268
+ }
269
+
270
+ .header-menu-nav__drawer-close-icon {
271
+ width: 1.5rem;
272
+ height: 1.5rem;
273
+ }
274
+
275
+ .header-menu-nav__mobile-drawer-list {
276
+ list-style: none;
277
+ margin: 0;
278
+ padding: 0;
279
+ }
280
+
281
+ .header-menu-nav__mobile-drawer-item {
282
+ border-bottom: 0.125rem solid rgba(0, 0, 0, 0.08);
283
+ }
284
+
285
+ .header-menu-nav__mobile-drawer-link {
286
+ display: block;
287
+ padding: 1rem 0;
288
+ color: #000;
289
+ text-decoration: none;
290
+ font-weight: 500;
291
+ font-size: 1.2rem;
292
+ }
293
+
294
+ .header-menu-nav__mobile-drawer-sublist {
295
+ list-style: none;
296
+ margin: 0;
297
+ padding: 0 0 0 1rem;
298
+ border-left: 0.125rem solid rgba(0, 0, 0, 0.08);
299
+ display: none;
300
+ }
301
+
302
+ .header-menu-nav__mobile-drawer-item.open .header-menu-nav__mobile-drawer-sublist {
303
+ display: block;
304
+ }
305
+
306
+ .header-menu-nav__mobile-drawer-sublink {
307
+ display: block;
308
+ padding: 0.75rem 0;
309
+ color: #000;
310
+ text-decoration: none;
311
+ font-size: 0.875rem;
312
+ }
313
+ }
314
+
315
+ /* Hide mobile drawer on desktop */
316
+ @media (min-width: 769px) {
317
+ .header-menu-nav__mobile-drawer {
318
+ display: none !important;
319
+ }
320
+ }
321
+ </style>
322
+
323
+ <script>
324
+ (function() {
325
+ 'use strict';
326
+
327
+ // Prevent multiple initializations
328
+ if (window.__headerMenuInitialized) {
329
+ console.log('[HeaderMenu] Already initialized, skipping');
330
+ return;
331
+ }
332
+ window.__headerMenuInitialized = true;
333
+
334
+ console.log('[HeaderMenu] Initializing (once)...');
335
+
336
+ function init() {
337
+ const menuNav = document.querySelector('[data-header-menu-nav]');
338
+
339
+ if (!menuNav) {
340
+ console.error('[HeaderMenu] Menu nav not found');
341
+ return;
342
+ }
343
+
344
+ // Move drawer to body level to avoid parent display:none issues
345
+ const drawer = document.getElementById('header-menu-mobile-drawer');
346
+ if (drawer && drawer.parentElement && drawer.parentElement !== document.body) {
347
+ console.log('[HeaderMenu] Moving drawer to body level');
348
+ document.body.appendChild(drawer);
349
+ }
350
+
351
+ const hasServerMenu = menuNav.getAttribute('data-server-menu') === 'true';
352
+ const menuList = menuNav.querySelector('[data-menu-list]');
353
+ const hasMenuItems = menuList && menuList.children.length > 0;
354
+
355
+ console.log('[HeaderMenu] Status:', {
356
+ hasServerMenu: hasServerMenu,
357
+ hasMenuItems: hasMenuItems,
358
+ itemCount: menuList ? menuList.children.length : 0
359
+ });
360
+
361
+ // Only fetch from API if NO menu items exist
362
+ if (!hasMenuItems) {
363
+ console.log('[HeaderMenu] No menu items found, loading from API...');
364
+ loadMainMenu(menuNav);
365
+ } else {
366
+ console.log('[HeaderMenu] Menu items already present, skipping API call');
367
+ setupSubmenuToggles();
368
+ }
369
+
370
+ // Setup hamburger menu with retry (will find drawer internally)
371
+ setupHamburgerMenu();
372
+ }
373
+
374
+ function setupHamburgerMenu() {
375
+ let attempts = 0;
376
+ const maxAttempts = 15;
377
+
378
+ function trySetup() {
379
+ const toggle = document.querySelector('[data-header-menu-toggle]');
380
+ const drawer = document.getElementById('header-menu-mobile-drawer');
381
+ const drawerClose = document.querySelector('[data-header-menu-close]');
382
+
383
+ if (toggle && drawer) {
384
+ console.log('[HeaderMenu] Setting up hamburger menu', {
385
+ toggleFound: !!toggle,
386
+ drawerFound: !!drawer,
387
+ drawerCloseFound: !!drawerClose
388
+ });
389
+
390
+ // Use a flag to prevent duplicate listeners
391
+ if (toggle.dataset.menuListenerAttached === 'true') {
392
+ console.log('[HeaderMenu] Listener already attached, skipping');
393
+ return true;
394
+ }
395
+ toggle.dataset.menuListenerAttached = 'true';
396
+
397
+ // Toggle button click handler (only on mobile)
398
+ toggle.addEventListener('click', function(e) {
399
+ // Double check we're on mobile
400
+ if (window.innerWidth > 768) {
401
+ console.log('[HeaderMenu] Ignoring toggle click on desktop');
402
+ return;
403
+ }
404
+ e.preventDefault();
405
+ e.stopPropagation();
406
+ console.log('[HeaderMenu] Toggle clicked');
407
+ const currentDrawer = document.getElementById('header-menu-mobile-drawer');
408
+ if (!currentDrawer) {
409
+ console.error('[HeaderMenu] Drawer not found when toggling');
410
+ return;
411
+ }
412
+ const isOpen = currentDrawer.classList.contains('active');
413
+ console.log('[HeaderMenu] Drawer state:', isOpen ? 'open' : 'closed');
414
+ if (isOpen) {
415
+ closeDrawer(currentDrawer, toggle);
416
+ } else {
417
+ openDrawer(currentDrawer, toggle);
418
+ }
419
+ });
420
+
421
+ // Close button handler
422
+ if (drawerClose) {
423
+ drawerClose.addEventListener('click', function(e) {
424
+ e.preventDefault();
425
+ e.stopPropagation();
426
+ console.log('[HeaderMenu] Close button clicked');
427
+ const currentDrawer = document.getElementById('header-menu-mobile-drawer');
428
+ if (currentDrawer) {
429
+ closeDrawer(currentDrawer, toggle);
430
+ }
431
+ });
432
+ }
433
+
434
+ // Close on Escape key (only add once)
435
+ if (!window.__headerMenuEscapeHandler) {
436
+ window.__headerMenuEscapeHandler = function(e) {
437
+ if (e.key === 'Escape' && window.innerWidth <= 768) {
438
+ const currentDrawer = document.getElementById('header-menu-mobile-drawer');
439
+ if (currentDrawer && currentDrawer.classList.contains('active')) {
440
+ console.log('[HeaderMenu] Escape key pressed');
441
+ closeDrawer(currentDrawer, toggle);
442
+ }
443
+ }
444
+ };
445
+ document.addEventListener('keydown', window.__headerMenuEscapeHandler);
446
+ }
447
+
448
+ // Close on outside click (only add once)
449
+ if (!window.__headerMenuOverlayHandler) {
450
+ window.__headerMenuOverlayHandler = function(e) {
451
+ if (window.innerWidth > 768) return;
452
+ const currentDrawer = document.getElementById('header-menu-mobile-drawer');
453
+ if (currentDrawer && currentDrawer.classList.contains('active')) {
454
+ const drawerContent = currentDrawer.querySelector('.header-menu-nav__mobile-drawer-content');
455
+ // Close if clicking the backdrop (drawer itself) but not the content
456
+ if (e.target === currentDrawer || (drawerContent && !drawerContent.contains(e.target) && currentDrawer.contains(e.target))) {
457
+ console.log('[HeaderMenu] Backdrop clicked - closing drawer');
458
+ closeDrawer(currentDrawer, toggle);
459
+ }
460
+ }
461
+ };
462
+ document.addEventListener('click', window.__headerMenuOverlayHandler);
463
+ }
464
+
465
+ console.log('[HeaderMenu] Hamburger menu setup complete');
466
+ return true;
467
+ } else {
468
+ attempts++;
469
+ if (attempts < maxAttempts) {
470
+ setTimeout(trySetup, 100);
471
+ } else {
472
+ console.warn('[HeaderMenu] Could not find hamburger menu elements after', maxAttempts, 'attempts', {
473
+ toggleFound: !!toggle,
474
+ drawerFound: !!drawer
475
+ });
476
+ }
477
+ return false;
478
+ }
479
+ }
480
+
481
+ trySetup();
482
+ }
483
+
484
+ function openDrawer(drawer, toggle) {
485
+ if (!drawer) {
486
+ console.error('[HeaderMenu] Cannot open drawer - drawer not found');
487
+ return;
488
+ }
489
+ // Only open on mobile
490
+ if (window.innerWidth > 768) {
491
+ console.log('[HeaderMenu] Ignoring open on desktop');
492
+ return;
493
+ }
494
+ console.log('[HeaderMenu] Opening drawer', {
495
+ drawerElement: drawer,
496
+ drawerId: drawer.id,
497
+ hasActiveClass: drawer.classList.contains('active'),
498
+ windowWidth: window.innerWidth,
499
+ computedStyle: window.getComputedStyle(drawer).display,
500
+ zIndex: window.getComputedStyle(drawer).zIndex,
501
+ opacity: window.getComputedStyle(drawer).opacity,
502
+ visibility: window.getComputedStyle(drawer).visibility
503
+ });
504
+
505
+ drawer.classList.add('active');
506
+ drawer.setAttribute('aria-hidden', 'false');
507
+
508
+ // Force browser reflow to ensure styles are applied
509
+ void drawer.offsetHeight;
510
+
511
+ if (toggle) {
512
+ toggle.setAttribute('aria-expanded', 'true');
513
+ toggle.classList.add('is-open');
514
+ }
515
+
516
+ // Prevent body scroll
517
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
518
+ document.body.style.paddingRight = scrollbarWidth + 'px';
519
+ document.body.style.overflow = 'hidden';
520
+ document.documentElement.style.overflow = 'hidden';
521
+
522
+ // Verify drawer is visible after opening
523
+ setTimeout(() => {
524
+ const computedStyle = window.getComputedStyle(drawer);
525
+ const content = drawer.querySelector('.header-menu-nav__mobile-drawer-content');
526
+ const contentStyle = content ? window.getComputedStyle(content) : null;
527
+ console.log('[HeaderMenu] Drawer state after opening:', {
528
+ hasActiveClass: drawer.classList.contains('active'),
529
+ display: computedStyle.display,
530
+ opacity: computedStyle.opacity,
531
+ visibility: computedStyle.visibility,
532
+ zIndex: computedStyle.zIndex,
533
+ contentTransform: contentStyle ? contentStyle.transform : 'N/A',
534
+ contentDisplay: contentStyle ? contentStyle.display : 'N/A'
535
+ });
536
+ }, 100);
537
+
538
+ console.log('[HeaderMenu] Drawer opened successfully');
539
+ }
540
+
541
+ function closeDrawer(drawer, toggle) {
542
+ if (!drawer) {
543
+ console.error('[HeaderMenu] Cannot close drawer - drawer not found');
544
+ return;
545
+ }
546
+ console.log('[HeaderMenu] Closing drawer');
547
+ drawer.classList.remove('active');
548
+ drawer.setAttribute('aria-hidden', 'true');
549
+ if (toggle) {
550
+ toggle.setAttribute('aria-expanded', 'false');
551
+ toggle.classList.remove('is-open');
552
+ }
553
+ // Restore body scroll
554
+ document.body.style.paddingRight = '';
555
+ document.body.style.overflow = '';
556
+ document.documentElement.style.overflow = '';
557
+ console.log('[HeaderMenu] Drawer closed successfully');
558
+ }
559
+
560
+ async function loadMainMenu(menuNav) {
561
+ const menuList = menuNav.querySelector('[data-menu-list]');
562
+ const loadingEl = menuNav.querySelector('[data-menu-loading]');
563
+ const errorEl = menuNav.querySelector('[data-menu-error]');
564
+ const mobileMenuList = document.querySelector('[data-mobile-menu-list]');
565
+
566
+ try {
567
+ if (loadingEl) loadingEl.style.display = 'block';
568
+ if (errorEl) errorEl.style.display = 'none';
569
+
570
+ console.log('[HeaderMenu] Fetching menus from API...');
571
+ const menusResponse = await fetch('/webstoreapi/menus', {
572
+ headers: {
573
+ 'Accept': 'application/json',
574
+ 'X-Requested-With': 'XMLHttpRequest'
575
+ }
576
+ });
577
+
578
+ if (!menusResponse.ok) throw new Error('Failed to fetch menus: ' + menusResponse.status);
579
+
580
+ const menus = await menusResponse.json();
581
+ console.log('[HeaderMenu] Fetched menus:', menus);
582
+
583
+ if (!Array.isArray(menus) || menus.length === 0) {
584
+ throw new Error('No menus available');
585
+ }
586
+
587
+ // Find main menu
588
+ let mainMenu = menus.find(m => (m.type || '').toLowerCase().trim() === 'main menu');
589
+ if (!mainMenu) {
590
+ console.log('[HeaderMenu] No "Main Menu" found, using first menu');
591
+ mainMenu = menus[0];
592
+ }
593
+
594
+ console.log('[HeaderMenu] Selected menu:', mainMenu);
595
+
596
+ // Fetch menu details
597
+ const menuResponse = await fetch('/webstoreapi/menus/' + mainMenu.id, {
598
+ headers: {
599
+ 'Accept': 'application/json',
600
+ 'X-Requested-With': 'XMLHttpRequest'
601
+ }
602
+ });
603
+
604
+ if (!menuResponse.ok) throw new Error('Failed to fetch menu details: ' + menuResponse.status);
605
+
606
+ const menuData = await menuResponse.json();
607
+ console.log('[HeaderMenu] Menu data:', menuData);
608
+
609
+ if (!menuData.items || menuData.items.length === 0) {
610
+ throw new Error('No menu items');
611
+ }
612
+
613
+ console.log('[HeaderMenu] Rendering', menuData.items.length, 'menu items');
614
+ renderMenuItems(menuList, menuData.items);
615
+ if (mobileMenuList) {
616
+ renderMobileMenuItems(mobileMenuList, menuData.items);
617
+ }
618
+
619
+ setupSubmenuToggles();
620
+
621
+ if (loadingEl) loadingEl.style.display = 'none';
622
+ console.log('[HeaderMenu] Menu loaded successfully');
623
+
624
+ } catch (error) {
625
+ console.error('[HeaderMenu] Error loading menu:', error);
626
+ if (loadingEl) loadingEl.style.display = 'none';
627
+ if (errorEl) errorEl.style.display = 'block';
628
+ }
629
+ }
630
+
631
+ /**
632
+ * Normalize menu link to ensure it's an absolute URL
633
+ * @param {string} link - Menu link from API
634
+ * @returns {string} Normalized absolute URL
635
+ */
636
+ function normalizeMenuLink(link) {
637
+ if (!link || link === '#' || link.trim() === '') {
638
+ return '#';
639
+ }
640
+
641
+ const trimmedLink = link.trim();
642
+
643
+ // If already an absolute URL (http:// or https://), return as-is
644
+ if (trimmedLink.startsWith('http://') || trimmedLink.startsWith('https://')) {
645
+ return trimmedLink;
646
+ }
647
+
648
+ // If already starts with /, it's absolute from root, return as-is
649
+ if (trimmedLink.startsWith('/')) {
650
+ return trimmedLink;
651
+ }
652
+
653
+ // Otherwise, prepend base URL to make it absolute
654
+ const baseUrl = window.location.origin;
655
+ // Remove leading slash from link if present, then add it back with baseUrl
656
+ const cleanLink = trimmedLink.startsWith('/') ? trimmedLink : '/' + trimmedLink;
657
+ return baseUrl + cleanLink;
658
+ }
659
+
660
+ function renderMenuItems(container, items) {
661
+ container.innerHTML = '';
662
+ items.forEach(item => {
663
+ const li = document.createElement('li');
664
+ li.className = 'header-menu-nav__item';
665
+ if (item.childItems && item.childItems.length > 0) {
666
+ li.classList.add('has-children');
667
+ }
668
+
669
+ const link = document.createElement('a');
670
+ link.href = normalizeMenuLink(item.link);
671
+ link.className = 'header-menu-nav__link';
672
+ link.textContent = item.name || item.title || '';
673
+ li.appendChild(link);
674
+
675
+ if (item.childItems && item.childItems.length > 0) {
676
+ const submenu = document.createElement('ul');
677
+ submenu.className = 'header-menu-nav__submenu';
678
+ item.childItems.forEach(child => {
679
+ const subLi = document.createElement('li');
680
+ subLi.className = 'header-menu-nav__submenu-item';
681
+ const subLink = document.createElement('a');
682
+ subLink.href = normalizeMenuLink(child.link);
683
+ subLink.className = 'header-menu-nav__submenu-link';
684
+ subLink.textContent = child.name || child.title || '';
685
+ subLi.appendChild(subLink);
686
+ submenu.appendChild(subLi);
687
+ });
688
+ li.appendChild(submenu);
689
+ }
690
+
691
+ container.appendChild(li);
692
+ });
693
+ }
694
+
695
+ function renderMobileMenuItems(container, items) {
696
+ container.innerHTML = '';
697
+ items.forEach(item => {
698
+ const li = document.createElement('li');
699
+ li.className = 'header-menu-nav__mobile-drawer-item';
700
+ if (item.childItems && item.childItems.length > 0) {
701
+ li.classList.add('has-children');
702
+ }
703
+
704
+ const link = document.createElement('a');
705
+ link.href = normalizeMenuLink(item.link);
706
+ link.className = 'header-menu-nav__mobile-drawer-link';
707
+ link.textContent = item.name || item.title || '';
708
+ li.appendChild(link);
709
+
710
+ if (item.childItems && item.childItems.length > 0) {
711
+ const submenu = document.createElement('ul');
712
+ submenu.className = 'header-menu-nav__mobile-drawer-sublist';
713
+ item.childItems.forEach(child => {
714
+ const subLi = document.createElement('li');
715
+ subLi.className = 'header-menu-nav__mobile-drawer-subitem';
716
+ const subLink = document.createElement('a');
717
+ subLink.href = normalizeMenuLink(child.link);
718
+ subLink.className = 'header-menu-nav__mobile-drawer-sublink';
719
+ subLink.textContent = child.name || child.title || '';
720
+ subLi.appendChild(subLink);
721
+ submenu.appendChild(subLi);
722
+ });
723
+ li.appendChild(submenu);
724
+ }
725
+
726
+ container.appendChild(li);
727
+ });
728
+ }
729
+
730
+ function setupSubmenuToggles() {
731
+ const drawer = document.getElementById('header-menu-mobile-drawer');
732
+ const drawerContainer = drawer ? drawer.querySelector('[data-mobile-menu-list]') : null;
733
+
734
+ // Get items from drawer
735
+ const items = drawerContainer ? drawerContainer.querySelectorAll('.header-menu-nav__mobile-drawer-item.has-children') : [];
736
+
737
+ console.log('[HeaderMenu] Setting up submenu toggles, found', items.length, 'items with children');
738
+
739
+ items.forEach(item => {
740
+ const link = item.querySelector('.header-menu-nav__mobile-drawer-link');
741
+ if (link) {
742
+ // Remove existing listener if any
743
+ const newLink = link.cloneNode(true);
744
+ link.parentNode.replaceChild(newLink, link);
745
+
746
+ newLink.addEventListener('click', function(e) {
747
+ const submenu = item.querySelector('.header-menu-nav__mobile-drawer-sublist');
748
+ if (submenu) {
749
+ e.preventDefault();
750
+ e.stopPropagation();
751
+ const isOpen = item.classList.contains('open');
752
+ item.classList.toggle('open');
753
+ console.log('[HeaderMenu] Submenu toggled:', item.classList.contains('open') ? 'opened' : 'closed');
754
+ }
755
+ });
756
+ }
757
+ });
758
+ }
759
+
760
+ /**
761
+ * Normalize menu links that were rendered server-side via Liquid
762
+ * This ensures links are absolute URLs even when rendered from templates
763
+ */
764
+ function normalizeServerRenderedLinks() {
765
+ const normalizeLink = (link) => {
766
+ if (!link || link === '#' || link.trim() === '') {
767
+ return '#';
768
+ }
769
+
770
+ const trimmedLink = link.trim();
771
+
772
+ // If already an absolute URL (http:// or https://), return as-is
773
+ if (trimmedLink.startsWith('http://') || trimmedLink.startsWith('https://')) {
774
+ return trimmedLink;
775
+ }
776
+
777
+ // If already starts with /, it's absolute from root, return as-is
778
+ if (trimmedLink.startsWith('/')) {
779
+ return trimmedLink;
780
+ }
781
+
782
+ // Otherwise, prepend base URL to make it absolute
783
+ const baseUrl = window.location.origin;
784
+ const cleanLink = trimmedLink.startsWith('/') ? trimmedLink : '/' + trimmedLink;
785
+ return baseUrl + cleanLink;
786
+ };
787
+
788
+ // Find all menu links in the server-rendered menu
789
+ const menuLinks = document.querySelectorAll('[data-header-menu-nav] .header-menu-nav__link, [data-header-menu-nav] .header-menu-nav__submenu-link, [data-header-menu-nav] .header-menu-nav__mobile-drawer-link, [data-header-menu-nav] .header-menu-nav__mobile-drawer-sublink');
790
+
791
+ menuLinks.forEach(link => {
792
+ const currentHref = link.getAttribute('href');
793
+ if (currentHref && currentHref !== '#') {
794
+ const normalizedHref = normalizeLink(currentHref);
795
+ if (normalizedHref !== currentHref) {
796
+ link.setAttribute('href', normalizedHref);
797
+ }
798
+ }
799
+ });
800
+ }
801
+
802
+ if (document.readyState === 'loading') {
803
+ document.addEventListener('DOMContentLoaded', function() {
804
+ init();
805
+ normalizeServerRenderedLinks();
806
+ });
807
+ } else {
808
+ init();
809
+ normalizeServerRenderedLinks();
810
+ }
811
+ })();
812
+ </script>