@akinon/next 2.0.0-beta.2 → 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 (189) hide show
  1. package/.eslintrc.js +12 -0
  2. package/CHANGELOG.md +377 -7
  3. package/__tests__/next-config.test.ts +83 -0
  4. package/__tests__/tsconfig.json +23 -0
  5. package/api/auth.ts +133 -44
  6. package/api/barcode-search.ts +59 -0
  7. package/api/cache.ts +41 -5
  8. package/api/client.ts +21 -4
  9. package/api/form.ts +85 -0
  10. package/api/image-proxy.ts +75 -0
  11. package/api/product-categories.ts +53 -0
  12. package/api/similar-product-list.ts +63 -0
  13. package/api/similar-products.ts +111 -0
  14. package/api/virtual-try-on.ts +382 -0
  15. package/assets/styles/index.scss +84 -0
  16. package/babel.config.js +6 -0
  17. package/bin/pz-generate-routes.js +115 -0
  18. package/bin/pz-prebuild.js +1 -0
  19. package/bin/pz-predev.js +1 -0
  20. package/bin/pz-run-tests.js +99 -0
  21. package/bin/run-prebuild-tests.js +46 -0
  22. package/components/accordion.tsx +20 -5
  23. package/components/button.tsx +51 -36
  24. package/components/client-root.tsx +138 -2
  25. package/components/file-input.tsx +65 -3
  26. package/components/index.ts +1 -0
  27. package/components/input.tsx +1 -1
  28. package/components/link.tsx +46 -16
  29. package/components/logger-popup.tsx +213 -0
  30. package/components/modal.tsx +32 -16
  31. package/components/plugin-module.tsx +62 -3
  32. package/components/price.tsx +2 -2
  33. package/components/select.tsx +1 -1
  34. package/components/selected-payment-option-view.tsx +21 -0
  35. package/components/theme-editor/blocks/accordion-block.tsx +136 -0
  36. package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
  37. package/components/theme-editor/blocks/button-block.tsx +593 -0
  38. package/components/theme-editor/blocks/counter-block.tsx +348 -0
  39. package/components/theme-editor/blocks/divider-block.tsx +20 -0
  40. package/components/theme-editor/blocks/embed-block.tsx +208 -0
  41. package/components/theme-editor/blocks/group-block.tsx +116 -0
  42. package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
  43. package/components/theme-editor/blocks/icon-block.tsx +230 -0
  44. package/components/theme-editor/blocks/image-block.tsx +137 -0
  45. package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
  46. package/components/theme-editor/blocks/input-block.tsx +123 -0
  47. package/components/theme-editor/blocks/link-block.tsx +216 -0
  48. package/components/theme-editor/blocks/lottie-block.tsx +325 -0
  49. package/components/theme-editor/blocks/map-block.tsx +89 -0
  50. package/components/theme-editor/blocks/slider-block.tsx +595 -0
  51. package/components/theme-editor/blocks/tab-block.tsx +10 -0
  52. package/components/theme-editor/blocks/text-block.tsx +52 -0
  53. package/components/theme-editor/blocks/video-block.tsx +122 -0
  54. package/components/theme-editor/components/action-toolbar.tsx +305 -0
  55. package/components/theme-editor/components/designer-overlay.tsx +74 -0
  56. package/components/theme-editor/components/with-designer-features.tsx +142 -0
  57. package/components/theme-editor/dynamic-font-loader.tsx +79 -0
  58. package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
  59. package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
  60. package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
  61. package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
  62. package/components/theme-editor/placeholder-registry.ts +31 -0
  63. package/components/theme-editor/sections/before-after-section.tsx +245 -0
  64. package/components/theme-editor/sections/contact-form-section.tsx +563 -0
  65. package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
  66. package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
  67. package/components/theme-editor/sections/divider-section.tsx +62 -0
  68. package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
  69. package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
  70. package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
  71. package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
  72. package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
  73. package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
  74. package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
  75. package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
  76. package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
  77. package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
  78. package/components/theme-editor/sections/section-wrapper.tsx +135 -0
  79. package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
  80. package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
  81. package/components/theme-editor/sections/tabs-section.tsx +578 -0
  82. package/components/theme-editor/theme-block.tsx +102 -0
  83. package/components/theme-editor/theme-placeholder-client.tsx +218 -0
  84. package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
  85. package/components/theme-editor/theme-placeholder.tsx +288 -0
  86. package/components/theme-editor/theme-section.tsx +1224 -0
  87. package/components/theme-editor/theme-settings-context.tsx +13 -0
  88. package/components/theme-editor/utils/index.ts +792 -0
  89. package/components/theme-editor/utils/iterator-utils.ts +234 -0
  90. package/components/theme-editor/utils/publish-window.ts +86 -0
  91. package/components/theme-editor/utils/visibility-rules.ts +188 -0
  92. package/data/client/account.ts +17 -2
  93. package/data/client/api.ts +2 -0
  94. package/data/client/basket.ts +66 -5
  95. package/data/client/checkout.ts +391 -99
  96. package/data/client/misc.ts +38 -2
  97. package/data/client/product.ts +19 -2
  98. package/data/client/user.ts +16 -8
  99. package/data/server/category.ts +11 -9
  100. package/data/server/flatpage.ts +11 -4
  101. package/data/server/form.ts +15 -4
  102. package/data/server/landingpage.ts +11 -4
  103. package/data/server/list.ts +5 -4
  104. package/data/server/menu.ts +11 -3
  105. package/data/server/product.ts +111 -55
  106. package/data/server/seo.ts +14 -4
  107. package/data/server/special-page.ts +5 -4
  108. package/data/server/widget.ts +90 -5
  109. package/data/urls.ts +16 -5
  110. package/hocs/client/with-segment-defaults.tsx +2 -2
  111. package/hocs/server/with-segment-defaults.tsx +65 -20
  112. package/hooks/index.ts +4 -0
  113. package/hooks/use-localization.ts +24 -10
  114. package/hooks/use-logger-context.tsx +114 -0
  115. package/hooks/use-logger.ts +92 -0
  116. package/hooks/use-loyalty-availability.ts +21 -0
  117. package/hooks/use-payment-options.ts +2 -1
  118. package/hooks/use-pz-params.ts +37 -0
  119. package/hooks/use-router.ts +51 -14
  120. package/hooks/use-sentry-uncaught-errors.ts +24 -0
  121. package/instrumentation/index.ts +10 -1
  122. package/instrumentation/node.ts +2 -20
  123. package/jest.config.js +25 -0
  124. package/lib/cache-handler.mjs +534 -16
  125. package/lib/cache.ts +272 -37
  126. package/localization/index.ts +2 -1
  127. package/localization/provider.tsx +2 -5
  128. package/middlewares/bfcache-headers.ts +18 -0
  129. package/middlewares/checkout-provider.ts +1 -1
  130. package/middlewares/complete-gpay.ts +32 -26
  131. package/middlewares/complete-masterpass.ts +33 -26
  132. package/middlewares/complete-wallet.ts +182 -0
  133. package/middlewares/default.ts +360 -215
  134. package/middlewares/index.ts +10 -2
  135. package/middlewares/locale.ts +34 -11
  136. package/middlewares/masterpass-rest-callback.ts +230 -0
  137. package/middlewares/oauth-login.ts +200 -57
  138. package/middlewares/pretty-url.ts +21 -8
  139. package/middlewares/redirection-payment.ts +32 -26
  140. package/middlewares/saved-card-redirection.ts +33 -26
  141. package/middlewares/three-d-redirection.ts +32 -26
  142. package/middlewares/url-redirection.ts +11 -1
  143. package/middlewares/wallet-complete-redirection.ts +206 -0
  144. package/package.json +25 -10
  145. package/plugins.d.ts +19 -4
  146. package/plugins.js +10 -1
  147. package/redux/actions.ts +47 -0
  148. package/redux/middlewares/checkout.ts +63 -138
  149. package/redux/middlewares/index.ts +14 -10
  150. package/redux/middlewares/pre-order/address.ts +7 -2
  151. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
  152. package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
  153. package/redux/middlewares/pre-order/delivery-option.ts +7 -1
  154. package/redux/middlewares/pre-order/index.ts +16 -10
  155. package/redux/middlewares/pre-order/installment-option.ts +8 -1
  156. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  157. package/redux/middlewares/pre-order/payment-option.ts +7 -1
  158. package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
  159. package/redux/middlewares/pre-order/redirection.ts +8 -2
  160. package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
  161. package/redux/middlewares/pre-order/shipping-option.ts +7 -1
  162. package/redux/middlewares/pre-order/shipping-step.ts +5 -1
  163. package/redux/reducers/checkout.ts +23 -3
  164. package/redux/reducers/index.ts +11 -3
  165. package/redux/reducers/root.ts +7 -2
  166. package/redux/reducers/widget.ts +80 -0
  167. package/sentry/index.ts +69 -13
  168. package/tailwind/content.js +16 -0
  169. package/types/commerce/account.ts +5 -1
  170. package/types/commerce/checkout.ts +35 -1
  171. package/types/commerce/widget.ts +33 -0
  172. package/types/index.ts +101 -6
  173. package/types/next-auth.d.ts +2 -2
  174. package/types/widget.ts +80 -0
  175. package/utils/app-fetch.ts +7 -2
  176. package/utils/generate-commerce-search-params.ts +3 -2
  177. package/utils/get-checkout-path.ts +3 -0
  178. package/utils/get-root-hostname.ts +28 -0
  179. package/utils/index.ts +64 -10
  180. package/utils/localization.ts +4 -0
  181. package/utils/mobile-3d-iframe.ts +8 -2
  182. package/utils/override-middleware.ts +7 -12
  183. package/utils/pz-segments.ts +92 -0
  184. package/utils/redirect-ignore.ts +35 -0
  185. package/utils/redirect.ts +9 -3
  186. package/utils/redirection-iframe.ts +8 -2
  187. package/utils/widget-styles.ts +107 -0
  188. package/views/error-page.tsx +93 -0
  189. package/with-pz-config.js +13 -6
@@ -0,0 +1,578 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { Section } from '../theme-section';
3
+ import ThemeBlock from '../theme-block';
4
+ import {
5
+ getCSSStyles,
6
+ resolveThemeCssVariables,
7
+ resolveThemeStyleObject
8
+ } from '../utils';
9
+ import { twMerge } from 'tailwind-merge';
10
+ import clsx from 'clsx';
11
+ import { useThemeSettingsContext } from '../theme-settings-context';
12
+
13
+ interface TabsSectionProps {
14
+ section: Section;
15
+ currentBreakpoint?: string;
16
+ placeholderId?: string;
17
+ isDesigner?: boolean;
18
+ selectedBlockId?: string | null;
19
+ }
20
+
21
+ const TabsSection: React.FC<TabsSectionProps> = ({
22
+ section,
23
+ currentBreakpoint = 'desktop',
24
+ placeholderId = '',
25
+ isDesigner = false,
26
+ selectedBlockId = null
27
+ }) => {
28
+ const [activeTabIndex, setActiveTabIndex] = useState(0);
29
+ const themeSettings = useThemeSettingsContext();
30
+
31
+ // Separate tab blocks from non-tab blocks (like headers)
32
+ const nonTabBlocks = section.blocks
33
+ .filter((block) => block.type !== 'tab' && (isDesigner ? true : !block.hidden))
34
+ .sort((a, b) => (a.order || 0) - (b.order || 0));
35
+
36
+ const tabBlocks = section.blocks
37
+ .filter((block) => block.type === 'tab' && (isDesigner ? true : !block.hidden))
38
+ .sort((a, b) => (a.order || 0) - (b.order || 0));
39
+
40
+ const getResponsiveValue = useCallback(
41
+ <T,>(value: unknown, fallback: T): T => {
42
+ if (value === undefined || value === null || value === '') return fallback;
43
+ if (typeof value === 'object' && !Array.isArray(value)) {
44
+ const responsiveValue = value as Record<string, T | undefined> & {
45
+ desktop?: T;
46
+ };
47
+ const matchedValue =
48
+ responsiveValue[currentBreakpoint] ?? responsiveValue.desktop;
49
+ return matchedValue !== undefined ? (matchedValue as T) : fallback;
50
+ }
51
+ return value as T;
52
+ },
53
+ [currentBreakpoint]
54
+ );
55
+
56
+ const findTabIndexBySelectedBlock = useCallback(
57
+ (blockId: string | null): number => {
58
+ if (!blockId) return -1;
59
+
60
+ const hasBlockInTree = (blocks?: Section['blocks']): boolean => {
61
+ if (!blocks || blocks.length === 0) return false;
62
+
63
+ for (const block of blocks) {
64
+ if (block.id === blockId) return true;
65
+ if (hasBlockInTree(block.blocks)) return true;
66
+ }
67
+
68
+ return false;
69
+ };
70
+
71
+ return tabBlocks.findIndex((tabBlock) => {
72
+ if (tabBlock.id === blockId) return true;
73
+ return hasBlockInTree(tabBlock.blocks);
74
+ });
75
+ },
76
+ [tabBlocks]
77
+ );
78
+
79
+ useEffect(() => {
80
+ if (tabBlocks.length === 0) {
81
+ if (activeTabIndex !== 0) setActiveTabIndex(0);
82
+ return;
83
+ }
84
+
85
+ const selectedTabIndex = findTabIndexBySelectedBlock(selectedBlockId);
86
+ if (selectedTabIndex >= 0) {
87
+ if (selectedTabIndex !== activeTabIndex) {
88
+ setActiveTabIndex(selectedTabIndex);
89
+ }
90
+ return;
91
+ }
92
+
93
+ if (activeTabIndex > tabBlocks.length - 1) {
94
+ setActiveTabIndex(tabBlocks.length - 1);
95
+ }
96
+ }, [
97
+ activeTabIndex,
98
+ findTabIndexBySelectedBlock,
99
+ selectedBlockId,
100
+ tabBlocks
101
+ ]);
102
+
103
+ const activeTabColor = resolveThemeCssVariables(
104
+ getResponsiveValue<string>(
105
+ section.styles?.['active-tab-color'],
106
+ 'var(--theme-primary)'
107
+ ),
108
+ themeSettings
109
+ );
110
+ const activeTabBorderColor = resolveThemeCssVariables(
111
+ getResponsiveValue<string>(
112
+ section.styles?.['active-tab-border-color'],
113
+ 'var(--theme-primary)'
114
+ ),
115
+ themeSettings
116
+ );
117
+ const activeTabBackgroundColor = resolveThemeCssVariables(
118
+ getResponsiveValue<string>(
119
+ section.styles?.['active-tab-background-color'],
120
+ 'transparent'
121
+ ),
122
+ themeSettings
123
+ );
124
+ const headerBorderBottom = resolveThemeCssVariables(
125
+ getResponsiveValue<string>(
126
+ section.styles?.['header-border-bottom'],
127
+ '1px solid #e5e7eb'
128
+ ),
129
+ themeSettings
130
+ );
131
+
132
+ const kebabToCamel = (str: string): string => {
133
+ return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
134
+ };
135
+
136
+ const getCSSProperties = (
137
+ properties?: Record<string, unknown>
138
+ ): React.CSSProperties => {
139
+ if (!properties) return {};
140
+
141
+ const styles: Record<string, string | number> = {};
142
+
143
+ Object.keys(properties).forEach((key) => {
144
+ const value = getResponsiveValue<string | number | undefined>(
145
+ properties[key],
146
+ undefined
147
+ );
148
+ if (value === undefined || value === null) return;
149
+
150
+ const camelKey = kebabToCamel(key);
151
+ let finalValue: string | number = value;
152
+
153
+ if (key.startsWith('padding-') || key.startsWith('margin-')) {
154
+ finalValue =
155
+ typeof value === 'number' ? `${value}px` : (value as string);
156
+ } else if (typeof value === 'string') {
157
+ finalValue = resolveThemeCssVariables(value, themeSettings);
158
+ }
159
+
160
+ styles[camelKey] = finalValue;
161
+ });
162
+
163
+ return styles as React.CSSProperties;
164
+ };
165
+
166
+ const sectionStyles = {
167
+ ...getCSSProperties(section.styles),
168
+ position: 'relative'
169
+ } as React.CSSProperties;
170
+
171
+ const containerStyles: Record<string, string | number> = {
172
+ ...(sectionStyles as Record<string, string | number>)
173
+ };
174
+
175
+ const maxWidth = getResponsiveValue(
176
+ section.styles?.['max-width'],
177
+ 'normal'
178
+ ) as string;
179
+ const maxWidthClass =
180
+ maxWidth === 'narrow'
181
+ ? 'max-w-4xl'
182
+ : maxWidth === 'normal'
183
+ ? 'max-w-7xl'
184
+ : '';
185
+ const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
186
+
187
+ if (tabBlocks.length === 0) {
188
+ return (
189
+ <div
190
+ className="p-4 text-gray-400 border border-dashed border-gray-300 rounded"
191
+ style={sectionStyles}
192
+ >
193
+ No tabs available
194
+ </div>
195
+ );
196
+ }
197
+
198
+ return (
199
+ <div
200
+ className={twMerge(
201
+ clsx(
202
+ 'relative z-10 group/blocks w-full',
203
+ hasMaxWidth && 'mx-auto',
204
+ maxWidthClass
205
+ )
206
+ )}
207
+ style={containerStyles as React.CSSProperties}
208
+ >
209
+ {/* Render non-tab blocks first (like FAQ Header) */}
210
+ {nonTabBlocks.map((block, index) => (
211
+ <ThemeBlock
212
+ key={block.id || `non-tab-block-${index}`}
213
+ block={block}
214
+ placeholderId={placeholderId}
215
+ sectionId={section.id}
216
+ isDesigner={isDesigner}
217
+ isSelected={selectedBlockId === block.id}
218
+ selectedBlockId={selectedBlockId}
219
+ onMoveUp={() => {
220
+ if (window.parent) {
221
+ window.parent.postMessage(
222
+ {
223
+ type: 'MOVE_BLOCK_UP',
224
+ data: {
225
+ placeholderId,
226
+ sectionId: section.id,
227
+ blockId: block.id
228
+ }
229
+ },
230
+ '*'
231
+ );
232
+ }
233
+ }}
234
+ onMoveDown={() => {
235
+ if (window.parent) {
236
+ window.parent.postMessage(
237
+ {
238
+ type: 'MOVE_BLOCK_DOWN',
239
+ data: {
240
+ placeholderId,
241
+ sectionId: section.id,
242
+ blockId: block.id
243
+ }
244
+ },
245
+ '*'
246
+ );
247
+ }
248
+ }}
249
+ onDuplicate={() => {
250
+ if (window.parent) {
251
+ window.parent.postMessage(
252
+ {
253
+ type: 'DUPLICATE_BLOCK',
254
+ data: {
255
+ placeholderId,
256
+ sectionId: section.id,
257
+ blockId: block.id
258
+ }
259
+ },
260
+ '*'
261
+ );
262
+ }
263
+ }}
264
+ onToggleVisibility={() => {
265
+ if (window.parent) {
266
+ window.parent.postMessage(
267
+ {
268
+ type: 'TOGGLE_BLOCK_VISIBILITY',
269
+ data: {
270
+ placeholderId,
271
+ sectionId: section.id,
272
+ blockId: block.id
273
+ }
274
+ },
275
+ '*'
276
+ );
277
+ }
278
+ }}
279
+ onDelete={() => {
280
+ if (window.parent) {
281
+ window.parent.postMessage(
282
+ {
283
+ type: 'DELETE_BLOCK',
284
+ data: {
285
+ placeholderId,
286
+ sectionId: section.id,
287
+ blockId: block.id
288
+ }
289
+ },
290
+ '*'
291
+ );
292
+ }
293
+ }}
294
+ onRename={(newLabel) => {
295
+ if (window.parent) {
296
+ window.parent.postMessage(
297
+ {
298
+ type: 'RENAME_BLOCK',
299
+ data: {
300
+ placeholderId,
301
+ sectionId: section.id,
302
+ blockId: block.id,
303
+ label: newLabel
304
+ }
305
+ },
306
+ '*'
307
+ );
308
+ }
309
+ }}
310
+ />
311
+ ))}
312
+
313
+ {/* Tab headers */}
314
+ <div
315
+ style={{
316
+ display: 'flex',
317
+ gap: '0px',
318
+ borderBottom: headerBorderBottom,
319
+ position: 'relative'
320
+ }}
321
+ >
322
+ {tabBlocks.map((tabBlock, index) => {
323
+ const headerBlock = tabBlock.blocks?.find(
324
+ (b) => b.label === 'Tab Header'
325
+ );
326
+
327
+ if (!headerBlock) return null;
328
+
329
+ const isActive = index === activeTabIndex;
330
+ const headerStyles = resolveThemeStyleObject(
331
+ getCSSStyles(headerBlock.styles || {}, themeSettings, currentBreakpoint),
332
+ themeSettings
333
+ );
334
+ const cleanHeaderStyles = {
335
+ ...headerStyles
336
+ } as Record<string, string | number>;
337
+ const headerTextColor = cleanHeaderStyles.color as string | undefined;
338
+
339
+ delete cleanHeaderStyles.border;
340
+ delete cleanHeaderStyles.borderBottom;
341
+ delete cleanHeaderStyles.backgroundColor;
342
+ delete cleanHeaderStyles.color;
343
+
344
+ return (
345
+ <div
346
+ key={tabBlock.id || `tab-header-${index}`}
347
+ className="tab-header-wrapper"
348
+ style={{
349
+ position: 'relative'
350
+ }}
351
+ >
352
+ {isDesigner && (
353
+ <div
354
+ style={{
355
+ position: 'absolute',
356
+ top: 0,
357
+ left: 0,
358
+ right: 0,
359
+ zIndex: 10,
360
+ pointerEvents: 'none',
361
+ height: '100%'
362
+ }}
363
+ >
364
+ <ThemeBlock
365
+ block={tabBlock}
366
+ placeholderId={placeholderId}
367
+ sectionId={section.id}
368
+ isDesigner={isDesigner}
369
+ isSelected={selectedBlockId === tabBlock.id}
370
+ selectedBlockId={selectedBlockId}
371
+ onMoveUp={() => {
372
+ if (window.parent) {
373
+ window.parent.postMessage(
374
+ {
375
+ type: 'MOVE_BLOCK_UP',
376
+ data: {
377
+ placeholderId,
378
+ sectionId: section.id,
379
+ blockId: tabBlock.id
380
+ }
381
+ },
382
+ '*'
383
+ );
384
+ }
385
+ }}
386
+ onMoveDown={() => {
387
+ if (window.parent) {
388
+ window.parent.postMessage(
389
+ {
390
+ type: 'MOVE_BLOCK_DOWN',
391
+ data: {
392
+ placeholderId,
393
+ sectionId: section.id,
394
+ blockId: tabBlock.id
395
+ }
396
+ },
397
+ '*'
398
+ );
399
+ }
400
+ }}
401
+ onDuplicate={() => {
402
+ if (window.parent) {
403
+ window.parent.postMessage(
404
+ {
405
+ type: 'DUPLICATE_BLOCK',
406
+ data: {
407
+ placeholderId,
408
+ sectionId: section.id,
409
+ blockId: tabBlock.id
410
+ }
411
+ },
412
+ '*'
413
+ );
414
+ }
415
+ }}
416
+ onToggleVisibility={() => {
417
+ if (window.parent) {
418
+ window.parent.postMessage(
419
+ {
420
+ type: 'TOGGLE_BLOCK_VISIBILITY',
421
+ data: {
422
+ placeholderId,
423
+ sectionId: section.id,
424
+ blockId: tabBlock.id
425
+ }
426
+ },
427
+ '*'
428
+ );
429
+ }
430
+ }}
431
+ onDelete={() => {
432
+ if (window.parent) {
433
+ window.parent.postMessage(
434
+ {
435
+ type: 'DELETE_BLOCK',
436
+ data: {
437
+ placeholderId,
438
+ sectionId: section.id,
439
+ blockId: tabBlock.id
440
+ }
441
+ },
442
+ '*'
443
+ );
444
+ }
445
+ }}
446
+ onRename={(newLabel) => {
447
+ if (window.parent) {
448
+ window.parent.postMessage(
449
+ {
450
+ type: 'RENAME_BLOCK',
451
+ data: {
452
+ placeholderId,
453
+ sectionId: section.id,
454
+ blockId: tabBlock.id,
455
+ label: newLabel
456
+ }
457
+ },
458
+ '*'
459
+ );
460
+ }
461
+ }}
462
+ />
463
+ </div>
464
+ )}
465
+
466
+ <div
467
+ style={{
468
+ ...cleanHeaderStyles,
469
+ cursor: 'pointer',
470
+ borderTop: 'none',
471
+ borderLeft: 'none',
472
+ borderRight: 'none',
473
+ borderBottom: isActive
474
+ ? `2px solid ${activeTabBorderColor}`
475
+ : '2px solid transparent',
476
+ backgroundColor: isActive
477
+ ? activeTabBackgroundColor
478
+ : 'transparent',
479
+ color: isActive
480
+ ? activeTabColor
481
+ : headerTextColor || 'inherit',
482
+ position: 'relative',
483
+ zIndex: 1
484
+ }}
485
+ onClick={() => setActiveTabIndex(index)}
486
+ >
487
+ {headerBlock.blocks
488
+ ?.filter((childBlock) => (isDesigner ? true : !childBlock.hidden))
489
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
490
+ .map((childBlock, childIndex) => {
491
+ // If tab is active, we want to override the child's color with the active tab color
492
+ // So we remove the color from the child's styles to let it inherit
493
+ const blockToRender =
494
+ isActive && childBlock.styles?.color
495
+ ? {
496
+ ...childBlock,
497
+ styles: {
498
+ ...childBlock.styles,
499
+ color: undefined
500
+ }
501
+ }
502
+ : childBlock;
503
+
504
+ return (
505
+ <ThemeBlock
506
+ key={childBlock.id || `header-child-${childIndex}`}
507
+ block={blockToRender}
508
+ placeholderId={placeholderId}
509
+ sectionId={section.id}
510
+ isDesigner={isDesigner}
511
+ isSelected={selectedBlockId === childBlock.id}
512
+ selectedBlockId={selectedBlockId}
513
+ currentBreakpoint={currentBreakpoint}
514
+ onMoveUp={undefined}
515
+ onMoveDown={undefined}
516
+ onDuplicate={undefined}
517
+ onToggleVisibility={undefined}
518
+ onDelete={undefined}
519
+ onRename={undefined}
520
+ />
521
+ );
522
+ })}
523
+ </div>
524
+ </div>
525
+ );
526
+ })}
527
+ </div>
528
+
529
+ <div>
530
+ {tabBlocks.map((tabBlock, index) => {
531
+ if (index !== activeTabIndex) return null;
532
+
533
+ const contentBlock = tabBlock.blocks?.find(
534
+ (b) => b.label === 'Tab Content'
535
+ );
536
+
537
+ if (!contentBlock) return null;
538
+
539
+ const contentStyles = resolveThemeStyleObject(
540
+ getCSSStyles(contentBlock.styles || {}, themeSettings, currentBreakpoint),
541
+ themeSettings
542
+ );
543
+
544
+ return (
545
+ <div
546
+ key={tabBlock.id || `tab-content-${index}`}
547
+ style={contentStyles}
548
+ >
549
+ {contentBlock.blocks
550
+ ?.filter((childBlock) => (isDesigner ? true : !childBlock.hidden))
551
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
552
+ .map((childBlock, childIndex) => (
553
+ <ThemeBlock
554
+ key={childBlock.id || `content-child-${childIndex}`}
555
+ block={childBlock}
556
+ placeholderId={placeholderId}
557
+ sectionId={section.id}
558
+ isDesigner={isDesigner}
559
+ isSelected={selectedBlockId === childBlock.id}
560
+ selectedBlockId={selectedBlockId}
561
+ currentBreakpoint={currentBreakpoint}
562
+ onMoveUp={undefined}
563
+ onMoveDown={undefined}
564
+ onDuplicate={undefined}
565
+ onToggleVisibility={undefined}
566
+ onDelete={undefined}
567
+ onRename={undefined}
568
+ />
569
+ ))}
570
+ </div>
571
+ );
572
+ })}
573
+ </div>
574
+ </div>
575
+ );
576
+ };
577
+
578
+ export default TabsSection;
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import blockRendererRegistry from './blocks/block-renderer-registry';
5
+ import { WithDesignerFeatures } from './components/with-designer-features';
6
+ import { isPublishWindowVisible } from './utils';
7
+
8
+ export interface Block {
9
+ id: string;
10
+ styleSourceId?: string;
11
+ type: string;
12
+ label: string;
13
+ value?: any;
14
+ properties: any;
15
+ styles: any;
16
+ order: number;
17
+ hidden: boolean;
18
+ locked?: boolean;
19
+ blocks?: Block[]; // Optional - only group blocks have nested blocks
20
+ isIterator?: boolean;
21
+ iteratorDataPath?: string;
22
+ }
23
+
24
+ interface ThemeBlockProps {
25
+ block: Block;
26
+ placeholderId: string;
27
+ selectedBlockId?: string | null;
28
+ sectionId: string;
29
+ isDesigner?: boolean;
30
+ isSelected?: boolean;
31
+ currentBreakpoint?: string;
32
+ onSelect?: (blockId: string) => void;
33
+ onMoveUp?: () => void;
34
+ onMoveDown?: () => void;
35
+ onDuplicate?: () => void;
36
+ onToggleVisibility?: () => void;
37
+ onDelete?: () => void;
38
+ onRename?: (newLabel: string) => void;
39
+ }
40
+
41
+ export default function ThemeBlock({
42
+ block,
43
+ placeholderId,
44
+ selectedBlockId,
45
+ sectionId,
46
+ isDesigner = false,
47
+ isSelected = false,
48
+ currentBreakpoint = 'desktop',
49
+ onSelect,
50
+ onMoveUp,
51
+ onMoveDown,
52
+ onDuplicate,
53
+ onToggleVisibility,
54
+ onDelete,
55
+ onRename
56
+ }: ThemeBlockProps) {
57
+ const BlockRenderer = blockRendererRegistry.getRenderer(block.type);
58
+
59
+ if (!BlockRenderer) {
60
+ return <div>Unknown block type: {block.type}</div>;
61
+ }
62
+
63
+ if (
64
+ !isDesigner &&
65
+ !isPublishWindowVisible(block.properties?.['publish-window'])
66
+ ) {
67
+ return null;
68
+ }
69
+
70
+ return (
71
+ <WithDesignerFeatures
72
+ block={block}
73
+ placeholderId={placeholderId}
74
+ sectionId={sectionId}
75
+ isDesigner={isDesigner}
76
+ isSelected={isSelected}
77
+ currentBreakpoint={currentBreakpoint}
78
+ onMoveUp={onMoveUp}
79
+ onMoveDown={onMoveDown}
80
+ onDuplicate={onDuplicate}
81
+ onToggleVisibility={onToggleVisibility}
82
+ onDelete={onDelete}
83
+ onRename={onRename}
84
+ >
85
+ <BlockRenderer
86
+ block={block}
87
+ placeholderId={placeholderId}
88
+ sectionId={sectionId}
89
+ isDesigner={isDesigner}
90
+ isSelected={isSelected}
91
+ selectedBlockId={selectedBlockId}
92
+ currentBreakpoint={currentBreakpoint}
93
+ onMoveUp={onMoveUp}
94
+ onMoveDown={onMoveDown}
95
+ onDuplicate={onDuplicate}
96
+ onToggleVisibility={onToggleVisibility}
97
+ onDelete={onDelete}
98
+ onRename={onRename}
99
+ />
100
+ </WithDesignerFeatures>
101
+ );
102
+ }