@developer_tribe/react-builder 1.0.1 → 1.0.3

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 (218) hide show
  1. package/dist/AttributesEditor.d.ts +3 -1
  2. package/dist/DeviceMockFrame.d.ts +2 -1
  3. package/dist/RenderPage.d.ts +5 -3
  4. package/dist/attributes-editor/Field.d.ts +17 -0
  5. package/dist/attributes-editor/FieldInfoTooltip.d.ts +7 -0
  6. package/dist/attributes-editor/LayoutPreviewPicker.d.ts +12 -0
  7. package/dist/attributes-editor/SpecialCategorySection.d.ts +20 -0
  8. package/dist/attributes-editor/types.d.ts +14 -0
  9. package/dist/background.jpg +0 -0
  10. package/dist/build-components/BackgroundImage/BackgroundImage.d.ts +5 -0
  11. package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +44 -0
  12. package/dist/build-components/Button/Button.d.ts +1 -1
  13. package/dist/build-components/Button/ButtonProps.generated.d.ts +33 -1
  14. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +34 -1
  15. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +32 -0
  16. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +32 -0
  17. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +34 -1
  18. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +34 -1
  19. package/dist/build-components/Image/ImageProps.generated.d.ts +32 -3
  20. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +34 -1
  21. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +32 -0
  22. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +32 -0
  23. package/dist/build-components/OnboardDot/OnboardDot.d.ts +1 -1
  24. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +29 -0
  25. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +11 -5
  26. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +32 -3
  27. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +31 -3
  28. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +32 -5
  29. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +11 -5
  30. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +11 -5
  31. package/dist/build-components/Text/TextProps.generated.d.ts +11 -5
  32. package/dist/build-components/View/ViewProps.generated.d.ts +10 -4
  33. package/dist/build-components/index.d.ts +2 -1
  34. package/dist/build-components/patterns.generated.d.ts +6288 -136
  35. package/dist/components/AttributesEditorPanel.d.ts +3 -4
  36. package/dist/components/Breadcrumb.d.ts +3 -1
  37. package/dist/components/Builder.d.ts +2 -1
  38. package/dist/components/BuilderButton.d.ts +9 -0
  39. package/dist/components/Checkbox.d.ts +17 -0
  40. package/dist/components/DeviceButton.d.ts +8 -0
  41. package/dist/components/DeviceNavigationBar.d.ts +10 -0
  42. package/dist/components/DeviceStatusBar.d.ts +9 -0
  43. package/dist/components/EditorHeader.d.ts +3 -8
  44. package/dist/index.cjs.js +5 -5
  45. package/dist/index.cjs.js.map +1 -1
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.esm.js +5 -5
  48. package/dist/index.esm.js.map +1 -1
  49. package/dist/mockOS/components/MockLaunchScreenComponent.d.ts +6 -0
  50. package/dist/mockOS/components/MockOSRouter.d.ts +8 -0
  51. package/dist/mockOS/components/PermissionModal.d.ts +9 -0
  52. package/dist/mockOS/context/MockOSContext.d.ts +36 -0
  53. package/dist/mockOS/hooks/useMockNavigation.d.ts +3 -0
  54. package/dist/mockOS/hooks/useMockPermission.d.ts +3 -0
  55. package/dist/mockOS/index.d.ts +9 -0
  56. package/dist/mockOS/managers/mockPermissionManager.d.ts +10 -0
  57. package/dist/mockOS/managers/navigationManager.d.ts +17 -0
  58. package/dist/modals/AddComponentModal.d.ts +8 -0
  59. package/dist/modals/ColorModal.d.ts +11 -0
  60. package/dist/modals/DeviceSelectorModal.d.ts +9 -0
  61. package/dist/modals/LocalicationModal.d.ts +8 -0
  62. package/dist/modals/Modal.d.ts +12 -0
  63. package/dist/modals/index.d.ts +5 -0
  64. package/dist/pages/ProjectPage.d.ts +3 -3
  65. package/dist/pages/tabs/BuilderPanel.d.ts +8 -0
  66. package/dist/pages/tabs/{DebugTab.d.ts → SideTool.d.ts} +2 -2
  67. package/dist/store.d.ts +7 -3
  68. package/dist/styles.css +1 -1
  69. package/dist/types/Project.d.ts +11 -0
  70. package/dist/utils/analyseNode.d.ts +1 -0
  71. package/dist/utils/extractTextStyle.d.ts +8 -1
  72. package/dist/utils/extractViewStyle.d.ts +8 -1
  73. package/dist/utils/parseColor.d.ts +7 -0
  74. package/dist/utils/patterns.d.ts +24 -0
  75. package/package.json +2 -1
  76. package/scripts/prebuild/utils/createGeneratedProps.js +11 -3
  77. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +45 -6
  78. package/scripts/prebuild/utils/validatePatternJson.js +13 -5
  79. package/src/AttributesEditor.tsx +493 -310
  80. package/src/DeviceMockFrame.tsx +21 -37
  81. package/src/RenderPage.tsx +86 -7
  82. package/src/assets/images/android.svg +42 -42
  83. package/src/assets/images/apple.svg +15 -15
  84. package/src/attributes-editor/Field.tsx +669 -0
  85. package/src/attributes-editor/FieldInfoTooltip.tsx +49 -0
  86. package/src/attributes-editor/LayoutPreviewPicker.tsx +199 -0
  87. package/src/attributes-editor/SpecialCategorySection.tsx +285 -0
  88. package/src/attributes-editor/types.ts +30 -0
  89. package/src/build-components/BackgroundImage/BackgroundImage.tsx +87 -0
  90. package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +60 -0
  91. package/src/build-components/BackgroundImage/pattern.json +45 -0
  92. package/src/build-components/Button/Button.tsx +37 -2
  93. package/src/build-components/Button/ButtonProps.generated.ts +44 -1
  94. package/src/build-components/Button/pattern.json +31 -2
  95. package/src/build-components/Carousel/Carousel.tsx +39 -2
  96. package/src/build-components/Carousel/CarouselProps.generated.ts +46 -1
  97. package/src/build-components/Carousel/pattern.json +10 -0
  98. package/src/build-components/CarouselButtons/CarouselButtons.tsx +21 -2
  99. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +43 -0
  100. package/src/build-components/CarouselButtons/pattern.json +22 -0
  101. package/src/build-components/CarouselDots/CarouselDots.tsx +49 -8
  102. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +43 -0
  103. package/src/build-components/CarouselDots/pattern.json +15 -0
  104. package/src/build-components/CarouselItem/CarouselItem.tsx +21 -2
  105. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +46 -1
  106. package/src/build-components/CarouselItem/pattern.json +7 -0
  107. package/src/build-components/CarouselProvider/CarouselProvider.tsx +21 -2
  108. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +46 -1
  109. package/src/build-components/CarouselProvider/pattern.json +7 -0
  110. package/src/build-components/Image/Image.tsx +33 -2
  111. package/src/build-components/Image/ImageProps.generated.ts +43 -3
  112. package/src/build-components/Image/pattern.json +46 -3
  113. package/src/build-components/Onboard/Onboard.tsx +6 -1
  114. package/src/build-components/Onboard/OnboardProps.generated.ts +46 -1
  115. package/src/build-components/Onboard/pattern.json +11 -0
  116. package/src/build-components/OnboardButton/OnboardButton.tsx +54 -6
  117. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +43 -0
  118. package/src/build-components/OnboardButton/pattern.json +71 -5
  119. package/src/build-components/OnboardButtons/OnboardButtons.tsx +33 -11
  120. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +43 -0
  121. package/src/build-components/OnboardButtons/pattern.json +70 -4
  122. package/src/build-components/OnboardDot/OnboardDot.tsx +113 -4
  123. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +29 -0
  124. package/src/build-components/OnboardDot/pattern.json +55 -2
  125. package/src/build-components/OnboardFooter/OnboardFooter.tsx +20 -4
  126. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +11 -5
  127. package/src/build-components/OnboardFooter/pattern.json +58 -2
  128. package/src/build-components/OnboardImage/OnboardImage.tsx +49 -5
  129. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +43 -3
  130. package/src/build-components/OnboardImage/pattern.json +21 -0
  131. package/src/build-components/OnboardItem/OnboardItem.tsx +17 -1
  132. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +42 -3
  133. package/src/build-components/OnboardItem/pattern.json +38 -2
  134. package/src/build-components/OnboardProvider/OnboardProvider.tsx +52 -18
  135. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +44 -5
  136. package/src/build-components/OnboardProvider/pattern.json +44 -5
  137. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +11 -5
  138. package/src/build-components/OnboardSubtitle/pattern.json +7 -1
  139. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +11 -5
  140. package/src/build-components/OnboardTitle/pattern.json +7 -1
  141. package/src/build-components/RenderNode.generated.tsx +3 -0
  142. package/src/build-components/Text/Text.tsx +34 -6
  143. package/src/build-components/Text/TextProps.generated.ts +11 -5
  144. package/src/build-components/Text/pattern.json +38 -2
  145. package/src/build-components/View/View.tsx +33 -6
  146. package/src/build-components/View/ViewProps.generated.ts +10 -4
  147. package/src/build-components/View/pattern.json +285 -19
  148. package/src/build-components/index.ts +5 -0
  149. package/src/build-components/patterns.generated.ts +6346 -143
  150. package/src/components/AttributesEditorPanel.tsx +17 -64
  151. package/src/components/Breadcrumb.tsx +37 -5
  152. package/src/components/Builder.tsx +311 -108
  153. package/src/components/BuilderButton.tsx +127 -0
  154. package/src/components/Checkbox.tsx +81 -0
  155. package/src/components/DeviceButton.tsx +39 -0
  156. package/src/components/DeviceNavigationBar.tsx +201 -0
  157. package/src/components/DeviceStatusBar.tsx +85 -0
  158. package/src/components/EditorHeader.tsx +26 -74
  159. package/src/index.ts +2 -2
  160. package/src/mockOS/components/MockLaunchScreenComponent.tsx +43 -0
  161. package/src/mockOS/components/MockOSRouter.tsx +123 -0
  162. package/src/mockOS/components/PermissionModal.tsx +270 -0
  163. package/src/mockOS/context/MockOSContext.tsx +179 -0
  164. package/src/mockOS/hooks/useMockNavigation.ts +11 -0
  165. package/src/mockOS/hooks/useMockPermission.ts +11 -0
  166. package/src/mockOS/index.ts +26 -0
  167. package/src/mockOS/managers/mockPermissionManager.ts +54 -0
  168. package/src/mockOS/managers/navigationManager.ts +91 -0
  169. package/src/modals/AddComponentModal.tsx +313 -0
  170. package/src/modals/ColorModal.tsx +425 -0
  171. package/src/modals/DeviceSelectorModal.tsx +57 -0
  172. package/src/modals/LocalicationModal.tsx +54 -0
  173. package/src/modals/Modal.tsx +57 -0
  174. package/src/modals/index.ts +5 -0
  175. package/src/pages/ProjectPage.tsx +307 -71
  176. package/src/pages/tabs/{BuilderTab.tsx → BuilderPanel.tsx} +13 -9
  177. package/src/pages/tabs/SideTool.tsx +259 -0
  178. package/src/size-matters/index.ts +27 -5
  179. package/src/store.ts +13 -5
  180. package/src/styles/base/_global.scss +404 -0
  181. package/src/styles/components/_attributes-editor.scss +273 -0
  182. package/src/styles/components/_editor-shell.scss +212 -0
  183. package/src/styles/components/_mockos-router.scss +140 -0
  184. package/src/styles/components/_ui-components.scss +183 -0
  185. package/src/styles/foundation/_colors.scss +8 -0
  186. package/src/styles/{_mixins.scss → foundation/_mixins.scss} +5 -4
  187. package/src/styles/{_reset.scss → foundation/_reset.scss} +5 -2
  188. package/src/styles/foundation/_sizes.scss +37 -0
  189. package/src/styles/foundation/_typography.scss +4 -0
  190. package/src/styles/foundation/_variables.scss +3 -0
  191. package/src/styles/index.scss +22 -136
  192. package/src/styles/layout/_builder.scss +124 -0
  193. package/src/styles/layout/_pages.scss +3 -0
  194. package/src/styles/modals/_add-component.scss +122 -0
  195. package/src/styles/modals/_color-modal.scss +159 -0
  196. package/src/styles/modals/_device-selector.scss +18 -0
  197. package/src/styles/modals/_localication-modal.scss +68 -0
  198. package/src/styles/modals/_modal-shell.scss +46 -0
  199. package/src/styles/utilities/_carousel.scss +125 -0
  200. package/src/types/Project.ts +14 -0
  201. package/src/types/images.d.ts +8 -0
  202. package/src/utils/analyseNode.ts +98 -0
  203. package/src/utils/extractTextStyle.ts +28 -10
  204. package/src/utils/extractViewStyle.ts +77 -9
  205. package/src/utils/parseColor.ts +43 -0
  206. package/src/utils/patterns.ts +33 -0
  207. package/dist/build-components/OnboardDot/OnboardExpandingDotProps.generated.d.ts +0 -10
  208. package/dist/pages/tabs/BuilderTab.d.ts +0 -9
  209. package/dist/pages/tabs/PreviewTab.d.ts +0 -3
  210. package/src/build-components/OnboardDot/OnboardExpandingDotProps.generated.ts +0 -20
  211. package/src/pages/tabs/DebugTab.tsx +0 -23
  212. package/src/pages/tabs/PreviewTab.tsx +0 -194
  213. package/src/styles/_variables.scss +0 -27
  214. package/src/styles/builder.scss +0 -60
  215. package/src/styles/components.scss +0 -88
  216. package/src/styles/editor.scss +0 -174
  217. package/src/styles/global.scss +0 -200
  218. package/src/styles/pages.scss +0 -2
@@ -1,21 +1,23 @@
1
- import { useEffect, useMemo, useState } from 'react';
2
- import { AttributesEditor, Node, NodeData } from '..';
1
+ import { AttributesEditor, Node, NodeData, ProjectColors } from '..';
3
2
  import { useLogRender } from '../utils/useLogRender';
3
+ import { useRenderStore } from '../store';
4
4
 
5
5
  interface AttributesEditorPanelProps {
6
- current: Node;
7
6
  attributes: any;
8
7
  onChange: (data: Node) => void;
9
- setCurrent: (current: Node) => void;
8
+ projectColors?: ProjectColors;
10
9
  }
11
10
 
12
11
  export function AttributesEditorPanel({
13
- current,
14
12
  attributes,
15
13
  onChange,
16
- setCurrent,
14
+ projectColors,
17
15
  }: AttributesEditorPanelProps) {
18
16
  useLogRender('AttributesEditorPanel');
17
+ const { current, setCurrent } = useRenderStore((s) => ({
18
+ current: s.current,
19
+ setCurrent: s.setCurrent,
20
+ }));
19
21
  if (!current) return null;
20
22
 
21
23
  function replaceNode(root: Node, target: Node, next: Node): Node {
@@ -44,68 +46,19 @@ export function AttributesEditorPanel({
44
46
  }
45
47
  return root;
46
48
  }
47
- const [draft, setDraft] = useState<Node>(current);
48
- const [isAllowedTopAccept, setIsAllowedTopAccept] = useState<boolean>(false);
49
-
50
- const isEqual = useMemo(() => {
51
- try {
52
- return JSON.stringify(current) === JSON.stringify(draft);
53
- } catch {
54
- return current === draft;
55
- }
56
- }, [current, draft]);
57
-
58
- useEffect(() => {
59
- setDraft(current);
60
- setIsAllowedTopAccept(false);
61
- }, [current, attributes]);
62
-
63
- useEffect(() => {
64
- setIsAllowedTopAccept(!isEqual);
65
- }, [isEqual]);
49
+ const handleAttributesChange = (next: Node) => {
50
+ const root = attributes as Node;
51
+ const updated = replaceNode(root, current, next);
52
+ onChange(updated);
53
+ setCurrent(next);
54
+ };
66
55
 
67
56
  return (
68
57
  <div style={{ padding: 12 }}>
69
- <h2>{(current as NodeData).type ?? current?.toString() ?? '?'}</h2>{' '}
70
- <div
71
- style={{
72
- display: 'flex',
73
- alignItems: 'center',
74
- gap: 8,
75
- padding: '16px 0',
76
- }}
77
- >
78
- <h3 style={{ marginTop: 0, marginBottom: 0, flex: '1 1 auto' }}>
79
- Attributes
80
- </h3>
81
- <button
82
- disabled={!isAllowedTopAccept}
83
- onClick={() => {
84
- setDraft(current);
85
- setIsAllowedTopAccept(false);
86
- }}
87
- >
88
- Revert
89
- </button>
90
- <button
91
- disabled={!isAllowedTopAccept}
92
- onClick={() => {
93
- const root = attributes as Node;
94
- const updated = replaceNode(root, current, draft);
95
- onChange(updated);
96
- setIsAllowedTopAccept(false);
97
- setCurrent(draft);
98
- }}
99
- >
100
- Accept
101
- </button>
102
- </div>
103
58
  <AttributesEditor
104
- node={draft}
105
- onChange={(next: Node) => {
106
- setDraft(next);
107
- setIsAllowedTopAccept(true);
108
- }}
59
+ node={current}
60
+ onChange={handleAttributesChange}
61
+ projectColors={projectColors}
109
62
  />
110
63
  </div>
111
64
  );
@@ -10,34 +10,66 @@ type BreadcrumbProps = {
10
10
  items: BreadcrumbItem[];
11
11
  separator?: ReactNode;
12
12
  ariaLabel?: string;
13
+ onBack?: () => void;
14
+ backLabel?: string;
13
15
  };
14
16
 
15
17
  export function Breadcrumb({
16
18
  items,
17
19
  separator = '/',
18
20
  ariaLabel = 'Breadcrumb',
21
+ onBack,
22
+ backLabel = 'Back',
19
23
  }: BreadcrumbProps) {
20
24
  useLogRender('Breadcrumb');
21
25
  return (
22
26
  <nav className="breadcrumb" aria-label={ariaLabel}>
27
+ {onBack && (
28
+ <button
29
+ type="button"
30
+ className="breadcrumb__back"
31
+ onClick={onBack}
32
+ aria-label="Go back"
33
+ >
34
+ <span className="breadcrumb__back-icon" aria-hidden>
35
+
36
+ </span>
37
+ <span>{backLabel}</span>
38
+ </button>
39
+ )}
23
40
  <ol className="breadcrumb__list">
24
41
  {items.map((item, index) => {
25
42
  const isLast = index === items.length - 1;
43
+ const isClickable = !isLast && (item.to || item.onClick);
26
44
  return (
27
- <li className="breadcrumb__item" key={`${item.label}-${index}`}>
45
+ <li
46
+ className={`breadcrumb__item ${isClickable ? 'breadcrumb__item--clickable' : ''}`}
47
+ key={`${item.label}-${index}`}
48
+ onClick={isClickable ? item.onClick : undefined}
49
+ role={isClickable ? 'button' : undefined}
50
+ tabIndex={isClickable ? 0 : undefined}
51
+ onKeyDown={
52
+ isClickable && item.onClick
53
+ ? (e) => {
54
+ if (e.key === 'Enter' || e.key === ' ') {
55
+ e.preventDefault();
56
+ item.onClick?.();
57
+ }
58
+ }
59
+ : undefined
60
+ }
61
+ >
28
62
  {index > 0 && (
29
63
  <span className="breadcrumb__separator" aria-hidden>
30
64
  {separator}
31
65
  </span>
32
66
  )}
33
- {isLast || !item.to ? (
67
+ {isLast || (!item.to && !item.onClick) ? (
34
68
  <span className="breadcrumb__current" aria-current="page">
35
69
  {item.label}
36
70
  </span>
37
71
  ) : (
38
- <p onClick={item.onClick} className="breadcrumb__link">
39
- {item.label}
40
- </p>
72
+ <span className="breadcrumb__link">{item.label}</span>
41
73
  )}
42
74
  </li>
43
75
  );
@@ -1,4 +1,4 @@
1
- import { useMemo, useState } from 'react';
1
+ import { useCallback, useMemo, useState } from 'react';
2
2
  import {
3
3
  isNodeArray,
4
4
  isNodeNullOrUndefined,
@@ -11,40 +11,36 @@ import {
11
11
  import { Breadcrumb } from './Breadcrumb';
12
12
  import { useLogRender } from '../utils/useLogRender';
13
13
  import { getDefaultsForType, getPatternByType } from '../utils/patterns';
14
+ import { AddComponentModal } from '../modals/AddComponentModal';
15
+ import { BuilderButton } from './BuilderButton';
14
16
 
15
17
  type BuilderEditorProps = {
16
18
  data: Node;
17
19
  setData: (data: Node) => void;
18
20
  current: Node;
19
21
  setCurrent: (current: Node) => void;
22
+ onDeleteNode: (node: Node) => void;
20
23
  };
21
24
 
22
25
  interface BuilderEditorComponentProps {
23
26
  node: Node;
24
27
  onClick: (node: Node) => void;
28
+ onAdd?: () => void;
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;
25
33
  }
26
34
 
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) {
35
+ function BuilderComponent({
36
+ node,
37
+ onClick,
38
+ onAdd,
39
+ onDelete,
40
+ onReorder,
41
+ onMoveChildUp,
42
+ onMoveChildDown,
43
+ }: BuilderEditorComponentProps) {
48
44
  if (isNodeNullOrUndefined(node)) {
49
45
  return <div className="builder__placeholder">Null or undefined</div>;
50
46
  }
@@ -56,33 +52,77 @@ function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
56
52
  );
57
53
  }
58
54
 
55
+ const addButton = onAdd && (
56
+ <button
57
+ type="button"
58
+ className="editor-button builder__add-button"
59
+ onClick={onAdd}
60
+ >
61
+ <span className="builder__add-button-icon" aria-hidden="true">
62
+ +
63
+ </span>
64
+ <span>Add component</span>
65
+ </button>
66
+ );
67
+
59
68
  if (isNodeArray(node)) {
69
+ const list = node as Node[];
70
+
71
+ const moveItem = (index: number, direction: -1 | 1) => {
72
+ if (!onReorder) return;
73
+ const targetIndex = index + direction;
74
+ if (targetIndex < 0 || targetIndex >= list.length) return;
75
+ const updated = [...list];
76
+ const [moved] = updated.splice(index, 1);
77
+ updated.splice(targetIndex, 0, moved);
78
+ onReorder(list, updated);
79
+ };
80
+
60
81
  return (
61
82
  <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
- />
83
+ {list.map((item, index) => (
84
+ <div key={index} className="builder__list-item">
85
+ <BuilderButton
86
+ onClick={() => {
87
+ onClick(item);
88
+ }}
89
+ node={item}
90
+ onDelete={onDelete}
91
+ onMoveUp={
92
+ onReorder && index > 0 ? () => moveItem(index, -1) : undefined
93
+ }
94
+ onMoveDown={
95
+ onReorder && index < list.length - 1
96
+ ? () => moveItem(index, 1)
97
+ : undefined
98
+ }
99
+ />
100
+ </div>
70
101
  ))}
102
+ {addButton}
71
103
  </div>
72
104
  );
73
105
  }
74
106
 
75
107
  const nodeData = node as NodeData<NodeDefaultAttribute>;
76
- const children = nodeData.children
77
- ? isNodeArray(nodeData.children)
78
- ? (nodeData.children as Node[])
79
- : [nodeData.children]
108
+ const rawChildren = nodeData.children;
109
+ const hasArrayChildren = isNodeArray(rawChildren);
110
+ const children = rawChildren
111
+ ? hasArrayChildren
112
+ ? (rawChildren as Node[])
113
+ : [rawChildren]
80
114
  : null;
81
115
 
82
116
  return (
83
117
  <div className="builder__node">
84
- <p className="builder__node-type">{nodeData.type}</p>
85
118
  <div className="builder__children">
119
+ <BuilderButton
120
+ onClick={() => {
121
+ onClick(node);
122
+ }}
123
+ node={node}
124
+ onDelete={onDelete}
125
+ />
86
126
  {children &&
87
127
  children.map((child, index) => (
88
128
  <BuilderButton
@@ -91,9 +131,23 @@ function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
91
131
  }}
92
132
  key={index}
93
133
  node={child}
134
+ onDelete={onDelete}
135
+ onMoveUp={
136
+ onMoveChildUp && hasArrayChildren && index > 0
137
+ ? () => onMoveChildUp(node, index)
138
+ : undefined
139
+ }
140
+ onMoveDown={
141
+ onMoveChildDown &&
142
+ hasArrayChildren &&
143
+ index < (children as Node[]).length - 1
144
+ ? () => onMoveChildDown(node, index)
145
+ : undefined
146
+ }
94
147
  />
95
148
  ))}
96
149
  </div>
150
+ {addButton}
97
151
  </div>
98
152
  );
99
153
  }
@@ -103,12 +157,165 @@ export function Builder({
103
157
  setData,
104
158
  current,
105
159
  setCurrent,
160
+ onDeleteNode,
106
161
  }: BuilderEditorProps) {
107
162
  useLogRender('Builder');
108
- const [crumbs, setCrumbs] = useState<string[]>(['root']);
163
+ const [isAddModalOpen, setIsAddModalOpen] = useState(false);
164
+ const breadcrumbPath = useMemo(() => {
165
+ const path = findNodePath(data, current);
166
+ if (path.length) return path;
167
+ if (!isNodeNullOrUndefined(current)) return [current];
168
+ if (!isNodeNullOrUndefined(data)) return [data];
169
+ return [];
170
+ }, [data, current]);
171
+
172
+ const handleNodeSelect = useCallback(
173
+ (node: Node) => {
174
+ setCurrent(node);
175
+ },
176
+ [setCurrent],
177
+ );
178
+
109
179
  const breadcrumbItems = useMemo(
110
- () => crumbs.map((c, idx) => ({ label: c })),
111
- [crumbs],
180
+ () =>
181
+ breadcrumbPath.map((node, index) => ({
182
+ label: getNodeLabel(node),
183
+ onClick:
184
+ index === breadcrumbPath.length - 1
185
+ ? undefined
186
+ : () => handleNodeSelect(node),
187
+ })),
188
+ [breadcrumbPath, handleNodeSelect],
189
+ );
190
+
191
+ const handleBackClick = useCallback(() => {
192
+ if (breadcrumbPath.length < 2) return;
193
+ handleNodeSelect(breadcrumbPath[breadcrumbPath.length - 2]);
194
+ }, [breadcrumbPath, handleNodeSelect]);
195
+
196
+ const handleAddChild = useCallback(
197
+ (type: string) => {
198
+ if (
199
+ isNodeNullOrUndefined(current) ||
200
+ isNodeString(current) ||
201
+ isNodeArray(current)
202
+ ) {
203
+ return;
204
+ }
205
+
206
+ const parent = current as NodeData<NodeDefaultAttribute>;
207
+ const nextChild = createDefaultNode(type);
208
+ const updatedParent: NodeData<NodeDefaultAttribute> = {
209
+ ...parent,
210
+ children: appendChild(parent.children, nextChild),
211
+ };
212
+ const updatedRoot = replaceNode(data, current, updatedParent);
213
+ setData(updatedRoot);
214
+ setCurrent(updatedParent);
215
+ },
216
+ [current, data, setData, setCurrent],
217
+ );
218
+
219
+ const allowedChildTypes = useMemo(
220
+ () => getAllowedChildTypes(current),
221
+ [current],
222
+ );
223
+ const parentType = useMemo(() => {
224
+ if (
225
+ isNodeNullOrUndefined(current) ||
226
+ isNodeString(current) ||
227
+ isNodeArray(current)
228
+ ) {
229
+ return null;
230
+ }
231
+ return (current as NodeData<NodeDefaultAttribute>).type ?? null;
232
+ }, [current]);
233
+ const canAddChild = allowedChildTypes.length > 0;
234
+
235
+ const handleOpenAddModal = useCallback(() => {
236
+ if (!canAddChild) return;
237
+ setIsAddModalOpen(true);
238
+ }, [canAddChild]);
239
+
240
+ const handleCloseAddModal = useCallback(() => {
241
+ setIsAddModalOpen(false);
242
+ }, []);
243
+
244
+ const handleAddChildFromModal = useCallback(
245
+ (type: string) => {
246
+ handleAddChild(type);
247
+ setIsAddModalOpen(false);
248
+ },
249
+ [handleAddChild],
250
+ );
251
+
252
+ const handleReorder = useCallback(
253
+ (prev: Node[], next: Node[]) => {
254
+ const updatedRoot = replaceNode(data, prev, next);
255
+ setData(updatedRoot);
256
+ if (current === prev) {
257
+ setCurrent(next);
258
+ }
259
+ },
260
+ [current, data, setCurrent, setData],
261
+ );
262
+
263
+ const handleMoveChildUp = useCallback(
264
+ (parentNode: Node, childIndex: number) => {
265
+ if (
266
+ isNodeNullOrUndefined(parentNode) ||
267
+ isNodeString(parentNode) ||
268
+ isNodeArray(parentNode)
269
+ ) {
270
+ return;
271
+ }
272
+ const parentData = parentNode as NodeData<NodeDefaultAttribute>;
273
+ const children = parentData.children;
274
+ if (!Array.isArray(children)) return;
275
+ if (childIndex <= 0 || childIndex >= children.length) return;
276
+ const updatedChildren = [...children];
277
+ const [movedChild] = updatedChildren.splice(childIndex, 1);
278
+ updatedChildren.splice(childIndex - 1, 0, movedChild);
279
+ const updatedParent: NodeData<NodeDefaultAttribute> = {
280
+ ...parentData,
281
+ children: updatedChildren,
282
+ };
283
+ const updatedRoot = replaceNode(data, parentNode, updatedParent);
284
+ setData(updatedRoot);
285
+ if (current === parentNode) {
286
+ setCurrent(updatedParent);
287
+ }
288
+ },
289
+ [current, data, setCurrent, setData],
290
+ );
291
+
292
+ const handleMoveChildDown = useCallback(
293
+ (parentNode: Node, childIndex: number) => {
294
+ if (
295
+ isNodeNullOrUndefined(parentNode) ||
296
+ isNodeString(parentNode) ||
297
+ isNodeArray(parentNode)
298
+ ) {
299
+ return;
300
+ }
301
+ const parentData = parentNode as NodeData<NodeDefaultAttribute>;
302
+ const children = parentData.children;
303
+ if (!Array.isArray(children)) return;
304
+ if (childIndex < 0 || childIndex >= children.length - 1) return;
305
+ const updatedChildren = [...children];
306
+ const [movedChild] = updatedChildren.splice(childIndex, 1);
307
+ updatedChildren.splice(childIndex + 1, 0, movedChild);
308
+ const updatedParent: NodeData<NodeDefaultAttribute> = {
309
+ ...parentData,
310
+ children: updatedChildren,
311
+ };
312
+ const updatedRoot = replaceNode(data, parentNode, updatedParent);
313
+ setData(updatedRoot);
314
+ if (current === parentNode) {
315
+ setCurrent(updatedParent);
316
+ }
317
+ },
318
+ [current, data, setCurrent, setData],
112
319
  );
113
320
 
114
321
  function replaceNode(root: Node, target: Node, next: Node): Node {
@@ -194,79 +401,75 @@ export function Builder({
194
401
 
195
402
  return (
196
403
  <div className="builder">
197
- <Breadcrumb items={breadcrumbItems} />
198
-
199
- <div className="builder__current">
200
- {crumbs[crumbs.length - 1] + ' ( ' + crumbs.length + '. level )'}
201
- </div>
404
+ <Breadcrumb
405
+ items={breadcrumbItems}
406
+ onBack={breadcrumbPath.length > 1 ? handleBackClick : undefined}
407
+ />
202
408
  <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
- }}
409
+ onClick={handleNodeSelect}
410
+ onAdd={canAddChild ? handleOpenAddModal : undefined}
411
+ onDelete={onDeleteNode}
412
+ onReorder={handleReorder}
413
+ onMoveChildUp={handleMoveChildUp}
414
+ onMoveChildDown={handleMoveChildDown}
212
415
  node={current}
213
416
  />
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
- })()}
417
+ {isAddModalOpen && (
418
+ <AddComponentModal
419
+ allowedChildTypes={allowedChildTypes}
420
+ parentType={parentType}
421
+ onSelect={handleAddChildFromModal}
422
+ onClose={handleCloseAddModal}
423
+ />
424
+ )}
270
425
  </div>
271
426
  );
272
427
  }
428
+
429
+ function appendChild(children: Node, childToAppend: Node): Node {
430
+ if (Array.isArray(children)) {
431
+ return [...children, childToAppend];
432
+ }
433
+ if (
434
+ children === null ||
435
+ children === undefined ||
436
+ typeof children === 'string'
437
+ ) {
438
+ return [childToAppend];
439
+ }
440
+ return [children as Node, childToAppend];
441
+ }
442
+
443
+ function getNodeLabel(node: Node): string {
444
+ if (isNodeNullOrUndefined(node)) return 'Empty';
445
+ if (isNodeString(node)) return node as string;
446
+ if (isNodeArray(node)) return 'Collection';
447
+ return (node as NodeData<NodeDefaultAttribute>).type ?? 'Node';
448
+ }
449
+
450
+ function findNodePath(root: Node, target: Node): Node[] {
451
+ if (root === null || root === undefined) return [];
452
+ if (root === target) return [root];
453
+ if (typeof root === 'string') return [];
454
+ if (Array.isArray(root)) {
455
+ for (const child of root) {
456
+ const childPath = findNodePath(child, target);
457
+ if (childPath.length) {
458
+ return childPath;
459
+ }
460
+ }
461
+ return [];
462
+ }
463
+
464
+ const nodeData = root as NodeData<NodeDefaultAttribute>;
465
+ const children = nodeData.children;
466
+ if (!children) return [];
467
+ const childList = Array.isArray(children) ? children : [children];
468
+ for (const child of childList) {
469
+ const childPath = findNodePath(child, target);
470
+ if (childPath.length) {
471
+ return [root, ...childPath];
472
+ }
473
+ }
474
+ return [];
475
+ }