@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,382 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * Header Text Slider Section Registrar
5
- *
6
- * This component registers the "Text Slider" section for the header placeholder.
7
- * The text slider section is a native widget that can be added via "Add Section" button.
8
- * It displays scrolling/sliding text messages in the utility bar.
9
- */
10
-
11
- import {
12
- createContext,
13
- useContext,
14
- useEffect,
15
- useRef,
16
- useState,
17
- PropsWithChildren
18
- } from 'react';
19
-
20
- // Constants
21
- export const HEADER_TEXT_SLIDER_PLACEHOLDER_ID = 'header';
22
- export const HEADER_TEXT_SLIDER_SECTION_ID = 'header-text-slider';
23
- export const HEADER_TEXT_SLIDER_WIDGET_SLUG = 'header-text-slider-settings-2';
24
-
25
- // Global flag to track if registration has been done
26
- declare global {
27
- interface Window {
28
- __headerTextSliderRegistered?: boolean;
29
- }
30
- }
31
-
32
- /**
33
- * Check if running inside designer iframe
34
- */
35
- function isInDesignerMode(): boolean {
36
- if (typeof window === 'undefined') return false;
37
- return window.self !== window.top;
38
- }
39
-
40
- interface ThemeSection {
41
- id: string;
42
- visible?: boolean;
43
- properties?: Record<string, unknown>;
44
- styles?: Record<string, unknown>;
45
- }
46
-
47
- interface ThemePlaceholder {
48
- slug: string;
49
- sections: ThemeSection[];
50
- }
51
-
52
- export interface TextSliderItem {
53
- text: string;
54
- link?: string;
55
- }
56
-
57
- export interface HeaderTextSliderProperties {
58
- visible?: boolean;
59
- items?: TextSliderItem[];
60
- autoPlay?: boolean;
61
- autoPlayInterval?: number;
62
- showArrows?: boolean;
63
- }
64
-
65
- interface HeaderTextSliderContextValue {
66
- isDesigner: boolean;
67
- isTextSliderSectionSelected: boolean;
68
- isSectionVisible: boolean;
69
- properties: HeaderTextSliderProperties;
70
- sectionStyles: Record<string, unknown>;
71
- }
72
-
73
- const HeaderTextSliderContext = createContext<HeaderTextSliderContextValue>({
74
- isDesigner: false,
75
- isTextSliderSectionSelected: false,
76
- isSectionVisible: false,
77
- properties: {
78
- items: [{ text: 'Welcome to our store!' }],
79
- autoPlay: true,
80
- autoPlayInterval: 3000,
81
- showArrows: true
82
- },
83
- sectionStyles: {}
84
- });
85
-
86
- export const useHeaderTextSlider = () => useContext(HeaderTextSliderContext);
87
-
88
- interface HeaderTextSliderRegistrarProps extends PropsWithChildren {
89
- /**
90
- * Initial settings from server-side parsing (to avoid flash)
91
- */
92
- initialSettings?: {
93
- sectionStyles?: Record<string, unknown>;
94
- properties?: HeaderTextSliderProperties;
95
- };
96
- }
97
-
98
- /**
99
- * HeaderTextSliderRegistrar
100
- *
101
- * Registers the Text Slider native section with Theme Editor.
102
- * The section can be added via "Add Section" button in the header placeholder.
103
- */
104
- export default function HeaderTextSliderRegistrar({
105
- children,
106
- initialSettings = {}
107
- }: HeaderTextSliderRegistrarProps) {
108
- const isDesignerRef = useRef(false);
109
- const [sectionProperties, setSectionProperties] =
110
- useState<HeaderTextSliderProperties>(
111
- initialSettings.properties || {
112
- items: [{ text: 'Welcome to our store!' }],
113
- autoPlay: true,
114
- autoPlayInterval: 3000,
115
- showArrows: true
116
- }
117
- );
118
- const [sectionStyles, setSectionStyles] = useState<Record<string, unknown>>(
119
- initialSettings.sectionStyles || {}
120
- );
121
- const [isTextSliderSelected, setIsTextSliderSelected] = useState(false);
122
- const [isSectionVisible, setIsSectionVisible] = useState(
123
- initialSettings.properties?.visible ?? false
124
- );
125
- const hasRegisteredNativeWidget = useRef(false);
126
-
127
- const hasInitialSettings = !!(
128
- (initialSettings.sectionStyles &&
129
- Object.keys(initialSettings.sectionStyles).length > 0) ||
130
- (initialSettings.properties &&
131
- Object.keys(initialSettings.properties).length > 0)
132
- );
133
-
134
- useEffect(() => {
135
- isDesignerRef.current = isInDesignerMode();
136
- }, []);
137
-
138
- const isDesigner = isDesignerRef.current;
139
-
140
- // Register native widget - only once on mount
141
- useEffect(() => {
142
- const isInIframe =
143
- typeof window !== 'undefined' && window.self !== window.top;
144
- if (!isInIframe || !window.parent) {
145
- return;
146
- }
147
-
148
- // Skip if already registered
149
- if (
150
- typeof window !== 'undefined' &&
151
- window.__headerTextSliderRegistered &&
152
- hasRegisteredNativeWidget.current
153
- ) {
154
- return;
155
- }
156
-
157
- // Send native widget registration to Theme Editor
158
- // Use initial settings for registration, not dynamic state
159
- const nativeWidgetConfig = {
160
- placeholderId: HEADER_TEXT_SLIDER_PLACEHOLDER_ID,
161
- autoAdd: false,
162
- section: {
163
- id: HEADER_TEXT_SLIDER_SECTION_ID,
164
- type: 'native',
165
- label: 'Text Slider',
166
- blocks: [],
167
- properties: initialSettings.properties || {
168
- items: [{ text: 'Welcome to our store!' }],
169
- autoPlay: true,
170
- autoPlayInterval: 3000,
171
- showArrows: true
172
- },
173
- styles: initialSettings.sectionStyles || {}
174
- }
175
- };
176
-
177
- window.parent.postMessage(
178
- {
179
- type: 'REGISTER_NATIVE_WIDGETS',
180
- data: { widgets: [nativeWidgetConfig] }
181
- },
182
- '*'
183
- );
184
-
185
- // Mark as registered
186
- window.__headerTextSliderRegistered = true;
187
- hasRegisteredNativeWidget.current = true;
188
- // eslint-disable-next-line react-hooks/exhaustive-deps
189
- }, []);
190
-
191
- // Apply highlight style to text slider container when section is selected
192
- useEffect(() => {
193
- if (typeof window === 'undefined') return;
194
-
195
- const textSliderContainer = document.querySelector(
196
- '[data-section-id="header-text-slider"]'
197
- );
198
- if (!textSliderContainer) return;
199
-
200
- if (isTextSliderSelected) {
201
- (textSliderContainer as HTMLElement).style.outline = '2px solid #3b82f6';
202
- (textSliderContainer as HTMLElement).style.outlineOffset = '-2px';
203
- } else {
204
- (textSliderContainer as HTMLElement).style.outline = '';
205
- (textSliderContainer as HTMLElement).style.outlineOffset = '';
206
- }
207
-
208
- return () => {
209
- (textSliderContainer as HTMLElement).style.outline = '';
210
- (textSliderContainer as HTMLElement).style.outlineOffset = '';
211
- };
212
- }, [isTextSliderSelected]);
213
-
214
- const hasReceivedInitialTheme = useRef(false);
215
-
216
- // Listen for theme updates and selection changes from Theme Editor
217
- useEffect(() => {
218
- if (typeof window === 'undefined') return;
219
-
220
- const handleMessage = (event: MessageEvent) => {
221
- const { type, data } = event.data || {};
222
-
223
- // Handle theme updates
224
- if (
225
- (type === 'UPDATE_THEME' || type === 'LOAD_THEME') &&
226
- data?.theme?.placeholders
227
- ) {
228
- const placeholder = data.theme.placeholders?.find(
229
- (p: ThemePlaceholder) => p.slug === HEADER_TEXT_SLIDER_PLACEHOLDER_ID
230
- );
231
-
232
- const textSliderSection = placeholder?.sections?.find(
233
- (s: ThemeSection) => s.id === HEADER_TEXT_SLIDER_SECTION_ID
234
- );
235
-
236
- if (textSliderSection) {
237
- const sectionVisible = textSliderSection.properties?.visible;
238
- if (sectionVisible !== undefined) {
239
- const isVisible =
240
- typeof sectionVisible === 'boolean'
241
- ? sectionVisible
242
- : sectionVisible?.desktop === true ||
243
- sectionVisible?.mobile === true;
244
- setIsSectionVisible(isVisible);
245
- } else if (typeof textSliderSection.visible === 'boolean') {
246
- setIsSectionVisible(textSliderSection.visible);
247
- } else {
248
- setIsSectionVisible(true);
249
- }
250
-
251
- // Skip property/style updates if we have initial settings and haven't
252
- // received the first theme yet (to avoid format mismatch issues)
253
- // After first theme load, we use UPDATE_SECTION_PROPERTY for changes
254
- if (hasInitialSettings && !hasReceivedInitialTheme.current) {
255
- hasReceivedInitialTheme.current = true;
256
- // Don't apply properties/styles from UPDATE_THEME when we have server-side initial settings
257
- // Individual property changes will come via UPDATE_SECTION_PROPERTY
258
- return;
259
- }
260
-
261
- // Only apply updates if we don't have initial settings (fresh load)
262
- if (!hasInitialSettings) {
263
- if (textSliderSection.properties) {
264
- setSectionProperties((prev) => {
265
- const newProps =
266
- textSliderSection.properties as HeaderTextSliderProperties;
267
- if (JSON.stringify(prev) === JSON.stringify(newProps)) {
268
- return prev;
269
- }
270
- return newProps;
271
- });
272
- }
273
-
274
- if (textSliderSection.styles) {
275
- setSectionStyles((prev) => {
276
- if (
277
- JSON.stringify(prev) ===
278
- JSON.stringify(textSliderSection.styles)
279
- ) {
280
- return prev;
281
- }
282
- return textSliderSection.styles;
283
- });
284
- }
285
- }
286
- } else if (placeholder) {
287
- setIsSectionVisible(false);
288
- }
289
- }
290
-
291
- // Handle property updates
292
- if (type === 'UPDATE_SECTION_PROPERTY' || type === 'UPDATE_PROPERTY') {
293
- const { sectionId, key, value, properties } = data || {};
294
-
295
- if (sectionId === HEADER_TEXT_SLIDER_SECTION_ID) {
296
- if (key && value !== undefined) {
297
- setSectionProperties((prev) => ({
298
- ...prev,
299
- [key]: value
300
- }));
301
- }
302
- if (properties) {
303
- setSectionProperties(properties as HeaderTextSliderProperties);
304
- }
305
- }
306
- }
307
-
308
- // Handle style updates for the section
309
- if (type === 'UPDATE_SECTION_STYLE' || type === 'UPDATE_STYLE') {
310
- const { sectionId, key, value, styles } = data || {};
311
-
312
- if (sectionId === HEADER_TEXT_SLIDER_SECTION_ID) {
313
- if (key && value !== undefined) {
314
- setSectionStyles((prev) => ({
315
- ...prev,
316
- [key]: value
317
- }));
318
- }
319
- if (styles) {
320
- setSectionStyles(styles);
321
- }
322
- }
323
- }
324
-
325
- // Handle selection changes
326
- if (type === 'SELECT_SECTION') {
327
- const { placeholderId, sectionId } = data || {};
328
-
329
- const isSelected =
330
- placeholderId === HEADER_TEXT_SLIDER_PLACEHOLDER_ID &&
331
- sectionId === HEADER_TEXT_SLIDER_SECTION_ID;
332
-
333
- setIsTextSliderSelected(isSelected);
334
- }
335
-
336
- // Handle block selection
337
- if (type === 'SELECT_BLOCK') {
338
- const { sectionId } = data || {};
339
- if (sectionId === HEADER_TEXT_SLIDER_SECTION_ID) {
340
- setIsTextSliderSelected(true);
341
- } else {
342
- setIsTextSliderSelected(false);
343
- }
344
- }
345
-
346
- // Handle deselection
347
- if (type === 'DESELECT' || type === 'CLEAR_SELECTION') {
348
- setIsTextSliderSelected(false);
349
- }
350
-
351
- // Handle visibility toggle
352
- if (type === 'TOGGLE_SECTION_VISIBILITY') {
353
- const { sectionId } = data || {};
354
- if (sectionId === HEADER_TEXT_SLIDER_SECTION_ID) {
355
- setIsSectionVisible((prev) => !prev);
356
- }
357
- }
358
- };
359
-
360
- window.addEventListener('message', handleMessage);
361
- return () => window.removeEventListener('message', handleMessage);
362
- }, [hasInitialSettings, isDesigner]);
363
-
364
- // If no children, just return null (registration-only mode)
365
- if (!children) {
366
- return null;
367
- }
368
-
369
- return (
370
- <HeaderTextSliderContext.Provider
371
- value={{
372
- isDesigner,
373
- isTextSliderSectionSelected: isTextSliderSelected,
374
- isSectionVisible,
375
- properties: sectionProperties,
376
- sectionStyles
377
- }}
378
- >
379
- {children}
380
- </HeaderTextSliderContext.Provider>
381
- );
382
- }
@@ -1,262 +0,0 @@
1
- 'use client';
2
-
3
- /**
4
- * Inline Search Component
5
- *
6
- * A simplified search input that appears directly in the header
7
- * for the "two-row" layout. Unlike the modal search, this is always
8
- * visible and expands inline results.
9
- */
10
-
11
- import { useState, useRef, useMemo, useCallback } from 'react';
12
- import { useRouter } from '@akinon/next/hooks';
13
- import { Icon } from '@theme/components';
14
- import { ROUTES } from '@theme/routes';
15
- import clsx from 'clsx';
16
- import { useDesignerFeatures } from '@akinon/next/components/theme-editor/hooks/use-designer-features';
17
-
18
- interface InlineSearchProps {
19
- placeholder?: string;
20
- className?: string;
21
- style?: React.CSSProperties;
22
- iconStyles?: Record<string, unknown>;
23
- /** Block ID for theme editor selection */
24
- blockId?: string;
25
- /** Whether in designer mode (disables interactions) */
26
- isDesigner?: boolean;
27
- /** Placeholder ID for theme editor */
28
- placeholderId?: string;
29
- /** Section ID for theme editor */
30
- sectionId?: string;
31
- /** Whether this block is currently selected */
32
- isSelected?: boolean;
33
- }
34
-
35
- /**
36
- * Extract responsive style value for the current breakpoint
37
- * Styles from theme editor come as { desktop: "value", tablet: "value", ... }
38
- */
39
- function extractStyleValue(
40
- value: unknown,
41
- breakpoint = 'desktop'
42
- ): string | number | undefined {
43
- if (value === null || value === undefined) return undefined;
44
- if (typeof value === 'string' || typeof value === 'number') return value;
45
- if (typeof value === 'object' && value !== null) {
46
- const obj = value as Record<string, string | number>;
47
- // Try current breakpoint, fallback to desktop, then first available value
48
- return obj[breakpoint] ?? obj['desktop'] ?? Object.values(obj)[0];
49
- }
50
- return undefined;
51
- }
52
-
53
- /**
54
- * Convert theme editor style object to CSS properties
55
- * Supports both kebab-case (from theme editor) and camelCase (from server parser)
56
- */
57
- function convertStyles(
58
- styles: Record<string, unknown> | undefined,
59
- breakpoint = 'desktop'
60
- ): React.CSSProperties {
61
- if (!styles) return {};
62
-
63
- const result: React.CSSProperties = {};
64
-
65
- // Map theme editor keys (kebab-case) to CSS property names (camelCase)
66
- const styleMap: Record<string, keyof React.CSSProperties> = {
67
- // Kebab-case keys (from theme editor postMessage)
68
- width: 'width',
69
- 'max-width': 'maxWidth',
70
- height: 'height',
71
- 'background-color': 'backgroundColor',
72
- 'border-color': 'borderColor',
73
- 'border-width': 'borderWidth',
74
- 'border-radius': 'borderRadius',
75
- 'font-size': 'fontSize',
76
- 'padding-left': 'paddingLeft',
77
- 'padding-right': 'paddingRight',
78
- // CamelCase keys (from server-side parser or direct style pass)
79
- maxWidth: 'maxWidth',
80
- backgroundColor: 'backgroundColor',
81
- borderColor: 'borderColor',
82
- borderWidth: 'borderWidth',
83
- borderRadius: 'borderRadius',
84
- fontSize: 'fontSize',
85
- paddingLeft: 'paddingLeft',
86
- paddingRight: 'paddingRight'
87
- };
88
-
89
- Object.entries(styles).forEach(([key, value]) => {
90
- const cssKey = styleMap[key];
91
- if (cssKey) {
92
- const extracted = extractStyleValue(value, breakpoint);
93
- if (extracted !== undefined) {
94
- (result as Record<string, unknown>)[cssKey] = extracted;
95
- }
96
- }
97
- });
98
-
99
- return result;
100
- }
101
-
102
- export default function InlineSearch({
103
- placeholder = 'Search...',
104
- className,
105
- style,
106
- iconStyles,
107
- blockId,
108
- isDesigner = false,
109
- placeholderId = 'header',
110
- sectionId = 'header-search',
111
- isSelected = false
112
- }: InlineSearchProps) {
113
- const router = useRouter();
114
- const inputRef = useRef<HTMLInputElement>(null);
115
- const [searchText, setSearchText] = useState('');
116
-
117
- // Convert theme editor styles to CSS properties
118
- const computedStyles = useMemo(
119
- () => convertStyles(style as unknown as Record<string, unknown>),
120
- [style]
121
- );
122
-
123
- // Extract font-size for the input element
124
- const inputStyle = useMemo(() => {
125
- const fontSize = computedStyles.fontSize;
126
- return fontSize ? { fontSize } : {};
127
- }, [computedStyles]);
128
-
129
- // Extract icon color and size from block styles
130
- const iconColor = useMemo(() => {
131
- const color = extractStyleValue(iconStyles?.color);
132
- return typeof color === 'string' ? color : undefined;
133
- }, [iconStyles]);
134
-
135
- const iconSize = useMemo(() => {
136
- // iconSize comes from properties, not styles - check both
137
- const size =
138
- extractStyleValue(iconStyles?.iconSize) ||
139
- extractStyleValue(iconStyles?.['icon-size']);
140
- if (size === undefined) return 18;
141
- const numSize = typeof size === 'string' ? parseInt(size, 10) : size;
142
- return isNaN(numSize) ? 18 : numSize;
143
- }, [iconStyles]);
144
-
145
- // Extract custom icon SVG from block properties
146
- const customIcon = useMemo(() => {
147
- const icon = iconStyles?.icon;
148
- if (!icon) return null;
149
- // Handle responsive object format
150
- const iconValue =
151
- typeof icon === 'object' && icon !== null && 'desktop' in icon
152
- ? (icon as { desktop?: string }).desktop
153
- : icon;
154
- // Check if it's SVG content
155
- if (typeof iconValue === 'string' && iconValue.includes('<svg')) {
156
- return iconValue;
157
- }
158
- return null;
159
- }, [iconStyles]);
160
-
161
- // Designer features for theme editor selection
162
- const { handleClick: handleDesignerClick } = useDesignerFeatures({
163
- blockId: blockId || 'header-search-icon',
164
- placeholderId,
165
- sectionId,
166
- isDesigner,
167
- blockInfo: {
168
- id: blockId || 'header-search-icon',
169
- type: 'icon-button',
170
- label: 'Search Icon'
171
- }
172
- });
173
-
174
- const handleSearch = () => {
175
- if (searchText.trim() !== '') {
176
- router.push(
177
- `${ROUTES.LIST}/?search_text=${encodeURIComponent(searchText)}`
178
- );
179
- setSearchText('');
180
- }
181
- };
182
-
183
- // Handle click - in designer mode, select the block; otherwise do nothing on container
184
- const handleContainerClick = useCallback(
185
- (e: React.MouseEvent) => {
186
- if (isDesigner) {
187
- e.preventDefault();
188
- e.stopPropagation();
189
- handleDesignerClick(e);
190
- }
191
- },
192
- [isDesigner, handleDesignerClick]
193
- );
194
-
195
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
196
- if (e.key === 'Enter') {
197
- handleSearch();
198
- }
199
- };
200
-
201
- return (
202
- <div
203
- data-block-id={blockId}
204
- onClick={handleContainerClick}
205
- className={clsx(
206
- 'flex items-center border px-3 flex-shrink-0',
207
- // Only apply default Tailwind classes if no inline style override
208
- !computedStyles.height && 'h-10',
209
- !computedStyles.borderRadius && 'rounded-md',
210
- !computedStyles.backgroundColor && 'bg-white',
211
- !computedStyles.borderColor && 'border-gray-300',
212
- isDesigner && 'cursor-pointer',
213
- isSelected && 'ring-2 ring-blue-500 ring-offset-1',
214
- className
215
- )}
216
- style={computedStyles}
217
- >
218
- <input
219
- ref={inputRef}
220
- type="text"
221
- value={searchText}
222
- onChange={(e) => setSearchText(e.target.value)}
223
- onKeyDown={handleKeyDown}
224
- placeholder={placeholder}
225
- className={clsx(
226
- 'flex-1 text-sm text-black-750 bg-transparent border-none outline-none',
227
- 'placeholder:text-gray-400',
228
- isDesigner && 'pointer-events-none'
229
- )}
230
- style={inputStyle}
231
- />
232
- <button
233
- type="button"
234
- onClick={handleSearch}
235
- className={clsx(
236
- 'flex items-center justify-center ml-2 p-1 hover:bg-gray-100 rounded transition-colors',
237
- isDesigner && 'pointer-events-none'
238
- )}
239
- aria-label="Search"
240
- >
241
- {customIcon ? (
242
- <div
243
- className="flex items-center justify-center"
244
- style={{
245
- width: iconSize,
246
- height: iconSize,
247
- color: iconColor || undefined
248
- }}
249
- dangerouslySetInnerHTML={{ __html: customIcon }}
250
- />
251
- ) : (
252
- <Icon
253
- name="search"
254
- size={iconSize}
255
- className={iconColor ? undefined : 'text-gray-600'}
256
- style={iconColor ? { color: iconColor } : undefined}
257
- />
258
- )}
259
- </button>
260
- </div>
261
- );
262
- }