@developer_tribe/react-builder 0.1.32 → 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.
Files changed (67) 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 +5 -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 +9 -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 +8 -18
  18. package/dist/styles.css +1 -1
  19. package/dist/types/PreviewConfig.d.ts +6 -3
  20. package/dist/types/Project.d.ts +2 -2
  21. package/dist/utils/copyNode.d.ts +2 -0
  22. package/package.json +16 -9
  23. package/scripts/prebuild/utils/createBuildComponentsIndex.js +15 -1
  24. package/src/DeviceMockFrame.tsx +20 -31
  25. package/src/RenderPage.tsx +3 -38
  26. package/src/assets/images/android.svg +43 -0
  27. package/src/assets/images/apple.svg +16 -0
  28. package/src/assets/images/background.jpg +0 -0
  29. package/src/assets/samples/carousel-sample.json +2 -3
  30. package/src/assets/samples/getSamples.ts +49 -12
  31. package/src/assets/samples/simple-1.json +1 -2
  32. package/src/assets/samples/simple-2.json +1 -2
  33. package/src/assets/samples/vpn-onboard-1.json +1 -2
  34. package/src/assets/samples/vpn-onboard-2.json +1 -2
  35. package/src/assets/samples/vpn-onboard-3.json +1 -2
  36. package/src/assets/samples/vpn-onboard-4.json +1 -2
  37. package/src/assets/samples/vpn-onboard-5.json +1 -2
  38. package/src/assets/samples/vpn-onboard-6.json +1 -2
  39. package/src/build-components/OnboardButton/OnboardButton.tsx +5 -4
  40. package/src/build-components/OnboardButtons/OnboardButtons.tsx +5 -7
  41. package/src/build-components/OnboardFooter/OnboardFooter.tsx +3 -3
  42. package/src/build-components/Text/Text.tsx +3 -3
  43. package/src/build-components/index.ts +22 -0
  44. package/src/components/AttributesEditorPanel.tsx +110 -0
  45. package/src/components/Breadcrumb.tsx +46 -0
  46. package/src/components/Builder.tsx +270 -0
  47. package/src/components/EditorHeader.tsx +184 -0
  48. package/src/index.ts +5 -4
  49. package/src/pages/ProjectPage.tsx +112 -0
  50. package/src/pages/tabs/BuilderTab.tsx +31 -0
  51. package/src/pages/tabs/DebugTab.tsx +21 -0
  52. package/src/pages/tabs/PreviewTab.tsx +192 -0
  53. package/src/size-matters/index.ts +5 -1
  54. package/src/store.ts +26 -38
  55. package/src/styles/_mixins.scss +21 -0
  56. package/src/styles/_variables.scss +27 -0
  57. package/src/styles/builder.scss +60 -0
  58. package/src/styles/components.scss +88 -0
  59. package/src/styles/editor.scss +174 -0
  60. package/src/styles/global.scss +200 -0
  61. package/src/styles/index.scss +7 -0
  62. package/src/styles/pages.scss +2 -0
  63. package/src/types/PreviewConfig.ts +14 -5
  64. package/src/types/Project.ts +2 -2
  65. package/src/utils/copyNode.ts +7 -0
  66. package/src/utils/extractTextStyle.ts +4 -2
  67. package/src/utils/getDevices.ts +1 -0
@@ -8,16 +8,14 @@ import { useRenderStore } from '../../store';
8
8
 
9
9
  function OnboardButtons({ node }: OnboardButtonsComponentProps) {
10
10
  node = useNode(node);
11
- const { screenStyle, theme } = useRenderStore((s) => ({
12
- screenStyle: s.screenStyle,
13
- theme: s.theme,
11
+ const { appConfig } = useRenderStore((s) => ({
12
+ appConfig: s.appConfig,
14
13
  }));
15
14
  const seperatorColorDefault =
16
- theme === 'light'
17
- ? screenStyle.light.seperatorColor
18
- : screenStyle.dark.seperatorColor;
15
+ appConfig.theme === 'light'
16
+ ? appConfig.screenStyle.light.seperatorColor
17
+ : appConfig.screenStyle.dark.seperatorColor;
19
18
  const ctx = useContext(onboardContext) ?? {};
20
- const emblaApi = ctx.emblaApi;
21
19
  const [selectedIndex, setSelectedIndex] = useState(ctx.selectedIndex ?? 0);
22
20
 
23
21
  useEffect(() => {
@@ -87,10 +87,10 @@ function buildSegments(
87
87
 
88
88
  function OnboardFooter({ node }: OnboardFooterComponentProps) {
89
89
  node = useNode(node);
90
- const { defaultLanguage, localication } = useRenderStore((s) => ({
91
- defaultLanguage: s.defaultLanguage,
92
- localication: s.localication,
90
+ const { appConfig } = useRenderStore((s) => ({
91
+ appConfig: s.appConfig,
93
92
  }));
93
+ const { localication, defaultLanguage } = appConfig;
94
94
  const t = (key?: string) =>
95
95
  key ? (localication?.[defaultLanguage ?? 'en']?.[key] ?? key) : '';
96
96
 
@@ -6,10 +6,10 @@ import { extractTextStyle } from '../../utils/extractTextStyle';
6
6
 
7
7
  function Text({ node }: TextComponentProps) {
8
8
  node = useNode(node);
9
- const { defaultLanguage, localication } = useRenderStore((s) => ({
10
- defaultLanguage: s.defaultLanguage,
11
- localication: s.localication,
9
+ const { appConfig } = useRenderStore((s) => ({
10
+ appConfig: s.appConfig,
12
11
  }));
12
+ const { defaultLanguage, localication } = appConfig;
13
13
  const keyOrText: string = node.children as string;
14
14
  const style = extractTextStyle(node);
15
15
 
@@ -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,
@@ -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
+ }