@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
@@ -10,6 +10,7 @@ import { safeDeepClone } from '../utils/safe-clone';
10
10
  import { Section } from '../theme-section';
11
11
  import { getCSSStyles, getResponsiveValue } from '../utils';
12
12
 
13
+ import { postToEditor } from '../editor-bridge';
13
14
  interface PreOrderLaunchBannerSectionProps {
14
15
  section: Section;
15
16
  currentBreakpoint?: string;
@@ -497,9 +498,7 @@ export default function PreOrderLaunchBannerSection({
497
498
  properties: block.properties
498
499
  ? { ...block.properties }
499
500
  : block.properties,
500
- styles: block.styles
501
- ? safeDeepClone(block.styles)
502
- : block.styles,
501
+ styles: block.styles ? safeDeepClone(block.styles) : block.styles,
503
502
  blocks: undefined
504
503
  };
505
504
 
@@ -611,73 +610,55 @@ export default function PreOrderLaunchBannerSection({
611
610
  currentBreakpoint={currentBreakpoint}
612
611
  onMoveUp={() => {
613
612
  if (window.parent) {
614
- window.parent.postMessage(
615
- {
616
- type: 'MOVE_BLOCK_UP',
617
- data: { placeholderId, sectionId: section.id, blockId: block.id }
618
- },
619
- '*'
620
- );
613
+ postToEditor({
614
+ type: 'MOVE_BLOCK_UP',
615
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
616
+ });
621
617
  }
622
618
  }}
623
619
  onMoveDown={() => {
624
620
  if (window.parent) {
625
- window.parent.postMessage(
626
- {
627
- type: 'MOVE_BLOCK_DOWN',
628
- data: { placeholderId, sectionId: section.id, blockId: block.id }
629
- },
630
- '*'
631
- );
621
+ postToEditor({
622
+ type: 'MOVE_BLOCK_DOWN',
623
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
624
+ });
632
625
  }
633
626
  }}
634
627
  onDuplicate={() => {
635
628
  if (window.parent) {
636
- window.parent.postMessage(
637
- {
638
- type: 'DUPLICATE_BLOCK',
639
- data: { placeholderId, sectionId: section.id, blockId: block.id }
640
- },
641
- '*'
642
- );
629
+ postToEditor({
630
+ type: 'DUPLICATE_BLOCK',
631
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
632
+ });
643
633
  }
644
634
  }}
645
635
  onToggleVisibility={() => {
646
636
  if (window.parent) {
647
- window.parent.postMessage(
648
- {
649
- type: 'TOGGLE_BLOCK_VISIBILITY',
650
- data: { placeholderId, sectionId: section.id, blockId: block.id }
651
- },
652
- '*'
653
- );
637
+ postToEditor({
638
+ type: 'TOGGLE_BLOCK_VISIBILITY',
639
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
640
+ });
654
641
  }
655
642
  }}
656
643
  onDelete={() => {
657
644
  if (window.parent) {
658
- window.parent.postMessage(
659
- {
660
- type: 'DELETE_BLOCK',
661
- data: { placeholderId, sectionId: section.id, blockId: block.id }
662
- },
663
- '*'
664
- );
645
+ postToEditor({
646
+ type: 'DELETE_BLOCK',
647
+ data: { placeholderId, sectionId: section.id, blockId: block.id }
648
+ });
665
649
  }
666
650
  }}
667
651
  onRename={(newLabel) => {
668
652
  if (window.parent) {
669
- window.parent.postMessage(
670
- {
671
- type: 'RENAME_BLOCK',
672
- data: {
673
- placeholderId,
674
- sectionId: section.id,
675
- blockId: block.id,
676
- label: newLabel
677
- }
678
- },
679
- '*'
680
- );
653
+ postToEditor({
654
+ type: 'RENAME_BLOCK',
655
+ data: {
656
+ placeholderId,
657
+ sectionId: section.id,
658
+ blockId: block.id,
659
+ label: newLabel
660
+ }
661
+ });
681
662
  }
682
663
  }}
683
664
  />
@@ -6,6 +6,7 @@ import { twMerge } from 'tailwind-merge';
6
6
  import clsx from 'clsx';
7
7
  import { Section } from '../theme-section';
8
8
 
9
+ import { isTrustedEditorEvent, postToEditor } from '../editor-bridge';
9
10
  interface SectionWrapperProps {
10
11
  section: Section;
11
12
  placeholderId: string;
@@ -67,6 +68,7 @@ export default function SectionWrapper({
67
68
  if (!isDesigner) return;
68
69
 
69
70
  const handleMessage = (event: MessageEvent) => {
71
+ if (!isTrustedEditorEvent(event)) return;
70
72
  if (
71
73
  event.data?.type === 'SCROLL_TO_SECTION' &&
72
74
  event.data?.data?.sectionId === section.id
@@ -88,16 +90,13 @@ export default function SectionWrapper({
88
90
  onSelect(section.id);
89
91
 
90
92
  if (window.parent) {
91
- window.parent.postMessage(
92
- {
93
- type: 'SELECT_SECTION',
94
- data: {
95
- placeholderId,
96
- sectionId: section.id
97
- }
98
- },
99
- '*'
100
- );
93
+ postToEditor({
94
+ type: 'SELECT_SECTION',
95
+ data: {
96
+ placeholderId,
97
+ sectionId: section.id
98
+ }
99
+ });
101
100
  }
102
101
  }
103
102
  };
@@ -18,6 +18,8 @@ import {
18
18
  escapeCssAttributeValue
19
19
  } from '../utils/css-safety';
20
20
 
21
+ import { postToEditor } from '../editor-bridge';
22
+ import { PreviewToEditorMessageType } from '@akinon/theme-contract';
21
23
  interface ShippingThresholdProgressSectionProps {
22
24
  section: Section;
23
25
  currentBreakpoint?: string;
@@ -132,7 +134,11 @@ const toResponsiveStyle = (
132
134
  existing: unknown,
133
135
  nextValue: string
134
136
  ): Record<string, string> => {
135
- if (typeof existing === 'object' && existing !== null && !Array.isArray(existing)) {
137
+ if (
138
+ typeof existing === 'object' &&
139
+ existing !== null &&
140
+ !Array.isArray(existing)
141
+ ) {
136
142
  const existingObject = existing as Record<string, unknown>;
137
143
  const responsiveKeys = Object.keys(existingObject).filter((key) =>
138
144
  ['desktop', 'tablet', 'mobile'].includes(key)
@@ -287,7 +293,11 @@ const ShippingThresholdProgressSection: React.FC<
287
293
  );
288
294
 
289
295
  const useLiveBasket = parseBoolean(
290
- getResponsiveValue(section.properties?.['use-live-basket'], currentBreakpoint, true),
296
+ getResponsiveValue(
297
+ section.properties?.['use-live-basket'],
298
+ currentBreakpoint,
299
+ true
300
+ ),
291
301
  true
292
302
  );
293
303
 
@@ -297,7 +307,11 @@ const ShippingThresholdProgressSection: React.FC<
297
307
 
298
308
  const targetAmount = Math.max(
299
309
  parseAmount(
300
- getResponsiveValue(section.properties?.['target-amount'], currentBreakpoint, 1000),
310
+ getResponsiveValue(
311
+ section.properties?.['target-amount'],
312
+ currentBreakpoint,
313
+ 1000
314
+ ),
301
315
  1000
302
316
  ),
303
317
  1
@@ -326,8 +340,11 @@ const ShippingThresholdProgressSection: React.FC<
326
340
 
327
341
  const currencySymbol =
328
342
  String(
329
- getResponsiveValue(section.properties?.['currency-symbol'], currentBreakpoint, 'TL') ||
343
+ getResponsiveValue(
344
+ section.properties?.['currency-symbol'],
345
+ currentBreakpoint,
330
346
  'TL'
347
+ ) || 'TL'
331
348
  ).trim() || 'TL';
332
349
 
333
350
  const tokenContext = {
@@ -379,13 +396,21 @@ const ShippingThresholdProgressSection: React.FC<
379
396
  ) || 'Continue to checkout'
380
397
  );
381
398
  const showStartLabel = parseBoolean(
382
- getResponsiveValue(section.properties?.['show-start-label'], currentBreakpoint, false),
399
+ getResponsiveValue(
400
+ section.properties?.['show-start-label'],
401
+ currentBreakpoint,
402
+ false
403
+ ),
383
404
  false
384
405
  );
385
406
 
386
407
  const progressTrackColor = resolveThemeCssVariables(
387
408
  String(
388
- getResponsiveValue(section.styles?.['progress-track-color'], currentBreakpoint, '#e2e8f0')
409
+ getResponsiveValue(
410
+ section.styles?.['progress-track-color'],
411
+ currentBreakpoint,
412
+ '#e2e8f0'
413
+ )
389
414
  ),
390
415
  themeSettings
391
416
  );
@@ -412,7 +437,11 @@ const ShippingThresholdProgressSection: React.FC<
412
437
  const progressBarHeight = Math.min(
413
438
  Math.max(
414
439
  parseAmount(
415
- getResponsiveValue(section.styles?.['progress-bar-height'], currentBreakpoint, 14),
440
+ getResponsiveValue(
441
+ section.styles?.['progress-bar-height'],
442
+ currentBreakpoint,
443
+ 14
444
+ ),
416
445
  14
417
446
  ),
418
447
  8
@@ -448,10 +477,11 @@ const ShippingThresholdProgressSection: React.FC<
448
477
  progressPercent
449
478
  );
450
479
 
451
- const { fillBlockId, markerBlockId, wrapperBlockId, startLabelBlockId } = useMemo(
452
- () => findProgressBlockIds(sortedBlocks, currentBreakpoint),
453
- [sortedBlocks, currentBreakpoint]
454
- );
480
+ const { fillBlockId, markerBlockId, wrapperBlockId, startLabelBlockId } =
481
+ useMemo(
482
+ () => findProgressBlockIds(sortedBlocks, currentBreakpoint),
483
+ [sortedBlocks, currentBreakpoint]
484
+ );
455
485
 
456
486
  const dynamicProgressCss = useMemo(() => {
457
487
  const rules: string[] = [];
@@ -477,12 +507,16 @@ const ShippingThresholdProgressSection: React.FC<
477
507
 
478
508
  if (markerBlockId) {
479
509
  const escapedMarkerId = escapeCssAttributeValue(markerBlockId);
480
- rules.push(`[data-block-id="${escapedMarkerId}"]{display:none !important;}`);
510
+ rules.push(
511
+ `[data-block-id="${escapedMarkerId}"]{display:none !important;}`
512
+ );
481
513
  }
482
514
 
483
515
  if (startLabelBlockId && !showStartLabel) {
484
516
  const escapedStartLabelId = escapeCssAttributeValue(startLabelBlockId);
485
- rules.push(`[data-block-id="${escapedStartLabelId}"]{display:none !important;}`);
517
+ rules.push(
518
+ `[data-block-id="${escapedStartLabelId}"]{display:none !important;}`
519
+ );
486
520
  }
487
521
 
488
522
  // Neutralize any </style> breakout from editor-controlled color/radius
@@ -529,22 +563,27 @@ const ShippingThresholdProgressSection: React.FC<
529
563
  )
530
564
  );
531
565
 
532
- const sectionStyles = getCSSStyles(filteredStyles, themeSettings, currentBreakpoint);
566
+ const sectionStyles = getCSSStyles(
567
+ filteredStyles,
568
+ themeSettings,
569
+ currentBreakpoint
570
+ );
533
571
 
534
- const postBlockAction = (type: string, blockId: string, label?: string) => {
572
+ const postBlockAction = (
573
+ type: PreviewToEditorMessageType,
574
+ blockId: string,
575
+ label?: string
576
+ ) => {
535
577
  if (!window.parent) return;
536
- window.parent.postMessage(
537
- {
538
- type,
539
- data: {
540
- placeholderId,
541
- sectionId: section.id,
542
- blockId,
543
- ...(label ? { label } : {})
544
- }
545
- },
546
- '*'
547
- );
578
+ postToEditor({
579
+ type,
580
+ data: {
581
+ placeholderId,
582
+ sectionId: section.id,
583
+ blockId,
584
+ ...(label ? { label } : {})
585
+ }
586
+ });
548
587
  };
549
588
 
550
589
  const renderBlock = (block: Block) => (
@@ -560,9 +599,13 @@ const ShippingThresholdProgressSection: React.FC<
560
599
  onMoveUp={() => postBlockAction('MOVE_BLOCK_UP', block.id)}
561
600
  onMoveDown={() => postBlockAction('MOVE_BLOCK_DOWN', block.id)}
562
601
  onDuplicate={() => postBlockAction('DUPLICATE_BLOCK', block.id)}
563
- onToggleVisibility={() => postBlockAction('TOGGLE_BLOCK_VISIBILITY', block.id)}
602
+ onToggleVisibility={() =>
603
+ postBlockAction('TOGGLE_BLOCK_VISIBILITY', block.id)
604
+ }
564
605
  onDelete={() => postBlockAction('DELETE_BLOCK', block.id)}
565
- onRename={(newLabel) => postBlockAction('RENAME_BLOCK', block.id, newLabel)}
606
+ onRename={(newLabel) =>
607
+ postBlockAction('RENAME_BLOCK', block.id, newLabel)
608
+ }
566
609
  />
567
610
  );
568
611
 
@@ -9,6 +9,8 @@ import { Section } from '../theme-section';
9
9
  import { useThemeSettingsContext } from '../theme-settings-context';
10
10
  import { getCSSStyles, getResponsiveValue } from '../utils';
11
11
 
12
+ import { postToEditor } from '../editor-bridge';
13
+ import { PreviewToEditorMessageType } from '@akinon/theme-contract';
12
14
  interface StatsCounterSectionProps {
13
15
  section: Section;
14
16
  currentBreakpoint?: string;
@@ -167,7 +169,9 @@ const resolveTextValue = (raw: unknown): string | null => {
167
169
  return null;
168
170
  };
169
171
 
170
- const getCounterTargetFromText = (value: string): Omit<CounterTarget, 'blockId'> | null => {
172
+ const getCounterTargetFromText = (
173
+ value: string
174
+ ): Omit<CounterTarget, 'blockId'> | null => {
171
175
  const plain = stripHtml(value);
172
176
  const tokenMatch = plain.match(NUMBER_TOKEN_REGEX);
173
177
 
@@ -242,7 +246,10 @@ const injectAnimatedValues = (
242
246
  blocks: Block[],
243
247
  animatedValues: Record<string, string>
244
248
  ): Block[] => {
245
- const replaceTokenInContent = (content: string, replacement: string): string => {
249
+ const replaceTokenInContent = (
250
+ content: string,
251
+ replacement: string
252
+ ): string => {
246
253
  if (content.includes('<') && content.includes('>')) {
247
254
  return content.replace(/>([^<]*)</, (fullMatch, innerText: string) => {
248
255
  const nextInner = innerText.replace(NUMBER_TOKEN_REGEX, replacement);
@@ -260,10 +267,7 @@ const injectAnimatedValues = (
260
267
  styles: block.styles ? { ...block.styles } : block.styles
261
268
  };
262
269
 
263
- if (
264
- cloned.type === 'text' &&
265
- animatedValues[cloned.id] !== undefined
266
- ) {
270
+ if (cloned.type === 'text' && animatedValues[cloned.id] !== undefined) {
267
271
  const replacement = animatedValues[cloned.id];
268
272
 
269
273
  if (typeof cloned.value === 'string') {
@@ -327,7 +331,10 @@ const StatsCounterSection: React.FC<StatsCounterSectionProps> = ({
327
331
  [section.blocks, isDesigner]
328
332
  );
329
333
 
330
- const targets = useMemo(() => collectCounterTargets(sortedBlocks), [sortedBlocks]);
334
+ const targets = useMemo(
335
+ () => collectCounterTargets(sortedBlocks),
336
+ [sortedBlocks]
337
+ );
331
338
  const targetsKey = useMemo(
332
339
  () =>
333
340
  targets
@@ -340,22 +347,36 @@ const StatsCounterSection: React.FC<StatsCounterSectionProps> = ({
340
347
  );
341
348
 
342
349
  const countUpEnabled = parseBoolean(
343
- getResponsiveValue(section.properties?.['count-up-enabled'], currentBreakpoint, true),
350
+ getResponsiveValue(
351
+ section.properties?.['count-up-enabled'],
352
+ currentBreakpoint,
353
+ true
354
+ ),
344
355
  true
345
356
  );
346
357
  const countUpDuration = clampNumber(
347
358
  parseNumber(
348
- getResponsiveValue(section.properties?.['count-up-duration'], currentBreakpoint, 1400),
359
+ getResponsiveValue(
360
+ section.properties?.['count-up-duration'],
361
+ currentBreakpoint,
362
+ 1400
363
+ ),
349
364
  1400
350
365
  ),
351
366
  300,
352
367
  10000
353
368
  );
354
369
  const animationEasing = parseAnimationEasing(
355
- getResponsiveValue(section.properties?.['count-up-easing'], currentBreakpoint, 'linear')
370
+ getResponsiveValue(
371
+ section.properties?.['count-up-easing'],
372
+ currentBreakpoint,
373
+ 'linear'
374
+ )
356
375
  );
357
376
 
358
- const [animatedValues, setAnimatedValues] = useState<Record<string, string>>({});
377
+ const [animatedValues, setAnimatedValues] = useState<Record<string, string>>(
378
+ {}
379
+ );
359
380
  const targetsRef = useRef<CounterTarget[]>(targets);
360
381
 
361
382
  useEffect(() => {
@@ -396,7 +417,9 @@ const StatsCounterSection: React.FC<StatsCounterSectionProps> = ({
396
417
 
397
418
  const tick = (now: number) => {
398
419
  const progress = Math.min((now - startTime) / duration, 1);
399
- setAnimatedValues(buildValues(applyAnimationEasing(progress, animationEasing)));
420
+ setAnimatedValues(
421
+ buildValues(applyAnimationEasing(progress, animationEasing))
422
+ );
400
423
 
401
424
  if (progress < 1) {
402
425
  rafId = window.requestAnimationFrame(tick);
@@ -432,24 +455,31 @@ const StatsCounterSection: React.FC<StatsCounterSectionProps> = ({
432
455
  const hasMaxWidth = maxWidth !== 'none' && maxWidth !== 'full';
433
456
 
434
457
  const filteredStyles = Object.fromEntries(
435
- Object.entries(section.styles || {}).filter(([key]) => !['max-width'].includes(key))
458
+ Object.entries(section.styles || {}).filter(
459
+ ([key]) => !['max-width'].includes(key)
460
+ )
461
+ );
462
+ const sectionStyles = getCSSStyles(
463
+ filteredStyles,
464
+ themeSettings,
465
+ currentBreakpoint
436
466
  );
437
- const sectionStyles = getCSSStyles(filteredStyles, themeSettings, currentBreakpoint);
438
467
 
439
- const postBlockAction = (type: string, blockId: string, label?: string) => {
468
+ const postBlockAction = (
469
+ type: PreviewToEditorMessageType,
470
+ blockId: string,
471
+ label?: string
472
+ ) => {
440
473
  if (!window.parent) return;
441
- window.parent.postMessage(
442
- {
443
- type,
444
- data: {
445
- placeholderId,
446
- sectionId: section.id,
447
- blockId,
448
- ...(label ? { label } : {})
449
- }
450
- },
451
- '*'
452
- );
474
+ postToEditor({
475
+ type,
476
+ data: {
477
+ placeholderId,
478
+ sectionId: section.id,
479
+ blockId,
480
+ ...(label ? { label } : {})
481
+ }
482
+ });
453
483
  };
454
484
 
455
485
  const renderBlock = (block: Block) => (
@@ -465,16 +495,24 @@ const StatsCounterSection: React.FC<StatsCounterSectionProps> = ({
465
495
  onMoveUp={() => postBlockAction('MOVE_BLOCK_UP', block.id)}
466
496
  onMoveDown={() => postBlockAction('MOVE_BLOCK_DOWN', block.id)}
467
497
  onDuplicate={() => postBlockAction('DUPLICATE_BLOCK', block.id)}
468
- onToggleVisibility={() => postBlockAction('TOGGLE_BLOCK_VISIBILITY', block.id)}
498
+ onToggleVisibility={() =>
499
+ postBlockAction('TOGGLE_BLOCK_VISIBILITY', block.id)
500
+ }
469
501
  onDelete={() => postBlockAction('DELETE_BLOCK', block.id)}
470
- onRename={(newLabel) => postBlockAction('RENAME_BLOCK', block.id, newLabel)}
502
+ onRename={(newLabel) =>
503
+ postBlockAction('RENAME_BLOCK', block.id, newLabel)
504
+ }
471
505
  />
472
506
  );
473
507
 
474
508
  return (
475
509
  <div
476
510
  className={twMerge(
477
- clsx('stats-counter-section w-full', hasMaxWidth && 'mx-auto', maxWidthClass)
511
+ clsx(
512
+ 'stats-counter-section w-full',
513
+ hasMaxWidth && 'mx-auto',
514
+ maxWidthClass
515
+ )
478
516
  )}
479
517
  style={sectionStyles}
480
518
  >