@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.
- package/CHANGELOG.md +12 -28
- package/package.json +4 -3
- package/src/__tests__/theme-contract.test.ts +130 -0
- package/src/blocks/__tests__/text-block.test.tsx +8 -1
- package/src/blocks/__tests__/video-block.test.tsx +14 -7
- package/src/blocks/counter-block.tsx +44 -56
- package/src/blocks/group-block.tsx +23 -22
- package/src/blocks/hotspot-block.tsx +171 -80
- package/src/blocks/input-block.tsx +15 -17
- package/src/blocks/link-block.tsx +18 -21
- package/src/blocks/slider-block.tsx +15 -12
- package/src/components/with-designer-features.tsx +2 -0
- package/src/editor-bridge.ts +160 -0
- package/src/hooks/use-designer-features.tsx +30 -37
- package/src/placeholder-registry.ts +5 -7
- package/src/sections/__tests__/countdown-campaign-banner-section.test.tsx +8 -4
- package/src/sections/__tests__/coupon-banner-section.test.tsx +4 -2
- package/src/sections/before-after-section.tsx +189 -134
- package/src/sections/contact-form-section.tsx +185 -60
- package/src/sections/countdown-campaign-banner-section.tsx +73 -27
- package/src/sections/coupon-banner-section.tsx +152 -48
- package/src/sections/featured-product-spotlight-section.tsx +67 -66
- package/src/sections/find-in-store-section.tsx +37 -25
- package/src/sections/hover-showcase-section.tsx +221 -89
- package/src/sections/image-hotspot-section.tsx +208 -94
- package/src/sections/installment-options-section.tsx +268 -259
- package/src/sections/notification-banner-section.tsx +111 -25
- package/src/sections/order-tracking-lookup-section.tsx +272 -247
- package/src/sections/posts-slider-section.tsx +17 -16
- package/src/sections/pre-order-launch-banner-section.tsx +31 -50
- package/src/sections/section-wrapper.tsx +9 -10
- package/src/sections/shipping-threshold-progress-section.tsx +72 -29
- package/src/sections/stats-counter-section.tsx +68 -30
- package/src/sections/tabs-section.tsx +124 -147
- package/src/theme-placeholder-client.tsx +11 -14
- package/src/theme-placeholder-wrapper.tsx +42 -51
- package/src/theme-section.tsx +27 -34
- package/src/utils/__tests__/transform-styles.test.ts +97 -0
- package/src/utils/index.ts +9 -0
package/src/theme-section.tsx
CHANGED
|
@@ -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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 =
|
|
1002
|
-
|
|
1003
|
-
window.parent
|
|
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
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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
|
+
});
|
package/src/utils/index.ts
CHANGED
|
@@ -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 (
|