@akinon/pz-theme 2.0.35-beta.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 +9 -3
  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
@@ -14,6 +14,8 @@ import {
14
14
  import { devWarn } from './utils/dev-warn';
15
15
  import { safeDeepClone } from './utils/safe-clone';
16
16
 
17
+ import { isTrustedEditorEvent, postToEditor } from './editor-bridge';
18
+ import { PreviewToEditorMessageType } from '@akinon/theme-contract';
17
19
  export interface Section {
18
20
  id: string;
19
21
  type: string;
@@ -119,6 +121,7 @@ export default function ThemeSection({
119
121
  if (!isDesigner) return;
120
122
 
121
123
  const handleMessage = (event: MessageEvent) => {
124
+ if (!isTrustedEditorEvent(event)) return;
122
125
  if (
123
126
  event.data?.type === 'SCROLL_TO_SECTION' &&
124
127
  event.data?.data?.sectionId === effectiveSection.id
@@ -140,16 +143,13 @@ export default function ThemeSection({
140
143
  onSelect(effectiveSection.id);
141
144
 
142
145
  if (window.parent) {
143
- window.parent.postMessage(
144
- {
145
- type: 'SELECT_SECTION',
146
- data: {
147
- placeholderId,
148
- sectionId: effectiveSection.id
149
- }
150
- },
151
- '*'
152
- );
146
+ postToEditor({
147
+ type: 'SELECT_SECTION',
148
+ data: {
149
+ placeholderId,
150
+ sectionId: effectiveSection.id
151
+ }
152
+ });
153
153
  }
154
154
  }
155
155
  };
@@ -825,9 +825,7 @@ export default function ThemeSection({
825
825
  properties: target.properties
826
826
  ? { ...target.properties }
827
827
  : target.properties,
828
- styles: target.styles
829
- ? safeDeepClone(target.styles)
830
- : target.styles,
828
+ styles: target.styles ? safeDeepClone(target.styles) : target.styles,
831
829
  blocks: target.blocks
832
830
  ? target.blocks.map((child, childIndex) =>
833
831
  cloneWithIndex(child, `${suffix}-${childIndex}`)
@@ -998,36 +996,31 @@ export default function ThemeSection({
998
996
  isDesigner
999
997
  })
1000
998
  : resolvedBlock;
1001
- const createActionHandler = (actionType: string) => () => {
1002
- if (window.parent) {
1003
- window.parent.postMessage(
1004
- {
999
+ const createActionHandler =
1000
+ (actionType: PreviewToEditorMessageType) => () => {
1001
+ if (window.parent) {
1002
+ postToEditor({
1005
1003
  type: actionType,
1006
1004
  data: {
1007
1005
  placeholderId,
1008
1006
  sectionId: effectiveSection.id,
1009
1007
  blockId: block.id
1010
1008
  }
1011
- },
1012
- '*'
1013
- );
1014
- }
1015
- };
1009
+ });
1010
+ }
1011
+ };
1016
1012
 
1017
1013
  const handleRename = (newLabel: string) => {
1018
1014
  if (window.parent) {
1019
- window.parent.postMessage(
1020
- {
1021
- type: 'RENAME_BLOCK',
1022
- data: {
1023
- placeholderId,
1024
- sectionId: effectiveSection.id,
1025
- blockId: block.id,
1026
- label: newLabel
1027
- }
1028
- },
1029
- '*'
1030
- );
1015
+ postToEditor({
1016
+ type: 'RENAME_BLOCK',
1017
+ data: {
1018
+ placeholderId,
1019
+ sectionId: effectiveSection.id,
1020
+ blockId: block.id,
1021
+ label: newLabel
1022
+ }
1023
+ });
1031
1024
  }
1032
1025
  };
1033
1026
  const renderBlockWithActions = (renderedBlock: Block) => (
@@ -0,0 +1,97 @@
1
+ import { generateThemeCSS, getCSSStyles } from '../index';
2
+
3
+ // Minimal Section factory — generateThemeCSS only reads id/styles/blocks.
4
+ const sectionWith = (
5
+ styles: Record<string, unknown>,
6
+ blocks: Record<string, unknown>[] = []
7
+ ) =>
8
+ [
9
+ {
10
+ id: 'sec',
11
+ type: 'default',
12
+ name: 'sec',
13
+ label: 'sec',
14
+ properties: {},
15
+ styles,
16
+ blocks,
17
+ order: 0,
18
+ hidden: false
19
+ }
20
+ ] as never;
21
+
22
+ // Minimal block factory — collectBlockCSS only reads id/styleSourceId/styles/blocks.
23
+ const blockWith = (styles: Record<string, unknown>) => ({
24
+ id: 'blk',
25
+ type: 'text',
26
+ styles,
27
+ blocks: []
28
+ });
29
+
30
+ const transformStyles = {
31
+ scale: { desktop: '1.1' },
32
+ rotate: { desktop: '45deg' },
33
+ translate: { desktop: '10px 20px', mobile: '5px 0px' }
34
+ };
35
+
36
+ describe('generateThemeCSS — independent transform properties (scale/rotate/translate)', () => {
37
+ it('emits scale, rotate and translate verbatim in the base section rule and the mobile value in the media query', () => {
38
+ const css = generateThemeCSS(sectionWith(transformStyles));
39
+
40
+ const [baseCss, mobileCss] = css.split('@media (max-width: 1023px)');
41
+
42
+ expect(baseCss).toContain('[data-section-id="sec"]');
43
+ expect(baseCss).toContain('scale: 1.1');
44
+ expect(baseCss).toContain('rotate: 45deg');
45
+ expect(baseCss).toContain('translate: 10px 20px');
46
+
47
+ expect(mobileCss).toBeDefined();
48
+ expect(mobileCss).toContain('[data-section-id="sec"]');
49
+ expect(mobileCss).toContain('translate: 5px 0px');
50
+ });
51
+
52
+ it('emits scale, rotate and translate under the [data-block-id] selector for a block', () => {
53
+ const css = generateThemeCSS(sectionWith({}, [blockWith(transformStyles)]));
54
+
55
+ const [baseCss, mobileCss] = css.split('@media (max-width: 1023px)');
56
+
57
+ expect(baseCss).toContain('[data-block-id="blk"]');
58
+ expect(baseCss).toContain('scale: 1.1');
59
+ expect(baseCss).toContain('rotate: 45deg');
60
+ expect(baseCss).toContain('translate: 10px 20px');
61
+
62
+ expect(mobileCss).toBeDefined();
63
+ expect(mobileCss).toContain('[data-block-id="blk"]');
64
+ expect(mobileCss).toContain('translate: 5px 0px');
65
+ });
66
+
67
+ it('designer mobile mode emits mobile values with desktop fallback and no media query', () => {
68
+ const css = generateThemeCSS(sectionWith(transformStyles), 'mobile');
69
+
70
+ expect(css).toContain('[data-section-id="sec"]');
71
+ // Mobile value wins for translate; scale/rotate fall back to desktop.
72
+ expect(css).toContain('translate: 5px 0px');
73
+ expect(css).toContain('scale: 1.1');
74
+ expect(css).toContain('rotate: 45deg');
75
+ expect(css).not.toContain('translate: 10px 20px');
76
+ expect(css).not.toContain('@media');
77
+ });
78
+ });
79
+
80
+ describe('getCSSStyles — inline skip for scale/rotate/translate', () => {
81
+ it('skips scale, rotate and translate but keeps raw transform and other properties', () => {
82
+ const inline = getCSSStyles({
83
+ scale: { desktop: '1.1' },
84
+ rotate: { desktop: '45deg' },
85
+ translate: { desktop: '10px 20px' },
86
+ transform: { desktop: 'rotate(180deg)' },
87
+ color: { desktop: '#fff' }
88
+ });
89
+
90
+ expect(inline).not.toHaveProperty('scale');
91
+ expect(inline).not.toHaveProperty('rotate');
92
+ expect(inline).not.toHaveProperty('translate');
93
+ expect(inline).toHaveProperty('transform', 'rotate(180deg)');
94
+ // Default text-opacity (100) converts hex colors to rgba in the inline path.
95
+ expect(inline).toHaveProperty('color', 'rgba(255, 255, 255, 1)');
96
+ });
97
+ });
@@ -316,6 +316,15 @@ export const getCSSStyles = (
316
316
  return;
317
317
  }
318
318
 
319
+ // `scale`/`rotate`/`translate` are rendered once via the generated [data-block-id]/[data-section-id] stylesheet; skipping them inline prevents double application on inner elements.
320
+ if (
321
+ normalizedKey === 'scale' ||
322
+ normalizedKey === 'rotate' ||
323
+ normalizedKey === 'translate'
324
+ ) {
325
+ return;
326
+ }
327
+
319
328
  const camelKey = kebabToCamel(key);
320
329
 
321
330
  if (