@developer_tribe/react-builder 1.2.18 → 1.2.20
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/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +2 -1
- package/dist/build-components/patterns.generated.d.ts +23 -8
- package/dist/index.cjs.js +3 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.web.cjs.js +4 -4
- package/dist/index.web.cjs.js.map +1 -1
- package/dist/index.web.esm.js +3 -3
- package/dist/index.web.esm.js.map +1 -1
- package/dist/pages/ProjectDebug.d.ts +9 -1
- package/dist/pages/ProjectMigrationPage.d.ts +3 -1
- package/dist/pages/ProjectValidationPage.d.ts +3 -2
- package/dist/styles.css +1 -1
- package/dist/utils/applyJsonTransform.d.ts +13 -0
- package/dist/utils/repairNodeKeys.d.ts +11 -0
- package/dist/utils/safeJsonStringify.d.ts +1 -0
- package/dist/utils/wrapNodeInMain.d.ts +2 -0
- package/package.json +1 -1
- package/src/RenderPage.tsx +17 -46
- package/src/assets/meta.json +1 -1
- package/src/assets/samples/carousel-sample.json +51 -51
- package/src/assets/samples/paywall-1.json +77 -77
- package/src/assets/samples/paywall-2.json +76 -76
- package/src/assets/samples/simple-1.json +13 -13
- package/src/assets/samples/simple-2.json +97 -97
- package/src/assets/samples/unmigrated-builder-1.1.1.json +25 -25
- package/src/assets/samples/unmigrated-builder1.json +1 -1
- package/src/assets/samples/unvalidated-builder1.json +15 -15
- package/src/assets/samples/unvalidated-crash1.json +4 -4
- package/src/assets/samples/vpn-onboard-1.json +100 -78
- package/src/assets/samples/vpn-onboard-2.json +97 -75
- package/src/assets/samples/vpn-onboard-3.json +103 -79
- package/src/assets/samples/vpn-onboard-4.json +103 -79
- package/src/assets/samples/vpn-onboard-5.json +139 -108
- package/src/assets/samples/vpn-onboard-6.json +100 -81
- package/src/build-components/CarouselDots/CarouselDots.tsx +112 -12
- package/src/build-components/OnboardDot/OnboardDot.tsx +74 -40
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +2 -1
- package/src/build-components/OnboardDot/pattern.json +28 -10
- package/src/build-components/PaywallProvider/PaywallProvider.tsx +2 -3
- package/src/build-components/Text/Text.tsx +4 -9
- package/src/build-components/patterns.generated.ts +23 -8
- package/src/build-components/useNode.ts +20 -4
- package/src/components/AttributesEditorPanel.tsx +13 -1
- package/src/components/Builder.tsx +19 -5
- package/src/components/EditorHeader.tsx +16 -6
- package/src/components/JsonTextEditor.tsx +41 -0
- package/src/pages/DebugJsonPage.tsx +104 -4
- package/src/pages/ProjectDebug.tsx +66 -28
- package/src/pages/ProjectMigrationPage.tsx +15 -0
- package/src/pages/ProjectPage.tsx +160 -23
- package/src/pages/ProjectValidationPage.tsx +64 -1
- package/src/styles/layout/_project-validation.scss +29 -0
- package/src/styles/utilities/_carousel.scss +0 -32
- package/src/utils/__special_exceptions.ts +9 -3
- package/src/utils/analyseNodeByPatterns.ts +16 -6
- package/src/utils/applyJsonTransform.ts +19 -0
- package/src/utils/novaToJson.ts +7 -3
- package/src/utils/repairNodeKeys.ts +90 -0
- package/src/utils/safeJsonStringify.ts +18 -0
- package/src/utils/wrapNodeInMain.ts +67 -0
|
@@ -20,8 +20,12 @@ function OnboardDot({ node }: OnboardDotComponentProps) {
|
|
|
20
20
|
const attributeName = node.type ?? 'OnboardDot';
|
|
21
21
|
const attributeKey = node.key ?? generatedId;
|
|
22
22
|
const attrs = node.attributes;
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
const stylesBag =
|
|
24
|
+
((attrs as any)?.styles as Record<string, unknown> | undefined) ??
|
|
25
|
+
((attrs as any)?.style as Record<string, unknown> | undefined) ??
|
|
26
|
+
undefined;
|
|
27
|
+
const dotType =
|
|
28
|
+
(stylesBag?.dotType as any) ?? (attrs as any)?.dotType ?? 'normal_dot';
|
|
25
29
|
const GHOST_DOT_DARK_COLOR = '#E4E5E7';
|
|
26
30
|
const GHOST_DOT_LIGHT_COLOR = '#F7F7F9';
|
|
27
31
|
const {
|
|
@@ -37,12 +41,32 @@ function OnboardDot({ node }: OnboardDotComponentProps) {
|
|
|
37
41
|
const inactiveDotColor = isDark
|
|
38
42
|
? GHOST_DOT_DARK_COLOR
|
|
39
43
|
: GHOST_DOT_LIGHT_COLOR;
|
|
40
|
-
const inactiveDotOpacity =
|
|
41
|
-
|
|
44
|
+
const inactiveDotOpacity =
|
|
45
|
+
(stylesBag?.inactive_dot_opacity as number | undefined) ??
|
|
46
|
+
(attrs as any)?.inactive_dot_opacity ??
|
|
47
|
+
0.3;
|
|
48
|
+
const inactiveDotColorOverride =
|
|
49
|
+
(stylesBag?.inactive_dot_color as string | undefined) ??
|
|
50
|
+
(attrs as any)?.inactive_dot_color;
|
|
51
|
+
const activeDotColor =
|
|
52
|
+
(stylesBag?.active_dot_color as string | undefined) ??
|
|
53
|
+
(attrs as any)?.active_dot_color;
|
|
42
54
|
const resolvedActiveDotColor = useMemo(
|
|
43
55
|
() => parseColor(activeDotColor, { theme: appConfig.theme, projectColors }),
|
|
44
56
|
[activeDotColor, appConfig.theme, projectColors],
|
|
45
57
|
);
|
|
58
|
+
const resolvedInactiveDotColor = useMemo(() => {
|
|
59
|
+
const parsed = parseColor(inactiveDotColorOverride, {
|
|
60
|
+
theme: appConfig.theme,
|
|
61
|
+
projectColors,
|
|
62
|
+
});
|
|
63
|
+
return parsed ?? inactiveDotColor;
|
|
64
|
+
}, [
|
|
65
|
+
inactiveDotColor,
|
|
66
|
+
inactiveDotColorOverride,
|
|
67
|
+
appConfig.theme,
|
|
68
|
+
projectColors,
|
|
69
|
+
]);
|
|
46
70
|
|
|
47
71
|
const extractedStyle = useExtractViewStyle(node);
|
|
48
72
|
const baseStyle = useMemo(() => {
|
|
@@ -58,27 +82,31 @@ function OnboardDot({ node }: OnboardDotComponentProps) {
|
|
|
58
82
|
baseStyle,
|
|
59
83
|
isSelected ? SELECTED_OUTLINE_STYLE : undefined,
|
|
60
84
|
);
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
const dotThicknessRaw =
|
|
86
|
+
(stylesBag?.dot_thickness as any) ?? (attrs as any)?.dot_thickness;
|
|
87
|
+
const dotSizeCss = useMemo((): string => {
|
|
88
|
+
const parsed = parseSize(dotThicknessRaw);
|
|
89
|
+
if (parsed === undefined) return '10px';
|
|
90
|
+
if (typeof parsed === 'number') return `${parsed}px`;
|
|
91
|
+
if (typeof parsed === 'string' && parsed.trim()) return parsed;
|
|
92
|
+
return '10px';
|
|
93
|
+
}, [dotThicknessRaw]);
|
|
94
|
+
const dotGapCss = useMemo((): string => {
|
|
95
|
+
// Prefer px math when possible; otherwise fall back to 10px/3.
|
|
96
|
+
const px =
|
|
97
|
+
typeof dotSizeCss === 'string' && dotSizeCss.trim().endsWith('px')
|
|
98
|
+
? Number.parseFloat(dotSizeCss)
|
|
99
|
+
: Number.NaN;
|
|
100
|
+
const n = Number.isFinite(px) ? px : 10;
|
|
101
|
+
return `${Math.max(0, n / 3)}px`;
|
|
102
|
+
}, [dotSizeCss]);
|
|
103
|
+
const gapValue = (style as any)?.gap ?? dotGapCss;
|
|
104
|
+
const containerStyle = useMergedStyle(style, {
|
|
105
|
+
display: 'flex',
|
|
106
|
+
flexWrap: 'wrap',
|
|
107
|
+
gap: gapValue,
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
});
|
|
82
110
|
|
|
83
111
|
const onboardApi = useContext(onboardContext);
|
|
84
112
|
const emblaApi = onboardApi?.emblaApi;
|
|
@@ -115,16 +143,12 @@ function OnboardDot({ node }: OnboardDotComponentProps) {
|
|
|
115
143
|
>
|
|
116
144
|
{scrollSnaps.map((snap, index) => {
|
|
117
145
|
const isDotSelected = selectedIndex === index;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
dotStyles['--embla-dot-color'] = resolvedActiveDotColor;
|
|
125
|
-
} else if (!isDotSelected) {
|
|
126
|
-
dotStyles['--embla-dot-color'] = inactiveDotColor;
|
|
127
|
-
}
|
|
146
|
+
const resolvedColor =
|
|
147
|
+
isDotSelected && resolvedActiveDotColor
|
|
148
|
+
? resolvedActiveDotColor
|
|
149
|
+
: resolvedInactiveDotColor;
|
|
150
|
+
const activeFallback = '#007AFF';
|
|
151
|
+
const dotColor = resolvedColor ?? activeFallback;
|
|
128
152
|
|
|
129
153
|
return (
|
|
130
154
|
<button
|
|
@@ -132,13 +156,23 @@ function OnboardDot({ node }: OnboardDotComponentProps) {
|
|
|
132
156
|
onClick={() => {
|
|
133
157
|
emblaApi?.scrollTo(snap);
|
|
134
158
|
}}
|
|
135
|
-
className=
|
|
136
|
-
style={
|
|
159
|
+
className="embla__dot"
|
|
160
|
+
style={{
|
|
161
|
+
width: dotSizeCss,
|
|
162
|
+
height: dotSizeCss,
|
|
163
|
+
backgroundColor: dotColor,
|
|
164
|
+
opacity: isDotSelected ? 1 : inactiveDotOpacity,
|
|
165
|
+
borderRadius: '9999px',
|
|
166
|
+
border: 0,
|
|
167
|
+
padding: 0,
|
|
168
|
+
margin: 0,
|
|
169
|
+
display: 'inline-block',
|
|
170
|
+
cursor: 'pointer',
|
|
171
|
+
boxSizing: 'border-box',
|
|
172
|
+
}}
|
|
137
173
|
aria-label={`Go to slide ${index + 1}`}
|
|
138
174
|
aria-current={isDotSelected ? 'true' : undefined}
|
|
139
|
-
|
|
140
|
-
{/* Dot visuals are rendered via CSS (`.embla__dot:after`). */}
|
|
141
|
-
</button>
|
|
175
|
+
/>
|
|
142
176
|
);
|
|
143
177
|
})}
|
|
144
178
|
</div>
|
|
@@ -69,8 +69,9 @@ export interface OnboardDotPropsGenerated {
|
|
|
69
69
|
title?: string;
|
|
70
70
|
description?: string;
|
|
71
71
|
dotType?: DotTypeOptionType;
|
|
72
|
+
dot_thickness?: string;
|
|
72
73
|
inactive_dot_opacity?: number;
|
|
73
|
-
|
|
74
|
+
inactive_dot_color?: string;
|
|
74
75
|
active_dot_color?: string;
|
|
75
76
|
flexDirection?: never;
|
|
76
77
|
alignItems?: never;
|
|
@@ -15,18 +15,29 @@
|
|
|
15
15
|
"sliding_dot",
|
|
16
16
|
"liquid_like"
|
|
17
17
|
],
|
|
18
|
+
"dot_thickness": "size",
|
|
18
19
|
"inactive_dot_opacity": "number",
|
|
19
|
-
"
|
|
20
|
+
"inactive_dot_color": "color",
|
|
20
21
|
"active_dot_color": "color",
|
|
21
22
|
"flexDirection": "never",
|
|
22
23
|
"alignItems": "never",
|
|
23
24
|
"justifyContent": "never"
|
|
24
25
|
}
|
|
25
26
|
},
|
|
27
|
+
"defaults": {
|
|
28
|
+
"dotType": "expanding_dot",
|
|
29
|
+
"dot_thickness": 10,
|
|
30
|
+
"inactive_dot_opacity": 0.3,
|
|
31
|
+
"active_dot_color": "#007AFF",
|
|
32
|
+
"style": {
|
|
33
|
+
"flexDirection": "row",
|
|
34
|
+
"alignItems": "center",
|
|
35
|
+
"justifyContent": "center",
|
|
36
|
+
"gap": "12@s"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
26
39
|
"meta": {
|
|
27
|
-
"desiredParent": [
|
|
28
|
-
">OnboardProvider"
|
|
29
|
-
],
|
|
40
|
+
"desiredParent": [">OnboardProvider"],
|
|
30
41
|
"label": "Onboard Dot",
|
|
31
42
|
"description": "Renders onboarding progress dots.",
|
|
32
43
|
"styles": {
|
|
@@ -42,21 +53,28 @@
|
|
|
42
53
|
"description": "Opacity for inactive dots.",
|
|
43
54
|
"category": "style",
|
|
44
55
|
"specialCategory": null,
|
|
45
|
-
"sort":
|
|
56
|
+
"sort": 3
|
|
46
57
|
},
|
|
47
|
-
"
|
|
48
|
-
"label": "
|
|
49
|
-
"description": "
|
|
58
|
+
"inactive_dot_color": {
|
|
59
|
+
"label": "Inactive Dot Color",
|
|
60
|
+
"description": "Color of inactive dots.",
|
|
50
61
|
"category": "style",
|
|
51
62
|
"specialCategory": null,
|
|
52
|
-
"sort":
|
|
63
|
+
"sort": 4
|
|
64
|
+
},
|
|
65
|
+
"dot_thickness": {
|
|
66
|
+
"label": "Dot Thickness",
|
|
67
|
+
"description": "Dot size/diameter.",
|
|
68
|
+
"category": "style",
|
|
69
|
+
"specialCategory": null,
|
|
70
|
+
"sort": 2
|
|
53
71
|
},
|
|
54
72
|
"active_dot_color": {
|
|
55
73
|
"label": "Active Dot Color",
|
|
56
74
|
"description": "Color of the active dot.",
|
|
57
75
|
"category": "style",
|
|
58
76
|
"specialCategory": null,
|
|
59
|
-
"sort":
|
|
77
|
+
"sort": 5
|
|
60
78
|
}
|
|
61
79
|
}
|
|
62
80
|
}
|
|
@@ -62,6 +62,8 @@ function PaywallProvider({ node }: PaywallProviderComponentProps) {
|
|
|
62
62
|
|
|
63
63
|
const [selectedProductId, setSelectedProductId] = useState<string>('');
|
|
64
64
|
const [isBackAllowed, setIsBackAllowed] = useState<boolean>(false);
|
|
65
|
+
useChangeDelayByPaywall(node, setIsBackAllowed);
|
|
66
|
+
useMockOSBackHandler(isBackAllowed);
|
|
65
67
|
useEffect(() => {
|
|
66
68
|
const list = Array.isArray(products) ? products : [];
|
|
67
69
|
if (list.length === 0) {
|
|
@@ -75,9 +77,6 @@ function PaywallProvider({ node }: PaywallProviderComponentProps) {
|
|
|
75
77
|
}
|
|
76
78
|
}, [products, selectedProductId]);
|
|
77
79
|
|
|
78
|
-
useChangeDelayByPaywall(node, setIsBackAllowed);
|
|
79
|
-
useMockOSBackHandler(isBackAllowed);
|
|
80
|
-
|
|
81
80
|
const selectedProduct = useMemo(() => {
|
|
82
81
|
const list = Array.isArray(products) ? products : [];
|
|
83
82
|
return (
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
useId,
|
|
3
|
-
useLayoutEffect,
|
|
4
|
-
useMemo,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
} from 'react';
|
|
1
|
+
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
8
2
|
import type { TextComponentProps } from './TextProps.generated';
|
|
9
3
|
import useNode from '../useNode';
|
|
10
4
|
import { useBuilderParams } from '../../components/BuilderProvider';
|
|
@@ -17,9 +11,10 @@ import { useLocalize } from '../../hooks/useLocalize';
|
|
|
17
11
|
function Text({ node }: TextComponentProps) {
|
|
18
12
|
useLogRender('Text');
|
|
19
13
|
node = useNode(node);
|
|
20
|
-
const generatedId = useId();
|
|
21
14
|
const attributeName = node.sourceType ?? node.type ?? 'text';
|
|
22
|
-
|
|
15
|
+
// Only use real node keys for selection. `useId()` values (e.g. ":r13:") are
|
|
16
|
+
// not present in the persisted node tree, so they break click-to-select.
|
|
17
|
+
const attributeKey = node.key;
|
|
23
18
|
const textRef = useRef<HTMLParagraphElement | null>(null);
|
|
24
19
|
const [autoFontSizePx, setAutoFontSizePx] = useState<number | null>(null);
|
|
25
20
|
const { appConfig, previewMode, selectedKey } = useBuilderParams();
|
|
@@ -6103,8 +6103,9 @@ export const patterns = [
|
|
|
6103
6103
|
'sliding_dot',
|
|
6104
6104
|
'liquid_like',
|
|
6105
6105
|
],
|
|
6106
|
+
dot_thickness: 'size',
|
|
6106
6107
|
inactive_dot_opacity: 'number',
|
|
6107
|
-
|
|
6108
|
+
inactive_dot_color: 'color',
|
|
6108
6109
|
active_dot_color: 'color',
|
|
6109
6110
|
flexDirection: 'never',
|
|
6110
6111
|
alignItems: 'never',
|
|
@@ -6169,21 +6170,28 @@ export const patterns = [
|
|
|
6169
6170
|
description: 'Opacity for inactive dots.',
|
|
6170
6171
|
category: 'style',
|
|
6171
6172
|
specialCategory: null,
|
|
6172
|
-
sort:
|
|
6173
|
+
sort: 3,
|
|
6173
6174
|
},
|
|
6174
|
-
|
|
6175
|
-
label: '
|
|
6176
|
-
description: '
|
|
6175
|
+
inactive_dot_color: {
|
|
6176
|
+
label: 'Inactive Dot Color',
|
|
6177
|
+
description: 'Color of inactive dots.',
|
|
6177
6178
|
category: 'style',
|
|
6178
6179
|
specialCategory: null,
|
|
6179
|
-
sort:
|
|
6180
|
+
sort: 4,
|
|
6181
|
+
},
|
|
6182
|
+
dot_thickness: {
|
|
6183
|
+
label: 'Dot Thickness',
|
|
6184
|
+
description: 'Dot size/diameter.',
|
|
6185
|
+
category: 'style',
|
|
6186
|
+
specialCategory: null,
|
|
6187
|
+
sort: 2,
|
|
6180
6188
|
},
|
|
6181
6189
|
active_dot_color: {
|
|
6182
6190
|
label: 'Active Dot Color',
|
|
6183
6191
|
description: 'Color of the active dot.',
|
|
6184
6192
|
category: 'style',
|
|
6185
6193
|
specialCategory: null,
|
|
6186
|
-
sort:
|
|
6194
|
+
sort: 5,
|
|
6187
6195
|
},
|
|
6188
6196
|
},
|
|
6189
6197
|
attributes: {
|
|
@@ -6440,13 +6448,20 @@ export const patterns = [
|
|
|
6440
6448
|
},
|
|
6441
6449
|
defaults: {
|
|
6442
6450
|
style: {
|
|
6443
|
-
flexDirection: '
|
|
6451
|
+
flexDirection: 'row',
|
|
6444
6452
|
position: 'relative',
|
|
6445
6453
|
zIndex: 1,
|
|
6446
6454
|
alignSelf: 'flex-start',
|
|
6447
6455
|
flexGrow: 0,
|
|
6448
6456
|
flexShrink: 0,
|
|
6457
|
+
alignItems: 'center',
|
|
6458
|
+
justifyContent: 'center',
|
|
6459
|
+
gap: '12@s',
|
|
6449
6460
|
},
|
|
6461
|
+
dotType: 'expanding_dot',
|
|
6462
|
+
dot_thickness: 10,
|
|
6463
|
+
inactive_dot_opacity: 0.3,
|
|
6464
|
+
active_dot_color: '#007AFF',
|
|
6450
6465
|
},
|
|
6451
6466
|
types: {},
|
|
6452
6467
|
},
|
|
@@ -9,18 +9,33 @@ export default function useNode<
|
|
|
9
9
|
if (!defaults) return node;
|
|
10
10
|
const nodeAttributes = ((node.attributes as T) ?? ({} as T)) as T & {
|
|
11
11
|
style?: Record<string, unknown>;
|
|
12
|
+
styles?: Record<string, unknown>;
|
|
12
13
|
};
|
|
13
14
|
const defaultAttributes = defaults as T as T & {
|
|
14
15
|
style?: Record<string, unknown>;
|
|
16
|
+
styles?: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
// Merge style from both defaults.style and defaults.styles (for schemaVersion=2 compatibility)
|
|
19
|
+
const defaultStyle = {
|
|
20
|
+
...(defaultAttributes?.styles ?? {}),
|
|
21
|
+
...(defaultAttributes?.style ?? {}),
|
|
22
|
+
};
|
|
23
|
+
// Merge node style from both node.attributes.style and node.attributes.styles
|
|
24
|
+
const nodeStyle = {
|
|
25
|
+
...(nodeAttributes?.styles ?? {}),
|
|
26
|
+
...(nodeAttributes?.style ?? {}),
|
|
27
|
+
};
|
|
28
|
+
const mergedStyle = {
|
|
29
|
+
...defaultStyle,
|
|
30
|
+
...nodeStyle,
|
|
15
31
|
};
|
|
16
32
|
const mergedAttributes: T = {
|
|
17
33
|
...(defaultAttributes as T),
|
|
18
34
|
...(nodeAttributes as T),
|
|
19
35
|
// Deep merge `style` so default style values aren't lost when the node provides partial style overrides.
|
|
20
|
-
style
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
},
|
|
36
|
+
// Keep both `style` (for runtime back-compat) and `styles` (for editor schemaVersion=2) in sync.
|
|
37
|
+
style: mergedStyle,
|
|
38
|
+
styles: mergedStyle,
|
|
24
39
|
} as T;
|
|
25
40
|
if (
|
|
26
41
|
mergedAttributes &&
|
|
@@ -29,6 +44,7 @@ export default function useNode<
|
|
|
29
44
|
Object.keys((mergedAttributes as any).style).length === 0
|
|
30
45
|
) {
|
|
31
46
|
delete (mergedAttributes as any).style;
|
|
47
|
+
delete (mergedAttributes as any).styles;
|
|
32
48
|
}
|
|
33
49
|
return { ...node, attributes: mergedAttributes };
|
|
34
50
|
}
|
|
@@ -3,6 +3,7 @@ import type { Node } from '../types/Node';
|
|
|
3
3
|
import type { ProjectColors } from '../types/Project';
|
|
4
4
|
import { useLogRender } from '../utils/useLogRender';
|
|
5
5
|
import { useRenderStore } from '../store';
|
|
6
|
+
import { findNodeByKey } from '../utils/nodeTree';
|
|
6
7
|
|
|
7
8
|
interface AttributesEditorPanelProps {
|
|
8
9
|
attributes: any;
|
|
@@ -22,6 +23,16 @@ export function AttributesEditorPanel({
|
|
|
22
23
|
}));
|
|
23
24
|
if (!current) return null;
|
|
24
25
|
|
|
26
|
+
const currentKey =
|
|
27
|
+
typeof current === 'object' && !Array.isArray(current) && 'key' in current
|
|
28
|
+
? ((current as any).key as string | undefined)
|
|
29
|
+
: undefined;
|
|
30
|
+
const resolvedCurrent =
|
|
31
|
+
currentKey && attributes
|
|
32
|
+
? findNodeByKey(attributes as Node, currentKey)
|
|
33
|
+
: null;
|
|
34
|
+
const nodeForEditor = resolvedCurrent ?? current;
|
|
35
|
+
|
|
25
36
|
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
26
37
|
if (root === target) return next;
|
|
27
38
|
if (root === null || root === undefined) return root;
|
|
@@ -58,7 +69,8 @@ export function AttributesEditorPanel({
|
|
|
58
69
|
return (
|
|
59
70
|
<div className="attributes-editor-panel">
|
|
60
71
|
<AttributesEditor
|
|
61
|
-
|
|
72
|
+
key={currentKey ?? undefined}
|
|
73
|
+
node={nodeForEditor}
|
|
62
74
|
onChange={handleAttributesChange}
|
|
63
75
|
projectColors={projectColors}
|
|
64
76
|
/>
|
|
@@ -11,6 +11,8 @@ import { useLogRender } from '../utils/useLogRender';
|
|
|
11
11
|
import { getDefaultsForType, getPatternByType } from '../utils/patterns';
|
|
12
12
|
import { AddComponentModal } from '../modals/AddComponentModal';
|
|
13
13
|
import { BuilderButton } from './BuilderButton';
|
|
14
|
+
import { generateRandomKeyForNode } from '../utils/generateRandomKeyForNode';
|
|
15
|
+
import { collectNodeKeys } from '../utils/repairNodeKeys';
|
|
14
16
|
|
|
15
17
|
type BuilderEditorProps = {
|
|
16
18
|
data: Node;
|
|
@@ -156,6 +158,7 @@ export function Builder({
|
|
|
156
158
|
}: BuilderEditorProps) {
|
|
157
159
|
useLogRender('Builder');
|
|
158
160
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
161
|
+
const usedKeys = useMemo(() => collectNodeKeys(data), [data]);
|
|
159
162
|
const breadcrumbPath = useMemo(() => {
|
|
160
163
|
const path = findNodePath(data, current);
|
|
161
164
|
if (path.length) return path;
|
|
@@ -190,7 +193,8 @@ export function Builder({
|
|
|
190
193
|
|
|
191
194
|
const handleAddChild = useCallback(
|
|
192
195
|
(type: string) => {
|
|
193
|
-
const
|
|
196
|
+
const nextUsedKeys = new Set(usedKeys);
|
|
197
|
+
const nextChild = createDefaultNode(type, nextUsedKeys);
|
|
194
198
|
|
|
195
199
|
// Root (or selection) can be empty/null-ish: allow creating the first node.
|
|
196
200
|
if (isNodeNullOrUndefined(current)) {
|
|
@@ -247,7 +251,7 @@ export function Builder({
|
|
|
247
251
|
setData(updatedRoot);
|
|
248
252
|
setCurrent(updatedParent);
|
|
249
253
|
},
|
|
250
|
-
[current, data, setData, setCurrent],
|
|
254
|
+
[current, data, setData, setCurrent, usedKeys],
|
|
251
255
|
);
|
|
252
256
|
|
|
253
257
|
const allowedChildTypes = useMemo(
|
|
@@ -383,17 +387,26 @@ export function Builder({
|
|
|
383
387
|
return root;
|
|
384
388
|
}
|
|
385
389
|
|
|
386
|
-
function createDefaultNode(
|
|
390
|
+
function createDefaultNode(
|
|
391
|
+
type: string,
|
|
392
|
+
nextUsedKeys: Set<string>,
|
|
393
|
+
): NodeData<NodeDefaultAttribute> {
|
|
387
394
|
const pattern = getPatternByType(type)?.pattern;
|
|
388
395
|
const defaults = getDefaultsForType(type) ?? {};
|
|
389
396
|
const childrenSchema = pattern?.children as unknown;
|
|
397
|
+
let key = '';
|
|
398
|
+
do {
|
|
399
|
+
key = generateRandomKeyForNode(type);
|
|
400
|
+
} while (nextUsedKeys.has(key));
|
|
401
|
+
nextUsedKeys.add(key);
|
|
390
402
|
|
|
391
403
|
// Special-case: CarouselProvider MUST contain a Carousel container inside the viewport
|
|
392
404
|
// otherwise embla-carousel will crash (it expects viewport.firstChild.children).
|
|
393
405
|
if (type === 'CarouselProvider') {
|
|
394
406
|
return {
|
|
395
407
|
type,
|
|
396
|
-
|
|
408
|
+
key,
|
|
409
|
+
children: createDefaultNode('Carousel', nextUsedKeys),
|
|
397
410
|
attributes: { ...defaults },
|
|
398
411
|
} as NodeData<NodeDefaultAttribute>;
|
|
399
412
|
}
|
|
@@ -412,13 +425,14 @@ export function Builder({
|
|
|
412
425
|
children = null;
|
|
413
426
|
} else if (typeof childrenSchema === 'string') {
|
|
414
427
|
// Specific child type like 'CarouselItem' – seed with one child to match the pattern.
|
|
415
|
-
children = [createDefaultNode(childrenSchema)];
|
|
428
|
+
children = [createDefaultNode(childrenSchema, nextUsedKeys)];
|
|
416
429
|
} else {
|
|
417
430
|
children = null;
|
|
418
431
|
}
|
|
419
432
|
|
|
420
433
|
return {
|
|
421
434
|
type,
|
|
435
|
+
key,
|
|
422
436
|
children,
|
|
423
437
|
attributes: { ...defaults },
|
|
424
438
|
} as NodeData<NodeDefaultAttribute>;
|
|
@@ -3,6 +3,7 @@ import type { Device } from '../types/Device';
|
|
|
3
3
|
import type { Node } from '../types/Node';
|
|
4
4
|
import { copyNode } from '../utils/copyNode';
|
|
5
5
|
import { getDevices } from '../utils/getDevices';
|
|
6
|
+
import { collectNodeKeys, repairNodeKeys } from '../utils/repairNodeKeys';
|
|
6
7
|
import { useRenderStore } from '../store';
|
|
7
8
|
import { useLogRender } from '../utils/useLogRender';
|
|
8
9
|
import { DeviceButton } from './DeviceButton';
|
|
@@ -120,7 +121,9 @@ export function EditorHeader({
|
|
|
120
121
|
if (!current || !editorData || !setEditorData) return;
|
|
121
122
|
if (!copiedNode) return;
|
|
122
123
|
const cloned = JSON.parse(JSON.stringify(copiedNode)) as Node;
|
|
123
|
-
const
|
|
124
|
+
const usedKeys = collectNodeKeys(editorData);
|
|
125
|
+
const repaired = repairNodeKeys(cloned, usedKeys);
|
|
126
|
+
const updated = replaceNode(editorData, current, repaired);
|
|
124
127
|
useRenderStore.setState({
|
|
125
128
|
copiedNode: null,
|
|
126
129
|
});
|
|
@@ -129,7 +132,7 @@ export function EditorHeader({
|
|
|
129
132
|
// Important: selection is stored by reference. After replacing `current` in the tree,
|
|
130
133
|
// we must point selection to the new (cloned) node reference to keep "current node"
|
|
131
134
|
// in sync with what’s rendered/edited.
|
|
132
|
-
setCurrent(
|
|
135
|
+
setCurrent(repaired);
|
|
133
136
|
};
|
|
134
137
|
|
|
135
138
|
const cloneNode = (node: Node): Node =>
|
|
@@ -137,7 +140,7 @@ export function EditorHeader({
|
|
|
137
140
|
|
|
138
141
|
const handleReplaceFromSample = (sample: Project) => {
|
|
139
142
|
if (!setEditorData) return;
|
|
140
|
-
const next = cloneNode(sample.data);
|
|
143
|
+
const next = repairNodeKeys(cloneNode(sample.data));
|
|
141
144
|
setEditorData(next);
|
|
142
145
|
setCurrent(next);
|
|
143
146
|
if (sample.appConfig) setAppConfig(sample.appConfig);
|
|
@@ -148,6 +151,7 @@ export function EditorHeader({
|
|
|
148
151
|
const handlePasteFromSample = (sample: Project) => {
|
|
149
152
|
if (!current || !editorData || !setEditorData) return;
|
|
150
153
|
const incoming = cloneNode(sample.data);
|
|
154
|
+
const usedKeys = collectNodeKeys(editorData);
|
|
151
155
|
|
|
152
156
|
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
153
157
|
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
@@ -181,13 +185,19 @@ export function EditorHeader({
|
|
|
181
185
|
toast.error('Sample has no children to paste');
|
|
182
186
|
return;
|
|
183
187
|
}
|
|
188
|
+
const repairedPasteNodes = pasteNodes.map((node) =>
|
|
189
|
+
repairNodeKeys(node, usedKeys),
|
|
190
|
+
);
|
|
184
191
|
let nextChildren: Node;
|
|
185
192
|
if (!prevChildren) {
|
|
186
|
-
nextChildren =
|
|
193
|
+
nextChildren =
|
|
194
|
+
repairedPasteNodes.length === 1
|
|
195
|
+
? repairedPasteNodes[0]
|
|
196
|
+
: repairedPasteNodes;
|
|
187
197
|
} else if (Array.isArray(prevChildren)) {
|
|
188
|
-
nextChildren = [...prevChildren, ...
|
|
198
|
+
nextChildren = [...prevChildren, ...repairedPasteNodes];
|
|
189
199
|
} else {
|
|
190
|
-
nextChildren = [prevChildren, ...
|
|
200
|
+
nextChildren = [prevChildren, ...repairedPasteNodes];
|
|
191
201
|
}
|
|
192
202
|
|
|
193
203
|
const nextNode: Node = { ...current, children: nextChildren } as Node;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import type { Node } from '../types/Node';
|
|
3
|
+
import { wrapNodeInMain } from '../utils/wrapNodeInMain';
|
|
2
4
|
|
|
3
5
|
type JsonTextEditorProps = {
|
|
4
6
|
value: unknown;
|
|
@@ -74,6 +76,34 @@ export function JsonTextEditor({
|
|
|
74
76
|
}
|
|
75
77
|
};
|
|
76
78
|
|
|
79
|
+
const handleWrapInMain = () => {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(text) as unknown;
|
|
82
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
83
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
84
|
+
|
|
85
|
+
// Support both:
|
|
86
|
+
// - node JSON (wrap root)
|
|
87
|
+
// - project JSON (wrap `data`)
|
|
88
|
+
const nextValue =
|
|
89
|
+
isRecord(parsed) && 'data' in parsed
|
|
90
|
+
? {
|
|
91
|
+
...(parsed as any),
|
|
92
|
+
data: wrapNodeInMain((parsed as any).data as Node),
|
|
93
|
+
}
|
|
94
|
+
: wrapNodeInMain(parsed as Node);
|
|
95
|
+
|
|
96
|
+
setText(JSON.stringify(nextValue, null, 2));
|
|
97
|
+
setParseError(null);
|
|
98
|
+
setApplyError(null);
|
|
99
|
+
setParsedValue(nextValue);
|
|
100
|
+
// Intentionally NOT calling onChange here:
|
|
101
|
+
// user can review diff and press "Apply" explicitly.
|
|
102
|
+
} catch (e) {
|
|
103
|
+
setParseError(e instanceof Error ? e.message : 'Invalid JSON');
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
77
107
|
const headerLabel = rootName ? `${rootName}.json` : 'data.json';
|
|
78
108
|
|
|
79
109
|
return (
|
|
@@ -108,6 +138,17 @@ export function JsonTextEditor({
|
|
|
108
138
|
>
|
|
109
139
|
Format
|
|
110
140
|
</button>
|
|
141
|
+
{!readOnly && (
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
className="editor-button"
|
|
145
|
+
onClick={handleWrapInMain}
|
|
146
|
+
disabled={!onChange}
|
|
147
|
+
title={onChange ? 'Wrap root in Main and apply' : 'Read only'}
|
|
148
|
+
>
|
|
149
|
+
Wrap in Main
|
|
150
|
+
</button>
|
|
151
|
+
)}
|
|
111
152
|
{!readOnly && (
|
|
112
153
|
<button
|
|
113
154
|
type="button"
|