@akinon/pz-theme 2.0.35-rc.0 → 2.0.35

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 (39) hide show
  1. package/CHANGELOG.md +12 -28
  2. package/package.json +4 -3
  3. package/src/__tests__/theme-contract.test.ts +130 -0
  4. package/src/blocks/__tests__/text-block.test.tsx +8 -1
  5. package/src/blocks/__tests__/video-block.test.tsx +14 -7
  6. package/src/blocks/counter-block.tsx +44 -56
  7. package/src/blocks/group-block.tsx +23 -22
  8. package/src/blocks/hotspot-block.tsx +171 -80
  9. package/src/blocks/input-block.tsx +15 -17
  10. package/src/blocks/link-block.tsx +18 -21
  11. package/src/blocks/slider-block.tsx +15 -12
  12. package/src/components/with-designer-features.tsx +2 -0
  13. package/src/editor-bridge.ts +160 -0
  14. package/src/hooks/use-designer-features.tsx +30 -37
  15. package/src/placeholder-registry.ts +5 -7
  16. package/src/sections/__tests__/countdown-campaign-banner-section.test.tsx +8 -4
  17. package/src/sections/__tests__/coupon-banner-section.test.tsx +4 -2
  18. package/src/sections/before-after-section.tsx +189 -134
  19. package/src/sections/contact-form-section.tsx +185 -60
  20. package/src/sections/countdown-campaign-banner-section.tsx +73 -27
  21. package/src/sections/coupon-banner-section.tsx +152 -48
  22. package/src/sections/featured-product-spotlight-section.tsx +67 -66
  23. package/src/sections/find-in-store-section.tsx +37 -25
  24. package/src/sections/hover-showcase-section.tsx +221 -89
  25. package/src/sections/image-hotspot-section.tsx +208 -94
  26. package/src/sections/installment-options-section.tsx +268 -259
  27. package/src/sections/notification-banner-section.tsx +111 -25
  28. package/src/sections/order-tracking-lookup-section.tsx +272 -247
  29. package/src/sections/posts-slider-section.tsx +17 -16
  30. package/src/sections/pre-order-launch-banner-section.tsx +31 -50
  31. package/src/sections/section-wrapper.tsx +9 -10
  32. package/src/sections/shipping-threshold-progress-section.tsx +72 -29
  33. package/src/sections/stats-counter-section.tsx +68 -30
  34. package/src/sections/tabs-section.tsx +124 -147
  35. package/src/theme-placeholder-client.tsx +11 -14
  36. package/src/theme-placeholder-wrapper.tsx +42 -51
  37. package/src/theme-section.tsx +27 -34
  38. package/src/utils/__tests__/transform-styles.test.ts +97 -0
  39. package/src/utils/index.ts +9 -0
@@ -28,6 +28,8 @@ import {
28
28
  resolveThemeCssVariables
29
29
  } from '../utils';
30
30
 
31
+ import { postToEditor } from '../editor-bridge';
32
+ import { PreviewToEditorMessageType } from '@akinon/theme-contract';
31
33
  interface FindInStoreSectionProps {
32
34
  section: Section;
33
35
  currentBreakpoint?: string;
@@ -649,11 +651,15 @@ export default function FindInStoreSection({
649
651
  const trimmedInitialProduct = initialProductInput.trim();
650
652
  if (!trimmedInitialProduct) return;
651
653
 
652
- setProductUrlInput(currentValue => currentValue || trimmedInitialProduct);
654
+ setProductUrlInput(
655
+ (currentValue) => currentValue || trimmedInitialProduct
656
+ );
653
657
  setIsResolvingProduct(true);
654
658
 
655
659
  try {
656
- const resolvedPk = await resolveProductPkFromInput(trimmedInitialProduct);
660
+ const resolvedPk = await resolveProductPkFromInput(
661
+ trimmedInitialProduct
662
+ );
657
663
  if (!cancelled && resolvedPk) {
658
664
  setResolvedProductPk(resolvedPk);
659
665
  setSearchError('');
@@ -710,10 +716,10 @@ export default function FindInStoreSection({
710
716
  maxWidth === 'narrow'
711
717
  ? 'max-w-4xl'
712
718
  : maxWidth === 'normal'
713
- ? 'max-w-7xl'
714
- : maxWidth === 'full'
715
- ? 'w-full'
716
- : '';
719
+ ? 'max-w-7xl'
720
+ : maxWidth === 'full'
721
+ ? 'w-full'
722
+ : '';
717
723
  const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
718
724
 
719
725
  const filteredStyles = Object.fromEntries(
@@ -1045,20 +1051,21 @@ export default function FindInStoreSection({
1045
1051
  currentBreakpoint
1046
1052
  );
1047
1053
 
1048
- const postBlockAction = (type: string, blockId: string, label?: string) => {
1054
+ const postBlockAction = (
1055
+ type: PreviewToEditorMessageType,
1056
+ blockId: string,
1057
+ label?: string
1058
+ ) => {
1049
1059
  if (!window.parent) return;
1050
- window.parent.postMessage(
1051
- {
1052
- type,
1053
- data: {
1054
- placeholderId,
1055
- sectionId: section.id,
1056
- blockId,
1057
- ...(label ? { label } : {})
1058
- }
1059
- },
1060
- '*'
1061
- );
1060
+ postToEditor({
1061
+ type,
1062
+ data: {
1063
+ placeholderId,
1064
+ sectionId: section.id,
1065
+ blockId,
1066
+ ...(label ? { label } : {})
1067
+ }
1068
+ });
1062
1069
  };
1063
1070
 
1064
1071
  const renderBlock = (block: Block, keyOverride?: string) => {
@@ -1376,7 +1383,10 @@ export default function FindInStoreSection({
1376
1383
  const searchInputStyle = mergeControlStyles(inputStyle, searchInputBlock);
1377
1384
  const citySelectStyle = mergeControlStyles(inputStyle, citySelectBlock);
1378
1385
  const sizeSelectStyle = mergeControlStyles(inputStyle, sizeSelectBlock);
1379
- const searchButtonControlStyle = mergeControlStyles(buttonStyle, searchButtonBlock);
1386
+ const searchButtonControlStyle = mergeControlStyles(
1387
+ buttonStyle,
1388
+ searchButtonBlock
1389
+ );
1380
1390
  const availabilityButtonControlStyle = mergeControlStyles(
1381
1391
  buttonStyle,
1382
1392
  availabilityButtonBlock
@@ -1608,7 +1618,9 @@ export default function FindInStoreSection({
1608
1618
  <input
1609
1619
  type="text"
1610
1620
  value={productUrlInput}
1611
- onChange={(event) => setProductUrlInput(event.target.value)}
1621
+ onChange={(event) =>
1622
+ setProductUrlInput(event.target.value)
1623
+ }
1612
1624
  placeholder={urlPlaceholder}
1613
1625
  inputMode="text"
1614
1626
  style={searchInputStyle}
@@ -1732,7 +1744,7 @@ export default function FindInStoreSection({
1732
1744
  }
1733
1745
  )}
1734
1746
 
1735
- {sizeOptions.length > 0 && (
1747
+ {sizeOptions.length > 0 &&
1736
1748
  renderSelectableElement(
1737
1749
  sizeSelectBlock,
1738
1750
  <select
@@ -1756,8 +1768,7 @@ export default function FindInStoreSection({
1756
1768
  {
1757
1769
  keyOverride: 'size-select'
1758
1770
  }
1759
- )
1760
- )}
1771
+ )}
1761
1772
  </div>
1762
1773
 
1763
1774
  {renderSelectableElement(
@@ -1769,7 +1780,8 @@ export default function FindInStoreSection({
1769
1780
  onClick={handleSubmit}
1770
1781
  onMouseEnter={(event) => {
1771
1782
  if (!stockLoading && isProductResolved) {
1772
- event.currentTarget.style.backgroundColor = buttonHoverBg;
1783
+ event.currentTarget.style.backgroundColor =
1784
+ buttonHoverBg;
1773
1785
  }
1774
1786
  }}
1775
1787
  onMouseLeave={(event) => {
@@ -13,6 +13,8 @@ import {
13
13
  import { Section } from '../theme-section';
14
14
  import { WithDesignerFeatures } from '../components/with-designer-features';
15
15
 
16
+ import { postToEditor } from '../editor-bridge';
17
+ import { PreviewToEditorMessageType } from '@akinon/theme-contract';
16
18
  interface HoverShowcaseSectionProps {
17
19
  section: Section;
18
20
  currentBreakpoint?: string;
@@ -33,11 +35,13 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
33
35
  const themeSettings = useThemeSettingsContext();
34
36
 
35
37
  const [activeIndex, setActiveIndex] = useState(0);
36
-
38
+
37
39
  useEffect(() => {
38
40
  if (isDesigner && selectedBlockId && section.blocks) {
39
41
  const index = section.blocks.findIndex(
40
- (b: Block) => b.id === selectedBlockId || b.blocks?.some((child: Block) => child.id === selectedBlockId)
42
+ (b: Block) =>
43
+ b.id === selectedBlockId ||
44
+ b.blocks?.some((child: Block) => child.id === selectedBlockId)
41
45
  );
42
46
  if (index !== -1) {
43
47
  setActiveIndex(index);
@@ -45,17 +49,15 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
45
49
  }
46
50
  }, [isDesigner, selectedBlockId, section.blocks]);
47
51
 
48
- const resolveLocalizedValue = (
49
- value: unknown,
50
- fallback: string
51
- ): string => {
52
+ const resolveLocalizedValue = (value: unknown, fallback: string): string => {
52
53
  if (value == null) return fallback;
53
54
 
54
55
  if (typeof value === 'object' && !Array.isArray(value)) {
55
56
  const localized = value as Record<string, unknown>;
56
57
 
57
58
  if (localized[locale] != null) return String(localized[locale]);
58
- if (localized[defaultLocale] != null) return String(localized[defaultLocale]);
59
+ if (localized[defaultLocale] != null)
60
+ return String(localized[defaultLocale]);
59
61
 
60
62
  const responsive = getResponsiveValue(value, currentBreakpoint);
61
63
  if (responsive != null && typeof responsive !== 'object') {
@@ -79,7 +81,11 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
79
81
  return styles[kebabKey] ?? styles[camelKey];
80
82
  };
81
83
 
82
- const maxWidth = getResponsiveValue(getStyleEntry('max-width'), currentBreakpoint, 'normal');
84
+ const maxWidth = getResponsiveValue(
85
+ getStyleEntry('max-width'),
86
+ currentBreakpoint,
87
+ 'normal'
88
+ );
83
89
  const maxWidthClass =
84
90
  maxWidth === 'narrow'
85
91
  ? 'max-w-4xl'
@@ -89,43 +95,60 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
89
95
  const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
90
96
 
91
97
  const inactiveColor = resolveThemeCssVariables(
92
- String(getResponsiveValue(getStyleEntry('inactive-color'), currentBreakpoint, '#9ca3af')),
98
+ String(
99
+ getResponsiveValue(
100
+ getStyleEntry('inactive-color'),
101
+ currentBreakpoint,
102
+ '#9ca3af'
103
+ )
104
+ ),
93
105
  themeSettings
94
106
  );
95
107
  const activeColor = resolveThemeCssVariables(
96
- String(getResponsiveValue(getStyleEntry('active-color'), currentBreakpoint, '#111827')),
108
+ String(
109
+ getResponsiveValue(
110
+ getStyleEntry('active-color'),
111
+ currentBreakpoint,
112
+ '#111827'
113
+ )
114
+ ),
97
115
  themeSettings
98
116
  );
99
-
117
+
100
118
  const isMobile = currentBreakpoint === 'mobile';
101
119
 
102
- const items = (section.blocks || []).filter((b: Block) => b.type === 'hover-showcase-item');
120
+ const items = (section.blocks || []).filter(
121
+ (b: Block) => b.type === 'hover-showcase-item'
122
+ );
103
123
 
104
124
  if (items.length === 0) {
105
125
  if (isDesigner) {
106
126
  return (
107
127
  <div className="w-full flex items-center justify-center p-8 border border-dashed border-gray-300 rounded-lg">
108
- <p className="text-gray-500">Add Hover Showcase Item blocks to see them here.</p>
128
+ <p className="text-gray-500">
129
+ Add Hover Showcase Item blocks to see them here.
130
+ </p>
109
131
  </div>
110
132
  );
111
133
  }
112
134
  return null;
113
135
  }
114
136
 
115
- const sendDesignerMessage = (type: string, blockId: string, label?: string) => {
137
+ const sendDesignerMessage = (
138
+ type: PreviewToEditorMessageType,
139
+ blockId: string,
140
+ label?: string
141
+ ) => {
116
142
  if (isDesigner && window.parent) {
117
- window.parent.postMessage(
118
- {
119
- type,
120
- data: {
121
- placeholderId,
122
- sectionId: section.id,
123
- blockId,
124
- ...(label && { label })
125
- }
126
- },
127
- '*'
128
- );
143
+ postToEditor({
144
+ type,
145
+ data: {
146
+ placeholderId,
147
+ sectionId: section.id,
148
+ blockId,
149
+ ...(label && { label })
150
+ }
151
+ });
129
152
  }
130
153
  };
131
154
  const renderMobile = isDesigner ? isMobile : true;
@@ -134,23 +157,46 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
134
157
  return (
135
158
  <div
136
159
  className={`w-full mx-auto ${hasMaxWidth ? maxWidthClass : ''}`}
137
- style={getCSSStyles(section.styles as Record<string, unknown>, themeSettings as Record<string, unknown>, currentBreakpoint)}
160
+ style={getCSSStyles(
161
+ section.styles as Record<string, unknown>,
162
+ themeSettings as Record<string, unknown>,
163
+ currentBreakpoint
164
+ )}
138
165
  >
139
166
  {renderMobile && (
140
- <div className={`flex flex-col gap-6 px-4 ${isDesigner ? '' : 'block md:hidden'}`}>
167
+ <div
168
+ className={`flex flex-col gap-6 px-4 ${
169
+ isDesigner ? '' : 'block md:hidden'
170
+ }`}
171
+ >
141
172
  {items.map((item: Block, index: number) => {
142
173
  const props = item.properties || {};
143
174
  const itemStyles = item.styles || {};
144
- const title = resolveLocalizedValue(props.title, `Item ${index + 1}`);
175
+ const title = resolveLocalizedValue(
176
+ props.title,
177
+ `Item ${index + 1}`
178
+ );
145
179
  const subtitle = resolveLocalizedValue(props.subtitle, '');
146
180
  const link = resolveLocalizedValue(props.link, '');
147
181
  const isActive = index === activeIndex;
148
182
 
149
183
  // Inside mobile render block, force evaluation of values for 'mobile' instead of the generic currentBreakpoint
150
184
  const evalBreakpoint = isDesigner ? currentBreakpoint : 'mobile';
151
- const mobileLayout = getResponsiveValue(getStyleEntry('mobile-layout'), evalBreakpoint, 'list');
152
- const titleFontSize = getResponsiveValue(itemStyles['title-font-size'], evalBreakpoint, '24px');
153
- const subtitleFontSize = getResponsiveValue(itemStyles['subtitle-font-size'], evalBreakpoint, '14px');
185
+ const mobileLayout = getResponsiveValue(
186
+ getStyleEntry('mobile-layout'),
187
+ evalBreakpoint,
188
+ 'list'
189
+ );
190
+ const titleFontSize = getResponsiveValue(
191
+ itemStyles['title-font-size'],
192
+ evalBreakpoint,
193
+ '24px'
194
+ );
195
+ const subtitleFontSize = getResponsiveValue(
196
+ itemStyles['subtitle-font-size'],
197
+ evalBreakpoint,
198
+ '14px'
199
+ );
154
200
 
155
201
  return (
156
202
  <WithDesignerFeatures
@@ -163,27 +209,70 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
163
209
  currentBreakpoint={currentBreakpoint}
164
210
  className="flex flex-col gap-3 cursor-pointer p-2 rounded relative"
165
211
  onMoveUp={() => sendDesignerMessage('MOVE_BLOCK_UP', item.id)}
166
- onMoveDown={() => sendDesignerMessage('MOVE_BLOCK_DOWN', item.id)}
167
- onDuplicate={() => sendDesignerMessage('DUPLICATE_BLOCK', item.id)}
168
- onToggleVisibility={() => sendDesignerMessage('TOGGLE_BLOCK_VISIBILITY', item.id)}
212
+ onMoveDown={() =>
213
+ sendDesignerMessage('MOVE_BLOCK_DOWN', item.id)
214
+ }
215
+ onDuplicate={() =>
216
+ sendDesignerMessage('DUPLICATE_BLOCK', item.id)
217
+ }
218
+ onToggleVisibility={() =>
219
+ sendDesignerMessage('TOGGLE_BLOCK_VISIBILITY', item.id)
220
+ }
169
221
  onDelete={() => sendDesignerMessage('DELETE_BLOCK', item.id)}
170
- onRename={(newLabel) => sendDesignerMessage('RENAME_BLOCK', item.id, newLabel)}
222
+ onRename={(newLabel) =>
223
+ sendDesignerMessage('RENAME_BLOCK', item.id, newLabel)
224
+ }
171
225
  >
172
- <div className="flex flex-col" onClick={() => setActiveIndex(index)}>
226
+ <div
227
+ className="flex flex-col"
228
+ onClick={() => setActiveIndex(index)}
229
+ >
173
230
  {link ? (
174
231
  <a href={link} className="decoration-none">
175
- <h3 style={{ fontSize: String(titleFontSize), color: activeColor, transition: 'color 0.3s' }} className="font-semibold leading-tight m-0">{title}</h3>
232
+ <h3
233
+ style={{
234
+ fontSize: String(titleFontSize),
235
+ color: activeColor,
236
+ transition: 'color 0.3s'
237
+ }}
238
+ className="font-semibold leading-tight m-0"
239
+ >
240
+ {title}
241
+ </h3>
176
242
  </a>
177
243
  ) : (
178
- <h3 style={{ fontSize: String(titleFontSize), color: activeColor, transition: 'color 0.3s' }} className="font-semibold leading-tight m-0">{title}</h3>
244
+ <h3
245
+ style={{
246
+ fontSize: String(titleFontSize),
247
+ color: activeColor,
248
+ transition: 'color 0.3s'
249
+ }}
250
+ className="font-semibold leading-tight m-0"
251
+ >
252
+ {title}
253
+ </h3>
179
254
  )}
180
255
  {subtitle && (
181
- <p style={{ fontSize: String(subtitleFontSize), color: inactiveColor }} className="mt-1 mb-0 opacity-80">{subtitle}</p>
256
+ <p
257
+ style={{
258
+ fontSize: String(subtitleFontSize),
259
+ color: inactiveColor
260
+ }}
261
+ className="mt-1 mb-0 opacity-80"
262
+ >
263
+ {subtitle}
264
+ </p>
182
265
  )}
183
266
  </div>
184
-
267
+
185
268
  {mobileLayout === 'accordion' ? (
186
- <div className={`overflow-hidden transition-all duration-300 ${isActive ? 'max-h-[600px] opacity-100 mt-2' : 'max-h-0 opacity-0'}`}>
269
+ <div
270
+ className={`overflow-hidden transition-all duration-300 ${
271
+ isActive
272
+ ? 'max-h-[600px] opacity-100 mt-2'
273
+ : 'max-h-0 opacity-0'
274
+ }`}
275
+ >
187
276
  {item.blocks?.map((block: Block) => (
188
277
  <ThemeBlock
189
278
  key={block.id}
@@ -197,7 +286,7 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
197
286
  ))}
198
287
  </div>
199
288
  ) : (
200
- <div className="mt-2">
289
+ <div className="mt-2">
201
290
  {item.blocks?.map((block: Block) => (
202
291
  <ThemeBlock
203
292
  key={block.id}
@@ -218,21 +307,44 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
218
307
  )}
219
308
 
220
309
  {renderDesktop && (
221
- <div className={`px-4 md:px-8 lg:px-12 items-center min-h-[500px] ${isDesigner ? 'flex' : 'hidden md:flex'}`}>
222
- <div className="w-1/2 flex flex-col justify-center gap-1 relative z-10 pr-8">
310
+ <div
311
+ className={`px-4 md:px-8 lg:px-12 items-center min-h-[500px] ${
312
+ isDesigner ? 'flex' : 'hidden md:flex'
313
+ }`}
314
+ >
315
+ <div className="w-1/2 flex flex-col justify-center gap-1 relative z-10 pr-8">
223
316
  {items.map((item: Block, index: number) => {
224
317
  const props = item.properties || {};
225
318
  const itemStyles = item.styles || {};
226
- const title = resolveLocalizedValue(props.title, `Item ${index + 1}`);
319
+ const title = resolveLocalizedValue(
320
+ props.title,
321
+ `Item ${index + 1}`
322
+ );
227
323
  const subtitle = resolveLocalizedValue(props.subtitle, '');
228
324
  const link = resolveLocalizedValue(props.link, '');
229
325
  const isActive = index === activeIndex;
230
326
 
231
327
  const evalBreakpoint = isDesigner ? currentBreakpoint : 'desktop';
232
- const titleFontSize = getResponsiveValue(itemStyles['title-font-size'], evalBreakpoint, '32px');
233
- const subtitleFontSize = getResponsiveValue(itemStyles['subtitle-font-size'], evalBreakpoint, '16px');
234
- const pt = getResponsiveValue(itemStyles['padding-top'], evalBreakpoint, 16);
235
- const pb = getResponsiveValue(itemStyles['padding-bottom'], evalBreakpoint, 16);
328
+ const titleFontSize = getResponsiveValue(
329
+ itemStyles['title-font-size'],
330
+ evalBreakpoint,
331
+ '32px'
332
+ );
333
+ const subtitleFontSize = getResponsiveValue(
334
+ itemStyles['subtitle-font-size'],
335
+ evalBreakpoint,
336
+ '16px'
337
+ );
338
+ const pt = getResponsiveValue(
339
+ itemStyles['padding-top'],
340
+ evalBreakpoint,
341
+ 16
342
+ );
343
+ const pb = getResponsiveValue(
344
+ itemStyles['padding-bottom'],
345
+ evalBreakpoint,
346
+ 16
347
+ );
236
348
 
237
349
  return (
238
350
  <WithDesignerFeatures
@@ -246,42 +358,60 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
246
358
  className="flex flex-col cursor-pointer group w-max rounded px-2 relative"
247
359
  style={{ paddingTop: `${pt}px`, paddingBottom: `${pb}px` }}
248
360
  onMoveUp={() => sendDesignerMessage('MOVE_BLOCK_UP', item.id)}
249
- onMoveDown={() => sendDesignerMessage('MOVE_BLOCK_DOWN', item.id)}
250
- onDuplicate={() => sendDesignerMessage('DUPLICATE_BLOCK', item.id)}
251
- onToggleVisibility={() => sendDesignerMessage('TOGGLE_BLOCK_VISIBILITY', item.id)}
361
+ onMoveDown={() =>
362
+ sendDesignerMessage('MOVE_BLOCK_DOWN', item.id)
363
+ }
364
+ onDuplicate={() =>
365
+ sendDesignerMessage('DUPLICATE_BLOCK', item.id)
366
+ }
367
+ onToggleVisibility={() =>
368
+ sendDesignerMessage('TOGGLE_BLOCK_VISIBILITY', item.id)
369
+ }
252
370
  onDelete={() => sendDesignerMessage('DELETE_BLOCK', item.id)}
253
- onRename={(newLabel) => sendDesignerMessage('RENAME_BLOCK', item.id, newLabel)}
371
+ onRename={(newLabel) =>
372
+ sendDesignerMessage('RENAME_BLOCK', item.id, newLabel)
373
+ }
254
374
  >
255
- <div
375
+ <div
256
376
  className="flex flex-col relative w-full"
257
377
  onMouseEnter={() => setActiveIndex(index)}
258
378
  >
259
379
  {link ? (
260
380
  <a href={link} className="decoration-none">
261
- <h3
262
- style={{ fontSize: String(titleFontSize), color: isActive ? activeColor : inactiveColor, transition: 'color 0.4s ease' }}
381
+ <h3
382
+ style={{
383
+ fontSize: String(titleFontSize),
384
+ color: isActive ? activeColor : inactiveColor,
385
+ transition: 'color 0.4s ease'
386
+ }}
263
387
  className="font-semibold leading-tight m-0 hover:opacity-80"
264
388
  >
265
389
  {title}
266
390
  </h3>
267
391
  </a>
268
392
  ) : (
269
- <h3
270
- style={{ fontSize: String(titleFontSize), color: isActive ? activeColor : inactiveColor, transition: 'color 0.4s ease' }}
393
+ <h3
394
+ style={{
395
+ fontSize: String(titleFontSize),
396
+ color: isActive ? activeColor : inactiveColor,
397
+ transition: 'color 0.4s ease'
398
+ }}
271
399
  className="font-semibold leading-tight m-0"
272
400
  >
273
401
  {title}
274
402
  </h3>
275
403
  )}
276
404
  {subtitle && (
277
- <p
278
- style={{
279
- fontSize: String(subtitleFontSize),
280
- color: inactiveColor,
281
- opacity: isActive ? 1 : 0,
405
+ <p
406
+ style={{
407
+ fontSize: String(subtitleFontSize),
408
+ color: inactiveColor,
409
+ opacity: isActive ? 1 : 0,
282
410
  maxHeight: isActive ? '40px' : '0px',
283
- transform: isActive ? 'translateY(0)' : 'translateY(-10px)'
284
- }}
411
+ transform: isActive
412
+ ? 'translateY(0)'
413
+ : 'translateY(-10px)'
414
+ }}
285
415
  className="mt-2 mb-0 overflow-hidden transition-all duration-400 ease-out"
286
416
  >
287
417
  {subtitle}
@@ -291,32 +421,34 @@ const HoverShowcaseSection: React.FC<HoverShowcaseSectionProps> = ({
291
421
  </WithDesignerFeatures>
292
422
  );
293
423
  })}
294
- </div>
424
+ </div>
295
425
 
296
- <div className="w-1/2 relative h-full min-h-[400px] flex items-center justify-center">
426
+ <div className="w-1/2 relative h-full min-h-[400px] flex items-center justify-center">
297
427
  {items.map((item: Block, index: number) => {
298
- const isActive = index === activeIndex;
299
-
300
- return (
301
- <div
302
- key={item.id}
303
- className={`absolute inset-0 transition-opacity duration-500 ease-in-out flex items-center justify-center ${isActive ? 'opacity-100 z-10' : 'opacity-0 z-0'}`}
304
- >
305
- {item.blocks?.map((block: Block) => (
306
- <ThemeBlock
307
- key={block.id}
308
- block={block}
309
- currentBreakpoint={currentBreakpoint}
310
- isDesigner={isDesigner}
311
- placeholderId={placeholderId}
312
- sectionId={section.id}
313
- selectedBlockId={selectedBlockId}
314
- />
315
- ))}
316
- </div>
317
- );
428
+ const isActive = index === activeIndex;
429
+
430
+ return (
431
+ <div
432
+ key={item.id}
433
+ className={`absolute inset-0 transition-opacity duration-500 ease-in-out flex items-center justify-center ${
434
+ isActive ? 'opacity-100 z-10' : 'opacity-0 z-0'
435
+ }`}
436
+ >
437
+ {item.blocks?.map((block: Block) => (
438
+ <ThemeBlock
439
+ key={block.id}
440
+ block={block}
441
+ currentBreakpoint={currentBreakpoint}
442
+ isDesigner={isDesigner}
443
+ placeholderId={placeholderId}
444
+ sectionId={section.id}
445
+ selectedBlockId={selectedBlockId}
446
+ />
447
+ ))}
448
+ </div>
449
+ );
318
450
  })}
319
- </div>
451
+ </div>
320
452
  </div>
321
453
  )}
322
454
  </div>