@developer_tribe/react-builder 0.1.31 → 1.1.0
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/DeviceMockFrame.d.ts +1 -17
- package/dist/RenderPage.d.ts +1 -9
- package/dist/build-components/Button/ButtonProps.generated.d.ts +2 -1
- package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +2 -1
- package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +2 -1
- package/dist/build-components/Image/ImageProps.generated.d.ts +2 -1
- package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +8 -4
- package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +6 -3
- package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +2 -1
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +10 -5
- package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +4 -1
- package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +4 -2
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +10 -5
- package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +10 -5
- package/dist/build-components/Text/TextProps.generated.d.ts +10 -5
- package/dist/build-components/View/ViewProps.generated.d.ts +6 -3
- package/dist/build-components/index.d.ts +1 -0
- package/dist/build-components/patterns.generated.d.ts +7 -2
- package/dist/components/AttributesEditorPanel.d.ts +9 -0
- package/dist/components/Breadcrumb.d.ts +13 -0
- package/dist/components/Builder.d.ts +9 -0
- package/dist/components/EditorHeader.d.ts +15 -0
- package/dist/index.cjs.js +7 -4
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.esm.js +7 -4
- package/dist/index.esm.js.map +1 -0
- package/dist/pages/ProjectPage.d.ts +9 -0
- package/dist/pages/tabs/BuilderTab.d.ts +9 -0
- package/dist/pages/tabs/DebugTab.d.ts +7 -0
- package/dist/pages/tabs/PreviewTab.d.ts +3 -0
- package/dist/store.d.ts +8 -18
- package/dist/styles.css +1 -1
- package/dist/types/PreviewConfig.d.ts +6 -3
- package/dist/types/Project.d.ts +2 -2
- package/dist/utils/copyNode.d.ts +2 -0
- package/package.json +17 -9
- package/scripts/prebuild/utils/createBuildComponentsIndex.js +15 -1
- package/scripts/prebuild/utils/createGeneratedProps.js +64 -5
- package/src/DeviceMockFrame.tsx +20 -31
- package/src/RenderPage.tsx +3 -38
- package/src/assets/images/android.svg +43 -0
- package/src/assets/images/apple.svg +16 -0
- package/src/assets/images/background.jpg +0 -0
- package/src/assets/samples/carousel-sample.json +2 -3
- package/src/assets/samples/getSamples.ts +51 -8
- package/src/assets/samples/simple-1.json +1 -2
- package/src/assets/samples/simple-2.json +1 -2
- package/src/assets/samples/vpn-onboard-1.json +1 -2
- package/src/assets/samples/vpn-onboard-2.json +1 -2
- package/src/assets/samples/vpn-onboard-3.json +1 -2
- package/src/assets/samples/vpn-onboard-4.json +1 -2
- package/src/assets/samples/vpn-onboard-5.json +1024 -0
- package/src/assets/samples/vpn-onboard-6.json +708 -0
- package/src/build-components/Button/ButtonProps.generated.ts +14 -12
- package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +6 -1
- package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +9 -7
- package/src/build-components/Image/ImageProps.generated.ts +3 -1
- package/src/build-components/OnboardButton/OnboardButton.tsx +5 -4
- package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +14 -9
- package/src/build-components/OnboardButton/pattern.json +3 -2
- package/src/build-components/OnboardButtons/OnboardButtons.tsx +5 -7
- package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +10 -3
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +9 -7
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +3 -3
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +33 -22
- package/src/build-components/OnboardImage/OnboardImage.tsx +24 -1
- package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +5 -1
- package/src/build-components/OnboardImage/pattern.json +3 -5
- package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +5 -2
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +33 -22
- package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +33 -22
- package/src/build-components/Text/Text.tsx +3 -3
- package/src/build-components/Text/TextProps.generated.ts +33 -22
- package/src/build-components/View/ViewProps.generated.ts +18 -9
- package/src/build-components/index.ts +22 -0
- package/src/build-components/patterns.generated.ts +7 -2
- package/src/components/AttributesEditorPanel.tsx +110 -0
- package/src/components/Breadcrumb.tsx +46 -0
- package/src/components/Builder.tsx +270 -0
- package/src/components/EditorHeader.tsx +184 -0
- package/src/index.ts +5 -4
- package/src/pages/ProjectPage.tsx +112 -0
- package/src/pages/tabs/BuilderTab.tsx +31 -0
- package/src/pages/tabs/DebugTab.tsx +21 -0
- package/src/pages/tabs/PreviewTab.tsx +192 -0
- package/src/size-matters/index.ts +5 -1
- package/src/store.ts +26 -38
- package/src/styles/_mixins.scss +21 -0
- package/src/styles/_variables.scss +27 -0
- package/src/styles/builder.scss +60 -0
- package/src/styles/components.scss +88 -0
- package/src/styles/editor.scss +174 -0
- package/src/styles/global.scss +200 -0
- package/src/styles/index.scss +7 -0
- package/src/styles/pages.scss +2 -0
- package/src/types/PreviewConfig.ts +14 -5
- package/src/types/Project.ts +2 -2
- package/src/utils/copyNode.ts +7 -0
- package/src/utils/extractTextStyle.ts +4 -2
- package/src/utils/getDevices.ts +1 -0
- package/src/utils/novaToJson.ts +5 -0
|
@@ -2,19 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
import type { NodeData } from '../../types/Node';
|
|
4
4
|
|
|
5
|
+
export type FlexDirectionOptionType = 'row' | 'column';
|
|
6
|
+
export type AlignItemsOptionType =
|
|
7
|
+
| 'flex-start'
|
|
8
|
+
| 'center'
|
|
9
|
+
| 'flex-end'
|
|
10
|
+
| 'stretch'
|
|
11
|
+
| 'baseline';
|
|
12
|
+
export type JustifyContentOptionType =
|
|
13
|
+
| 'flex-start'
|
|
14
|
+
| 'center'
|
|
15
|
+
| 'flex-end'
|
|
16
|
+
| 'space-between'
|
|
17
|
+
| 'space-around'
|
|
18
|
+
| 'space-evenly';
|
|
19
|
+
|
|
5
20
|
export interface ViewPropsGenerated {
|
|
6
21
|
child: string;
|
|
7
22
|
attributes: {
|
|
8
23
|
scrollable?: boolean;
|
|
9
|
-
flexDirection?:
|
|
10
|
-
alignItems?:
|
|
11
|
-
justifyContent?:
|
|
12
|
-
| 'flex-start'
|
|
13
|
-
| 'center'
|
|
14
|
-
| 'flex-end'
|
|
15
|
-
| 'space-between'
|
|
16
|
-
| 'space-around'
|
|
17
|
-
| 'space-evenly';
|
|
24
|
+
flexDirection?: FlexDirectionOptionType;
|
|
25
|
+
alignItems?: AlignItemsOptionType;
|
|
26
|
+
justifyContent?: JustifyContentOptionType;
|
|
18
27
|
gap?: string;
|
|
19
28
|
padding?: number;
|
|
20
29
|
paddingHorizontal?: string;
|
|
@@ -4,6 +4,28 @@ export { default as RenderNode } from './RenderNode.generated';
|
|
|
4
4
|
|
|
5
5
|
export { patterns } from './patterns.generated';
|
|
6
6
|
|
|
7
|
+
export const allcomponentNames = [
|
|
8
|
+
'button',
|
|
9
|
+
'carousel',
|
|
10
|
+
'carouselButtons',
|
|
11
|
+
'carouselDots',
|
|
12
|
+
'carouselItem',
|
|
13
|
+
'carouselProvider',
|
|
14
|
+
'image',
|
|
15
|
+
'Onboard',
|
|
16
|
+
'OnboardButton',
|
|
17
|
+
'OnboardButtons',
|
|
18
|
+
'OnboardDot',
|
|
19
|
+
'OnboardFooter',
|
|
20
|
+
'OnboardImage',
|
|
21
|
+
'OnboardItem',
|
|
22
|
+
'OnboardProvider',
|
|
23
|
+
'OnboardSubtitle',
|
|
24
|
+
'OnboardTitle',
|
|
25
|
+
'text',
|
|
26
|
+
'view',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
7
29
|
export type {
|
|
8
30
|
ButtonPropsGenerated,
|
|
9
31
|
ButtonComponentProps,
|
|
@@ -117,8 +117,9 @@ export const patterns = [
|
|
|
117
117
|
types: {
|
|
118
118
|
EventObject: {
|
|
119
119
|
type: ['Permission', 'Navigate'],
|
|
120
|
-
permission: ['att', 'notification', 'rating', 'null'],
|
|
121
|
-
navigate_to:
|
|
120
|
+
permission: ['att', 'notification', 'rating', 'GDPR', 'null'],
|
|
121
|
+
navigate_to: 'string',
|
|
122
|
+
targetIndex: 'number',
|
|
122
123
|
},
|
|
123
124
|
},
|
|
124
125
|
},
|
|
@@ -239,8 +240,12 @@ export const patterns = [
|
|
|
239
240
|
height: 'number',
|
|
240
241
|
resizeMode: ['cover', 'contain', 'stretch', 'center'],
|
|
241
242
|
borderRadius: 'number',
|
|
243
|
+
video_url: 'string',
|
|
244
|
+
lottie: 'string',
|
|
242
245
|
},
|
|
243
246
|
},
|
|
247
|
+
types: {},
|
|
248
|
+
defaults: {},
|
|
244
249
|
},
|
|
245
250
|
{
|
|
246
251
|
schemaVersion: 1,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { AttributesEditor, Node, NodeData } from '..';
|
|
3
|
+
|
|
4
|
+
interface AttributesEditorPanelProps {
|
|
5
|
+
current: Node;
|
|
6
|
+
attributes: any;
|
|
7
|
+
onChange: (data: Node) => void;
|
|
8
|
+
setCurrent: (current: Node) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function AttributesEditorPanel({
|
|
12
|
+
current,
|
|
13
|
+
attributes,
|
|
14
|
+
onChange,
|
|
15
|
+
setCurrent,
|
|
16
|
+
}: AttributesEditorPanelProps) {
|
|
17
|
+
if (!current) return null;
|
|
18
|
+
|
|
19
|
+
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
20
|
+
if (root === target) return next;
|
|
21
|
+
if (root === null || root === undefined) return root;
|
|
22
|
+
if (typeof root === 'string') return root;
|
|
23
|
+
if (Array.isArray(root)) {
|
|
24
|
+
let changed = false;
|
|
25
|
+
const arr = root.map((item) => {
|
|
26
|
+
const r = replaceNode(item, target, next);
|
|
27
|
+
if (r !== item) changed = true;
|
|
28
|
+
return r;
|
|
29
|
+
});
|
|
30
|
+
return changed ? arr : root;
|
|
31
|
+
}
|
|
32
|
+
const data = root as any;
|
|
33
|
+
if ('children' in data) {
|
|
34
|
+
const prev = data.children;
|
|
35
|
+
const replaced = Array.isArray(prev)
|
|
36
|
+
? prev.map((c: Node) => replaceNode(c, target, next))
|
|
37
|
+
: replaceNode(prev as Node, target, next);
|
|
38
|
+
if (replaced !== prev) {
|
|
39
|
+
data.children = replaced;
|
|
40
|
+
return { ...data, children: replaced } as Node;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
const [draft, setDraft] = useState<Node>(current);
|
|
46
|
+
const [isAllowedTopAccept, setIsAllowedTopAccept] = useState<boolean>(false);
|
|
47
|
+
|
|
48
|
+
const isEqual = useMemo(() => {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.stringify(current) === JSON.stringify(draft);
|
|
51
|
+
} catch {
|
|
52
|
+
return current === draft;
|
|
53
|
+
}
|
|
54
|
+
}, [current, draft]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setDraft(current);
|
|
58
|
+
setIsAllowedTopAccept(false);
|
|
59
|
+
}, [current, attributes]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
setIsAllowedTopAccept(!isEqual);
|
|
63
|
+
}, [isEqual]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div style={{ padding: 12 }}>
|
|
67
|
+
<h2>{(current as NodeData).type ?? current?.toString() ?? '?'}</h2>{' '}
|
|
68
|
+
<div
|
|
69
|
+
style={{
|
|
70
|
+
display: 'flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
gap: 8,
|
|
73
|
+
padding: '16px 0',
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<h3 style={{ marginTop: 0, marginBottom: 0, flex: '1 1 auto' }}>
|
|
77
|
+
Attributes
|
|
78
|
+
</h3>
|
|
79
|
+
<button
|
|
80
|
+
disabled={!isAllowedTopAccept}
|
|
81
|
+
onClick={() => {
|
|
82
|
+
setDraft(current);
|
|
83
|
+
setIsAllowedTopAccept(false);
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
Revert
|
|
87
|
+
</button>
|
|
88
|
+
<button
|
|
89
|
+
disabled={!isAllowedTopAccept}
|
|
90
|
+
onClick={() => {
|
|
91
|
+
const root = attributes as Node;
|
|
92
|
+
const updated = replaceNode(root, current, draft);
|
|
93
|
+
onChange(updated);
|
|
94
|
+
setIsAllowedTopAccept(false);
|
|
95
|
+
setCurrent(draft);
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
Accept
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
<AttributesEditor
|
|
102
|
+
node={draft}
|
|
103
|
+
onChange={(next: Node) => {
|
|
104
|
+
setDraft(next);
|
|
105
|
+
setIsAllowedTopAccept(true);
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
export type BreadcrumbItem = {
|
|
3
|
+
label: string;
|
|
4
|
+
to?: string;
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type BreadcrumbProps = {
|
|
9
|
+
items: BreadcrumbItem[];
|
|
10
|
+
separator?: ReactNode;
|
|
11
|
+
ariaLabel?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function Breadcrumb({
|
|
15
|
+
items,
|
|
16
|
+
separator = '/',
|
|
17
|
+
ariaLabel = 'Breadcrumb',
|
|
18
|
+
}: BreadcrumbProps) {
|
|
19
|
+
return (
|
|
20
|
+
<nav className="breadcrumb" aria-label={ariaLabel}>
|
|
21
|
+
<ol className="breadcrumb__list">
|
|
22
|
+
{items.map((item, index) => {
|
|
23
|
+
const isLast = index === items.length - 1;
|
|
24
|
+
return (
|
|
25
|
+
<li className="breadcrumb__item" key={`${item.label}-${index}`}>
|
|
26
|
+
{index > 0 && (
|
|
27
|
+
<span className="breadcrumb__separator" aria-hidden>
|
|
28
|
+
{separator}
|
|
29
|
+
</span>
|
|
30
|
+
)}
|
|
31
|
+
{isLast || !item.to ? (
|
|
32
|
+
<span className="breadcrumb__current" aria-current="page">
|
|
33
|
+
{item.label}
|
|
34
|
+
</span>
|
|
35
|
+
) : (
|
|
36
|
+
<p onClick={item.onClick} className="breadcrumb__link">
|
|
37
|
+
{item.label}
|
|
38
|
+
</p>
|
|
39
|
+
)}
|
|
40
|
+
</li>
|
|
41
|
+
);
|
|
42
|
+
})}
|
|
43
|
+
</ol>
|
|
44
|
+
</nav>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
isNodeArray,
|
|
4
|
+
isNodeNullOrUndefined,
|
|
5
|
+
isNodeString,
|
|
6
|
+
Node,
|
|
7
|
+
NodeData,
|
|
8
|
+
NodeDefaultAttribute,
|
|
9
|
+
allcomponentNames,
|
|
10
|
+
} from '..';
|
|
11
|
+
import { Breadcrumb } from './Breadcrumb';
|
|
12
|
+
import { getDefaultsForType, getPatternByType } from '../utils/patterns';
|
|
13
|
+
|
|
14
|
+
type BuilderEditorProps = {
|
|
15
|
+
data: Node;
|
|
16
|
+
setData: (data: Node) => void;
|
|
17
|
+
current: Node;
|
|
18
|
+
setCurrent: (current: Node) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface BuilderEditorComponentProps {
|
|
22
|
+
node: Node;
|
|
23
|
+
onClick: (node: Node) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function BuilderButton({ node, onClick }: { node: Node; onClick: () => void }) {
|
|
27
|
+
if (isNodeNullOrUndefined(node)) {
|
|
28
|
+
return <div className="builder__placeholder">Null or undefined</div>;
|
|
29
|
+
}
|
|
30
|
+
if (isNodeString(node)) {
|
|
31
|
+
return <div className="builder__text">{node as string}</div>;
|
|
32
|
+
}
|
|
33
|
+
const nodeData = node as NodeData<NodeDefaultAttribute>;
|
|
34
|
+
|
|
35
|
+
let extra = '';
|
|
36
|
+
if (nodeData.attributes?.condition) {
|
|
37
|
+
extra = ` (${nodeData.attributes.condition} ${nodeData.attributes.conditionVariable})`;
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
<a onClick={onClick} className="builder__button">
|
|
41
|
+
{nodeData.type} {extra}
|
|
42
|
+
</a>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
|
|
47
|
+
if (isNodeNullOrUndefined(node)) {
|
|
48
|
+
return <div className="builder__placeholder">Null or undefined</div>;
|
|
49
|
+
}
|
|
50
|
+
if (isNodeString(node)) {
|
|
51
|
+
return (
|
|
52
|
+
<div className="builder__text">
|
|
53
|
+
{node as string} (Please define a node)
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (isNodeArray(node)) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="builder__list">
|
|
61
|
+
{(node as Node[]).map((item, index) => (
|
|
62
|
+
<BuilderButton
|
|
63
|
+
onClick={() => {
|
|
64
|
+
onClick(item);
|
|
65
|
+
}}
|
|
66
|
+
key={index}
|
|
67
|
+
node={item}
|
|
68
|
+
/>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const nodeData = node as NodeData<NodeDefaultAttribute>;
|
|
75
|
+
const children = nodeData.children
|
|
76
|
+
? isNodeArray(nodeData.children)
|
|
77
|
+
? (nodeData.children as Node[])
|
|
78
|
+
: [nodeData.children]
|
|
79
|
+
: null;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="builder__node">
|
|
83
|
+
<p className="builder__node-type">{nodeData.type}</p>
|
|
84
|
+
<div className="builder__children">
|
|
85
|
+
{children &&
|
|
86
|
+
children.map((child, index) => (
|
|
87
|
+
<BuilderButton
|
|
88
|
+
onClick={() => {
|
|
89
|
+
onClick(child);
|
|
90
|
+
}}
|
|
91
|
+
key={index}
|
|
92
|
+
node={child}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function Builder({
|
|
101
|
+
data,
|
|
102
|
+
setData,
|
|
103
|
+
current,
|
|
104
|
+
setCurrent,
|
|
105
|
+
}: BuilderEditorProps) {
|
|
106
|
+
const [crumbs, setCrumbs] = useState<string[]>(['root']);
|
|
107
|
+
const breadcrumbItems = useMemo(
|
|
108
|
+
() => crumbs.map((c, idx) => ({ label: c })),
|
|
109
|
+
[crumbs],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
113
|
+
if (root === target) return next;
|
|
114
|
+
if (root === null || root === undefined) return root;
|
|
115
|
+
if (typeof root === 'string') return root;
|
|
116
|
+
if (Array.isArray(root)) {
|
|
117
|
+
let changed = false;
|
|
118
|
+
const arr = root.map((item) => {
|
|
119
|
+
const r = replaceNode(item, target, next);
|
|
120
|
+
if (r !== item) changed = true;
|
|
121
|
+
return r;
|
|
122
|
+
});
|
|
123
|
+
return changed ? arr : root;
|
|
124
|
+
}
|
|
125
|
+
const data = root as any;
|
|
126
|
+
if ('children' in data) {
|
|
127
|
+
const prev = data.children;
|
|
128
|
+
const replaced = Array.isArray(prev)
|
|
129
|
+
? prev.map((c: Node) => replaceNode(c, target, next))
|
|
130
|
+
: replaceNode(prev as Node, target, next);
|
|
131
|
+
if (replaced !== prev) {
|
|
132
|
+
data.children = replaced;
|
|
133
|
+
return { ...data, children: replaced } as Node;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return root;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createDefaultNode(type: string): NodeData<NodeDefaultAttribute> {
|
|
140
|
+
const pattern = getPatternByType(type)?.pattern;
|
|
141
|
+
const defaults = getDefaultsForType(type) ?? {};
|
|
142
|
+
let children: Node = '';
|
|
143
|
+
const childrenSchema = pattern?.children as unknown;
|
|
144
|
+
if (childrenSchema === 'never') {
|
|
145
|
+
children = '';
|
|
146
|
+
} else if (childrenSchema === 'string') {
|
|
147
|
+
children = '';
|
|
148
|
+
} else if (
|
|
149
|
+
childrenSchema === 'node' ||
|
|
150
|
+
(Array.isArray(childrenSchema) && childrenSchema.includes('node'))
|
|
151
|
+
) {
|
|
152
|
+
children = [];
|
|
153
|
+
} else if (typeof childrenSchema === 'string') {
|
|
154
|
+
// Specific child type like 'carouselItem' – initialize as empty array to allow multiple
|
|
155
|
+
children = [];
|
|
156
|
+
} else {
|
|
157
|
+
children = '';
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
type,
|
|
161
|
+
children,
|
|
162
|
+
attributes: { ...defaults },
|
|
163
|
+
} as NodeData<NodeDefaultAttribute>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getAllowedChildTypes(parent: Node): string[] {
|
|
167
|
+
if (
|
|
168
|
+
isNodeNullOrUndefined(parent) ||
|
|
169
|
+
isNodeString(parent) ||
|
|
170
|
+
isNodeArray(parent)
|
|
171
|
+
)
|
|
172
|
+
return [];
|
|
173
|
+
const parentData = parent as NodeData;
|
|
174
|
+
const parentType = parentData.type;
|
|
175
|
+
// Special rule: limit OnboardButtons to OnboardButton only
|
|
176
|
+
if (parentType === 'OnboardButtons') return ['OnboardButton'];
|
|
177
|
+
const childrenSchema = getPatternByType(parentType)?.pattern
|
|
178
|
+
?.children as unknown;
|
|
179
|
+
if (!childrenSchema) return [];
|
|
180
|
+
if (childrenSchema === 'never' || childrenSchema === 'string') return [];
|
|
181
|
+
if (
|
|
182
|
+
childrenSchema === 'node' ||
|
|
183
|
+
(Array.isArray(childrenSchema) && childrenSchema.includes('node'))
|
|
184
|
+
) {
|
|
185
|
+
return [...allcomponentNames];
|
|
186
|
+
}
|
|
187
|
+
if (typeof childrenSchema === 'string') {
|
|
188
|
+
return [childrenSchema];
|
|
189
|
+
}
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className="builder">
|
|
195
|
+
<Breadcrumb items={breadcrumbItems} />
|
|
196
|
+
|
|
197
|
+
<div className="builder__current">
|
|
198
|
+
{crumbs[crumbs.length - 1] + ' ( ' + crumbs.length + '. level )'}
|
|
199
|
+
</div>
|
|
200
|
+
<BuilderComponent
|
|
201
|
+
onClick={(node: Node) => {
|
|
202
|
+
setCurrent(node);
|
|
203
|
+
setCrumbs((crumbs) => [
|
|
204
|
+
...crumbs,
|
|
205
|
+
typeof node === 'string'
|
|
206
|
+
? node
|
|
207
|
+
: (node as NodeData<NodeDefaultAttribute>).type,
|
|
208
|
+
]);
|
|
209
|
+
}}
|
|
210
|
+
node={current}
|
|
211
|
+
/>
|
|
212
|
+
{!isNodeNullOrUndefined(current) &&
|
|
213
|
+
!isNodeString(current) &&
|
|
214
|
+
!isNodeArray(current) &&
|
|
215
|
+
(() => {
|
|
216
|
+
const allowed = getAllowedChildTypes(current);
|
|
217
|
+
if (allowed.length === 0) return null;
|
|
218
|
+
return (
|
|
219
|
+
<div
|
|
220
|
+
style={{
|
|
221
|
+
display: 'flex',
|
|
222
|
+
flexWrap: 'wrap',
|
|
223
|
+
gap: 8,
|
|
224
|
+
paddingTop: 12,
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{allowed.map((t) => (
|
|
228
|
+
<button
|
|
229
|
+
key={t}
|
|
230
|
+
className="editor-button"
|
|
231
|
+
onClick={() => {
|
|
232
|
+
const parent = current as NodeData<NodeDefaultAttribute>;
|
|
233
|
+
const nextChild = createDefaultNode(t);
|
|
234
|
+
let nextChildren: Node;
|
|
235
|
+
if (Array.isArray(parent.children)) {
|
|
236
|
+
nextChildren = [
|
|
237
|
+
...(parent.children as Node[]),
|
|
238
|
+
nextChild,
|
|
239
|
+
];
|
|
240
|
+
} else if (
|
|
241
|
+
parent.children === null ||
|
|
242
|
+
parent.children === undefined ||
|
|
243
|
+
typeof parent.children === 'string'
|
|
244
|
+
) {
|
|
245
|
+
nextChildren = [nextChild];
|
|
246
|
+
} else {
|
|
247
|
+
nextChildren = [parent.children as Node, nextChild];
|
|
248
|
+
}
|
|
249
|
+
const updatedParent: NodeData<NodeDefaultAttribute> = {
|
|
250
|
+
...parent,
|
|
251
|
+
children: nextChildren,
|
|
252
|
+
};
|
|
253
|
+
const updatedRoot = replaceNode(
|
|
254
|
+
data,
|
|
255
|
+
current,
|
|
256
|
+
updatedParent,
|
|
257
|
+
);
|
|
258
|
+
setData(updatedRoot);
|
|
259
|
+
setCurrent(updatedParent);
|
|
260
|
+
}}
|
|
261
|
+
>
|
|
262
|
+
Add {t}
|
|
263
|
+
</button>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
})()}
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import { Device, getDevices, Node, copyNode } from '..';
|
|
3
|
+
import { useRenderStore } from '../store';
|
|
4
|
+
import { Breadcrumb, BreadcrumbItem } from './Breadcrumb';
|
|
5
|
+
|
|
6
|
+
const devices = getDevices();
|
|
7
|
+
|
|
8
|
+
interface EditorHeaderProps {
|
|
9
|
+
onSaveProject?: () => void;
|
|
10
|
+
current?: Node;
|
|
11
|
+
editorData?: Node;
|
|
12
|
+
setEditorData?: (data: Node) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DeviceButtonProps {
|
|
16
|
+
device: Device;
|
|
17
|
+
selectedDevice: Device | null;
|
|
18
|
+
setSelectedDevice: (device: Device) => void;
|
|
19
|
+
}
|
|
20
|
+
export function DeviceButton({
|
|
21
|
+
device,
|
|
22
|
+
selectedDevice,
|
|
23
|
+
setSelectedDevice,
|
|
24
|
+
}: DeviceButtonProps) {
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
className={`editor-device-button ${selectedDevice === device ? 'editor-device-button--selected' : ''}`}
|
|
28
|
+
onClick={() => setSelectedDevice(device)}
|
|
29
|
+
>
|
|
30
|
+
{device.name} <br />
|
|
31
|
+
{device.width}x{device.height}
|
|
32
|
+
</button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function EditorHeader({
|
|
37
|
+
onSaveProject,
|
|
38
|
+
current,
|
|
39
|
+
editorData,
|
|
40
|
+
setEditorData,
|
|
41
|
+
}: EditorHeaderProps) {
|
|
42
|
+
const [isDevicesModalOpen, setIsDevicesModalOpen] = useState(false);
|
|
43
|
+
const copiedNode = useRenderStore((s) => s.copiedNode);
|
|
44
|
+
const { device, setDevice } = useRenderStore((s) => ({
|
|
45
|
+
device: s.device,
|
|
46
|
+
setDevice: s.setDevice,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
50
|
+
if (root === target) return next;
|
|
51
|
+
if (root === null || root === undefined) return root;
|
|
52
|
+
if (typeof root === 'string') return root;
|
|
53
|
+
if (Array.isArray(root)) {
|
|
54
|
+
let changed = false;
|
|
55
|
+
const arr = root.map((item) => {
|
|
56
|
+
const r = replaceNode(item, target, next);
|
|
57
|
+
if (r !== item) changed = true;
|
|
58
|
+
return r;
|
|
59
|
+
});
|
|
60
|
+
return changed ? arr : root;
|
|
61
|
+
}
|
|
62
|
+
const data = root as any;
|
|
63
|
+
if ('children' in data) {
|
|
64
|
+
const prev = data.children;
|
|
65
|
+
const replaced = Array.isArray(prev)
|
|
66
|
+
? prev.map((c: Node) => replaceNode(c, target, next))
|
|
67
|
+
: replaceNode(prev as Node, target, next);
|
|
68
|
+
if (replaced !== prev) {
|
|
69
|
+
data.children = replaced;
|
|
70
|
+
return { ...data, children: replaced } as Node;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return root;
|
|
74
|
+
}
|
|
75
|
+
const handleCopy = () => {
|
|
76
|
+
if (current) copyNode(current);
|
|
77
|
+
};
|
|
78
|
+
const handlePaste = () => {
|
|
79
|
+
if (!current || !editorData || !setEditorData) return;
|
|
80
|
+
if (!copiedNode) return;
|
|
81
|
+
const cloned = JSON.parse(JSON.stringify(copiedNode)) as Node;
|
|
82
|
+
const updated = replaceNode(editorData, current, cloned);
|
|
83
|
+
useRenderStore.setState({
|
|
84
|
+
copiedNode: null,
|
|
85
|
+
});
|
|
86
|
+
useRenderStore.getState().forceRender();
|
|
87
|
+
setEditorData(updated);
|
|
88
|
+
};
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
className="editor-header"
|
|
92
|
+
role="region"
|
|
93
|
+
aria-label="Editor utility header"
|
|
94
|
+
>
|
|
95
|
+
<div className="editor-header__devices">
|
|
96
|
+
{devices.slice(0, 5).map((device: Device) => (
|
|
97
|
+
<DeviceButton
|
|
98
|
+
key={device.name}
|
|
99
|
+
selectedDevice={device}
|
|
100
|
+
setSelectedDevice={setDevice}
|
|
101
|
+
device={device}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
<button
|
|
105
|
+
className="editor-device-button"
|
|
106
|
+
aria-label="More devices"
|
|
107
|
+
onClick={() => setIsDevicesModalOpen(true)}
|
|
108
|
+
>
|
|
109
|
+
More devices
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="editor-header__actions">
|
|
113
|
+
<button
|
|
114
|
+
className="editor-button editor-save-button"
|
|
115
|
+
aria-label="Save project data"
|
|
116
|
+
onClick={() => onSaveProject && onSaveProject()}
|
|
117
|
+
>
|
|
118
|
+
Save
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
className="editor-button editor-save-previewconfig-button"
|
|
122
|
+
aria-label="Save previewConfig"
|
|
123
|
+
onClick={() => useRenderStore.getState().forceRender()}
|
|
124
|
+
>
|
|
125
|
+
Force Render
|
|
126
|
+
</button>
|
|
127
|
+
<button
|
|
128
|
+
className="editor-button"
|
|
129
|
+
aria-label="Copy node"
|
|
130
|
+
onClick={handleCopy}
|
|
131
|
+
>
|
|
132
|
+
Copy
|
|
133
|
+
</button>
|
|
134
|
+
{copiedNode && (
|
|
135
|
+
<button
|
|
136
|
+
className="editor-button"
|
|
137
|
+
aria-label="Paste node"
|
|
138
|
+
onClick={handlePaste}
|
|
139
|
+
>
|
|
140
|
+
Paste
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
{isDevicesModalOpen && (
|
|
145
|
+
<div
|
|
146
|
+
className="editor-modal"
|
|
147
|
+
role="dialog"
|
|
148
|
+
aria-modal="true"
|
|
149
|
+
aria-labelledby="device-selector-title"
|
|
150
|
+
>
|
|
151
|
+
<div
|
|
152
|
+
className="editor-modal__overlay"
|
|
153
|
+
onClick={() => setIsDevicesModalOpen(false)}
|
|
154
|
+
/>
|
|
155
|
+
<div className="editor-modal__content">
|
|
156
|
+
<div className="editor-modal__header">
|
|
157
|
+
<h3 id="device-selector-title">Select a device</h3>
|
|
158
|
+
<button
|
|
159
|
+
className="editor-button"
|
|
160
|
+
aria-label="Close device selector"
|
|
161
|
+
onClick={() => setIsDevicesModalOpen(false)}
|
|
162
|
+
>
|
|
163
|
+
Close
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="editor-device-grid" role="list">
|
|
167
|
+
{devices.map((device: Device) => (
|
|
168
|
+
<DeviceButton
|
|
169
|
+
key={device.name}
|
|
170
|
+
selectedDevice={device}
|
|
171
|
+
setSelectedDevice={(d: Device) => {
|
|
172
|
+
setDevice(d);
|
|
173
|
+
setIsDevicesModalOpen(false);
|
|
174
|
+
}}
|
|
175
|
+
device={device}
|
|
176
|
+
/>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|