@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.
Files changed (86) hide show
  1. package/dist/DeviceMockFrame.d.ts +1 -17
  2. package/dist/RenderPage.d.ts +1 -9
  3. package/dist/build-components/index.d.ts +1 -0
  4. package/dist/components/AttributesEditorPanel.d.ts +9 -0
  5. package/dist/components/Breadcrumb.d.ts +13 -0
  6. package/dist/components/Builder.d.ts +9 -0
  7. package/dist/components/EditorHeader.d.ts +15 -0
  8. package/dist/index.cjs.js +6 -5
  9. package/dist/index.cjs.js.map +1 -0
  10. package/dist/index.d.ts +8 -4
  11. package/dist/index.esm.js +6 -5
  12. package/dist/index.esm.js.map +1 -0
  13. package/dist/pages/ProjectPage.d.ts +11 -0
  14. package/dist/pages/tabs/BuilderTab.d.ts +9 -0
  15. package/dist/pages/tabs/DebugTab.d.ts +7 -0
  16. package/dist/pages/tabs/PreviewTab.d.ts +3 -0
  17. package/dist/store.d.ts +17 -18
  18. package/dist/styles.css +1 -1
  19. package/dist/types/PreviewConfig.d.ts +6 -3
  20. package/dist/types/Project.d.ts +12 -2
  21. package/dist/utils/copyNode.d.ts +2 -0
  22. package/dist/utils/logger.d.ts +11 -0
  23. package/dist/utils/useLogRender.d.ts +1 -0
  24. package/package.json +16 -9
  25. package/scripts/prebuild/utils/createBuildComponentsIndex.js +15 -1
  26. package/src/AttributesEditor.tsx +2 -0
  27. package/src/DeviceMockFrame.tsx +22 -31
  28. package/src/RenderPage.tsx +5 -42
  29. package/src/assets/images/android.svg +43 -0
  30. package/src/assets/images/apple.svg +16 -0
  31. package/src/assets/images/background.jpg +0 -0
  32. package/src/assets/samples/carousel-sample.json +2 -3
  33. package/src/assets/samples/getSamples.ts +49 -12
  34. package/src/assets/samples/simple-1.json +1 -2
  35. package/src/assets/samples/simple-2.json +1 -2
  36. package/src/assets/samples/vpn-onboard-1.json +1 -2
  37. package/src/assets/samples/vpn-onboard-2.json +1 -2
  38. package/src/assets/samples/vpn-onboard-3.json +1 -2
  39. package/src/assets/samples/vpn-onboard-4.json +1 -2
  40. package/src/assets/samples/vpn-onboard-5.json +1 -2
  41. package/src/assets/samples/vpn-onboard-6.json +1 -2
  42. package/src/build-components/Button/Button.tsx +2 -0
  43. package/src/build-components/Carousel/Carousel.tsx +2 -0
  44. package/src/build-components/CarouselButtons/CarouselButtons.tsx +2 -0
  45. package/src/build-components/CarouselDots/CarouselDots.tsx +2 -0
  46. package/src/build-components/CarouselItem/CarouselItem.tsx +2 -0
  47. package/src/build-components/Image/Image.tsx +2 -0
  48. package/src/build-components/Onboard/Onboard.tsx +2 -0
  49. package/src/build-components/OnboardButton/OnboardButton.tsx +7 -4
  50. package/src/build-components/OnboardButtons/OnboardButtons.tsx +7 -7
  51. package/src/build-components/OnboardDot/OnboardDot.tsx +2 -0
  52. package/src/build-components/OnboardFooter/OnboardFooter.tsx +5 -3
  53. package/src/build-components/OnboardImage/OnboardImage.tsx +2 -0
  54. package/src/build-components/OnboardItem/OnboardItem.tsx +2 -0
  55. package/src/build-components/OnboardProvider/OnboardProvider.tsx +2 -0
  56. package/src/build-components/OnboardSubtitle/OnboardSubtitle.tsx +2 -0
  57. package/src/build-components/OnboardTitle/OnboardTitle.tsx +2 -0
  58. package/src/build-components/Text/Text.tsx +5 -3
  59. package/src/build-components/View/View.tsx +2 -0
  60. package/src/build-components/index.ts +22 -0
  61. package/src/components/AttributesEditorPanel.tsx +112 -0
  62. package/src/components/Breadcrumb.tsx +48 -0
  63. package/src/components/Builder.tsx +272 -0
  64. package/src/components/EditorHeader.tsx +186 -0
  65. package/src/index.ts +8 -4
  66. package/src/pages/ProjectPage.tsx +152 -0
  67. package/src/pages/tabs/BuilderTab.tsx +33 -0
  68. package/src/pages/tabs/DebugTab.tsx +23 -0
  69. package/src/pages/tabs/PreviewTab.tsx +194 -0
  70. package/src/size-matters/index.ts +5 -1
  71. package/src/store.ts +60 -38
  72. package/src/styles/_mixins.scss +21 -0
  73. package/src/styles/_variables.scss +27 -0
  74. package/src/styles/builder.scss +60 -0
  75. package/src/styles/components.scss +88 -0
  76. package/src/styles/editor.scss +174 -0
  77. package/src/styles/global.scss +200 -0
  78. package/src/styles/index.scss +7 -0
  79. package/src/styles/pages.scss +2 -0
  80. package/src/types/PreviewConfig.ts +14 -5
  81. package/src/types/Project.ts +15 -2
  82. package/src/utils/copyNode.ts +7 -0
  83. package/src/utils/extractTextStyle.ts +4 -2
  84. package/src/utils/getDevices.ts +1 -0
  85. package/src/utils/logger.ts +76 -0
  86. 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
+ }