@akinon/pz-theme 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 (62) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +27 -0
  3. package/readme.md +23 -0
  4. package/src/blocks/accordion-block.tsx +136 -0
  5. package/src/blocks/block-renderer-registry.tsx +77 -0
  6. package/src/blocks/button-block.tsx +593 -0
  7. package/src/blocks/counter-block.tsx +348 -0
  8. package/src/blocks/divider-block.tsx +20 -0
  9. package/src/blocks/embed-block.tsx +208 -0
  10. package/src/blocks/group-block.tsx +116 -0
  11. package/src/blocks/hotspot-block.tsx +147 -0
  12. package/src/blocks/icon-block.tsx +230 -0
  13. package/src/blocks/image-block.tsx +142 -0
  14. package/src/blocks/image-gallery-block.tsx +269 -0
  15. package/src/blocks/input-block.tsx +123 -0
  16. package/src/blocks/link-block.tsx +216 -0
  17. package/src/blocks/lottie-block.tsx +325 -0
  18. package/src/blocks/map-block.tsx +89 -0
  19. package/src/blocks/slider-block.tsx +595 -0
  20. package/src/blocks/tab-block.tsx +10 -0
  21. package/src/blocks/text-block.tsx +52 -0
  22. package/src/blocks/video-block.tsx +122 -0
  23. package/src/components/action-toolbar.tsx +305 -0
  24. package/src/components/designer-overlay.tsx +74 -0
  25. package/src/components/with-designer-features.tsx +142 -0
  26. package/src/dynamic-font-loader.tsx +79 -0
  27. package/src/hooks/use-designer-features.tsx +100 -0
  28. package/src/hooks/use-visibility-context.ts +27 -0
  29. package/src/index.ts +21 -0
  30. package/src/placeholder-registry.ts +31 -0
  31. package/src/sections/before-after-section.tsx +245 -0
  32. package/src/sections/contact-form-section.tsx +564 -0
  33. package/src/sections/countdown-campaign-banner-section.tsx +433 -0
  34. package/src/sections/coupon-banner-section.tsx +710 -0
  35. package/src/sections/divider-section.tsx +62 -0
  36. package/src/sections/featured-product-spotlight-section.tsx +507 -0
  37. package/src/sections/find-in-store-section.tsx +1995 -0
  38. package/src/sections/hover-showcase-section.tsx +326 -0
  39. package/src/sections/image-hotspot-section.tsx +142 -0
  40. package/src/sections/installment-options-section.tsx +1065 -0
  41. package/src/sections/notification-banner-section.tsx +173 -0
  42. package/src/sections/order-tracking-lookup-section.tsx +1379 -0
  43. package/src/sections/posts-slider-section.tsx +472 -0
  44. package/src/sections/pre-order-launch-banner-section.tsx +687 -0
  45. package/src/sections/section-renderer-registry.tsx +89 -0
  46. package/src/sections/section-wrapper.tsx +135 -0
  47. package/src/sections/shipping-threshold-progress-section.tsx +586 -0
  48. package/src/sections/stats-counter-section.tsx +486 -0
  49. package/src/sections/tabs-section.tsx +578 -0
  50. package/src/theme-block.tsx +102 -0
  51. package/src/theme-page-context.tsx +27 -0
  52. package/src/theme-placeholder-client.tsx +218 -0
  53. package/src/theme-placeholder-wrapper.tsx +786 -0
  54. package/src/theme-placeholder.tsx +305 -0
  55. package/src/theme-section.tsx +1241 -0
  56. package/src/theme-settings-context.tsx +13 -0
  57. package/src/utils/index.ts +791 -0
  58. package/src/utils/iterator-utils.test.ts +224 -0
  59. package/src/utils/iterator-utils.ts +617 -0
  60. package/src/utils/page-context-discovery.ts +119 -0
  61. package/src/utils/publish-window.ts +86 -0
  62. package/src/utils/visibility-rules.ts +188 -0
@@ -0,0 +1,786 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useMemo } from 'react';
4
+ import ThemeSection, { Section } from './theme-section';
5
+ import {
6
+ registerPlaceholder,
7
+ unregisterPlaceholder
8
+ } from './placeholder-registry';
9
+ import { useThemePageContext } from './theme-page-context';
10
+ import { ThemeSettingsProvider } from './theme-settings-context';
11
+ import { generateThemeCSS } from './utils';
12
+ import { useVisibilityContext } from './hooks/use-visibility-context';
13
+ import { createPageContextDiscovery } from './utils/page-context-discovery';
14
+ import { applyVisibilityRulesToSections } from './utils/visibility-rules';
15
+ import { isPublishWindowVisible } from './utils/publish-window';
16
+ import { buildClientRequestUrl } from '@akinon/next/utils';
17
+
18
+ // Helper function to inject theme colors as CSS custom properties
19
+ const injectThemeColors = (settings: Record<string, unknown>) => {
20
+ if (!settings) return;
21
+
22
+ const root = document.documentElement;
23
+
24
+ // Primary Styling colors
25
+ if (settings.primaryColor) {
26
+ root.style.setProperty('--theme-primary', settings.primaryColor as string);
27
+ }
28
+ if (settings.secondaryColor) {
29
+ root.style.setProperty(
30
+ '--theme-secondary',
31
+ settings.secondaryColor as string
32
+ );
33
+ }
34
+ if (settings.color3) {
35
+ root.style.setProperty('--theme-color3', settings.color3 as string);
36
+ }
37
+ if (settings.color4) {
38
+ root.style.setProperty('--theme-color4', settings.color4 as string);
39
+ }
40
+ if (settings.backgroundColor) {
41
+ root.style.setProperty(
42
+ '--theme-background',
43
+ settings.backgroundColor as string
44
+ );
45
+ document.body.style.backgroundColor = settings.backgroundColor as string;
46
+ }
47
+
48
+ // Also set opacity values if needed
49
+ if (settings.primaryOpacity !== undefined) {
50
+ root.style.setProperty(
51
+ '--theme-primary-opacity',
52
+ String(settings.primaryOpacity)
53
+ );
54
+ }
55
+ if (settings.secondaryOpacity !== undefined) {
56
+ root.style.setProperty(
57
+ '--theme-secondary-opacity',
58
+ String(settings.secondaryOpacity)
59
+ );
60
+ }
61
+ if (settings.color3Opacity !== undefined) {
62
+ root.style.setProperty(
63
+ '--theme-color3-opacity',
64
+ String(settings.color3Opacity)
65
+ );
66
+ }
67
+ if (settings.color4Opacity !== undefined) {
68
+ root.style.setProperty(
69
+ '--theme-color4-opacity',
70
+ String(settings.color4Opacity)
71
+ );
72
+ }
73
+ if (settings.backgroundOpacity !== undefined) {
74
+ root.style.setProperty(
75
+ '--theme-background-opacity',
76
+ String(settings.backgroundOpacity)
77
+ );
78
+ }
79
+ if (settings.breadcrumbSeparator) {
80
+ root.style.setProperty(
81
+ '--theme-breadcrumb-separator',
82
+ `"${settings.breadcrumbSeparator}"`
83
+ );
84
+ root.style.setProperty('--theme-breadcrumb-icon-display', 'none');
85
+ } else {
86
+ root.style.removeProperty('--theme-breadcrumb-separator');
87
+ root.style.removeProperty('--theme-breadcrumb-icon-display');
88
+ }
89
+
90
+ if (settings.mainMenuSticky !== undefined) {
91
+ const isSticky = settings.mainMenuSticky;
92
+ root.style.setProperty(
93
+ '--theme-header-position',
94
+ isSticky ? 'sticky' : 'relative'
95
+ );
96
+ root.style.setProperty('--theme-header-top', isSticky ? '0' : 'auto');
97
+ root.style.setProperty('--theme-header-z-index', isSticky ? '40' : 'auto');
98
+
99
+ // Sticky positioning requires ancestors to not have overflow: hidden
100
+ // We force overflow-x to 'clip' (modern browsers) or 'visible' to fix this
101
+ if (isSticky) {
102
+ document.body.style.overflowX = 'clip';
103
+ } else {
104
+ document.body.style.overflowX = '';
105
+ }
106
+ }
107
+ };
108
+
109
+ // Helper function to load Google Font
110
+ const loadGoogleFont = (fontFamily: string, fontWeight?: string) => {
111
+ if (!fontFamily || fontFamily === 'inherit') return;
112
+
113
+ const cleanFontFamily = fontFamily.replace(/['"]/g, '').trim();
114
+
115
+ // Skip system fonts
116
+ const systemFonts = [
117
+ 'Arial',
118
+ 'Helvetica',
119
+ 'Times New Roman',
120
+ 'Georgia',
121
+ 'Courier New',
122
+ 'Verdana'
123
+ ];
124
+ if (systemFonts.includes(cleanFontFamily)) return;
125
+
126
+ const weights = fontWeight || '100;300;400;500;600;700;800;900';
127
+ const fontParam = `${cleanFontFamily.replace(/\s+/g, '+')}:wght@${weights}`;
128
+ const fontUrl = `https://fonts.googleapis.com/css2?family=${fontParam}&display=swap`;
129
+
130
+ const linkId = `google-font-${cleanFontFamily
131
+ .replace(/\s+/g, '-')
132
+ .toLowerCase()}`;
133
+
134
+ // Check if already loaded
135
+ if (document.getElementById(linkId)) return;
136
+
137
+ const link = document.createElement('link');
138
+ link.id = linkId;
139
+ link.href = fontUrl;
140
+ link.rel = 'stylesheet';
141
+ document.head.appendChild(link);
142
+ };
143
+
144
+ interface ThemePlaceholderWrapperProps {
145
+ slug: string;
146
+ initialSections: Section[];
147
+ initialPlaceholderId: string;
148
+ isDesignMode: boolean;
149
+ dataSources?: any[];
150
+ initialThemeSettings?: Record<string, unknown> | null;
151
+ initialPageContext?: Record<string, unknown> | null;
152
+ }
153
+
154
+ declare global {
155
+ interface Window {
156
+ __iframeReadySent?: boolean;
157
+ __productPickerPreviewListenerAttached?: boolean;
158
+ }
159
+ }
160
+
161
+ const postProductPickerResponse = (
162
+ requestId: string,
163
+ ok: boolean,
164
+ result?: unknown,
165
+ error?: string
166
+ ) => {
167
+ if (!window.parent) {
168
+ return;
169
+ }
170
+
171
+ window.parent.postMessage(
172
+ {
173
+ type: 'PRODUCT_PICKER_RESPONSE',
174
+ data: {
175
+ requestId,
176
+ ok,
177
+ result,
178
+ error
179
+ }
180
+ },
181
+ '*'
182
+ );
183
+ };
184
+
185
+ const attachProductPickerPreviewListener = () => {
186
+ if (
187
+ typeof window === 'undefined' ||
188
+ window.__productPickerPreviewListenerAttached
189
+ ) {
190
+ return;
191
+ }
192
+
193
+ window.__productPickerPreviewListenerAttached = true;
194
+
195
+ window.addEventListener('message', async (event) => {
196
+ const type = event.data?.type;
197
+ const requestId = event.data?.data?.requestId;
198
+
199
+ if (
200
+ ![
201
+ 'PRODUCT_PICKER_SEARCH',
202
+ 'PRODUCT_PICKER_RESOLVE_URL',
203
+ 'PRODUCT_PICKER_GET_PRODUCT'
204
+ ].includes(type) ||
205
+ typeof requestId !== 'string'
206
+ ) {
207
+ return;
208
+ }
209
+
210
+ try {
211
+ if (type === 'PRODUCT_PICKER_SEARCH') {
212
+ const query = String(event.data?.data?.query || '').trim();
213
+
214
+ if (!query) {
215
+ postProductPickerResponse(requestId, true, { groups: [] });
216
+ return;
217
+ }
218
+
219
+ const response = await fetch(
220
+ buildClientRequestUrl(
221
+ `/autocomplete/?search_text=${encodeURIComponent(query)}`
222
+ )
223
+ );
224
+
225
+ if (!response.ok) {
226
+ throw new Error(
227
+ `Autocomplete request failed with ${response.status}.`
228
+ );
229
+ }
230
+
231
+ postProductPickerResponse(requestId, true, await response.json());
232
+ return;
233
+ }
234
+
235
+ if (type === 'PRODUCT_PICKER_RESOLVE_URL') {
236
+ const path = String(event.data?.data?.path || '').trim();
237
+
238
+ if (!path) {
239
+ postProductPickerResponse(requestId, true, { results: [] });
240
+ return;
241
+ }
242
+
243
+ const response = await fetch(
244
+ buildClientRequestUrl(
245
+ `/pretty_urls/?new_path__exact=${encodeURIComponent(path)}`
246
+ )
247
+ );
248
+
249
+ if (!response.ok) {
250
+ throw new Error(`Pretty URL request failed with ${response.status}.`);
251
+ }
252
+
253
+ postProductPickerResponse(requestId, true, await response.json());
254
+ return;
255
+ }
256
+
257
+ const productPk = Number.parseInt(String(event.data?.data?.pk || ''), 10);
258
+ if (!Number.isFinite(productPk) || productPk <= 0) {
259
+ throw new Error('Product PK is invalid.');
260
+ }
261
+
262
+ const response = await fetch(
263
+ buildClientRequestUrl(`/product/${productPk}/`)
264
+ );
265
+
266
+ if (!response.ok) {
267
+ throw new Error(`Product request failed with ${response.status}.`);
268
+ }
269
+
270
+ postProductPickerResponse(requestId, true, await response.json());
271
+ } catch (error) {
272
+ postProductPickerResponse(
273
+ requestId,
274
+ false,
275
+ undefined,
276
+ error instanceof Error ? error.message : 'Preview request failed.'
277
+ );
278
+ }
279
+ });
280
+ };
281
+
282
+ export default function ThemePlaceholderWrapper({
283
+ slug,
284
+ initialSections,
285
+ initialPlaceholderId,
286
+ isDesignMode,
287
+ dataSources: initialDataSources = [],
288
+ initialThemeSettings = null,
289
+ initialPageContext = null
290
+ }: ThemePlaceholderWrapperProps) {
291
+ const inheritedPageContext = useThemePageContext();
292
+ const [sections, setSections] = useState<Section[]>(initialSections);
293
+ const [placeholderSlug, setPlaceholderSlug] =
294
+ useState<string>(initialPlaceholderId);
295
+ const [selectedSectionId, setSelectedSectionId] = useState<string | null>(
296
+ null
297
+ );
298
+ const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
299
+ const [currentBreakpoint, setCurrentBreakpoint] = useState<string>('desktop');
300
+ const [isDesigner, setIsDesigner] = useState(isDesignMode);
301
+ const [dataSources, setDataSources] = useState<any[]>(initialDataSources);
302
+ const [themeSettings, setThemeSettings] = useState<Record<
303
+ string,
304
+ unknown
305
+ > | null>(initialThemeSettings);
306
+ const resolvedPageContext = useMemo(() => {
307
+ if (!inheritedPageContext && !initialPageContext) {
308
+ return null;
309
+ }
310
+
311
+ if (!inheritedPageContext) {
312
+ return initialPageContext;
313
+ }
314
+
315
+ if (!initialPageContext) {
316
+ return inheritedPageContext;
317
+ }
318
+
319
+ return {
320
+ ...initialPageContext,
321
+ ...inheritedPageContext
322
+ };
323
+ }, [inheritedPageContext, initialPageContext]);
324
+
325
+ // Inject theme colors when settings change
326
+ useEffect(() => {
327
+ if (themeSettings) {
328
+ injectThemeColors(themeSettings);
329
+ }
330
+ }, [themeSettings]);
331
+
332
+ // Inject custom fonts from theme settings
333
+ useEffect(() => {
334
+ if (!themeSettings?.customFonts) return;
335
+ const customFonts = themeSettings.customFonts as {
336
+ name: string;
337
+ sources: { dataUrl: string; format: string }[];
338
+ }[];
339
+
340
+ customFonts.forEach((font) => {
341
+ if (
342
+ !font.name ||
343
+ !Array.isArray(font.sources) ||
344
+ font.sources.length === 0
345
+ )
346
+ return;
347
+
348
+ const styleId = `custom-font-${font.name
349
+ .replace(/\s+/g, '-')
350
+ .toLowerCase()}`;
351
+ const existing = document.getElementById(styleId);
352
+ if (existing) existing.remove(); // Re-inject in case sources changed
353
+
354
+ const src = font.sources
355
+ .map((s) => `url('${s.dataUrl}') format('${s.format}')`)
356
+ .join(', ');
357
+
358
+ const style = document.createElement('style');
359
+ style.id = styleId;
360
+ style.textContent = `@font-face { font-family: '${font.name}'; src: ${src}; font-display: swap; }`;
361
+ document.head.appendChild(style);
362
+ });
363
+ }, [themeSettings]);
364
+
365
+ useEffect(() => {
366
+ if (typeof window === 'undefined') return;
367
+ if (window.self === window.top || !window.parent) return;
368
+
369
+ window.parent.postMessage(
370
+ {
371
+ type: 'PAGE_CONTEXT_DISCOVERY',
372
+ data: {
373
+ discovery: createPageContextDiscovery(resolvedPageContext)
374
+ }
375
+ },
376
+ '*'
377
+ );
378
+ }, [resolvedPageContext]);
379
+
380
+ // Load fonts from initial sections on mount (for production site, not iframe)
381
+ useEffect(() => {
382
+ if (typeof window === 'undefined') return;
383
+
384
+ // Helper to extract and load fonts from blocks
385
+ const loadFontsFromBlocks = (blocks: Section['blocks']) => {
386
+ if (!blocks) return;
387
+
388
+ blocks.forEach((block) => {
389
+ const fontFamily =
390
+ block.styles?.['font-family']?.desktop ||
391
+ block.styles?.['font-family']?.mobile;
392
+ if (fontFamily && fontFamily !== 'inherit') {
393
+ loadGoogleFont(fontFamily);
394
+ }
395
+
396
+ // Recursively check nested blocks
397
+ if (block.blocks) {
398
+ loadFontsFromBlocks(block.blocks as Section['blocks']);
399
+ }
400
+ });
401
+ };
402
+
403
+ // Load fonts from all initial sections
404
+ initialSections.forEach((section) => {
405
+ loadFontsFromBlocks(section.blocks);
406
+ });
407
+ }, [initialSections]);
408
+
409
+ // Handle responsive breakpoint detection when not in iframe (e.g. production site)
410
+ useEffect(() => {
411
+ if (typeof window === 'undefined') return;
412
+
413
+ const checkIsInIframe = () => {
414
+ try {
415
+ return window.self !== window.top;
416
+ } catch (e) {
417
+ return true;
418
+ }
419
+ };
420
+
421
+ if (!checkIsInIframe()) {
422
+ const checkBreakpoint = () => {
423
+ const width = window.innerWidth;
424
+ // Use 1024px as the cutoff for desktop/mobile based on DEFAULT_BREAKPOINTS
425
+ const newBreakpoint = width < 1024 ? 'mobile' : 'desktop';
426
+
427
+ setCurrentBreakpoint((prev) => {
428
+ if (prev !== newBreakpoint) {
429
+ return newBreakpoint;
430
+ }
431
+ return prev;
432
+ });
433
+ };
434
+
435
+ let timeoutId: ReturnType<typeof setTimeout>;
436
+ const handleResize = () => {
437
+ clearTimeout(timeoutId);
438
+ timeoutId = setTimeout(checkBreakpoint, 200);
439
+ };
440
+
441
+ // Initial check
442
+ checkBreakpoint();
443
+
444
+ window.addEventListener('resize', handleResize);
445
+ return () => {
446
+ window.removeEventListener('resize', handleResize);
447
+ clearTimeout(timeoutId);
448
+ };
449
+ }
450
+ }, []);
451
+
452
+ useEffect(() => {
453
+ if (!isDesigner) return;
454
+ if (dataSources && dataSources.length > 0) {
455
+ setSections((prevSections) => {
456
+ return prevSections.map((section) => {
457
+ if (section.dataSourceId) {
458
+ const dataSource = dataSources.find(
459
+ (ds: any) => ds.id === section.dataSourceId
460
+ );
461
+
462
+ return {
463
+ ...section,
464
+ dataSource: dataSource || section.dataSource
465
+ };
466
+ }
467
+ return section;
468
+ });
469
+ });
470
+ }
471
+ }, [dataSources, isDesigner]);
472
+
473
+ useEffect(() => {
474
+ if (typeof window === 'undefined') {
475
+ return;
476
+ }
477
+
478
+ const checkIsInIframe = () => {
479
+ return window.self !== window.top;
480
+ };
481
+
482
+ const isInIframe = checkIsInIframe();
483
+
484
+ if (isInIframe && window.parent) {
485
+ attachProductPickerPreviewListener();
486
+
487
+ // Only send IFRAME_READY once per page session to prevent re-initialization
488
+ if (!window.__iframeReadySent) {
489
+ window.parent.postMessage(
490
+ {
491
+ type: 'IFRAME_READY'
492
+ },
493
+ '*'
494
+ );
495
+ window.__iframeReadySent = true;
496
+ }
497
+
498
+ registerPlaceholder(slug);
499
+ }
500
+
501
+ const handleMessage = (event: MessageEvent) => {
502
+ switch (event.data?.type) {
503
+ case 'SET_THEME_EDITOR_COOKIE':
504
+ if (event.data.data) {
505
+ setIsDesigner(true);
506
+ }
507
+ break;
508
+
509
+ case 'LOAD_THEME':
510
+ case 'UPDATE_THEME': {
511
+ const theme = event.data.data?.theme;
512
+
513
+ // Inject theme colors as CSS custom properties
514
+ if (theme?.settings) {
515
+ injectThemeColors(theme.settings);
516
+ setThemeSettings(theme.settings);
517
+ }
518
+
519
+ // Load fonts from typography settings
520
+ if (
521
+ theme?.settings?.typography &&
522
+ Array.isArray(theme.settings.typography)
523
+ ) {
524
+ const uniqueFonts = new Set<string>();
525
+ theme.settings.typography.forEach(
526
+ (variant: { fontFamily?: string }) => {
527
+ if (variant.fontFamily && variant.fontFamily !== 'inherit') {
528
+ uniqueFonts.add(variant.fontFamily);
529
+ }
530
+ }
531
+ );
532
+ uniqueFonts.forEach((font) => loadGoogleFont(font));
533
+ }
534
+
535
+ // Load fonts from block styles (for text blocks with custom fonts)
536
+ if (theme?.placeholders) {
537
+ // Recursive function to load fonts from nested blocks
538
+ const loadFontsFromBlocks = (blocks: any[]) => {
539
+ blocks?.forEach((block) => {
540
+ const fontFamily =
541
+ block.styles?.['font-family']?.desktop ||
542
+ block.styles?.['font-family']?.mobile;
543
+ if (fontFamily && fontFamily !== 'inherit') {
544
+ loadGoogleFont(fontFamily);
545
+ }
546
+ // Recursively check nested blocks
547
+ if (block.blocks && block.blocks.length > 0) {
548
+ loadFontsFromBlocks(block.blocks);
549
+ }
550
+ });
551
+ };
552
+
553
+ theme.placeholders.forEach(
554
+ (p: {
555
+ sections?: Array<{
556
+ blocks?: Array<any>;
557
+ }>;
558
+ }) => {
559
+ p.sections?.forEach((section) => {
560
+ loadFontsFromBlocks(section.blocks || []);
561
+ });
562
+ }
563
+ );
564
+ }
565
+
566
+ if (theme?.placeholders) {
567
+ const placeholder = theme.placeholders.find(
568
+ (p: { slug: string }) => p.slug === slug
569
+ );
570
+
571
+ if (placeholder) {
572
+ setPlaceholderSlug(placeholder.slug);
573
+
574
+ // Store dataSources in state for editor mode
575
+ if (theme.dataSources) {
576
+ setDataSources(theme.dataSources);
577
+ }
578
+
579
+ if (placeholder.sections) {
580
+ // In editor mode: Complete override with theme editor's data
581
+ // This allows unsaved sections to render with editor's product data
582
+ setSections(() => {
583
+ return placeholder.sections.map((incomingSection: any) => {
584
+ let dataSource = null;
585
+ if (incomingSection.dataSourceId && theme.dataSources) {
586
+ dataSource = theme.dataSources.find(
587
+ (ds: any) => ds.id === incomingSection.dataSourceId
588
+ );
589
+ }
590
+
591
+ // Deep clone the section to ensure React detects changes
592
+ // Especially important for nested blocks and styles
593
+ return {
594
+ ...incomingSection,
595
+ blocks: incomingSection.blocks
596
+ ? JSON.parse(JSON.stringify(incomingSection.blocks))
597
+ : [],
598
+ styles: incomingSection.styles
599
+ ? { ...incomingSection.styles }
600
+ : {},
601
+ properties: incomingSection.properties
602
+ ? { ...incomingSection.properties }
603
+ : {},
604
+ dataSource: dataSource || null
605
+ };
606
+ });
607
+ });
608
+ }
609
+ }
610
+ }
611
+ break;
612
+ }
613
+
614
+ case 'SELECT_SECTION': {
615
+ const { sectionId } = event.data.data || {};
616
+ if (sectionId) {
617
+ setSelectedSectionId(sectionId);
618
+ setSelectedBlockId(null);
619
+ }
620
+ break;
621
+ }
622
+
623
+ case 'SELECT_BLOCK': {
624
+ const { blockId } = event.data.data || {};
625
+ if (blockId) {
626
+ setSelectedBlockId(blockId);
627
+ setSelectedSectionId(null);
628
+ }
629
+ break;
630
+ }
631
+
632
+ case 'CHANGE_BREAKPOINT': {
633
+ const { breakpoint } = event.data.data || {};
634
+ if (breakpoint) {
635
+ setCurrentBreakpoint(breakpoint);
636
+ }
637
+ break;
638
+ }
639
+
640
+ case 'CLEAR_SELECTION': {
641
+ setSelectedSectionId(null);
642
+ setSelectedBlockId(null);
643
+ break;
644
+ }
645
+
646
+ case 'LOAD_FONT': {
647
+ const { fontFamily, fontWeight } = event.data.data || {};
648
+ if (fontFamily) {
649
+ loadGoogleFont(fontFamily, fontWeight);
650
+ }
651
+ break;
652
+ }
653
+
654
+ case 'LOAD_CUSTOM_FONT': {
655
+ const { name, sources } = event.data.data || {};
656
+ if (!name || !Array.isArray(sources) || sources.length === 0) break;
657
+
658
+ const styleId = `custom-font-${name
659
+ .replace(/\s+/g, '-')
660
+ .toLowerCase()}`;
661
+ if (document.getElementById(styleId)) break;
662
+
663
+ const src = sources
664
+ .map(
665
+ (s: { dataUrl: string; format: string }) =>
666
+ `url('${s.dataUrl}') format('${s.format}')`
667
+ )
668
+ .join(', ');
669
+
670
+ const style = document.createElement('style');
671
+ style.id = styleId;
672
+ style.textContent = `@font-face { font-family: '${name}'; src: ${src}; font-display: swap; }`;
673
+ document.head.appendChild(style);
674
+ break;
675
+ }
676
+ }
677
+ };
678
+
679
+ window.addEventListener('message', handleMessage);
680
+ return () => {
681
+ window.removeEventListener('message', handleMessage);
682
+ unregisterPlaceholder(slug);
683
+ };
684
+ }, [slug]);
685
+
686
+ const sendAction = (
687
+ action: string,
688
+ data: {
689
+ placeholderId?: string;
690
+ sectionId?: string;
691
+ blockId?: string;
692
+ label?: string;
693
+ }
694
+ ) => {
695
+ if (window.parent) {
696
+ window.parent.postMessage(
697
+ {
698
+ type: action,
699
+ data
700
+ },
701
+ '*'
702
+ );
703
+ }
704
+ };
705
+
706
+ const dynamicCSS = useMemo(
707
+ () =>
708
+ generateThemeCSS(sections, isDesigner ? currentBreakpoint : undefined),
709
+ [sections, isDesigner, currentBreakpoint]
710
+ );
711
+ const visibilityContext = useVisibilityContext(currentBreakpoint);
712
+ const renderedSections = useMemo(
713
+ () => applyVisibilityRulesToSections(sections, visibilityContext),
714
+ [sections, visibilityContext]
715
+ );
716
+
717
+ return (
718
+ <ThemeSettingsProvider value={themeSettings}>
719
+ {dynamicCSS && <style dangerouslySetInnerHTML={{ __html: dynamicCSS }} />}
720
+ <div
721
+ data-placeholder={slug}
722
+ className="theme-placeholder"
723
+ style={{ backgroundColor: 'var(--theme-background)' }}
724
+ >
725
+ {renderedSections
726
+ .sort((a, b) => a.order - b.order)
727
+ .filter(
728
+ (section) =>
729
+ !section.hidden &&
730
+ (isDesigner ||
731
+ isPublishWindowVisible(section.properties?.['publish-window']))
732
+ )
733
+ .map((section) => (
734
+ <ThemeSection
735
+ key={section.id}
736
+ section={section}
737
+ placeholderId={placeholderSlug}
738
+ pageContext={resolvedPageContext}
739
+ isDesigner={isDesigner}
740
+ isSelected={selectedSectionId === section.id}
741
+ selectedBlockId={selectedBlockId}
742
+ currentBreakpoint={currentBreakpoint}
743
+ onSelect={setSelectedSectionId}
744
+ onMoveUp={() =>
745
+ sendAction('MOVE_SECTION_UP', {
746
+ placeholderId: placeholderSlug,
747
+ sectionId: section.id
748
+ })
749
+ }
750
+ onMoveDown={() =>
751
+ sendAction('MOVE_SECTION_DOWN', {
752
+ placeholderId: placeholderSlug,
753
+ sectionId: section.id
754
+ })
755
+ }
756
+ onDuplicate={() =>
757
+ sendAction('DUPLICATE_SECTION', {
758
+ placeholderId: placeholderSlug,
759
+ sectionId: section.id
760
+ })
761
+ }
762
+ onToggleVisibility={() =>
763
+ sendAction('TOGGLE_SECTION_VISIBILITY', {
764
+ placeholderId: placeholderSlug,
765
+ sectionId: section.id
766
+ })
767
+ }
768
+ onDelete={() =>
769
+ sendAction('DELETE_SECTION', {
770
+ placeholderId: placeholderSlug,
771
+ sectionId: section.id
772
+ })
773
+ }
774
+ onRename={(newLabel) =>
775
+ sendAction('RENAME_SECTION', {
776
+ placeholderId: placeholderSlug,
777
+ sectionId: section.id,
778
+ label: newLabel
779
+ })
780
+ }
781
+ />
782
+ ))}
783
+ </div>
784
+ </ThemeSettingsProvider>
785
+ );
786
+ }