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

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 (138) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/app-template/CHANGELOG.md +138 -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/dist/commands/plugins.js +23 -2
  66. package/package.json +1 -1
  67. package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +0 -15
  68. package/app-template/src/views/basket/basket-summary-context.tsx +0 -560
  69. package/app-template/src/views/basket/designer-context.tsx +0 -617
  70. package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +0 -190
  71. package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +0 -286
  72. package/app-template/src/views/breadcrumb/constants.ts +0 -15
  73. package/app-template/src/views/breadcrumb/index.tsx +0 -127
  74. package/app-template/src/views/category/native-widget-context.tsx +0 -257
  75. package/app-template/src/views/category/product-list-registrar.tsx +0 -665
  76. package/app-template/src/views/checkout/checkout-address-registrar.tsx +0 -254
  77. package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +0 -183
  78. package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +0 -259
  79. package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +0 -253
  80. package/app-template/src/views/checkout/checkout-summary-registrar.tsx +0 -183
  81. package/app-template/src/views/checkout/constants.ts +0 -5
  82. package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +0 -15
  83. package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +0 -18
  84. package/app-template/src/views/footer/footer-app-banner-context.tsx +0 -326
  85. package/app-template/src/views/footer/footer-bottom-context.tsx +0 -215
  86. package/app-template/src/views/footer/footer-bottom-wrapper.tsx +0 -74
  87. package/app-template/src/views/footer/footer-layout-constants.ts +0 -35
  88. package/app-template/src/views/footer/footer-layout-registrar.tsx +0 -342
  89. package/app-template/src/views/footer/footer-layout-switcher.tsx +0 -110
  90. package/app-template/src/views/footer/footer-menu-context.tsx +0 -211
  91. package/app-template/src/views/footer/footer-native-widgets.tsx +0 -60
  92. package/app-template/src/views/footer/footer-social-context.tsx +0 -254
  93. package/app-template/src/views/footer/footer-subscription-context.tsx +0 -210
  94. package/app-template/src/views/footer/footer-utils.ts +0 -43
  95. package/app-template/src/views/footer/footer-value-props-context.tsx +0 -326
  96. package/app-template/src/views/footer/logo-settings.ts +0 -183
  97. package/app-template/src/views/footer/native-widget-config.ts +0 -262
  98. package/app-template/src/views/footer/subscription-settings.ts +0 -122
  99. package/app-template/src/views/footer/use-footer-logo.ts +0 -162
  100. package/app-template/src/views/header/designer-context.tsx +0 -261
  101. package/app-template/src/views/header/header-announcement-registrar.tsx +0 -267
  102. package/app-template/src/views/header/header-client-wrapper.tsx +0 -496
  103. package/app-template/src/views/header/header-content.tsx +0 -1026
  104. package/app-template/src/views/header/header-currency-registrar.tsx +0 -348
  105. package/app-template/src/views/header/header-icons-context.tsx +0 -262
  106. package/app-template/src/views/header/header-language-registrar.tsx +0 -348
  107. package/app-template/src/views/header/header-layout-context.tsx +0 -143
  108. package/app-template/src/views/header/header-layout-registrar.tsx +0 -658
  109. package/app-template/src/views/header/header-logo-context.tsx +0 -228
  110. package/app-template/src/views/header/header-logo.tsx +0 -118
  111. package/app-template/src/views/header/header-mini-basket-context.tsx +0 -524
  112. package/app-template/src/views/header/header-search-registrar.tsx +0 -511
  113. package/app-template/src/views/header/header-text-slider-registrar.tsx +0 -382
  114. package/app-template/src/views/header/inline-search.tsx +0 -262
  115. package/app-template/src/views/header/navbar-menu-context.tsx +0 -219
  116. package/app-template/src/views/header/search/search-input.tsx +0 -61
  117. package/app-template/src/views/header/server-settings-parser.ts +0 -1105
  118. package/app-template/src/views/header/use-header-icons.ts +0 -241
  119. package/app-template/src/views/header/use-header-logo.ts +0 -213
  120. package/app-template/src/views/header/use-navbar-menu.ts +0 -179
  121. package/app-template/src/views/product/accordion-section.tsx +0 -61
  122. package/app-template/src/views/product/custom-button-group.tsx +0 -69
  123. package/app-template/src/views/product/favorites-button-section.tsx +0 -69
  124. package/app-template/src/views/product/find-in-store-section.tsx +0 -60
  125. package/app-template/src/views/product/product-info-section.tsx +0 -140
  126. package/app-template/src/views/product/quantity-section.tsx +0 -73
  127. package/app-template/src/views/product/sale-tag.tsx +0 -10
  128. package/app-template/src/views/product/share-section.tsx +0 -357
  129. package/app-template/src/views/product/variants-section.tsx +0 -126
  130. package/app-template/src/views/product-detail/constants.ts +0 -272
  131. package/app-template/src/views/product-detail/index.ts +0 -10
  132. package/app-template/src/views/product-detail/product-detail-registrar.tsx +0 -616
  133. package/app-template/src/widgets/footer-app-banner.tsx +0 -444
  134. package/app-template/src/widgets/footer-bottom.tsx +0 -127
  135. package/app-template/src/widgets/footer-menu-compact.tsx +0 -238
  136. package/app-template/src/widgets/footer-menu-two.tsx +0 -298
  137. package/app-template/src/widgets/footer-social-client.tsx +0 -251
  138. 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
- }