@akinon/projectzero 2.0.0-beta.20 → 2.0.0-beta.22

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 (140) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/app-template/CHANGELOG.md +170 -0
  3. package/app-template/next.config.mjs +0 -1
  4. package/app-template/package.json +31 -30
  5. package/app-template/src/app/[pz]/[...prettyurl]/page.tsx +2 -2
  6. package/app-template/src/app/[pz]/account/layout.tsx +2 -1
  7. package/app-template/src/app/{[commerce]/[locale]/[currency] → [pz]}/blog/[slug]/page.tsx +4 -2
  8. package/app-template/src/app/[pz]/category/[pk]/page.tsx +11 -1
  9. package/app-template/src/app/[pz]/group-product/[pk]/page.tsx +2 -2
  10. package/app-template/src/app/[pz]/layout.tsx +3 -1
  11. package/app-template/src/app/[pz]/list/page.tsx +11 -1
  12. package/app-template/src/app/[pz]/page.tsx +13 -35
  13. package/app-template/src/app/[pz]/pages/[slug]/page.tsx +19 -0
  14. package/app-template/src/app/[pz]/product/[pk]/page.tsx +2 -2
  15. package/app-template/src/app/api/barcode-search/route.ts +1 -1
  16. package/app-template/src/app/api/cache/route.ts +1 -1
  17. package/app-template/src/app/api/image-proxy/route.ts +1 -1
  18. package/app-template/src/app/api/logout/route.ts +1 -1
  19. package/app-template/src/app/api/product-categories/route.ts +1 -1
  20. package/app-template/src/app/api/similar-product-list/route.ts +1 -1
  21. package/app-template/src/app/api/similar-products/route.ts +1 -1
  22. package/app-template/src/app/api/virtual-try-on/route.ts +1 -1
  23. package/app-template/src/app/api/web-vitals/route.ts +1 -1
  24. package/app-template/src/components/quantity-selector.tsx +16 -4
  25. package/app-template/src/components/widget-content.tsx +3 -3
  26. package/app-template/src/routes/index.ts +6 -6
  27. package/app-template/src/utils/__tests__/theme-page-context.test.ts +145 -0
  28. package/app-template/src/utils/theme-page-context.ts +309 -0
  29. package/app-template/src/views/basket/basket-item.tsx +107 -691
  30. package/app-template/src/views/basket/index.ts +0 -2
  31. package/app-template/src/views/basket/summary.tsx +75 -496
  32. package/app-template/src/views/breadcrumb.tsx +38 -13
  33. package/app-template/src/views/category/category-header.tsx +66 -289
  34. package/app-template/src/views/category/category-info.tsx +24 -173
  35. package/app-template/src/views/category/filters/index.tsx +48 -208
  36. package/app-template/src/views/category/layout.tsx +5 -7
  37. package/app-template/src/views/checkout/index.tsx +0 -5
  38. package/app-template/src/views/checkout/steps/payment/index.tsx +2 -5
  39. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +1 -72
  40. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +40 -171
  41. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +12 -74
  42. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +45 -128
  43. package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +27 -232
  44. package/app-template/src/views/checkout/summary.tsx +29 -303
  45. package/app-template/src/views/footer.tsx +13 -415
  46. package/app-template/src/views/guest-login/index.tsx +1 -1
  47. package/app-template/src/views/header/action-menu.tsx +45 -277
  48. package/app-template/src/views/header/band.tsx +21 -6
  49. package/app-template/src/views/header/index.tsx +47 -109
  50. package/app-template/src/views/header/mini-basket.tsx +45 -820
  51. package/app-template/src/views/header/navbar.tsx +111 -178
  52. package/app-template/src/views/header/search/index.tsx +32 -71
  53. package/app-template/src/views/header/search/results.tsx +65 -127
  54. package/app-template/src/views/product/accordion-wrapper.tsx +43 -135
  55. package/app-template/src/views/product/index.ts +1 -1
  56. package/app-template/src/views/product/layout.tsx +7 -2
  57. package/app-template/src/views/product/misc-buttons.tsx +25 -339
  58. package/app-template/src/views/product/product-actions.tsx +8 -137
  59. package/app-template/src/views/product/product-info.tsx +31 -69
  60. package/app-template/src/views/product/product-share.tsx +8 -11
  61. package/app-template/src/views/product/slider.tsx +79 -117
  62. package/app-template/src/views/product-item/index.tsx +46 -119
  63. package/app-template/src/widgets/footer-social.tsx +16 -47
  64. package/app-template/src/widgets/footer-subscription/index.tsx +17 -183
  65. package/codemods/migrate-auth-v5/index.js +339 -0
  66. package/codemods/migrate-auth-v5/transform.js +86 -0
  67. package/dist/commands/plugins.js +23 -2
  68. package/package.json +1 -1
  69. package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +0 -15
  70. package/app-template/src/views/basket/basket-summary-context.tsx +0 -560
  71. package/app-template/src/views/basket/designer-context.tsx +0 -617
  72. package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +0 -190
  73. package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +0 -286
  74. package/app-template/src/views/breadcrumb/constants.ts +0 -15
  75. package/app-template/src/views/breadcrumb/index.tsx +0 -127
  76. package/app-template/src/views/category/native-widget-context.tsx +0 -257
  77. package/app-template/src/views/category/product-list-registrar.tsx +0 -665
  78. package/app-template/src/views/checkout/checkout-address-registrar.tsx +0 -254
  79. package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +0 -183
  80. package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +0 -259
  81. package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +0 -253
  82. package/app-template/src/views/checkout/checkout-summary-registrar.tsx +0 -183
  83. package/app-template/src/views/checkout/constants.ts +0 -5
  84. package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +0 -15
  85. package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +0 -18
  86. package/app-template/src/views/footer/footer-app-banner-context.tsx +0 -326
  87. package/app-template/src/views/footer/footer-bottom-context.tsx +0 -215
  88. package/app-template/src/views/footer/footer-bottom-wrapper.tsx +0 -74
  89. package/app-template/src/views/footer/footer-layout-constants.ts +0 -35
  90. package/app-template/src/views/footer/footer-layout-registrar.tsx +0 -342
  91. package/app-template/src/views/footer/footer-layout-switcher.tsx +0 -110
  92. package/app-template/src/views/footer/footer-menu-context.tsx +0 -211
  93. package/app-template/src/views/footer/footer-native-widgets.tsx +0 -60
  94. package/app-template/src/views/footer/footer-social-context.tsx +0 -254
  95. package/app-template/src/views/footer/footer-subscription-context.tsx +0 -210
  96. package/app-template/src/views/footer/footer-utils.ts +0 -43
  97. package/app-template/src/views/footer/footer-value-props-context.tsx +0 -326
  98. package/app-template/src/views/footer/logo-settings.ts +0 -183
  99. package/app-template/src/views/footer/native-widget-config.ts +0 -262
  100. package/app-template/src/views/footer/subscription-settings.ts +0 -122
  101. package/app-template/src/views/footer/use-footer-logo.ts +0 -162
  102. package/app-template/src/views/header/designer-context.tsx +0 -261
  103. package/app-template/src/views/header/header-announcement-registrar.tsx +0 -267
  104. package/app-template/src/views/header/header-client-wrapper.tsx +0 -496
  105. package/app-template/src/views/header/header-content.tsx +0 -1026
  106. package/app-template/src/views/header/header-currency-registrar.tsx +0 -348
  107. package/app-template/src/views/header/header-icons-context.tsx +0 -262
  108. package/app-template/src/views/header/header-language-registrar.tsx +0 -348
  109. package/app-template/src/views/header/header-layout-context.tsx +0 -143
  110. package/app-template/src/views/header/header-layout-registrar.tsx +0 -658
  111. package/app-template/src/views/header/header-logo-context.tsx +0 -228
  112. package/app-template/src/views/header/header-logo.tsx +0 -118
  113. package/app-template/src/views/header/header-mini-basket-context.tsx +0 -524
  114. package/app-template/src/views/header/header-search-registrar.tsx +0 -511
  115. package/app-template/src/views/header/header-text-slider-registrar.tsx +0 -382
  116. package/app-template/src/views/header/inline-search.tsx +0 -262
  117. package/app-template/src/views/header/navbar-menu-context.tsx +0 -219
  118. package/app-template/src/views/header/search/search-input.tsx +0 -61
  119. package/app-template/src/views/header/server-settings-parser.ts +0 -1105
  120. package/app-template/src/views/header/use-header-icons.ts +0 -241
  121. package/app-template/src/views/header/use-header-logo.ts +0 -213
  122. package/app-template/src/views/header/use-navbar-menu.ts +0 -179
  123. package/app-template/src/views/product/accordion-section.tsx +0 -61
  124. package/app-template/src/views/product/custom-button-group.tsx +0 -69
  125. package/app-template/src/views/product/favorites-button-section.tsx +0 -69
  126. package/app-template/src/views/product/find-in-store-section.tsx +0 -60
  127. package/app-template/src/views/product/product-info-section.tsx +0 -140
  128. package/app-template/src/views/product/quantity-section.tsx +0 -73
  129. package/app-template/src/views/product/sale-tag.tsx +0 -10
  130. package/app-template/src/views/product/share-section.tsx +0 -357
  131. package/app-template/src/views/product/variants-section.tsx +0 -126
  132. package/app-template/src/views/product-detail/constants.ts +0 -272
  133. package/app-template/src/views/product-detail/index.ts +0 -10
  134. package/app-template/src/views/product-detail/product-detail-registrar.tsx +0 -616
  135. package/app-template/src/widgets/footer-app-banner.tsx +0 -444
  136. package/app-template/src/widgets/footer-bottom.tsx +0 -127
  137. package/app-template/src/widgets/footer-menu-compact.tsx +0 -238
  138. package/app-template/src/widgets/footer-menu-two.tsx +0 -298
  139. package/app-template/src/widgets/footer-social-client.tsx +0 -251
  140. package/app-template/src/widgets/footer-value-props.tsx +0 -201
@@ -1,1026 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * Header Content Component
5
- *
6
- * Client component that renders the header content based on the selected layout.
7
- * Uses the HeaderLayoutContext to determine which layout to display.
8
- */
9
-
10
- import {
11
- ReactNode,
12
- useCallback,
13
- useMemo,
14
- useState,
15
- useEffect,
16
- useRef
17
- } from 'react';
18
- import clsx from 'clsx';
19
- import {
20
- useHeaderLayout,
21
- HeaderLayoutType,
22
- MenuPositionType,
23
- SearchPositionType,
24
- HEADER_LAYOUT_BLOCKS,
25
- HEADER_LAYOUT_PLACEHOLDER_ID,
26
- HEADER_LAYOUT_SECTION_ID
27
- } from './header-layout-registrar';
28
- import HeaderSearchRegistrar, {
29
- useHeaderSearch,
30
- InitialSearchSettings
31
- } from './header-search-registrar';
32
- import HeaderLanguageRegistrar, {
33
- useHeaderLanguage
34
- } from './header-language-registrar';
35
- import HeaderCurrencyRegistrar, {
36
- useHeaderCurrency
37
- } from './header-currency-registrar';
38
- import HeaderTextSliderRegistrar, {
39
- useHeaderTextSlider
40
- } from './header-text-slider-registrar';
41
- import HeaderAnnouncementRegistrar, {
42
- useHeaderAnnouncement
43
- } from './header-announcement-registrar';
44
- import InlineSearch from './inline-search';
45
- import { LanguageSelect } from '@theme/components/language-select';
46
- import { CurrencySelect } from '@theme/components/currency-select';
47
- import { useDesignerFeatures } from '@akinon/next/components/theme-editor/hooks/use-designer-features';
48
- import type {
49
- HeaderLanguageSettings,
50
- HeaderCurrencySettings,
51
- HeaderTextSliderSettings,
52
- HeaderAnnouncementSettings
53
- } from './server-settings-parser';
54
-
55
- interface HeaderContentProps {
56
- logo: ReactNode;
57
- navbar: ReactNode;
58
- icons: ReactNode;
59
- mobileHamburger: ReactNode;
60
- mobileMenu: ReactNode;
61
- className?: string;
62
- menuPosition?: MenuPositionType;
63
- searchPosition?: SearchPositionType;
64
- /** Initial search settings from server-side parsing (to avoid flash) */
65
- initialSearchSettings?: InitialSearchSettings;
66
- /** Initial language settings from server-side parsing (to avoid flash) */
67
- initialLanguageSettings?: HeaderLanguageSettings;
68
- /** Initial currency settings from server-side parsing (to avoid flash) */
69
- initialCurrencySettings?: HeaderCurrencySettings;
70
- /** Initial text slider settings from server-side parsing (to avoid flash) */
71
- initialTextSliderSettings?: HeaderTextSliderSettings;
72
- /** Initial announcement bar settings from server-side parsing (to avoid flash) */
73
- initialAnnouncementSettings?: HeaderAnnouncementSettings;
74
- }
75
-
76
- /**
77
- * Convert block styles to CSS properties
78
- */
79
- function convertBlockStyles(
80
- styles: Record<string, unknown> | undefined
81
- ): React.CSSProperties {
82
- if (!styles) return {};
83
-
84
- const result: Record<string, unknown> = {};
85
-
86
- // Map theme editor keys to CSS property names
87
- const styleMap: Record<string, string> = {
88
- 'background-color': 'backgroundColor',
89
- 'border-color': 'borderColor',
90
- 'border-width': 'borderWidth',
91
- 'border-radius': 'borderRadius',
92
- 'border-style': 'borderStyle',
93
- 'padding-top': 'paddingTop',
94
- 'padding-bottom': 'paddingBottom',
95
- 'padding-left': 'paddingLeft',
96
- 'padding-right': 'paddingRight',
97
- 'margin-top': 'marginTop',
98
- 'margin-bottom': 'marginBottom',
99
- 'margin-left': 'marginLeft',
100
- 'margin-right': 'marginRight',
101
- 'min-width': 'minWidth',
102
- 'max-width': 'maxWidth',
103
- 'font-size': 'fontSize',
104
- 'font-weight': 'fontWeight',
105
- 'font-family': 'fontFamily',
106
- 'line-height': 'lineHeight',
107
- 'text-align': 'textAlign',
108
- // Theme editor background properties
109
- bgColor: 'backgroundColor',
110
- BgOpacity: null // Skip, already applied via background
111
- };
112
-
113
- // Keys to skip (helper properties that shouldn't be applied directly)
114
- const skipKeys = new Set(['bgColor', 'BgOpacity']);
115
-
116
- Object.entries(styles).forEach(([key, value]) => {
117
- // Skip helper keys
118
- if (skipKeys.has(key)) return;
119
-
120
- const cssKey = styleMap[key] || key;
121
- // Skip if explicitly mapped to null
122
- if (cssKey === null) return;
123
-
124
- // Extract desktop value if responsive
125
- if (value && typeof value === 'object' && 'desktop' in value) {
126
- result[cssKey] = (value as Record<string, unknown>).desktop;
127
- } else {
128
- result[cssKey] = value;
129
- }
130
- });
131
-
132
- // Auto-add border-style: solid if border-width is set
133
- if (result.borderWidth && !result.borderStyle) {
134
- result.borderStyle = 'solid';
135
- }
136
-
137
- return result as React.CSSProperties;
138
- }
139
-
140
- /**
141
- * Selectable Row Container
142
- * Wraps row content with theme editor selection support
143
- */
144
- interface SelectableRowProps {
145
- blockId: string;
146
- blockLabel: string;
147
- children: ReactNode;
148
- className?: string;
149
- style?: React.CSSProperties;
150
- }
151
-
152
- function SelectableRow({
153
- blockId,
154
- blockLabel,
155
- children,
156
- className,
157
- style
158
- }: SelectableRowProps) {
159
- const { isDesigner, selectedBlockId, getBlockStyles } = useHeaderLayout();
160
-
161
- const { handleClick } = useDesignerFeatures({
162
- blockId,
163
- placeholderId: HEADER_LAYOUT_PLACEHOLDER_ID,
164
- sectionId: HEADER_LAYOUT_SECTION_ID,
165
- isDesigner,
166
- blockInfo: {
167
- id: blockId,
168
- type: 'container',
169
- label: blockLabel
170
- }
171
- });
172
-
173
- const isSelected = selectedBlockId === blockId;
174
- const blockStyles = getBlockStyles(blockId);
175
- const computedStyles = useMemo(
176
- () => convertBlockStyles(blockStyles),
177
- [blockStyles]
178
- );
179
-
180
- const handleContainerClick = useCallback(
181
- (e: React.MouseEvent) => {
182
- if (isDesigner) {
183
- e.preventDefault();
184
- e.stopPropagation();
185
- handleClick(e);
186
- }
187
- },
188
- [isDesigner, handleClick]
189
- );
190
-
191
- return (
192
- <div
193
- data-block-id={blockId}
194
- onClick={handleContainerClick}
195
- className={clsx(
196
- className,
197
- isDesigner && 'cursor-pointer',
198
- isSelected && 'ring-2 ring-blue-500 ring-inset'
199
- )}
200
- style={{ ...style, ...computedStyles }}
201
- >
202
- {children}
203
- </div>
204
- );
205
- }
206
-
207
- /**
208
- * Search Input Wrapper
209
- * Wraps the InlineSearch component with theme editor support
210
- */
211
- function SearchInputWrapper() {
212
- const {
213
- isSectionVisible,
214
- properties,
215
- sectionStyles,
216
- getBlockStyles,
217
- getBlockProperties,
218
- isDesigner,
219
- selectedBlockId
220
- } = useHeaderSearch();
221
-
222
- // Merge width (and potential responsive widths) from properties into styles
223
- // Must be called before early return to satisfy React hooks rules
224
- const mergedSectionStyles = useMemo(() => {
225
- const result = { ...(sectionStyles as Record<string, unknown>) };
226
-
227
- const resolveValue = (value: unknown) => {
228
- if (value === null || value === undefined) return undefined;
229
- if (typeof value === 'string' || typeof value === 'number') return value;
230
- if (typeof value === 'object') {
231
- const obj = value as Record<string, string | number>;
232
- return obj.desktop ?? obj.mobile ?? Object.values(obj)[0];
233
- }
234
- return undefined;
235
- };
236
-
237
- const width = resolveValue(properties.width);
238
- if (width !== undefined) {
239
- result.width = width;
240
- }
241
-
242
- return result as React.CSSProperties;
243
- }, [properties.width, sectionStyles]);
244
-
245
- if (!isSectionVisible) {
246
- return null;
247
- }
248
-
249
- const placeholder =
250
- typeof properties.placeholder === 'object'
251
- ? (properties.placeholder as Record<string, string>).desktop ||
252
- 'Search products...'
253
- : properties.placeholder || 'Search products...';
254
-
255
- // Merge block styles and properties for icon
256
- const iconBlockStyles = getBlockStyles('header-search-icon') || {};
257
- const iconBlockProps = getBlockProperties('header-search-icon') || {};
258
- const iconStyles = { ...iconBlockStyles, ...iconBlockProps };
259
-
260
- // Check if custom width is set to avoid w-full overriding it
261
- const hasCustomWidth = mergedSectionStyles.width !== undefined;
262
-
263
- return (
264
- <div data-section-id="header-search">
265
- <InlineSearch
266
- placeholder={placeholder}
267
- className={hasCustomWidth ? undefined : 'w-full'}
268
- style={mergedSectionStyles}
269
- iconStyles={iconStyles}
270
- blockId="header-search-icon"
271
- isDesigner={isDesigner}
272
- isSelected={selectedBlockId === 'header-search-icon'}
273
- />
274
- </div>
275
- );
276
- }
277
-
278
- /**
279
- * Language Select Wrapper
280
- * Wraps the LanguageSelect component with theme editor support
281
- */
282
- function LanguageSelectWrapper() {
283
- const {
284
- isSectionVisible,
285
- properties,
286
- sectionStyles,
287
- isDesigner,
288
- isLanguageSectionSelected
289
- } = useHeaderLanguage();
290
-
291
- // Convert styles for CSS using shared function
292
- const computedStyles = useMemo(
293
- () => convertBlockStyles(sectionStyles),
294
- [sectionStyles]
295
- );
296
-
297
- // Handle click to select section in designer mode
298
- const handleClick = useCallback(
299
- (e: React.MouseEvent) => {
300
- if (isDesigner && window.parent) {
301
- e.preventDefault();
302
- e.stopPropagation();
303
- window.parent.postMessage(
304
- {
305
- type: 'SELECT_SECTION',
306
- data: {
307
- placeholderId: 'header',
308
- sectionId: 'header-language'
309
- }
310
- },
311
- '*'
312
- );
313
- }
314
- },
315
- [isDesigner]
316
- );
317
-
318
- if (!isSectionVisible) {
319
- return null;
320
- }
321
-
322
- const showIcon =
323
- typeof properties.showIcon === 'object'
324
- ? (properties.showIcon as Record<string, boolean>).desktop ?? true
325
- : properties.showIcon ?? true;
326
-
327
- const labelFormat =
328
- typeof properties.labelFormat === 'object'
329
- ? (properties.labelFormat as Record<string, string>).desktop ?? 'full'
330
- : properties.labelFormat ?? 'full';
331
-
332
- // Custom icon from properties - handle responsive format
333
- const customIcon =
334
- typeof properties.icon === 'object'
335
- ? (properties.icon as Record<string, string>).desktop ?? ''
336
- : (properties.icon as string) ?? '';
337
-
338
- return (
339
- <div
340
- data-section-id="header-language"
341
- style={computedStyles}
342
- onClick={handleClick}
343
- className={clsx(
344
- isDesigner && 'cursor-pointer',
345
- isLanguageSectionSelected && 'ring-2 ring-blue-500 ring-inset'
346
- )}
347
- >
348
- <div style={{ pointerEvents: isDesigner ? 'none' : 'auto' }}>
349
- <LanguageSelect
350
- showIcon={showIcon}
351
- labelFormat={labelFormat as 'full' | 'short' | 'code'}
352
- customIcon={customIcon}
353
- />
354
- </div>
355
- </div>
356
- );
357
- }
358
-
359
- /**
360
- * Currency Select Wrapper
361
- * Wraps the CurrencySelect component with theme editor support
362
- */
363
- function CurrencySelectWrapper() {
364
- const {
365
- isSectionVisible,
366
- properties,
367
- sectionStyles,
368
- isDesigner,
369
- isCurrencySectionSelected
370
- } = useHeaderCurrency();
371
-
372
- // Convert styles for CSS using shared function
373
- const computedStyles = useMemo(
374
- () => convertBlockStyles(sectionStyles),
375
- [sectionStyles]
376
- );
377
-
378
- // Handle click to select section in designer mode
379
- const handleClick = useCallback(
380
- (e: React.MouseEvent) => {
381
- if (isDesigner && window.parent) {
382
- e.preventDefault();
383
- e.stopPropagation();
384
- window.parent.postMessage(
385
- {
386
- type: 'SELECT_SECTION',
387
- data: {
388
- placeholderId: 'header',
389
- sectionId: 'header-currency'
390
- }
391
- },
392
- '*'
393
- );
394
- }
395
- },
396
- [isDesigner]
397
- );
398
-
399
- if (!isSectionVisible) {
400
- return null;
401
- }
402
-
403
- const showIcon =
404
- typeof properties.showIcon === 'object'
405
- ? (properties.showIcon as Record<string, boolean>).desktop ?? true
406
- : properties.showIcon ?? true;
407
-
408
- const labelFormat =
409
- typeof properties.labelFormat === 'object'
410
- ? (properties.labelFormat as Record<string, string>).desktop ?? 'full'
411
- : properties.labelFormat ?? 'full';
412
-
413
- // Custom icon from properties - handle responsive format
414
- const customIcon =
415
- typeof properties.icon === 'object'
416
- ? (properties.icon as Record<string, string>).desktop ?? ''
417
- : (properties.icon as string) ?? '';
418
-
419
- return (
420
- <div
421
- data-section-id="header-currency"
422
- style={computedStyles}
423
- onClick={handleClick}
424
- className={clsx(
425
- isDesigner && 'cursor-pointer',
426
- isCurrencySectionSelected && 'ring-2 ring-blue-500 ring-inset'
427
- )}
428
- >
429
- <div style={{ pointerEvents: isDesigner ? 'none' : 'auto' }}>
430
- <CurrencySelect
431
- showIcon={showIcon}
432
- labelFormat={labelFormat as 'full' | 'symbol' | 'code'}
433
- customIcon={customIcon}
434
- />
435
- </div>
436
- </div>
437
- );
438
- }
439
-
440
- /**
441
- * Text Slider Wrapper
442
- * Wraps the Text Slider component with theme editor support
443
- * Positioned absolutely centered in the utility bar
444
- */
445
- const DEFAULT_ITEMS: Array<{ text: string; link?: string }> = [
446
- { text: 'Welcome to our store!' }
447
- ];
448
-
449
- function TextSliderWrapper() {
450
- const {
451
- isSectionVisible,
452
- properties,
453
- sectionStyles,
454
- isDesigner,
455
- isTextSliderSectionSelected
456
- } = useHeaderTextSlider();
457
-
458
- const [currentIndex, setCurrentIndex] = useState(0);
459
- const [prevIndex, setPrevIndex] = useState<number | null>(null);
460
- const [slideDirection, setSlideDirection] = useState<'left' | 'right'>(
461
- 'left'
462
- );
463
- const [isAnimating, setIsAnimating] = useState(false);
464
-
465
- // Convert styles for CSS using shared function
466
- const computedStyles = useMemo(
467
- () => convertBlockStyles(sectionStyles),
468
- [sectionStyles]
469
- );
470
-
471
- // Handle click to select section in designer mode
472
- const handleClick = useCallback(
473
- (e: React.MouseEvent) => {
474
- if (isDesigner && window.parent) {
475
- e.preventDefault();
476
- e.stopPropagation();
477
- window.parent.postMessage(
478
- {
479
- type: 'SELECT_SECTION',
480
- data: {
481
- placeholderId: 'header',
482
- sectionId: 'header-text-slider'
483
- }
484
- },
485
- '*'
486
- );
487
- }
488
- },
489
- [isDesigner]
490
- );
491
-
492
- // Stable reference to items using JSON comparison
493
- const itemsJsonRef = useRef<string>('');
494
- const itemsRef = useRef(DEFAULT_ITEMS);
495
-
496
- // Only update items ref when the actual data changes
497
- const propItemsJson = JSON.stringify(properties.items);
498
- if (propItemsJson !== itemsJsonRef.current) {
499
- itemsJsonRef.current = propItemsJson;
500
- const propItems = properties.items;
501
- if (Array.isArray(propItems) && propItems.length > 0) {
502
- itemsRef.current = propItems;
503
- } else {
504
- itemsRef.current = DEFAULT_ITEMS;
505
- }
506
- }
507
-
508
- const items = itemsRef.current;
509
- const autoPlay = properties.autoPlay !== false;
510
- const autoPlayInterval = properties.autoPlayInterval || 3000;
511
- const showArrows = properties.showArrows !== false;
512
-
513
- // Refs for values needed in effects/intervals
514
- const autoPlayRef = useRef(autoPlay);
515
- const autoPlayIntervalRef = useRef(autoPlayInterval);
516
- const itemsLengthRef = useRef(items.length);
517
- const isAnimatingRef = useRef(isAnimating);
518
- const currentIndexRef = useRef(currentIndex);
519
-
520
- // Update refs on each render
521
- autoPlayRef.current = autoPlay;
522
- autoPlayIntervalRef.current = autoPlayInterval;
523
- itemsLengthRef.current = items.length;
524
- isAnimatingRef.current = isAnimating;
525
- currentIndexRef.current = currentIndex;
526
-
527
- // Reset current index if out of bounds
528
- useEffect(() => {
529
- if (currentIndex >= itemsLengthRef.current) {
530
- setCurrentIndex(0);
531
- }
532
- }, [currentIndex]);
533
-
534
- // Handle slide transition
535
- const performSlide = useCallback(
536
- (direction: 'left' | 'right', newIndex: number) => {
537
- setIsAnimating(true);
538
- setSlideDirection(direction);
539
- setPrevIndex(currentIndexRef.current);
540
- setCurrentIndex(newIndex);
541
- setTimeout(() => {
542
- setPrevIndex(null);
543
- setIsAnimating(false);
544
- }, 400);
545
- },
546
- []
547
- );
548
-
549
- // Auto-play functionality
550
- useEffect(() => {
551
- if (!autoPlayRef.current || itemsLengthRef.current <= 1) return;
552
-
553
- const interval = setInterval(() => {
554
- if (
555
- !autoPlayRef.current ||
556
- itemsLengthRef.current <= 1 ||
557
- isAnimatingRef.current
558
- )
559
- return;
560
-
561
- const newIndex = (currentIndexRef.current + 1) % itemsLengthRef.current;
562
- performSlide('left', newIndex);
563
- }, autoPlayIntervalRef.current);
564
-
565
- return () => clearInterval(interval);
566
- }, [performSlide]);
567
-
568
- // Navigation handlers
569
- const goToPrev = useCallback(
570
- (e: React.MouseEvent) => {
571
- e.stopPropagation();
572
- if (isAnimating) return;
573
- const newIndex = (currentIndex - 1 + items.length) % items.length;
574
- performSlide('right', newIndex);
575
- },
576
- [currentIndex, items.length, isAnimating, performSlide]
577
- );
578
-
579
- const goToNext = useCallback(
580
- (e: React.MouseEvent) => {
581
- e.stopPropagation();
582
- if (isAnimating) return;
583
- const newIndex = (currentIndex + 1) % items.length;
584
- performSlide('left', newIndex);
585
- },
586
- [currentIndex, items.length, isAnimating, performSlide]
587
- );
588
-
589
- if (!isSectionVisible) {
590
- return null;
591
- }
592
-
593
- const currentItem = items[currentIndex];
594
-
595
- return (
596
- <div
597
- data-section-id="header-text-slider"
598
- onClick={handleClick}
599
- className={clsx(
600
- 'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2',
601
- 'flex items-center gap-2',
602
- isDesigner && 'cursor-pointer',
603
- isTextSliderSectionSelected && 'ring-2 ring-blue-500 ring-inset'
604
- )}
605
- style={{
606
- ...computedStyles,
607
- pointerEvents: isDesigner ? 'auto' : undefined
608
- }}
609
- >
610
- {/* Previous Arrow */}
611
- {showArrows && items.length > 1 && (
612
- <button
613
- onClick={goToPrev}
614
- className="flex items-center justify-center w-6 h-6 text-current hover:opacity-70 transition-opacity"
615
- style={{ pointerEvents: isDesigner ? 'none' : 'auto' }}
616
- >
617
- <svg
618
- width="16"
619
- height="16"
620
- viewBox="0 0 24 24"
621
- fill="none"
622
- stroke="currentColor"
623
- strokeWidth="2"
624
- >
625
- <polyline points="15,18 9,12 15,6" />
626
- </svg>
627
- </button>
628
- )}
629
-
630
- {/* Text Content with Slide Animation */}
631
- {/* Container uses grid to size based on longest item */}
632
- <div className="relative overflow-hidden">
633
- {/* Hidden items to establish container width - all invisible */}
634
- <div className="grid invisible" aria-hidden="true">
635
- {items.map((item, index) => (
636
- <span
637
- key={index}
638
- className="col-start-1 row-start-1 whitespace-nowrap"
639
- >
640
- {item.text}
641
- </span>
642
- ))}
643
- </div>
644
- {/* Exiting item (slides out) */}
645
- {prevIndex !== null && (
646
- <span
647
- key={`prev-${prevIndex}`}
648
- className={clsx(
649
- 'absolute inset-0 whitespace-nowrap text-center',
650
- slideDirection === 'left'
651
- ? 'animate-slide-out'
652
- : 'animate-slide-out [animation-direction:reverse]'
653
- )}
654
- >
655
- {items[prevIndex]?.link ? (
656
- <a href={items[prevIndex].link} className="hover:underline">
657
- {items[prevIndex].text}
658
- </a>
659
- ) : (
660
- items[prevIndex]?.text
661
- )}
662
- </span>
663
- )}
664
- {/* Entering item (slides in) */}
665
- <span
666
- key={`current-${currentIndex}-${
667
- prevIndex !== null ? 'animating' : 'static'
668
- }`}
669
- className={clsx(
670
- 'absolute inset-0 whitespace-nowrap text-center',
671
- prevIndex !== null &&
672
- (slideDirection === 'left'
673
- ? 'animate-slide-in'
674
- : 'animate-slide-in [animation-direction:reverse]')
675
- )}
676
- >
677
- {currentItem?.link ? (
678
- <a
679
- href={currentItem.link}
680
- className="hover:underline"
681
- style={{ pointerEvents: isDesigner ? 'none' : 'auto' }}
682
- >
683
- {currentItem.text}
684
- </a>
685
- ) : (
686
- currentItem?.text
687
- )}
688
- </span>
689
- </div>
690
-
691
- {/* Next Arrow */}
692
- {showArrows && items.length > 1 && (
693
- <button
694
- onClick={goToNext}
695
- className="flex items-center justify-center w-6 h-6 text-current hover:opacity-70 transition-opacity"
696
- style={{ pointerEvents: isDesigner ? 'none' : 'auto' }}
697
- >
698
- <svg
699
- width="16"
700
- height="16"
701
- viewBox="0 0 24 24"
702
- fill="none"
703
- stroke="currentColor"
704
- strokeWidth="2"
705
- >
706
- <polyline points="9,6 15,12 9,18" />
707
- </svg>
708
- </button>
709
- )}
710
- </div>
711
- );
712
- }
713
-
714
- function AnnouncementBarWrapper() {
715
- const {
716
- isSectionVisible,
717
- properties,
718
- sectionStyles,
719
- isDesigner,
720
- isAnnouncementSectionSelected
721
- } = useHeaderAnnouncement();
722
-
723
- const computedStyles = useMemo(
724
- () => convertBlockStyles(sectionStyles),
725
- [sectionStyles]
726
- );
727
-
728
- const resolvePropValue = (value: unknown, fallback = ''): string => {
729
- if (typeof value === 'string') return value;
730
- if (typeof value === 'object' && value !== null) {
731
- const responsiveValue = value as Record<string, string>;
732
- return (
733
- responsiveValue.desktop ||
734
- responsiveValue.mobile ||
735
- Object.values(responsiveValue)[0] ||
736
- fallback
737
- );
738
- }
739
- return fallback;
740
- };
741
-
742
- const text = resolvePropValue(properties.text, '');
743
- const link = resolvePropValue(properties.link, '');
744
- const target = resolvePropValue(properties.target, '_self');
745
-
746
- const handleClick = useCallback(
747
- (e: React.MouseEvent) => {
748
- if (isDesigner && window.parent) {
749
- e.preventDefault();
750
- e.stopPropagation();
751
- window.parent.postMessage(
752
- {
753
- type: 'SELECT_SECTION',
754
- data: {
755
- placeholderId: 'header',
756
- sectionId: 'header-announcement-bar'
757
- }
758
- },
759
- '*'
760
- );
761
- }
762
- },
763
- [isDesigner]
764
- );
765
-
766
- if (!isSectionVisible || !text) {
767
- return null;
768
- }
769
-
770
- return (
771
- <div
772
- data-section-id="header-announcement-bar"
773
- onClick={handleClick}
774
- className={clsx(
775
- 'w-full',
776
- isDesigner && 'cursor-pointer',
777
- isAnnouncementSectionSelected && 'ring-2 ring-blue-500 ring-inset'
778
- )}
779
- style={computedStyles}
780
- >
781
- <div className="container px-2.5 lg:px-5 xl:px-2.5 text-center">
782
- {link ? (
783
- <a
784
- href={link}
785
- target={target}
786
- rel={target === '_blank' ? 'noopener noreferrer' : undefined}
787
- className="hover:underline"
788
- style={{ pointerEvents: isDesigner ? 'none' : 'auto' }}
789
- >
790
- {text}
791
- </a>
792
- ) : (
793
- <span>{text}</span>
794
- )}
795
- </div>
796
- </div>
797
- );
798
- }
799
-
800
- /**
801
- * Utility Row Component
802
- * Shows Language and Currency selects when added via Theme Editor
803
- * Only renders when at least one of them is visible
804
- */
805
- function UtilityRow() {
806
- const { isSectionVisible: isLanguageVisible } = useHeaderLanguage();
807
- const { isSectionVisible: isCurrencyVisible } = useHeaderCurrency();
808
- const { isSectionVisible: isTextSliderVisible } = useHeaderTextSlider();
809
- const { getBlockStyles, utilityPosition } = useHeaderLayout();
810
-
811
- // Get block styles for utility row
812
- const blockStyles = getBlockStyles(HEADER_LAYOUT_BLOCKS.UTILITY_ROW.id);
813
- const computedStyles = useMemo(
814
- () => convertBlockStyles(blockStyles),
815
- [blockStyles]
816
- );
817
-
818
- // Don't render if nothing is visible
819
- if (!isLanguageVisible && !isCurrencyVisible && !isTextSliderVisible) {
820
- return null;
821
- }
822
-
823
- // Determine justify class based on utility position
824
- const justifyClass =
825
- utilityPosition === 'left' ? 'justify-start' : 'justify-end';
826
-
827
- // Check if custom padding is set to avoid Tailwind class overriding it
828
- const hasCustomPaddingY =
829
- computedStyles.paddingTop !== undefined ||
830
- computedStyles.paddingBottom !== undefined;
831
-
832
- return (
833
- <SelectableRow
834
- blockId={HEADER_LAYOUT_BLOCKS.UTILITY_ROW.id}
835
- blockLabel={HEADER_LAYOUT_BLOCKS.UTILITY_ROW.label}
836
- className="w-full bg-gray-100"
837
- style={computedStyles}
838
- >
839
- <div
840
- className={clsx(
841
- 'container relative flex items-center gap-4 px-2.5 lg:px-5 xl:px-2.5',
842
- justifyClass
843
- )}
844
- style={
845
- hasCustomPaddingY
846
- ? {
847
- paddingTop: computedStyles.paddingTop,
848
- paddingBottom: computedStyles.paddingBottom
849
- }
850
- : undefined
851
- }
852
- >
853
- {/* Text Slider - Absolute positioned in center */}
854
- <TextSliderWrapper />
855
-
856
- {/* Language and Currency - positioned based on utilityPosition */}
857
- <LanguageSelectWrapper />
858
- <CurrencySelectWrapper />
859
- </div>
860
- </SelectableRow>
861
- );
862
- }
863
-
864
- /**
865
- * Default Layout
866
- * Single row: [Logo + Navbar] | [Icons]
867
- */
868
- function DefaultLayout({
869
- logo,
870
- navbar,
871
- icons,
872
- mobileHamburger,
873
- mobileMenu,
874
- initialAnnouncementSettings,
875
- initialLanguageSettings,
876
- initialCurrencySettings,
877
- initialTextSliderSettings
878
- }: HeaderContentProps) {
879
- return (
880
- <>
881
- <HeaderAnnouncementRegistrar initialSettings={initialAnnouncementSettings}>
882
- <AnnouncementBarWrapper />
883
- </HeaderAnnouncementRegistrar>
884
-
885
- {/* Utility Row: Language & Currency Selects & Text Slider */}
886
- <HeaderTextSliderRegistrar initialSettings={initialTextSliderSettings}>
887
- <HeaderLanguageRegistrar initialSettings={initialLanguageSettings}>
888
- <HeaderCurrencyRegistrar initialSettings={initialCurrencySettings}>
889
- <UtilityRow />
890
- </HeaderCurrencyRegistrar>
891
- </HeaderLanguageRegistrar>
892
- </HeaderTextSliderRegistrar>
893
-
894
- <SelectableRow
895
- blockId={HEADER_LAYOUT_BLOCKS.MAIN_ROW.id}
896
- blockLabel={HEADER_LAYOUT_BLOCKS.MAIN_ROW.label}
897
- className="w-full"
898
- >
899
- <div className="container flex items-center justify-between px-2.5 lg:px-5 xl:px-2.5 py-5 sm:py-0">
900
- {mobileHamburger}
901
- <div className="flex items-center sm:gap-16">
902
- {logo}
903
- {navbar}
904
- </div>
905
- {icons}
906
- {mobileMenu}
907
- </div>
908
- </SelectableRow>
909
- </>
910
- );
911
- }
912
-
913
- /**
914
- * Two Row Layout
915
- * Row 1: Logo | Search Input | Icons
916
- * Row 2: Navigation Menu (full width, position configurable)
917
- */
918
- function TwoRowLayout({
919
- logo,
920
- navbar,
921
- icons,
922
- mobileHamburger,
923
- mobileMenu,
924
- menuPosition = 'center',
925
- searchPosition = 'center',
926
- initialSearchSettings,
927
- initialAnnouncementSettings,
928
- initialLanguageSettings,
929
- initialCurrencySettings,
930
- initialTextSliderSettings
931
- }: HeaderContentProps) {
932
- // Map menu position to justify class
933
- const menuPositionClass = {
934
- left: 'justify-start',
935
- center: 'justify-center',
936
- right: 'justify-end'
937
- }[menuPosition];
938
-
939
- const searchPositionClass = {
940
- left: 'justify-start',
941
- center: 'justify-center',
942
- right: 'justify-end'
943
- }[searchPosition];
944
-
945
- return (
946
- <>
947
- <HeaderAnnouncementRegistrar initialSettings={initialAnnouncementSettings}>
948
- <AnnouncementBarWrapper />
949
- </HeaderAnnouncementRegistrar>
950
-
951
- {/* Utility Row: Language & Currency Selects & Text Slider */}
952
- <HeaderTextSliderRegistrar initialSettings={initialTextSliderSettings}>
953
- <HeaderLanguageRegistrar initialSettings={initialLanguageSettings}>
954
- <HeaderCurrencyRegistrar initialSettings={initialCurrencySettings}>
955
- <UtilityRow />
956
- </HeaderCurrencyRegistrar>
957
- </HeaderLanguageRegistrar>
958
- </HeaderTextSliderRegistrar>
959
-
960
- {/* Row 1: Logo + Search + Icons */}
961
- <SelectableRow
962
- blockId={HEADER_LAYOUT_BLOCKS.TOP_ROW.id}
963
- blockLabel={HEADER_LAYOUT_BLOCKS.TOP_ROW.label}
964
- className="w-full"
965
- >
966
- <div className="container flex items-center justify-between px-2.5 sm:px-0">
967
- {mobileHamburger}
968
- <div className="flex items-center">{logo}</div>
969
- <div className={`hidden sm:flex flex-1 ${searchPositionClass} mx-8`}>
970
- <HeaderSearchRegistrar
971
- autoRegister
972
- initialSettings={initialSearchSettings}
973
- >
974
- <SearchInputWrapper />
975
- </HeaderSearchRegistrar>
976
- </div>
977
- {icons}
978
- </div>
979
- </SelectableRow>
980
-
981
- {/* Row 2: Navigation Menu */}
982
- <SelectableRow
983
- blockId={HEADER_LAYOUT_BLOCKS.BOTTOM_ROW.id}
984
- blockLabel={HEADER_LAYOUT_BLOCKS.BOTTOM_ROW.label}
985
- className="hidden sm:block w-full border-t border-gray-200"
986
- >
987
- <div className={`container flex ${menuPositionClass} px-2.5 sm:px-0`}>
988
- {navbar}
989
- </div>
990
- </SelectableRow>
991
-
992
- {mobileMenu}
993
- </>
994
- );
995
- }
996
-
997
- /**
998
- * Layout Map
999
- */
1000
- const LAYOUTS: Record<HeaderLayoutType, React.FC<HeaderContentProps>> = {
1001
- default: DefaultLayout,
1002
- 'two-row': TwoRowLayout
1003
- };
1004
-
1005
- /**
1006
- * HeaderContent
1007
- *
1008
- * Reads the current layout from context and renders appropriate layout.
1009
- */
1010
- export default function HeaderContent(props: HeaderContentProps) {
1011
- const { layout, menuPosition, searchPosition } = useHeaderLayout();
1012
- const LayoutComponent = LAYOUTS[layout] || LAYOUTS.default;
1013
-
1014
- return (
1015
- <LayoutComponent
1016
- {...props}
1017
- menuPosition={menuPosition}
1018
- searchPosition={searchPosition}
1019
- initialSearchSettings={props.initialSearchSettings}
1020
- initialAnnouncementSettings={props.initialAnnouncementSettings}
1021
- initialLanguageSettings={props.initialLanguageSettings}
1022
- initialCurrencySettings={props.initialCurrencySettings}
1023
- initialTextSliderSettings={props.initialTextSliderSettings}
1024
- />
1025
- );
1026
- }