@developer_tribe/react-builder 0.1.32 → 1.0.1
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/index.d.ts +1 -0
- 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 +6 -5
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.esm.js +6 -5
- package/dist/index.esm.js.map +1 -0
- package/dist/pages/ProjectPage.d.ts +11 -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 +17 -18
- package/dist/styles.css +1 -1
- package/dist/types/PreviewConfig.d.ts +6 -3
- package/dist/types/Project.d.ts +12 -2
- package/dist/utils/copyNode.d.ts +2 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/useLogRender.d.ts +1 -0
- package/package.json +16 -9
- package/scripts/prebuild/utils/createBuildComponentsIndex.js +15 -1
- package/src/AttributesEditor.tsx +2 -0
- package/src/DeviceMockFrame.tsx +22 -31
- package/src/RenderPage.tsx +5 -42
- 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 +49 -12
- 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 +1 -2
- package/src/assets/samples/vpn-onboard-6.json +1 -2
- package/src/build-components/Button/Button.tsx +2 -0
- package/src/build-components/Carousel/Carousel.tsx +2 -0
- package/src/build-components/CarouselButtons/CarouselButtons.tsx +2 -0
- package/src/build-components/CarouselDots/CarouselDots.tsx +2 -0
- package/src/build-components/CarouselItem/CarouselItem.tsx +2 -0
- package/src/build-components/Image/Image.tsx +2 -0
- package/src/build-components/Onboard/Onboard.tsx +2 -0
- package/src/build-components/OnboardButton/OnboardButton.tsx +7 -4
- package/src/build-components/OnboardButtons/OnboardButtons.tsx +7 -7
- package/src/build-components/OnboardDot/OnboardDot.tsx +2 -0
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +5 -3
- package/src/build-components/OnboardImage/OnboardImage.tsx +2 -0
- package/src/build-components/OnboardItem/OnboardItem.tsx +2 -0
- package/src/build-components/OnboardProvider/OnboardProvider.tsx +2 -0
- package/src/build-components/OnboardSubtitle/OnboardSubtitle.tsx +2 -0
- package/src/build-components/OnboardTitle/OnboardTitle.tsx +2 -0
- package/src/build-components/Text/Text.tsx +5 -3
- package/src/build-components/View/View.tsx +2 -0
- package/src/build-components/index.ts +22 -0
- package/src/components/AttributesEditorPanel.tsx +112 -0
- package/src/components/Breadcrumb.tsx +48 -0
- package/src/components/Builder.tsx +272 -0
- package/src/components/EditorHeader.tsx +186 -0
- package/src/index.ts +8 -4
- package/src/pages/ProjectPage.tsx +152 -0
- package/src/pages/tabs/BuilderTab.tsx +33 -0
- package/src/pages/tabs/DebugTab.tsx +23 -0
- package/src/pages/tabs/PreviewTab.tsx +194 -0
- package/src/size-matters/index.ts +5 -1
- package/src/store.ts +60 -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 +15 -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/logger.ts +76 -0
- package/src/utils/useLogRender.ts +13 -0
|
@@ -0,0 +1,272 @@
|
|
|
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 { useLogRender } from '../utils/useLogRender';
|
|
13
|
+
import { getDefaultsForType, getPatternByType } from '../utils/patterns';
|
|
14
|
+
|
|
15
|
+
type BuilderEditorProps = {
|
|
16
|
+
data: Node;
|
|
17
|
+
setData: (data: Node) => void;
|
|
18
|
+
current: Node;
|
|
19
|
+
setCurrent: (current: Node) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
interface BuilderEditorComponentProps {
|
|
23
|
+
node: Node;
|
|
24
|
+
onClick: (node: Node) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function BuilderButton({ node, onClick }: { node: Node; onClick: () => void }) {
|
|
28
|
+
if (isNodeNullOrUndefined(node)) {
|
|
29
|
+
return <div className="builder__placeholder">Null or undefined</div>;
|
|
30
|
+
}
|
|
31
|
+
if (isNodeString(node)) {
|
|
32
|
+
return <div className="builder__text">{node as string}</div>;
|
|
33
|
+
}
|
|
34
|
+
const nodeData = node as NodeData<NodeDefaultAttribute>;
|
|
35
|
+
|
|
36
|
+
let extra = '';
|
|
37
|
+
if (nodeData.attributes?.condition) {
|
|
38
|
+
extra = ` (${nodeData.attributes.condition} ${nodeData.attributes.conditionVariable})`;
|
|
39
|
+
}
|
|
40
|
+
return (
|
|
41
|
+
<a onClick={onClick} className="builder__button">
|
|
42
|
+
{nodeData.type} {extra}
|
|
43
|
+
</a>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
|
|
48
|
+
if (isNodeNullOrUndefined(node)) {
|
|
49
|
+
return <div className="builder__placeholder">Null or undefined</div>;
|
|
50
|
+
}
|
|
51
|
+
if (isNodeString(node)) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="builder__text">
|
|
54
|
+
{node as string} (Please define a node)
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isNodeArray(node)) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="builder__list">
|
|
62
|
+
{(node as Node[]).map((item, index) => (
|
|
63
|
+
<BuilderButton
|
|
64
|
+
onClick={() => {
|
|
65
|
+
onClick(item);
|
|
66
|
+
}}
|
|
67
|
+
key={index}
|
|
68
|
+
node={item}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const nodeData = node as NodeData<NodeDefaultAttribute>;
|
|
76
|
+
const children = nodeData.children
|
|
77
|
+
? isNodeArray(nodeData.children)
|
|
78
|
+
? (nodeData.children as Node[])
|
|
79
|
+
: [nodeData.children]
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="builder__node">
|
|
84
|
+
<p className="builder__node-type">{nodeData.type}</p>
|
|
85
|
+
<div className="builder__children">
|
|
86
|
+
{children &&
|
|
87
|
+
children.map((child, index) => (
|
|
88
|
+
<BuilderButton
|
|
89
|
+
onClick={() => {
|
|
90
|
+
onClick(child);
|
|
91
|
+
}}
|
|
92
|
+
key={index}
|
|
93
|
+
node={child}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function Builder({
|
|
102
|
+
data,
|
|
103
|
+
setData,
|
|
104
|
+
current,
|
|
105
|
+
setCurrent,
|
|
106
|
+
}: BuilderEditorProps) {
|
|
107
|
+
useLogRender('Builder');
|
|
108
|
+
const [crumbs, setCrumbs] = useState<string[]>(['root']);
|
|
109
|
+
const breadcrumbItems = useMemo(
|
|
110
|
+
() => crumbs.map((c, idx) => ({ label: c })),
|
|
111
|
+
[crumbs],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
115
|
+
if (root === target) return next;
|
|
116
|
+
if (root === null || root === undefined) return root;
|
|
117
|
+
if (typeof root === 'string') return root;
|
|
118
|
+
if (Array.isArray(root)) {
|
|
119
|
+
let changed = false;
|
|
120
|
+
const arr = root.map((item) => {
|
|
121
|
+
const r = replaceNode(item, target, next);
|
|
122
|
+
if (r !== item) changed = true;
|
|
123
|
+
return r;
|
|
124
|
+
});
|
|
125
|
+
return changed ? arr : root;
|
|
126
|
+
}
|
|
127
|
+
const data = root as any;
|
|
128
|
+
if ('children' in data) {
|
|
129
|
+
const prev = data.children;
|
|
130
|
+
const replaced = Array.isArray(prev)
|
|
131
|
+
? prev.map((c: Node) => replaceNode(c, target, next))
|
|
132
|
+
: replaceNode(prev as Node, target, next);
|
|
133
|
+
if (replaced !== prev) {
|
|
134
|
+
data.children = replaced;
|
|
135
|
+
return { ...data, children: replaced } as Node;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return root;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createDefaultNode(type: string): NodeData<NodeDefaultAttribute> {
|
|
142
|
+
const pattern = getPatternByType(type)?.pattern;
|
|
143
|
+
const defaults = getDefaultsForType(type) ?? {};
|
|
144
|
+
let children: Node = '';
|
|
145
|
+
const childrenSchema = pattern?.children as unknown;
|
|
146
|
+
if (childrenSchema === 'never') {
|
|
147
|
+
children = '';
|
|
148
|
+
} else if (childrenSchema === 'string') {
|
|
149
|
+
children = '';
|
|
150
|
+
} else if (
|
|
151
|
+
childrenSchema === 'node' ||
|
|
152
|
+
(Array.isArray(childrenSchema) && childrenSchema.includes('node'))
|
|
153
|
+
) {
|
|
154
|
+
children = [];
|
|
155
|
+
} else if (typeof childrenSchema === 'string') {
|
|
156
|
+
// Specific child type like 'carouselItem' – initialize as empty array to allow multiple
|
|
157
|
+
children = [];
|
|
158
|
+
} else {
|
|
159
|
+
children = '';
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
type,
|
|
163
|
+
children,
|
|
164
|
+
attributes: { ...defaults },
|
|
165
|
+
} as NodeData<NodeDefaultAttribute>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getAllowedChildTypes(parent: Node): string[] {
|
|
169
|
+
if (
|
|
170
|
+
isNodeNullOrUndefined(parent) ||
|
|
171
|
+
isNodeString(parent) ||
|
|
172
|
+
isNodeArray(parent)
|
|
173
|
+
)
|
|
174
|
+
return [];
|
|
175
|
+
const parentData = parent as NodeData;
|
|
176
|
+
const parentType = parentData.type;
|
|
177
|
+
// Special rule: limit OnboardButtons to OnboardButton only
|
|
178
|
+
if (parentType === 'OnboardButtons') return ['OnboardButton'];
|
|
179
|
+
const childrenSchema = getPatternByType(parentType)?.pattern
|
|
180
|
+
?.children as unknown;
|
|
181
|
+
if (!childrenSchema) return [];
|
|
182
|
+
if (childrenSchema === 'never' || childrenSchema === 'string') return [];
|
|
183
|
+
if (
|
|
184
|
+
childrenSchema === 'node' ||
|
|
185
|
+
(Array.isArray(childrenSchema) && childrenSchema.includes('node'))
|
|
186
|
+
) {
|
|
187
|
+
return [...allcomponentNames];
|
|
188
|
+
}
|
|
189
|
+
if (typeof childrenSchema === 'string') {
|
|
190
|
+
return [childrenSchema];
|
|
191
|
+
}
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className="builder">
|
|
197
|
+
<Breadcrumb items={breadcrumbItems} />
|
|
198
|
+
|
|
199
|
+
<div className="builder__current">
|
|
200
|
+
{crumbs[crumbs.length - 1] + ' ( ' + crumbs.length + '. level )'}
|
|
201
|
+
</div>
|
|
202
|
+
<BuilderComponent
|
|
203
|
+
onClick={(node: Node) => {
|
|
204
|
+
setCurrent(node);
|
|
205
|
+
setCrumbs((crumbs) => [
|
|
206
|
+
...crumbs,
|
|
207
|
+
typeof node === 'string'
|
|
208
|
+
? node
|
|
209
|
+
: (node as NodeData<NodeDefaultAttribute>).type,
|
|
210
|
+
]);
|
|
211
|
+
}}
|
|
212
|
+
node={current}
|
|
213
|
+
/>
|
|
214
|
+
{!isNodeNullOrUndefined(current) &&
|
|
215
|
+
!isNodeString(current) &&
|
|
216
|
+
!isNodeArray(current) &&
|
|
217
|
+
(() => {
|
|
218
|
+
const allowed = getAllowedChildTypes(current);
|
|
219
|
+
if (allowed.length === 0) return null;
|
|
220
|
+
return (
|
|
221
|
+
<div
|
|
222
|
+
style={{
|
|
223
|
+
display: 'flex',
|
|
224
|
+
flexWrap: 'wrap',
|
|
225
|
+
gap: 8,
|
|
226
|
+
paddingTop: 12,
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
{allowed.map((t) => (
|
|
230
|
+
<button
|
|
231
|
+
key={t}
|
|
232
|
+
className="editor-button"
|
|
233
|
+
onClick={() => {
|
|
234
|
+
const parent = current as NodeData<NodeDefaultAttribute>;
|
|
235
|
+
const nextChild = createDefaultNode(t);
|
|
236
|
+
let nextChildren: Node;
|
|
237
|
+
if (Array.isArray(parent.children)) {
|
|
238
|
+
nextChildren = [
|
|
239
|
+
...(parent.children as Node[]),
|
|
240
|
+
nextChild,
|
|
241
|
+
];
|
|
242
|
+
} else if (
|
|
243
|
+
parent.children === null ||
|
|
244
|
+
parent.children === undefined ||
|
|
245
|
+
typeof parent.children === 'string'
|
|
246
|
+
) {
|
|
247
|
+
nextChildren = [nextChild];
|
|
248
|
+
} else {
|
|
249
|
+
nextChildren = [parent.children as Node, nextChild];
|
|
250
|
+
}
|
|
251
|
+
const updatedParent: NodeData<NodeDefaultAttribute> = {
|
|
252
|
+
...parent,
|
|
253
|
+
children: nextChildren,
|
|
254
|
+
};
|
|
255
|
+
const updatedRoot = replaceNode(
|
|
256
|
+
data,
|
|
257
|
+
current,
|
|
258
|
+
updatedParent,
|
|
259
|
+
);
|
|
260
|
+
setData(updatedRoot);
|
|
261
|
+
setCurrent(updatedParent);
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
Add {t}
|
|
265
|
+
</button>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
})()}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
import { useLogRender } from '../utils/useLogRender';
|
|
6
|
+
|
|
7
|
+
const devices = getDevices();
|
|
8
|
+
|
|
9
|
+
interface EditorHeaderProps {
|
|
10
|
+
onSaveProject?: () => void;
|
|
11
|
+
current?: Node;
|
|
12
|
+
editorData?: Node;
|
|
13
|
+
setEditorData?: (data: Node) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DeviceButtonProps {
|
|
17
|
+
device: Device;
|
|
18
|
+
selectedDevice: Device | null;
|
|
19
|
+
setSelectedDevice: (device: Device) => void;
|
|
20
|
+
}
|
|
21
|
+
export function DeviceButton({
|
|
22
|
+
device,
|
|
23
|
+
selectedDevice,
|
|
24
|
+
setSelectedDevice,
|
|
25
|
+
}: DeviceButtonProps) {
|
|
26
|
+
return (
|
|
27
|
+
<button
|
|
28
|
+
className={`editor-device-button ${selectedDevice === device ? 'editor-device-button--selected' : ''}`}
|
|
29
|
+
onClick={() => setSelectedDevice(device)}
|
|
30
|
+
>
|
|
31
|
+
{device.name} <br />
|
|
32
|
+
{device.width}x{device.height}
|
|
33
|
+
</button>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function EditorHeader({
|
|
38
|
+
onSaveProject,
|
|
39
|
+
current,
|
|
40
|
+
editorData,
|
|
41
|
+
setEditorData,
|
|
42
|
+
}: EditorHeaderProps) {
|
|
43
|
+
useLogRender('EditorHeader');
|
|
44
|
+
const [isDevicesModalOpen, setIsDevicesModalOpen] = useState(false);
|
|
45
|
+
const copiedNode = useRenderStore((s) => s.copiedNode);
|
|
46
|
+
const { device, setDevice } = useRenderStore((s) => ({
|
|
47
|
+
device: s.device,
|
|
48
|
+
setDevice: s.setDevice,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
52
|
+
if (root === target) return next;
|
|
53
|
+
if (root === null || root === undefined) return root;
|
|
54
|
+
if (typeof root === 'string') return root;
|
|
55
|
+
if (Array.isArray(root)) {
|
|
56
|
+
let changed = false;
|
|
57
|
+
const arr = root.map((item) => {
|
|
58
|
+
const r = replaceNode(item, target, next);
|
|
59
|
+
if (r !== item) changed = true;
|
|
60
|
+
return r;
|
|
61
|
+
});
|
|
62
|
+
return changed ? arr : root;
|
|
63
|
+
}
|
|
64
|
+
const data = root as any;
|
|
65
|
+
if ('children' in data) {
|
|
66
|
+
const prev = data.children;
|
|
67
|
+
const replaced = Array.isArray(prev)
|
|
68
|
+
? prev.map((c: Node) => replaceNode(c, target, next))
|
|
69
|
+
: replaceNode(prev as Node, target, next);
|
|
70
|
+
if (replaced !== prev) {
|
|
71
|
+
data.children = replaced;
|
|
72
|
+
return { ...data, children: replaced } as Node;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return root;
|
|
76
|
+
}
|
|
77
|
+
const handleCopy = () => {
|
|
78
|
+
if (current) copyNode(current);
|
|
79
|
+
};
|
|
80
|
+
const handlePaste = () => {
|
|
81
|
+
if (!current || !editorData || !setEditorData) return;
|
|
82
|
+
if (!copiedNode) return;
|
|
83
|
+
const cloned = JSON.parse(JSON.stringify(copiedNode)) as Node;
|
|
84
|
+
const updated = replaceNode(editorData, current, cloned);
|
|
85
|
+
useRenderStore.setState({
|
|
86
|
+
copiedNode: null,
|
|
87
|
+
});
|
|
88
|
+
useRenderStore.getState().forceRender();
|
|
89
|
+
setEditorData(updated);
|
|
90
|
+
};
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
className="editor-header"
|
|
94
|
+
role="region"
|
|
95
|
+
aria-label="Editor utility header"
|
|
96
|
+
>
|
|
97
|
+
<div className="editor-header__devices">
|
|
98
|
+
{devices.slice(0, 5).map((device: Device) => (
|
|
99
|
+
<DeviceButton
|
|
100
|
+
key={device.name}
|
|
101
|
+
selectedDevice={device}
|
|
102
|
+
setSelectedDevice={setDevice}
|
|
103
|
+
device={device}
|
|
104
|
+
/>
|
|
105
|
+
))}
|
|
106
|
+
<button
|
|
107
|
+
className="editor-device-button"
|
|
108
|
+
aria-label="More devices"
|
|
109
|
+
onClick={() => setIsDevicesModalOpen(true)}
|
|
110
|
+
>
|
|
111
|
+
More devices
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="editor-header__actions">
|
|
115
|
+
<button
|
|
116
|
+
className="editor-button editor-save-button"
|
|
117
|
+
aria-label="Save project data"
|
|
118
|
+
onClick={() => onSaveProject && onSaveProject()}
|
|
119
|
+
>
|
|
120
|
+
Save
|
|
121
|
+
</button>
|
|
122
|
+
<button
|
|
123
|
+
className="editor-button editor-save-previewconfig-button"
|
|
124
|
+
aria-label="Save previewConfig"
|
|
125
|
+
onClick={() => useRenderStore.getState().forceRender()}
|
|
126
|
+
>
|
|
127
|
+
Force Render
|
|
128
|
+
</button>
|
|
129
|
+
<button
|
|
130
|
+
className="editor-button"
|
|
131
|
+
aria-label="Copy node"
|
|
132
|
+
onClick={handleCopy}
|
|
133
|
+
>
|
|
134
|
+
Copy
|
|
135
|
+
</button>
|
|
136
|
+
{copiedNode && (
|
|
137
|
+
<button
|
|
138
|
+
className="editor-button"
|
|
139
|
+
aria-label="Paste node"
|
|
140
|
+
onClick={handlePaste}
|
|
141
|
+
>
|
|
142
|
+
Paste
|
|
143
|
+
</button>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
{isDevicesModalOpen && (
|
|
147
|
+
<div
|
|
148
|
+
className="editor-modal"
|
|
149
|
+
role="dialog"
|
|
150
|
+
aria-modal="true"
|
|
151
|
+
aria-labelledby="device-selector-title"
|
|
152
|
+
>
|
|
153
|
+
<div
|
|
154
|
+
className="editor-modal__overlay"
|
|
155
|
+
onClick={() => setIsDevicesModalOpen(false)}
|
|
156
|
+
/>
|
|
157
|
+
<div className="editor-modal__content">
|
|
158
|
+
<div className="editor-modal__header">
|
|
159
|
+
<h3 id="device-selector-title">Select a device</h3>
|
|
160
|
+
<button
|
|
161
|
+
className="editor-button"
|
|
162
|
+
aria-label="Close device selector"
|
|
163
|
+
onClick={() => setIsDevicesModalOpen(false)}
|
|
164
|
+
>
|
|
165
|
+
Close
|
|
166
|
+
</button>
|
|
167
|
+
</div>
|
|
168
|
+
<div className="editor-device-grid" role="list">
|
|
169
|
+
{devices.map((device: Device) => (
|
|
170
|
+
<DeviceButton
|
|
171
|
+
key={device.name}
|
|
172
|
+
selectedDevice={device}
|
|
173
|
+
setSelectedDevice={(d: Device) => {
|
|
174
|
+
setDevice(d);
|
|
175
|
+
setIsDevicesModalOpen(false);
|
|
176
|
+
}}
|
|
177
|
+
device={device}
|
|
178
|
+
/>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import './styles/index.scss';
|
|
2
2
|
import AttributesEditor from './AttributesEditor';
|
|
3
3
|
|
|
4
|
-
export { TargetedScreenSize } from './types/TargetedScreenSize';
|
|
5
|
-
export { Node, NodeData, NodeDefaultAttribute } from './types/Node';
|
|
6
|
-
export { Project } from './types/Project';
|
|
4
|
+
export type { TargetedScreenSize } from './types/TargetedScreenSize';
|
|
5
|
+
export type { Node, NodeData, NodeDefaultAttribute } from './types/Node';
|
|
6
|
+
export type { Project } from './types/Project';
|
|
7
7
|
export {
|
|
8
8
|
isNodeNullOrUndefined,
|
|
9
9
|
isNodeString,
|
|
@@ -14,7 +14,6 @@ export {
|
|
|
14
14
|
export { getSamples } from './assets/samples/getSamples';
|
|
15
15
|
export { getBasicSamples } from './assets/samples/getSamples';
|
|
16
16
|
export { getOnboardSamples } from './assets/samples/getSamples';
|
|
17
|
-
export { PreviewConfig } from './types/PreviewConfig';
|
|
18
17
|
export { RenderPage } from './RenderPage';
|
|
19
18
|
export { DeviceMockFrame } from './DeviceMockFrame';
|
|
20
19
|
export { novaToJson } from './utils/novaToJson';
|
|
@@ -28,3 +27,8 @@ export { querySelector } from './utils/querySelector';
|
|
|
28
27
|
export { extractViewStyle } from './utils/extractViewStyle';
|
|
29
28
|
export { extractImageStyle } from './utils/extractImageStyle';
|
|
30
29
|
export { extractTextStyle } from './utils/extractTextStyle';
|
|
30
|
+
export { ProjectPage } from './pages/ProjectPage';
|
|
31
|
+
export type { ProjectPageProps, Tab } from './pages/ProjectPage';
|
|
32
|
+
export { copyNode } from './utils/copyNode';
|
|
33
|
+
export { logger } from './utils/logger';
|
|
34
|
+
export type { LogLevel } from './types/Project';
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Node, RenderPage, Project } from '..';
|
|
3
|
+
import { EditorHeader } from '../components/EditorHeader';
|
|
4
|
+
import { AttributesEditorPanel } from '../components/AttributesEditorPanel';
|
|
5
|
+
import { BuilderTab } from './tabs/BuilderTab';
|
|
6
|
+
import { PreviewTab } from './tabs/PreviewTab';
|
|
7
|
+
import { DebugTab } from './tabs/DebugTab';
|
|
8
|
+
import { AppConfig, defaultAppConfig } from '../types/PreviewConfig';
|
|
9
|
+
import { useRenderStore } from '../store';
|
|
10
|
+
import { logger } from '../utils/logger';
|
|
11
|
+
import { useLogRender } from '../utils/useLogRender';
|
|
12
|
+
import type { LogLevel } from '../types/Project';
|
|
13
|
+
|
|
14
|
+
export type Tab = 'builder' | 'preview' | 'debug';
|
|
15
|
+
|
|
16
|
+
export type ProjectPageProps = {
|
|
17
|
+
project: Project;
|
|
18
|
+
onSaveProject: (project: Project) => void;
|
|
19
|
+
appConfig?: AppConfig;
|
|
20
|
+
logLevel?: LogLevel;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function ProjectPage({
|
|
24
|
+
project,
|
|
25
|
+
appConfig = defaultAppConfig,
|
|
26
|
+
onSaveProject,
|
|
27
|
+
logLevel,
|
|
28
|
+
}: ProjectPageProps) {
|
|
29
|
+
useLogRender('ProjectPage');
|
|
30
|
+
const [current, setCurrent] = useState<Node>(project.data);
|
|
31
|
+
const [editorData, setEditorData] = useState<Node>(project.data);
|
|
32
|
+
const [tab, setTab] = useState<Tab>('builder');
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
logger.info('ProjectPage', 'mount', { projectName: project.name });
|
|
36
|
+
useRenderStore.getState().setAppConfig(appConfig);
|
|
37
|
+
logger.verbose('ProjectPage', 'appConfig applied', appConfig);
|
|
38
|
+
return () => {
|
|
39
|
+
logger.info('ProjectPage', 'unmount');
|
|
40
|
+
};
|
|
41
|
+
}, [appConfig, project.name]);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!logLevel) return;
|
|
45
|
+
logger.setLevel(logLevel);
|
|
46
|
+
}, [logLevel]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="container-full">
|
|
50
|
+
<EditorHeader
|
|
51
|
+
onSaveProject={() => {
|
|
52
|
+
logger.info('ProjectPage', 'save project', { name: project.name });
|
|
53
|
+
onSaveProject({
|
|
54
|
+
...project,
|
|
55
|
+
data: editorData,
|
|
56
|
+
});
|
|
57
|
+
}}
|
|
58
|
+
current={current}
|
|
59
|
+
editorData={editorData}
|
|
60
|
+
setEditorData={setEditorData}
|
|
61
|
+
/>
|
|
62
|
+
<div className="editor-container">
|
|
63
|
+
<div className="split-left">
|
|
64
|
+
<div>
|
|
65
|
+
<div
|
|
66
|
+
className="editor-tabs"
|
|
67
|
+
role="tablist"
|
|
68
|
+
aria-label="Editor tabs"
|
|
69
|
+
>
|
|
70
|
+
<button
|
|
71
|
+
className={`editor-tab ${tab === 'builder' ? 'editor-tab--active' : ''}`}
|
|
72
|
+
role="tab"
|
|
73
|
+
aria-selected={tab === 'builder'}
|
|
74
|
+
onClick={() => {
|
|
75
|
+
setTab('builder');
|
|
76
|
+
logger.info('ProjectPage', 'tab change', { to: 'builder' });
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
Builder
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
className={`editor-tab ${tab === 'preview' ? 'editor-tab--active' : ''}`}
|
|
83
|
+
role="tab"
|
|
84
|
+
aria-selected={tab === 'preview'}
|
|
85
|
+
onClick={() => {
|
|
86
|
+
setTab('preview');
|
|
87
|
+
logger.info('ProjectPage', 'tab change', { to: 'preview' });
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
Preview Config
|
|
91
|
+
</button>
|
|
92
|
+
<button
|
|
93
|
+
className={`editor-tab ${tab === 'debug' ? 'editor-tab--active' : ''}`}
|
|
94
|
+
role="tab"
|
|
95
|
+
aria-selected={tab === 'debug'}
|
|
96
|
+
onClick={() => {
|
|
97
|
+
setTab('debug');
|
|
98
|
+
logger.info('ProjectPage', 'tab change', { to: 'debug' });
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
Debug
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{tab === 'builder' && (
|
|
106
|
+
<BuilderTab
|
|
107
|
+
data={editorData}
|
|
108
|
+
setData={setEditorData}
|
|
109
|
+
current={current}
|
|
110
|
+
setCurrent={setCurrent}
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{tab === 'preview' && <PreviewTab />}
|
|
115
|
+
|
|
116
|
+
{tab === 'debug' && (
|
|
117
|
+
<DebugTab data={editorData} setData={setEditorData} />
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="split-right">
|
|
122
|
+
<div className="split-right-background" />
|
|
123
|
+
<RenderPage data={editorData} />
|
|
124
|
+
</div>
|
|
125
|
+
<div className="split-third">
|
|
126
|
+
<AttributesEditorPanel
|
|
127
|
+
current={current}
|
|
128
|
+
attributes={editorData}
|
|
129
|
+
onChange={(data) => {
|
|
130
|
+
setEditorData(data);
|
|
131
|
+
let nodeKey: string | undefined = undefined;
|
|
132
|
+
if (
|
|
133
|
+
data &&
|
|
134
|
+
typeof data === 'object' &&
|
|
135
|
+
!Array.isArray(data) &&
|
|
136
|
+
'key' in (data as any)
|
|
137
|
+
) {
|
|
138
|
+
nodeKey = (data as any).key as string | undefined;
|
|
139
|
+
}
|
|
140
|
+
logger.verbose(
|
|
141
|
+
'ProjectPage',
|
|
142
|
+
'attributes change',
|
|
143
|
+
nodeKey ? { nodeKey } : undefined,
|
|
144
|
+
);
|
|
145
|
+
}}
|
|
146
|
+
setCurrent={setCurrent}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Node } from '../..';
|
|
2
|
+
import { useLogRender } from '../../utils/useLogRender';
|
|
3
|
+
import { Builder } from '../../components/Builder';
|
|
4
|
+
|
|
5
|
+
type BuilderTabProps = {
|
|
6
|
+
data: Node;
|
|
7
|
+
setData: (data: Node) => void;
|
|
8
|
+
current: Node;
|
|
9
|
+
setCurrent: (current: Node) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function BuilderTab({
|
|
13
|
+
data,
|
|
14
|
+
setData,
|
|
15
|
+
current,
|
|
16
|
+
setCurrent,
|
|
17
|
+
}: BuilderTabProps) {
|
|
18
|
+
useLogRender('BuilderTab');
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
role="tabpanel"
|
|
22
|
+
className="editor-panel-builder editor-panel editor-panel--active"
|
|
23
|
+
aria-hidden={false}
|
|
24
|
+
>
|
|
25
|
+
<Builder
|
|
26
|
+
data={data}
|
|
27
|
+
setData={setData}
|
|
28
|
+
current={current}
|
|
29
|
+
setCurrent={setCurrent}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { JsonEditor } from 'json-edit-react';
|
|
2
|
+
import { Node } from '../..';
|
|
3
|
+
import { useLogRender } from '../../utils/useLogRender';
|
|
4
|
+
|
|
5
|
+
type DebugTabProps = {
|
|
6
|
+
data: Node;
|
|
7
|
+
setData: (data: Node) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function DebugTab({ data, setData }: DebugTabProps) {
|
|
11
|
+
useLogRender('DebugTab');
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
role="tabpanel"
|
|
15
|
+
className="editor-panel editor-panel--active editor-panels-debug"
|
|
16
|
+
aria-hidden={false}
|
|
17
|
+
>
|
|
18
|
+
<div style={{ flex: '1 1 auto', minHeight: 0, overflow: 'auto' }}>
|
|
19
|
+
<JsonEditor data={data as any} setData={setData as any} />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|