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

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 (223) hide show
  1. package/CHANGELOG.md +9 -7
  2. package/app-template/CHANGELOG.md +251 -204
  3. package/app-template/akinon.json +1 -1
  4. package/app-template/package.json +28 -28
  5. package/app-template/public/amex.svg +12 -0
  6. package/app-template/public/apple-pay.svg +16 -0
  7. package/app-template/public/assets/images/product-placeholder-1.jpg +0 -0
  8. package/app-template/public/assets/images/product-placeholder-2.jpg +0 -0
  9. package/app-template/public/assets/images/product-placeholder-3.jpg +0 -0
  10. package/app-template/public/assets/images/product-placeholder-4.jpg +0 -0
  11. package/app-template/public/google-pay.svg +16 -0
  12. package/app-template/public/locales/en/account.json +6 -3
  13. package/app-template/public/locales/en/auth.json +6 -7
  14. package/app-template/public/locales/en/basket.json +6 -6
  15. package/app-template/public/locales/en/blog.json +7 -0
  16. package/app-template/public/locales/en/category.json +3 -1
  17. package/app-template/public/locales/en/checkout.json +5 -4
  18. package/app-template/public/locales/en/common.json +11 -2
  19. package/app-template/public/locales/en/forgot_password.json +6 -7
  20. package/app-template/public/locales/en/product.json +4 -3
  21. package/app-template/public/locales/tr/account.json +6 -3
  22. package/app-template/public/locales/tr/auth.json +16 -17
  23. package/app-template/public/locales/tr/basket.json +4 -4
  24. package/app-template/public/locales/tr/blog.json +7 -0
  25. package/app-template/public/locales/tr/category.json +3 -1
  26. package/app-template/public/locales/tr/checkout.json +39 -38
  27. package/app-template/public/locales/tr/common.json +10 -1
  28. package/app-template/public/locales/tr/forgot_password.json +12 -13
  29. package/app-template/public/locales/tr/product.json +1 -0
  30. package/app-template/public/logo.svg +3 -27
  31. package/app-template/public/mastercard.svg +14 -0
  32. package/app-template/public/promotion-banner.jpg +0 -0
  33. package/app-template/public/shop-pay.svg +12 -0
  34. package/app-template/public/visa.svg +12 -0
  35. package/app-template/src/app/[commerce]/[locale]/[currency]/blog/[slug]/page.tsx +118 -0
  36. package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +15 -0
  37. package/app-template/src/app/api/theme-settings/route.ts +12 -0
  38. package/app-template/src/assets/fonts/pz-icon.css +211 -49
  39. package/app-template/src/assets/fonts/pz-icon.eot +0 -0
  40. package/app-template/src/assets/fonts/pz-icon.html +486 -0
  41. package/app-template/src/assets/fonts/pz-icon.scss +373 -49
  42. package/app-template/src/assets/fonts/pz-icon.svg +215 -53
  43. package/app-template/src/assets/fonts/pz-icon.ttf +0 -0
  44. package/app-template/src/assets/fonts/pz-icon.woff +0 -0
  45. package/app-template/src/assets/fonts/pz-icon.woff2 +0 -0
  46. package/app-template/src/assets/globals.scss +4 -0
  47. package/app-template/src/assets/icons/arrow-right.svg +3 -0
  48. package/app-template/src/assets/icons/cart.svg +4 -12
  49. package/app-template/src/assets/icons/check.svg +2 -18
  50. package/app-template/src/assets/icons/chevron-down.svg +2 -7
  51. package/app-template/src/assets/icons/delete.svg +3 -0
  52. package/app-template/src/assets/icons/facebook.svg +2 -8
  53. package/app-template/src/assets/icons/fav-off.svg +5 -0
  54. package/app-template/src/assets/icons/fav-on.svg +5 -0
  55. package/app-template/src/assets/icons/filter-and-sort.svg +3 -0
  56. package/app-template/src/assets/icons/heart.svg +3 -0
  57. package/app-template/src/assets/icons/instagram.svg +2 -13
  58. package/app-template/src/assets/icons/materials.svg +3 -0
  59. package/app-template/src/assets/icons/person.svg +4 -0
  60. package/app-template/src/assets/icons/pinterest.svg +5 -11
  61. package/app-template/src/assets/icons/ruler.svg +3 -0
  62. package/app-template/src/assets/icons/search.svg +8 -11
  63. package/app-template/src/assets/icons/share.svg +2 -9
  64. package/app-template/src/assets/icons/snapchat.svg +3 -0
  65. package/app-template/src/assets/icons/tiktok.svg +3 -0
  66. package/app-template/src/assets/icons/tumblr.svg +6 -0
  67. package/app-template/src/assets/icons/twitter.svg +2 -10
  68. package/app-template/src/assets/icons/vimeo.svg +3 -0
  69. package/app-template/src/assets/icons/youtube.svg +3 -0
  70. package/app-template/src/assets/icons/zoom.svg +8 -0
  71. package/app-template/src/components/accordion.tsx +33 -11
  72. package/app-template/src/components/action-tooltip.tsx +160 -0
  73. package/app-template/src/components/currency-select.tsx +149 -4
  74. package/app-template/src/components/icon.tsx +5 -6
  75. package/app-template/src/components/index.ts +4 -1
  76. package/app-template/src/components/language-select.tsx +88 -2
  77. package/app-template/src/components/pagination.tsx +132 -20
  78. package/app-template/src/components/quantity-input.tsx +63 -0
  79. package/app-template/src/components/quantity-selector.tsx +203 -0
  80. package/app-template/src/components/route-handler.tsx +50 -0
  81. package/app-template/src/components/select.tsx +89 -69
  82. package/app-template/src/components/types/index.ts +26 -0
  83. package/app-template/src/components/widget-content.tsx +323 -0
  84. package/app-template/src/data/server/theme.ts +70 -0
  85. package/app-template/src/hooks/use-fav-button.tsx +5 -2
  86. package/app-template/src/hooks/use-product-cart.ts +11 -8
  87. package/app-template/src/hooks/use-theme-settings.ts +42 -0
  88. package/app-template/src/lib/fonts.ts +149 -0
  89. package/app-template/src/settings.js +2 -2
  90. package/app-template/src/types/hookform-resolvers-yup.d.ts +28 -0
  91. package/app-template/src/types/widget.ts +169 -0
  92. package/app-template/src/utils/formatDate.ts +48 -0
  93. package/app-template/src/utils/styles.ts +71 -0
  94. package/app-template/src/views/account/contact-form.tsx +147 -130
  95. package/app-template/src/views/basket/basket-item.tsx +691 -107
  96. package/app-template/src/views/basket/basket-summary-context.tsx +560 -0
  97. package/app-template/src/views/basket/designer-context.tsx +617 -0
  98. package/app-template/src/views/basket/index.ts +2 -0
  99. package/app-template/src/views/basket/summary.tsx +496 -75
  100. package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +190 -0
  101. package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +286 -0
  102. package/app-template/src/views/breadcrumb/constants.ts +15 -0
  103. package/app-template/src/views/breadcrumb/index.tsx +127 -0
  104. package/app-template/src/views/breadcrumb.tsx +13 -38
  105. package/app-template/src/views/category/category-banner.tsx +4 -23
  106. package/app-template/src/views/category/category-header.tsx +289 -66
  107. package/app-template/src/views/category/category-info.tsx +173 -24
  108. package/app-template/src/views/category/filters/filter-item.tsx +138 -42
  109. package/app-template/src/views/category/filters/index.tsx +208 -48
  110. package/app-template/src/views/category/layout.tsx +7 -4
  111. package/app-template/src/views/category/native-widget-context.tsx +257 -0
  112. package/app-template/src/views/category/product-list-registrar.tsx +665 -0
  113. package/app-template/src/views/checkout/auth.tsx +64 -40
  114. package/app-template/src/views/checkout/checkout-address-registrar.tsx +254 -0
  115. package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +183 -0
  116. package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +259 -0
  117. package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +253 -0
  118. package/app-template/src/views/checkout/checkout-summary-registrar.tsx +183 -0
  119. package/app-template/src/views/checkout/constants.ts +5 -0
  120. package/app-template/src/views/checkout/index.tsx +5 -0
  121. package/app-template/src/views/checkout/layout/header.tsx +9 -5
  122. package/app-template/src/views/checkout/steps/payment/index.tsx +5 -2
  123. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +72 -1
  124. package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +15 -0
  125. package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +18 -0
  126. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +171 -40
  127. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +74 -12
  128. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +128 -45
  129. package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +232 -27
  130. package/app-template/src/views/checkout/summary.tsx +303 -29
  131. package/app-template/src/views/footer/footer-app-banner-context.tsx +326 -0
  132. package/app-template/src/views/footer/footer-bottom-context.tsx +215 -0
  133. package/app-template/src/views/footer/footer-bottom-wrapper.tsx +74 -0
  134. package/app-template/src/views/footer/footer-layout-constants.ts +35 -0
  135. package/app-template/src/views/footer/footer-layout-registrar.tsx +342 -0
  136. package/app-template/src/views/footer/footer-layout-switcher.tsx +110 -0
  137. package/app-template/src/views/footer/footer-menu-context.tsx +211 -0
  138. package/app-template/src/views/footer/footer-native-widgets.tsx +60 -0
  139. package/app-template/src/views/footer/footer-social-context.tsx +254 -0
  140. package/app-template/src/views/footer/footer-subscription-context.tsx +210 -0
  141. package/app-template/src/views/footer/footer-utils.ts +43 -0
  142. package/app-template/src/views/footer/footer-value-props-context.tsx +326 -0
  143. package/app-template/src/views/footer/logo-settings.ts +183 -0
  144. package/app-template/src/views/footer/native-widget-config.ts +262 -0
  145. package/app-template/src/views/footer/subscription-settings.ts +122 -0
  146. package/app-template/src/views/footer/use-footer-logo.ts +162 -0
  147. package/app-template/src/views/footer.tsx +415 -13
  148. package/app-template/src/views/guest-login/index.tsx +62 -58
  149. package/app-template/src/views/header/action-menu.tsx +277 -45
  150. package/app-template/src/views/header/band.tsx +6 -21
  151. package/app-template/src/views/header/designer-context.tsx +261 -0
  152. package/app-template/src/views/header/header-announcement-registrar.tsx +267 -0
  153. package/app-template/src/views/header/header-client-wrapper.tsx +496 -0
  154. package/app-template/src/views/header/header-content.tsx +1026 -0
  155. package/app-template/src/views/header/header-currency-registrar.tsx +348 -0
  156. package/app-template/src/views/header/header-icons-context.tsx +262 -0
  157. package/app-template/src/views/header/header-language-registrar.tsx +348 -0
  158. package/app-template/src/views/header/header-layout-context.tsx +143 -0
  159. package/app-template/src/views/header/header-layout-registrar.tsx +658 -0
  160. package/app-template/src/views/header/header-logo-context.tsx +228 -0
  161. package/app-template/src/views/header/header-logo.tsx +118 -0
  162. package/app-template/src/views/header/header-mini-basket-context.tsx +524 -0
  163. package/app-template/src/views/header/header-search-registrar.tsx +511 -0
  164. package/app-template/src/views/header/header-text-slider-registrar.tsx +382 -0
  165. package/app-template/src/views/header/index.tsx +109 -47
  166. package/app-template/src/views/header/inline-search.tsx +262 -0
  167. package/app-template/src/views/header/mini-basket.tsx +819 -44
  168. package/app-template/src/views/header/mobile-hamburger-button.tsx +5 -8
  169. package/app-template/src/views/header/mobile-menu.tsx +12 -0
  170. package/app-template/src/views/header/navbar-menu-context.tsx +219 -0
  171. package/app-template/src/views/header/navbar.tsx +178 -111
  172. package/app-template/src/views/header/search/index.tsx +71 -32
  173. package/app-template/src/views/header/search/results.tsx +127 -65
  174. package/app-template/src/views/header/search/search-input.tsx +61 -0
  175. package/app-template/src/views/header/server-settings-parser.ts +1105 -0
  176. package/app-template/src/views/header/use-header-icons.ts +241 -0
  177. package/app-template/src/views/header/use-header-logo.ts +213 -0
  178. package/app-template/src/views/header/use-navbar-menu.ts +179 -0
  179. package/app-template/src/views/login/index.tsx +54 -46
  180. package/app-template/src/views/product/accordion-section.tsx +61 -0
  181. package/app-template/src/views/product/accordion-wrapper.tsx +135 -43
  182. package/app-template/src/views/product/custom-button-group.tsx +69 -0
  183. package/app-template/src/views/product/favorites-button-section.tsx +69 -0
  184. package/app-template/src/views/product/find-in-store-section.tsx +60 -0
  185. package/app-template/src/views/product/index.ts +1 -0
  186. package/app-template/src/views/product/layout.tsx +6 -5
  187. package/app-template/src/views/product/misc-buttons.tsx +339 -25
  188. package/app-template/src/views/product/price-wrapper.tsx +3 -29
  189. package/app-template/src/views/product/product-actions.tsx +137 -8
  190. package/app-template/src/views/product/product-info-section.tsx +140 -0
  191. package/app-template/src/views/product/product-info.tsx +69 -31
  192. package/app-template/src/views/product/product-share.tsx +13 -8
  193. package/app-template/src/views/product/product-variants.tsx +2 -2
  194. package/app-template/src/views/product/quantity-section.tsx +73 -0
  195. package/app-template/src/views/product/sale-tag.tsx +10 -0
  196. package/app-template/src/views/product/share-section.tsx +357 -0
  197. package/app-template/src/views/product/slider.tsx +117 -79
  198. package/app-template/src/views/product/variant.tsx +69 -41
  199. package/app-template/src/views/product/variants-section.tsx +126 -0
  200. package/app-template/src/views/product-detail/constants.ts +272 -0
  201. package/app-template/src/views/product-detail/index.ts +10 -0
  202. package/app-template/src/views/product-detail/product-detail-registrar.tsx +616 -0
  203. package/app-template/src/views/product-item/index.tsx +119 -46
  204. package/app-template/src/views/register/index.tsx +14 -25
  205. package/app-template/src/views/share/index.tsx +9 -6
  206. package/app-template/src/views/widgets/home-hero-slider-content.tsx +41 -39
  207. package/app-template/src/widgets/flatpages/about-us/index.tsx +78 -0
  208. package/app-template/src/widgets/flatpages/blog-list/index.tsx +129 -0
  209. package/app-template/src/widgets/footer-app-banner.tsx +444 -0
  210. package/app-template/src/widgets/footer-bottom.tsx +127 -0
  211. package/app-template/src/widgets/footer-menu-compact.tsx +238 -0
  212. package/app-template/src/widgets/footer-menu-two.tsx +298 -0
  213. package/app-template/src/widgets/footer-social-client.tsx +251 -0
  214. package/app-template/src/widgets/footer-social.tsx +47 -16
  215. package/app-template/src/widgets/footer-subscription/footer-subscription-form.tsx +17 -14
  216. package/app-template/src/widgets/footer-subscription/index.tsx +183 -17
  217. package/app-template/src/widgets/footer-value-props.tsx +201 -0
  218. package/app-template/src/widgets/index.ts +7 -0
  219. package/app-template/src/widgets/schemas/about-us.json +46 -0
  220. package/app-template/src/widgets/schemas/blog-list.json +37 -0
  221. package/app-template/src/widgets/schemas/blog.json +29 -0
  222. package/app-template/tailwind.config.js +18 -2
  223. package/package.json +1 -1
@@ -0,0 +1,1026 @@
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
+ }