@developer_tribe/react-builder 1.0.2 → 1.0.4

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 (147) hide show
  1. package/dist/AttributesEditor.d.ts +3 -1
  2. package/dist/RenderPage.d.ts +2 -1
  3. package/dist/android.svg +43 -0
  4. package/dist/apple.svg +16 -0
  5. package/dist/attributes-editor/Field.d.ts +4 -2
  6. package/dist/attributes-editor/SizeField.d.ts +9 -0
  7. package/dist/attributes-editor/SpecialCategorySection.d.ts +2 -1
  8. package/dist/build-components/BackgroundImage/BackgroundImage.d.ts +5 -0
  9. package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +45 -0
  10. package/dist/build-components/Button/ButtonProps.generated.d.ts +8 -0
  11. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +8 -0
  12. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +8 -0
  13. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +8 -0
  14. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +8 -0
  15. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +8 -0
  16. package/dist/build-components/Image/ImageProps.generated.d.ts +8 -0
  17. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +8 -0
  18. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +8 -1
  19. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +8 -0
  20. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +9 -3
  21. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +8 -0
  22. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +9 -1
  23. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +8 -0
  24. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +8 -1
  25. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +8 -0
  26. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +8 -0
  27. package/dist/build-components/Text/TextProps.generated.d.ts +8 -0
  28. package/dist/build-components/View/ViewProps.generated.d.ts +8 -0
  29. package/dist/build-components/index.d.ts +2 -1
  30. package/dist/build-components/patterns.generated.d.ts +1612 -46
  31. package/dist/components/AttributesEditorPanel.d.ts +3 -4
  32. package/dist/components/Builder.d.ts +2 -1
  33. package/dist/components/BuilderButton.d.ts +9 -0
  34. package/dist/components/JsonTextEditor.d.ts +9 -0
  35. package/dist/index.cjs.js +5 -5
  36. package/dist/index.cjs.js.map +1 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.esm.js +5 -5
  39. package/dist/index.esm.js.map +1 -1
  40. package/dist/modals/ColorModal.d.ts +3 -1
  41. package/dist/pages/ProjectPage.d.ts +3 -3
  42. package/dist/pages/tabs/BuilderPanel.d.ts +8 -0
  43. package/dist/pages/tabs/SideTool.d.ts +8 -0
  44. package/dist/store.d.ts +9 -1
  45. package/dist/styles.css +1 -1
  46. package/dist/types/Project.d.ts +11 -0
  47. package/dist/utils/analyseNode.d.ts +1 -0
  48. package/dist/utils/extractImageStyle.d.ts +2 -1
  49. package/dist/utils/extractTextStyle.d.ts +8 -1
  50. package/dist/utils/extractViewStyle.d.ts +7 -1
  51. package/dist/utils/parseColor.d.ts +7 -0
  52. package/dist/utils/selection.d.ts +7 -0
  53. package/dist/utils/useMergedStyle.d.ts +2 -0
  54. package/package.json +2 -5
  55. package/src/.DS_Store +0 -0
  56. package/src/AttributesEditor.tsx +83 -16
  57. package/src/RenderPage.tsx +86 -4
  58. package/src/attributes-editor/Field.tsx +60 -165
  59. package/src/attributes-editor/SizeField.tsx +184 -0
  60. package/src/attributes-editor/SpecialCategorySection.tsx +12 -4
  61. package/src/build-components/BackgroundImage/BackgroundImage.tsx +77 -0
  62. package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +61 -0
  63. package/src/build-components/BackgroundImage/pattern.json +45 -0
  64. package/src/build-components/Button/Button.tsx +29 -4
  65. package/src/build-components/Button/ButtonProps.generated.ts +8 -0
  66. package/src/build-components/Carousel/Carousel.tsx +25 -3
  67. package/src/build-components/Carousel/CarouselProps.generated.ts +8 -0
  68. package/src/build-components/CarouselButtons/CarouselButtons.tsx +19 -4
  69. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +8 -0
  70. package/src/build-components/CarouselDots/CarouselDots.tsx +13 -4
  71. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +8 -0
  72. package/src/build-components/CarouselItem/CarouselItem.tsx +20 -4
  73. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +8 -0
  74. package/src/build-components/CarouselProvider/CarouselProvider.tsx +14 -3
  75. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +8 -0
  76. package/src/build-components/Image/Image.tsx +27 -9
  77. package/src/build-components/Image/ImageProps.generated.ts +8 -0
  78. package/src/build-components/Image/pattern.json +1 -9
  79. package/src/build-components/Onboard/Onboard.tsx +2 -2
  80. package/src/build-components/Onboard/OnboardProps.generated.ts +8 -0
  81. package/src/build-components/OnboardButton/OnboardButton.tsx +11 -7
  82. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +8 -1
  83. package/src/build-components/OnboardButtons/OnboardButtons.tsx +17 -5
  84. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +8 -0
  85. package/src/build-components/OnboardDot/OnboardDot.tsx +68 -39
  86. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +9 -3
  87. package/src/build-components/OnboardDot/pattern.json +3 -19
  88. package/src/build-components/OnboardFooter/OnboardFooter.tsx +37 -14
  89. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +8 -0
  90. package/src/build-components/OnboardImage/OnboardImage.tsx +28 -6
  91. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +9 -1
  92. package/src/build-components/OnboardItem/OnboardItem.tsx +15 -14
  93. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +8 -0
  94. package/src/build-components/OnboardProvider/OnboardProvider.tsx +35 -20
  95. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +8 -1
  96. package/src/build-components/OnboardProvider/pattern.json +0 -8
  97. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +8 -0
  98. package/src/build-components/OnboardSubtitle/pattern.json +1 -1
  99. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +8 -0
  100. package/src/build-components/OnboardTitle/pattern.json +1 -1
  101. package/src/build-components/RenderNode.generated.tsx +3 -0
  102. package/src/build-components/Text/Text.tsx +28 -10
  103. package/src/build-components/Text/TextProps.generated.ts +8 -0
  104. package/src/build-components/View/View.tsx +25 -3
  105. package/src/build-components/View/ViewProps.generated.ts +8 -0
  106. package/src/build-components/View/pattern.json +67 -1
  107. package/src/build-components/index.ts +5 -0
  108. package/src/build-components/patterns.generated.ts +1620 -46
  109. package/src/components/AttributesEditorPanel.tsx +13 -6
  110. package/src/components/Builder.tsx +200 -56
  111. package/src/components/BuilderButton.tsx +127 -0
  112. package/src/components/DeviceNavigationBar.tsx +0 -1
  113. package/src/components/EditorHeader.tsx +11 -1
  114. package/src/components/JsonTextEditor.tsx +185 -0
  115. package/src/index.ts +2 -2
  116. package/src/mockOS/components/MockOSRouter.tsx +17 -3
  117. package/src/mockOS/context/MockOSContext.tsx +0 -5
  118. package/src/mockOS/managers/mockPermissionManager.ts +0 -4
  119. package/src/mockOS/managers/navigationManager.ts +1 -6
  120. package/src/modals/ColorModal.tsx +306 -71
  121. package/src/modals/LocalicationModal.tsx +4 -5
  122. package/src/modals/Modal.tsx +8 -1
  123. package/src/pages/ProjectPage.tsx +299 -55
  124. package/src/pages/tabs/{BuilderTab.tsx → BuilderPanel.tsx} +13 -9
  125. package/src/pages/tabs/SideTool.tsx +260 -0
  126. package/src/size-matters/index.ts +6 -0
  127. package/src/store.ts +18 -1
  128. package/src/styles/base/_global.scss +163 -7
  129. package/src/styles/components/_attributes-editor.scss +12 -0
  130. package/src/styles/components/_editor-shell.scss +25 -0
  131. package/src/styles/foundation/_sizes.scss +1 -1
  132. package/src/styles/layout/_builder.scss +66 -10
  133. package/src/styles/modals/_color-modal.scss +59 -1
  134. package/src/styles/utilities/_carousel.scss +9 -8
  135. package/src/types/Project.ts +14 -0
  136. package/src/utils/analyseNode.ts +98 -0
  137. package/src/utils/extractImageStyle.ts +3 -6
  138. package/src/utils/extractTextStyle.ts +19 -82
  139. package/src/utils/extractViewStyle.ts +41 -12
  140. package/src/utils/parseColor.ts +43 -0
  141. package/src/utils/selection.ts +24 -0
  142. package/src/utils/useMergedStyle.ts +16 -0
  143. package/dist/pages/tabs/BuilderTab.d.ts +0 -9
  144. package/dist/pages/tabs/DebugTab.d.ts +0 -7
  145. package/dist/pages/tabs/PreviewTab.d.ts +0 -3
  146. package/src/pages/tabs/DebugTab.tsx +0 -64
  147. package/src/pages/tabs/PreviewTab.tsx +0 -206
@@ -1,20 +1,23 @@
1
- import { AttributesEditor, Node, NodeData } from '..';
1
+ import { AttributesEditor, Node, NodeData, ProjectColors } from '..';
2
2
  import { useLogRender } from '../utils/useLogRender';
3
+ import { useRenderStore } from '../store';
3
4
 
4
5
  interface AttributesEditorPanelProps {
5
- current: Node;
6
6
  attributes: any;
7
7
  onChange: (data: Node) => void;
8
- setCurrent: (current: Node) => void;
8
+ projectColors?: ProjectColors;
9
9
  }
10
10
 
11
11
  export function AttributesEditorPanel({
12
- current,
13
12
  attributes,
14
13
  onChange,
15
- setCurrent,
14
+ projectColors,
16
15
  }: AttributesEditorPanelProps) {
17
16
  useLogRender('AttributesEditorPanel');
17
+ const { current, setCurrent } = useRenderStore((s) => ({
18
+ current: s.current,
19
+ setCurrent: s.setCurrent,
20
+ }));
18
21
  if (!current) return null;
19
22
 
20
23
  function replaceNode(root: Node, target: Node, next: Node): Node {
@@ -52,7 +55,11 @@ export function AttributesEditorPanel({
52
55
 
53
56
  return (
54
57
  <div style={{ padding: 12 }}>
55
- <AttributesEditor node={current} onChange={handleAttributesChange} />
58
+ <AttributesEditor
59
+ node={current}
60
+ onChange={handleAttributesChange}
61
+ projectColors={projectColors}
62
+ />
56
63
  </div>
57
64
  );
58
65
  }
@@ -12,56 +12,35 @@ import { Breadcrumb } from './Breadcrumb';
12
12
  import { useLogRender } from '../utils/useLogRender';
13
13
  import { getDefaultsForType, getPatternByType } from '../utils/patterns';
14
14
  import { AddComponentModal } from '../modals/AddComponentModal';
15
+ import { BuilderButton } from './BuilderButton';
15
16
 
16
17
  type BuilderEditorProps = {
17
18
  data: Node;
18
19
  setData: (data: Node) => void;
19
20
  current: Node;
20
21
  setCurrent: (current: Node) => void;
22
+ onDeleteNode: (node: Node) => void;
21
23
  };
22
24
 
23
25
  interface BuilderEditorComponentProps {
24
26
  node: Node;
25
27
  onClick: (node: Node) => void;
26
28
  onAdd?: () => void;
27
- }
28
-
29
- function BuilderButton({ node, onClick }: { node: Node; onClick: () => void }) {
30
- if (isNodeNullOrUndefined(node)) {
31
- return <div className="builder__placeholder">Null or undefined</div>;
32
- }
33
- if (isNodeString(node)) {
34
- return <div className="builder__text">{node as string}</div>;
35
- }
36
- const nodeData = node as NodeData<NodeDefaultAttribute>;
37
-
38
- let extra = '';
39
- if (nodeData.attributes?.condition) {
40
- extra = ` (${nodeData.attributes.condition} ${nodeData.attributes.conditionVariable})`;
41
- }
42
- const patternLabel = getPatternByType(nodeData.type)?.meta?.label?.trim();
43
- const baseLabel =
44
- patternLabel && patternLabel.length > 0 ? patternLabel : nodeData.type;
45
- const conditionLabel = extra.trim() ? extra : '';
46
- const fullLabel = `${baseLabel}${conditionLabel}`.trim();
47
- return (
48
- <a onClick={onClick} className="builder__button">
49
- <span className="builder__button-label">{baseLabel}</span> <br />
50
- {conditionLabel && (
51
- <span className="builder__button-condition">{conditionLabel}</span>
52
- )}
53
- </a>
54
- );
29
+ onDelete?: (node: Node) => void;
30
+ onReorder?: (prev: Node[], next: Node[]) => void;
31
+ onMoveChildUp?: (parent: Node, childIndex: number) => void;
32
+ onMoveChildDown?: (parent: Node, childIndex: number) => void;
55
33
  }
56
34
 
57
35
  function BuilderComponent({
58
36
  node,
59
37
  onClick,
60
38
  onAdd,
39
+ onDelete,
40
+ onReorder,
41
+ onMoveChildUp,
42
+ onMoveChildDown,
61
43
  }: BuilderEditorComponentProps) {
62
- if (isNodeNullOrUndefined(node)) {
63
- return <div className="builder__placeholder">Null or undefined</div>;
64
- }
65
44
  if (isNodeString(node)) {
66
45
  return (
67
46
  <div className="builder__text">
@@ -84,16 +63,38 @@ function BuilderComponent({
84
63
  );
85
64
 
86
65
  if (isNodeArray(node)) {
66
+ const list = node as Node[];
67
+
68
+ const moveItem = (index: number, direction: -1 | 1) => {
69
+ if (!onReorder) return;
70
+ const targetIndex = index + direction;
71
+ if (targetIndex < 0 || targetIndex >= list.length) return;
72
+ const updated = [...list];
73
+ const [moved] = updated.splice(index, 1);
74
+ updated.splice(targetIndex, 0, moved);
75
+ onReorder(list, updated);
76
+ };
77
+
87
78
  return (
88
79
  <div className="builder__list">
89
- {(node as Node[]).map((item, index) => (
90
- <BuilderButton
91
- onClick={() => {
92
- onClick(item);
93
- }}
94
- key={index}
95
- node={item}
96
- />
80
+ {list.map((item, index) => (
81
+ <div key={index} className="builder__list-item">
82
+ <BuilderButton
83
+ onClick={() => {
84
+ onClick(item);
85
+ }}
86
+ node={item}
87
+ onDelete={onDelete}
88
+ onMoveUp={
89
+ onReorder && index > 0 ? () => moveItem(index, -1) : undefined
90
+ }
91
+ onMoveDown={
92
+ onReorder && index < list.length - 1
93
+ ? () => moveItem(index, 1)
94
+ : undefined
95
+ }
96
+ />
97
+ </div>
97
98
  ))}
98
99
  {addButton}
99
100
  </div>
@@ -101,15 +102,24 @@ function BuilderComponent({
101
102
  }
102
103
 
103
104
  const nodeData = node as NodeData<NodeDefaultAttribute>;
104
- const children = nodeData.children
105
- ? isNodeArray(nodeData.children)
106
- ? (nodeData.children as Node[])
107
- : [nodeData.children]
105
+ const rawChildren = nodeData?.children;
106
+ const hasArrayChildren = isNodeArray(rawChildren);
107
+ const children = rawChildren
108
+ ? hasArrayChildren
109
+ ? (rawChildren as Node[])
110
+ : [rawChildren]
108
111
  : null;
109
112
 
110
113
  return (
111
114
  <div className="builder__node">
112
115
  <div className="builder__children">
116
+ <BuilderButton
117
+ onClick={() => {
118
+ onClick(node);
119
+ }}
120
+ node={node}
121
+ onDelete={onDelete}
122
+ />
113
123
  {children &&
114
124
  children.map((child, index) => (
115
125
  <BuilderButton
@@ -118,6 +128,19 @@ function BuilderComponent({
118
128
  }}
119
129
  key={index}
120
130
  node={child}
131
+ onDelete={onDelete}
132
+ onMoveUp={
133
+ onMoveChildUp && hasArrayChildren && index > 0
134
+ ? () => onMoveChildUp(node, index)
135
+ : undefined
136
+ }
137
+ onMoveDown={
138
+ onMoveChildDown &&
139
+ hasArrayChildren &&
140
+ index < (children as Node[]).length - 1
141
+ ? () => onMoveChildDown(node, index)
142
+ : undefined
143
+ }
121
144
  />
122
145
  ))}
123
146
  </div>
@@ -131,6 +154,7 @@ export function Builder({
131
154
  setData,
132
155
  current,
133
156
  setCurrent,
157
+ onDeleteNode,
134
158
  }: BuilderEditorProps) {
135
159
  useLogRender('Builder');
136
160
  const [isAddModalOpen, setIsAddModalOpen] = useState(false);
@@ -168,16 +192,55 @@ export function Builder({
168
192
 
169
193
  const handleAddChild = useCallback(
170
194
  (type: string) => {
171
- if (
172
- isNodeNullOrUndefined(current) ||
173
- isNodeString(current) ||
174
- isNodeArray(current)
175
- ) {
195
+ const nextChild = createDefaultNode(type);
196
+
197
+ // Root (or selection) can be empty/null-ish: allow creating the first node.
198
+ if (isNodeNullOrUndefined(current)) {
199
+ // If the project itself is empty (or a placeholder string), replace it with the first node.
200
+ if (isNodeNullOrUndefined(data) || isNodeString(data)) {
201
+ setData(nextChild);
202
+ setCurrent(nextChild);
203
+ return;
204
+ }
205
+
206
+ // If the project root is a list, append into it.
207
+ if (Array.isArray(data)) {
208
+ const nextList = [...data, nextChild];
209
+ setData(nextList);
210
+ setCurrent(nextList);
211
+ return;
212
+ }
213
+
214
+ // Otherwise default to adding into the root node.
215
+ const parent = data as NodeData<NodeDefaultAttribute>;
216
+ const updatedParent: NodeData<NodeDefaultAttribute> = {
217
+ ...parent,
218
+ children: appendChild(parent.children, nextChild),
219
+ };
220
+ setData(updatedParent);
221
+ setCurrent(updatedParent);
222
+ return;
223
+ }
224
+
225
+ // If the root is a list, allow adding to it directly.
226
+ if (isNodeArray(current)) {
227
+ const nextList = [...(current as Node[]), nextChild];
228
+ const updatedRoot = replaceNode(data, current, nextList);
229
+ setData(updatedRoot);
230
+ setCurrent(nextList);
231
+ return;
232
+ }
233
+
234
+ // If the root is a placeholder string, allow replacing it with the first node.
235
+ if (isNodeString(current)) {
236
+ if (current === data) {
237
+ setData(nextChild);
238
+ setCurrent(nextChild);
239
+ }
176
240
  return;
177
241
  }
178
242
 
179
243
  const parent = current as NodeData<NodeDefaultAttribute>;
180
- const nextChild = createDefaultNode(type);
181
244
  const updatedParent: NodeData<NodeDefaultAttribute> = {
182
245
  ...parent,
183
246
  children: appendChild(parent.children, nextChild),
@@ -203,7 +266,11 @@ export function Builder({
203
266
  }
204
267
  return (current as NodeData<NodeDefaultAttribute>).type ?? null;
205
268
  }, [current]);
206
- const canAddChild = allowedChildTypes.length > 0;
269
+ const canAddChild =
270
+ allowedChildTypes.length > 0 ||
271
+ data === undefined ||
272
+ data === null ||
273
+ (Array.isArray(data) && data.length === 0);
207
274
 
208
275
  const handleOpenAddModal = useCallback(() => {
209
276
  if (!canAddChild) return;
@@ -222,6 +289,75 @@ export function Builder({
222
289
  [handleAddChild],
223
290
  );
224
291
 
292
+ const handleReorder = useCallback(
293
+ (prev: Node[], next: Node[]) => {
294
+ const updatedRoot = replaceNode(data, prev, next);
295
+ setData(updatedRoot);
296
+ if (current === prev) {
297
+ setCurrent(next);
298
+ }
299
+ },
300
+ [current, data, setCurrent, setData],
301
+ );
302
+
303
+ const handleMoveChildUp = useCallback(
304
+ (parentNode: Node, childIndex: number) => {
305
+ if (
306
+ isNodeNullOrUndefined(parentNode) ||
307
+ isNodeString(parentNode) ||
308
+ isNodeArray(parentNode)
309
+ ) {
310
+ return;
311
+ }
312
+ const parentData = parentNode as NodeData<NodeDefaultAttribute>;
313
+ const children = parentData.children;
314
+ if (!Array.isArray(children)) return;
315
+ if (childIndex <= 0 || childIndex >= children.length) return;
316
+ const updatedChildren = [...children];
317
+ const [movedChild] = updatedChildren.splice(childIndex, 1);
318
+ updatedChildren.splice(childIndex - 1, 0, movedChild);
319
+ const updatedParent: NodeData<NodeDefaultAttribute> = {
320
+ ...parentData,
321
+ children: updatedChildren,
322
+ };
323
+ const updatedRoot = replaceNode(data, parentNode, updatedParent);
324
+ setData(updatedRoot);
325
+ if (current === parentNode) {
326
+ setCurrent(updatedParent);
327
+ }
328
+ },
329
+ [current, data, setCurrent, setData],
330
+ );
331
+
332
+ const handleMoveChildDown = useCallback(
333
+ (parentNode: Node, childIndex: number) => {
334
+ if (
335
+ isNodeNullOrUndefined(parentNode) ||
336
+ isNodeString(parentNode) ||
337
+ isNodeArray(parentNode)
338
+ ) {
339
+ return;
340
+ }
341
+ const parentData = parentNode as NodeData<NodeDefaultAttribute>;
342
+ const children = parentData.children;
343
+ if (!Array.isArray(children)) return;
344
+ if (childIndex < 0 || childIndex >= children.length - 1) return;
345
+ const updatedChildren = [...children];
346
+ const [movedChild] = updatedChildren.splice(childIndex, 1);
347
+ updatedChildren.splice(childIndex + 1, 0, movedChild);
348
+ const updatedParent: NodeData<NodeDefaultAttribute> = {
349
+ ...parentData,
350
+ children: updatedChildren,
351
+ };
352
+ const updatedRoot = replaceNode(data, parentNode, updatedParent);
353
+ setData(updatedRoot);
354
+ if (current === parentNode) {
355
+ setCurrent(updatedParent);
356
+ }
357
+ },
358
+ [current, data, setCurrent, setData],
359
+ );
360
+
225
361
  function replaceNode(root: Node, target: Node, next: Node): Node {
226
362
  if (root === target) return next;
227
363
  if (root === null || root === undefined) return root;
@@ -277,12 +413,16 @@ export function Builder({
277
413
  }
278
414
 
279
415
  function getAllowedChildTypes(parent: Node): string[] {
280
- if (
281
- isNodeNullOrUndefined(parent) ||
282
- isNodeString(parent) ||
283
- isNodeArray(parent)
284
- )
285
- return [];
416
+ // Treat non-node containers (root empty, root arrays) as "View-like" containers:
417
+ // allow inserting any component type as a child.
418
+ if (isNodeNullOrUndefined(parent)) return [...allcomponentNames];
419
+ if (isNodeArray(parent) && (parent as Node[]).length === 0)
420
+ return [...allcomponentNames];
421
+ if (isNodeString(parent)) {
422
+ // Only allow adding when the string is the root placeholder.
423
+ return parent === data ? [...allcomponentNames] : [];
424
+ }
425
+
286
426
  const parentData = parent as NodeData;
287
427
  const parentType = parentData.type;
288
428
  // Special rule: limit OnboardButtons to OnboardButton only
@@ -312,6 +452,10 @@ export function Builder({
312
452
  <BuilderComponent
313
453
  onClick={handleNodeSelect}
314
454
  onAdd={canAddChild ? handleOpenAddModal : undefined}
455
+ onDelete={onDeleteNode}
456
+ onReorder={handleReorder}
457
+ onMoveChildUp={handleMoveChildUp}
458
+ onMoveChildDown={handleMoveChildDown}
315
459
  node={current}
316
460
  />
317
461
  {isAddModalOpen && (
@@ -0,0 +1,127 @@
1
+ import { useRef } from 'react';
2
+ import {
3
+ isNodeNullOrUndefined,
4
+ isNodeString,
5
+ Node,
6
+ NodeData,
7
+ NodeDefaultAttribute,
8
+ } from '..';
9
+ import { getPatternByType } from '../utils/patterns';
10
+
11
+ export type BuilderButtonProps = {
12
+ node: Node;
13
+ onClick: () => void;
14
+ onDelete?: (node: Node) => void;
15
+ onMoveUp?: () => void;
16
+ onMoveDown?: () => void;
17
+ };
18
+
19
+ export function BuilderButton({
20
+ node,
21
+ onClick,
22
+ onDelete,
23
+ onMoveUp,
24
+ onMoveDown,
25
+ }: BuilderButtonProps) {
26
+ if (isNodeNullOrUndefined(node)) {
27
+ return <div className="builder__placeholder">Null or undefined</div>;
28
+ }
29
+ if (isNodeString(node)) {
30
+ return <div className="builder__text">{node as string}</div>;
31
+ }
32
+ const nodeData = node as NodeData<NodeDefaultAttribute>;
33
+
34
+ const longPressTimeoutRef = useRef<number | null>(null);
35
+ const longPressTriggeredRef = useRef(false);
36
+
37
+ const handleDelete = () => {
38
+ if (onDelete) {
39
+ onDelete(node);
40
+ }
41
+ };
42
+
43
+ const clearLongPress = () => {
44
+ if (longPressTimeoutRef.current !== null) {
45
+ window.clearTimeout(longPressTimeoutRef.current);
46
+ longPressTimeoutRef.current = null;
47
+ }
48
+ };
49
+
50
+ const handlePressStart = () => {
51
+ longPressTriggeredRef.current = false;
52
+ longPressTimeoutRef.current = window.setTimeout(() => {
53
+ longPressTriggeredRef.current = true;
54
+ const shouldDelete = window.confirm('Do you want to delete');
55
+ if (shouldDelete) {
56
+ handleDelete();
57
+ }
58
+ }, 600);
59
+ };
60
+
61
+ const handlePressEnd = () => {
62
+ if (!longPressTriggeredRef.current) {
63
+ onClick();
64
+ }
65
+ clearLongPress();
66
+ };
67
+
68
+ const handlePressCancel = () => {
69
+ clearLongPress();
70
+ };
71
+
72
+ let extra = '';
73
+ if (nodeData.attributes?.condition) {
74
+ extra = ` (${nodeData.attributes.condition} ${nodeData.attributes.conditionVariable})`;
75
+ }
76
+ const patternLabel = getPatternByType(nodeData.type)?.meta?.label?.trim();
77
+ const baseLabel =
78
+ patternLabel && patternLabel.length > 0 ? patternLabel : nodeData.type;
79
+ const conditionLabel = extra.trim() ? extra : '';
80
+
81
+ return (
82
+ <div className="builder__button">
83
+ {(onMoveUp || onMoveDown) && (
84
+ <div className="builder__sort-controls">
85
+ <button
86
+ type="button"
87
+ className="builder__sort-button"
88
+ onClick={(event) => {
89
+ event.stopPropagation();
90
+ onMoveUp?.();
91
+ }}
92
+ disabled={!onMoveUp}
93
+ aria-label="Move up"
94
+ >
95
+
96
+ </button>
97
+ <button
98
+ type="button"
99
+ className="builder__sort-button"
100
+ onClick={(event) => {
101
+ event.stopPropagation();
102
+ onMoveDown?.();
103
+ }}
104
+ disabled={!onMoveDown}
105
+ aria-label="Move down"
106
+ >
107
+
108
+ </button>
109
+ </div>
110
+ )}
111
+ <a
112
+ className="builder__button-link"
113
+ onMouseDown={handlePressStart}
114
+ onMouseUp={handlePressEnd}
115
+ onMouseLeave={handlePressCancel}
116
+ onTouchStart={handlePressStart}
117
+ onTouchEnd={handlePressEnd}
118
+ onTouchCancel={handlePressCancel}
119
+ >
120
+ {baseLabel}
121
+ </a>
122
+ {conditionLabel && (
123
+ <span className="builder__button-condition">{conditionLabel}</span>
124
+ )}
125
+ </div>
126
+ );
127
+ }
@@ -37,7 +37,6 @@ export function DeviceNavigationBar({
37
37
  : 'rgba(0, 0, 0, 0.4)';
38
38
 
39
39
  function handleBackButton() {
40
- console.log('handleBackButton', context);
41
40
  if (context) {
42
41
  const canGoBack = context.goBack();
43
42
  // If can't go back, go to launchscreen
@@ -24,9 +24,14 @@ export function EditorHeader({
24
24
  useLogRender('EditorHeader');
25
25
  const [isDevicesModalOpen, setIsDevicesModalOpen] = useState(false);
26
26
  const copiedNode = useRenderStore((s) => s.copiedNode);
27
- const { device: selectedDevice, setDevice } = useRenderStore((s) => ({
27
+ const {
28
+ device: selectedDevice,
29
+ setDevice,
30
+ setCurrent,
31
+ } = useRenderStore((s) => ({
28
32
  device: s.device,
29
33
  setDevice: s.setDevice,
34
+ setCurrent: s.setCurrent,
30
35
  }));
31
36
 
32
37
  function replaceNode(root: Node, target: Node, next: Node): Node {
@@ -67,6 +72,11 @@ export function EditorHeader({
67
72
  copiedNode: null,
68
73
  });
69
74
  setEditorData(updated);
75
+ //TODO: current and editor must be sync!! and tested more
76
+ // Important: selection is stored by reference. After replacing `current` in the tree,
77
+ // we must point selection to the new (cloned) node reference to keep "current node"
78
+ // in sync with what’s rendered/edited.
79
+ setCurrent(cloned);
70
80
  };
71
81
  return (
72
82
  <div