@developer_tribe/react-builder 1.0.1 → 1.0.2

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 (187) hide show
  1. package/dist/DeviceMockFrame.d.ts +2 -1
  2. package/dist/RenderPage.d.ts +4 -3
  3. package/dist/attributes-editor/Field.d.ts +16 -0
  4. package/dist/attributes-editor/FieldInfoTooltip.d.ts +7 -0
  5. package/dist/attributes-editor/LayoutPreviewPicker.d.ts +12 -0
  6. package/dist/attributes-editor/SpecialCategorySection.d.ts +19 -0
  7. package/dist/attributes-editor/types.d.ts +14 -0
  8. package/dist/background.jpg +0 -0
  9. package/dist/build-components/Button/Button.d.ts +1 -1
  10. package/dist/build-components/Button/ButtonProps.generated.d.ts +26 -1
  11. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +27 -1
  12. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +25 -0
  13. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +25 -0
  14. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +27 -1
  15. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +27 -1
  16. package/dist/build-components/Image/ImageProps.generated.d.ts +25 -3
  17. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +27 -1
  18. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +25 -0
  19. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +25 -0
  20. package/dist/build-components/OnboardDot/OnboardDot.d.ts +1 -1
  21. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +22 -0
  22. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +4 -5
  23. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +25 -3
  24. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +24 -3
  25. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +25 -4
  26. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +4 -5
  27. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +4 -5
  28. package/dist/build-components/Text/TextProps.generated.d.ts +4 -5
  29. package/dist/build-components/View/ViewProps.generated.d.ts +3 -4
  30. package/dist/build-components/patterns.generated.d.ts +4855 -132
  31. package/dist/components/Breadcrumb.d.ts +3 -1
  32. package/dist/components/Checkbox.d.ts +17 -0
  33. package/dist/components/DeviceButton.d.ts +8 -0
  34. package/dist/components/DeviceNavigationBar.d.ts +10 -0
  35. package/dist/components/DeviceStatusBar.d.ts +9 -0
  36. package/dist/components/EditorHeader.d.ts +3 -8
  37. package/dist/index.cjs.js +5 -5
  38. package/dist/index.cjs.js.map +1 -1
  39. package/dist/index.esm.js +5 -5
  40. package/dist/index.esm.js.map +1 -1
  41. package/dist/mockOS/components/MockLaunchScreenComponent.d.ts +6 -0
  42. package/dist/mockOS/components/MockOSRouter.d.ts +8 -0
  43. package/dist/mockOS/components/PermissionModal.d.ts +9 -0
  44. package/dist/mockOS/context/MockOSContext.d.ts +36 -0
  45. package/dist/mockOS/hooks/useMockNavigation.d.ts +3 -0
  46. package/dist/mockOS/hooks/useMockPermission.d.ts +3 -0
  47. package/dist/mockOS/index.d.ts +9 -0
  48. package/dist/mockOS/managers/mockPermissionManager.d.ts +10 -0
  49. package/dist/mockOS/managers/navigationManager.d.ts +17 -0
  50. package/dist/modals/AddComponentModal.d.ts +8 -0
  51. package/dist/modals/ColorModal.d.ts +9 -0
  52. package/dist/modals/DeviceSelectorModal.d.ts +9 -0
  53. package/dist/modals/LocalicationModal.d.ts +8 -0
  54. package/dist/modals/Modal.d.ts +12 -0
  55. package/dist/modals/index.d.ts +5 -0
  56. package/dist/pages/ProjectPage.d.ts +1 -1
  57. package/dist/store.d.ts +0 -2
  58. package/dist/styles.css +1 -1
  59. package/dist/utils/patterns.d.ts +24 -0
  60. package/package.json +2 -1
  61. package/scripts/prebuild/utils/createGeneratedProps.js +11 -3
  62. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +45 -6
  63. package/scripts/prebuild/utils/validatePatternJson.js +13 -5
  64. package/src/AttributesEditor.tsx +433 -312
  65. package/src/DeviceMockFrame.tsx +21 -37
  66. package/src/RenderPage.tsx +5 -4
  67. package/src/assets/images/android.svg +42 -42
  68. package/src/assets/images/apple.svg +15 -15
  69. package/src/attributes-editor/Field.tsx +662 -0
  70. package/src/attributes-editor/FieldInfoTooltip.tsx +49 -0
  71. package/src/attributes-editor/LayoutPreviewPicker.tsx +199 -0
  72. package/src/attributes-editor/SpecialCategorySection.tsx +284 -0
  73. package/src/attributes-editor/types.ts +30 -0
  74. package/src/build-components/Button/Button.tsx +10 -2
  75. package/src/build-components/Button/ButtonProps.generated.ts +37 -1
  76. package/src/build-components/Button/pattern.json +31 -2
  77. package/src/build-components/Carousel/Carousel.tsx +15 -2
  78. package/src/build-components/Carousel/CarouselProps.generated.ts +39 -1
  79. package/src/build-components/Carousel/pattern.json +10 -0
  80. package/src/build-components/CarouselButtons/CarouselButtons.tsx +6 -2
  81. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +36 -0
  82. package/src/build-components/CarouselButtons/pattern.json +22 -0
  83. package/src/build-components/CarouselDots/CarouselDots.tsx +40 -8
  84. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +36 -0
  85. package/src/build-components/CarouselDots/pattern.json +15 -0
  86. package/src/build-components/CarouselItem/CarouselItem.tsx +5 -2
  87. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +39 -1
  88. package/src/build-components/CarouselItem/pattern.json +7 -0
  89. package/src/build-components/CarouselProvider/CarouselProvider.tsx +10 -2
  90. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +39 -1
  91. package/src/build-components/CarouselProvider/pattern.json +7 -0
  92. package/src/build-components/Image/Image.tsx +8 -2
  93. package/src/build-components/Image/ImageProps.generated.ts +36 -3
  94. package/src/build-components/Image/pattern.json +46 -3
  95. package/src/build-components/Onboard/Onboard.tsx +6 -1
  96. package/src/build-components/Onboard/OnboardProps.generated.ts +39 -1
  97. package/src/build-components/Onboard/pattern.json +11 -0
  98. package/src/build-components/OnboardButton/OnboardButton.tsx +46 -5
  99. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +36 -0
  100. package/src/build-components/OnboardButton/pattern.json +71 -5
  101. package/src/build-components/OnboardButtons/OnboardButtons.tsx +20 -10
  102. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +36 -0
  103. package/src/build-components/OnboardButtons/pattern.json +70 -4
  104. package/src/build-components/OnboardDot/OnboardDot.tsx +104 -4
  105. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +22 -0
  106. package/src/build-components/OnboardDot/pattern.json +54 -1
  107. package/src/build-components/OnboardFooter/OnboardFooter.tsx +9 -3
  108. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +4 -5
  109. package/src/build-components/OnboardFooter/pattern.json +58 -2
  110. package/src/build-components/OnboardImage/OnboardImage.tsx +27 -5
  111. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +36 -3
  112. package/src/build-components/OnboardImage/pattern.json +21 -0
  113. package/src/build-components/OnboardItem/OnboardItem.tsx +6 -1
  114. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +35 -3
  115. package/src/build-components/OnboardItem/pattern.json +38 -2
  116. package/src/build-components/OnboardProvider/OnboardProvider.tsx +20 -8
  117. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +37 -4
  118. package/src/build-components/OnboardProvider/pattern.json +51 -4
  119. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +4 -5
  120. package/src/build-components/OnboardSubtitle/pattern.json +6 -0
  121. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +4 -5
  122. package/src/build-components/OnboardTitle/pattern.json +6 -0
  123. package/src/build-components/Text/Text.tsx +7 -3
  124. package/src/build-components/Text/TextProps.generated.ts +4 -5
  125. package/src/build-components/Text/pattern.json +38 -2
  126. package/src/build-components/View/View.tsx +9 -6
  127. package/src/build-components/View/ViewProps.generated.ts +3 -4
  128. package/src/build-components/View/pattern.json +227 -19
  129. package/src/build-components/patterns.generated.ts +4905 -139
  130. package/src/components/AttributesEditorPanel.tsx +7 -61
  131. package/src/components/Breadcrumb.tsx +37 -5
  132. package/src/components/Builder.tsx +180 -77
  133. package/src/components/Checkbox.tsx +81 -0
  134. package/src/components/DeviceButton.tsx +39 -0
  135. package/src/components/DeviceNavigationBar.tsx +201 -0
  136. package/src/components/DeviceStatusBar.tsx +85 -0
  137. package/src/components/EditorHeader.tsx +26 -74
  138. package/src/mockOS/components/MockLaunchScreenComponent.tsx +43 -0
  139. package/src/mockOS/components/MockOSRouter.tsx +115 -0
  140. package/src/mockOS/components/PermissionModal.tsx +270 -0
  141. package/src/mockOS/context/MockOSContext.tsx +179 -0
  142. package/src/mockOS/hooks/useMockNavigation.ts +11 -0
  143. package/src/mockOS/hooks/useMockPermission.ts +11 -0
  144. package/src/mockOS/index.ts +26 -0
  145. package/src/mockOS/managers/mockPermissionManager.ts +54 -0
  146. package/src/mockOS/managers/navigationManager.ts +91 -0
  147. package/src/modals/AddComponentModal.tsx +313 -0
  148. package/src/modals/ColorModal.tsx +268 -0
  149. package/src/modals/DeviceSelectorModal.tsx +57 -0
  150. package/src/modals/LocalicationModal.tsx +54 -0
  151. package/src/modals/Modal.tsx +57 -0
  152. package/src/modals/index.ts +5 -0
  153. package/src/pages/ProjectPage.tsx +19 -21
  154. package/src/pages/tabs/DebugTab.tsx +50 -9
  155. package/src/pages/tabs/PreviewTab.tsx +52 -40
  156. package/src/size-matters/index.ts +21 -5
  157. package/src/store.ts +0 -4
  158. package/src/styles/{global.scss → base/_global.scss} +92 -39
  159. package/src/styles/components/_attributes-editor.scss +261 -0
  160. package/src/styles/{editor.scss → components/_editor-shell.scss} +72 -57
  161. package/src/styles/components/_mockos-router.scss +140 -0
  162. package/src/styles/components/_ui-components.scss +183 -0
  163. package/src/styles/foundation/_colors.scss +8 -0
  164. package/src/styles/{_mixins.scss → foundation/_mixins.scss} +5 -4
  165. package/src/styles/{_reset.scss → foundation/_reset.scss} +5 -2
  166. package/src/styles/foundation/_sizes.scss +37 -0
  167. package/src/styles/foundation/_typography.scss +4 -0
  168. package/src/styles/foundation/_variables.scss +3 -0
  169. package/src/styles/index.scss +22 -136
  170. package/src/styles/layout/_builder.scss +68 -0
  171. package/src/styles/layout/_pages.scss +3 -0
  172. package/src/styles/modals/_add-component.scss +122 -0
  173. package/src/styles/modals/_color-modal.scss +130 -0
  174. package/src/styles/modals/_device-selector.scss +18 -0
  175. package/src/styles/modals/_localication-modal.scss +68 -0
  176. package/src/styles/modals/_modal-shell.scss +46 -0
  177. package/src/styles/utilities/_carousel.scss +125 -0
  178. package/src/types/images.d.ts +8 -0
  179. package/src/utils/extractTextStyle.ts +4 -2
  180. package/src/utils/extractViewStyle.ts +51 -7
  181. package/src/utils/patterns.ts +33 -0
  182. package/dist/build-components/OnboardDot/OnboardExpandingDotProps.generated.d.ts +0 -10
  183. package/src/build-components/OnboardDot/OnboardExpandingDotProps.generated.ts +0 -20
  184. package/src/styles/_variables.scss +0 -27
  185. package/src/styles/builder.scss +0 -60
  186. package/src/styles/components.scss +0 -88
  187. package/src/styles/pages.scss +0 -2
@@ -1,4 +1,3 @@
1
- import { useEffect, useMemo, useState } from 'react';
2
1
  import { AttributesEditor, Node, NodeData } from '..';
3
2
  import { useLogRender } from '../utils/useLogRender';
4
3
 
@@ -44,69 +43,16 @@ export function AttributesEditorPanel({
44
43
  }
45
44
  return root;
46
45
  }
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]);
46
+ const handleAttributesChange = (next: Node) => {
47
+ const root = attributes as Node;
48
+ const updated = replaceNode(root, current, next);
49
+ onChange(updated);
50
+ setCurrent(next);
51
+ };
66
52
 
67
53
  return (
68
54
  <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
- <AttributesEditor
104
- node={draft}
105
- onChange={(next: Node) => {
106
- setDraft(next);
107
- setIsAllowedTopAccept(true);
108
- }}
109
- />
55
+ <AttributesEditor node={current} onChange={handleAttributesChange} />
110
56
  </div>
111
57
  );
112
58
  }
@@ -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,6 +11,7 @@ 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';
14
15
 
15
16
  type BuilderEditorProps = {
16
17
  data: Node;
@@ -22,6 +23,7 @@ type BuilderEditorProps = {
22
23
  interface BuilderEditorComponentProps {
23
24
  node: Node;
24
25
  onClick: (node: Node) => void;
26
+ onAdd?: () => void;
25
27
  }
26
28
 
27
29
  function BuilderButton({ node, onClick }: { node: Node; onClick: () => void }) {
@@ -37,14 +39,26 @@ function BuilderButton({ node, onClick }: { node: Node; onClick: () => void }) {
37
39
  if (nodeData.attributes?.condition) {
38
40
  extra = ` (${nodeData.attributes.condition} ${nodeData.attributes.conditionVariable})`;
39
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();
40
47
  return (
41
48
  <a onClick={onClick} className="builder__button">
42
- {nodeData.type} {extra}
49
+ <span className="builder__button-label">{baseLabel}</span> <br />
50
+ {conditionLabel && (
51
+ <span className="builder__button-condition">{conditionLabel}</span>
52
+ )}
43
53
  </a>
44
54
  );
45
55
  }
46
56
 
47
- function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
57
+ function BuilderComponent({
58
+ node,
59
+ onClick,
60
+ onAdd,
61
+ }: BuilderEditorComponentProps) {
48
62
  if (isNodeNullOrUndefined(node)) {
49
63
  return <div className="builder__placeholder">Null or undefined</div>;
50
64
  }
@@ -56,6 +70,19 @@ function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
56
70
  );
57
71
  }
58
72
 
73
+ const addButton = onAdd && (
74
+ <button
75
+ type="button"
76
+ className="editor-button builder__add-button"
77
+ onClick={onAdd}
78
+ >
79
+ <span className="builder__add-button-icon" aria-hidden="true">
80
+ +
81
+ </span>
82
+ <span>Add component</span>
83
+ </button>
84
+ );
85
+
59
86
  if (isNodeArray(node)) {
60
87
  return (
61
88
  <div className="builder__list">
@@ -68,6 +95,7 @@ function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
68
95
  node={item}
69
96
  />
70
97
  ))}
98
+ {addButton}
71
99
  </div>
72
100
  );
73
101
  }
@@ -81,7 +109,6 @@ function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
81
109
 
82
110
  return (
83
111
  <div className="builder__node">
84
- <p className="builder__node-type">{nodeData.type}</p>
85
112
  <div className="builder__children">
86
113
  {children &&
87
114
  children.map((child, index) => (
@@ -94,6 +121,7 @@ function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
94
121
  />
95
122
  ))}
96
123
  </div>
124
+ {addButton}
97
125
  </div>
98
126
  );
99
127
  }
@@ -105,10 +133,93 @@ export function Builder({
105
133
  setCurrent,
106
134
  }: BuilderEditorProps) {
107
135
  useLogRender('Builder');
108
- const [crumbs, setCrumbs] = useState<string[]>(['root']);
136
+ const [isAddModalOpen, setIsAddModalOpen] = useState(false);
137
+ const breadcrumbPath = useMemo(() => {
138
+ const path = findNodePath(data, current);
139
+ if (path.length) return path;
140
+ if (!isNodeNullOrUndefined(current)) return [current];
141
+ if (!isNodeNullOrUndefined(data)) return [data];
142
+ return [];
143
+ }, [data, current]);
144
+
145
+ const handleNodeSelect = useCallback(
146
+ (node: Node) => {
147
+ setCurrent(node);
148
+ },
149
+ [setCurrent],
150
+ );
151
+
109
152
  const breadcrumbItems = useMemo(
110
- () => crumbs.map((c, idx) => ({ label: c })),
111
- [crumbs],
153
+ () =>
154
+ breadcrumbPath.map((node, index) => ({
155
+ label: getNodeLabel(node),
156
+ onClick:
157
+ index === breadcrumbPath.length - 1
158
+ ? undefined
159
+ : () => handleNodeSelect(node),
160
+ })),
161
+ [breadcrumbPath, handleNodeSelect],
162
+ );
163
+
164
+ const handleBackClick = useCallback(() => {
165
+ if (breadcrumbPath.length < 2) return;
166
+ handleNodeSelect(breadcrumbPath[breadcrumbPath.length - 2]);
167
+ }, [breadcrumbPath, handleNodeSelect]);
168
+
169
+ const handleAddChild = useCallback(
170
+ (type: string) => {
171
+ if (
172
+ isNodeNullOrUndefined(current) ||
173
+ isNodeString(current) ||
174
+ isNodeArray(current)
175
+ ) {
176
+ return;
177
+ }
178
+
179
+ const parent = current as NodeData<NodeDefaultAttribute>;
180
+ const nextChild = createDefaultNode(type);
181
+ const updatedParent: NodeData<NodeDefaultAttribute> = {
182
+ ...parent,
183
+ children: appendChild(parent.children, nextChild),
184
+ };
185
+ const updatedRoot = replaceNode(data, current, updatedParent);
186
+ setData(updatedRoot);
187
+ setCurrent(updatedParent);
188
+ },
189
+ [current, data, setData, setCurrent],
190
+ );
191
+
192
+ const allowedChildTypes = useMemo(
193
+ () => getAllowedChildTypes(current),
194
+ [current],
195
+ );
196
+ const parentType = useMemo(() => {
197
+ if (
198
+ isNodeNullOrUndefined(current) ||
199
+ isNodeString(current) ||
200
+ isNodeArray(current)
201
+ ) {
202
+ return null;
203
+ }
204
+ return (current as NodeData<NodeDefaultAttribute>).type ?? null;
205
+ }, [current]);
206
+ const canAddChild = allowedChildTypes.length > 0;
207
+
208
+ const handleOpenAddModal = useCallback(() => {
209
+ if (!canAddChild) return;
210
+ setIsAddModalOpen(true);
211
+ }, [canAddChild]);
212
+
213
+ const handleCloseAddModal = useCallback(() => {
214
+ setIsAddModalOpen(false);
215
+ }, []);
216
+
217
+ const handleAddChildFromModal = useCallback(
218
+ (type: string) => {
219
+ handleAddChild(type);
220
+ setIsAddModalOpen(false);
221
+ },
222
+ [handleAddChild],
112
223
  );
113
224
 
114
225
  function replaceNode(root: Node, target: Node, next: Node): Node {
@@ -194,79 +305,71 @@ export function Builder({
194
305
 
195
306
  return (
196
307
  <div className="builder">
197
- <Breadcrumb items={breadcrumbItems} />
198
-
199
- <div className="builder__current">
200
- {crumbs[crumbs.length - 1] + ' ( ' + crumbs.length + '. level )'}
201
- </div>
308
+ <Breadcrumb
309
+ items={breadcrumbItems}
310
+ onBack={breadcrumbPath.length > 1 ? handleBackClick : undefined}
311
+ />
202
312
  <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
- }}
313
+ onClick={handleNodeSelect}
314
+ onAdd={canAddChild ? handleOpenAddModal : undefined}
212
315
  node={current}
213
316
  />
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
- })()}
317
+ {isAddModalOpen && (
318
+ <AddComponentModal
319
+ allowedChildTypes={allowedChildTypes}
320
+ parentType={parentType}
321
+ onSelect={handleAddChildFromModal}
322
+ onClose={handleCloseAddModal}
323
+ />
324
+ )}
270
325
  </div>
271
326
  );
272
327
  }
328
+
329
+ function appendChild(children: Node, childToAppend: Node): Node {
330
+ if (Array.isArray(children)) {
331
+ return [...children, childToAppend];
332
+ }
333
+ if (
334
+ children === null ||
335
+ children === undefined ||
336
+ typeof children === 'string'
337
+ ) {
338
+ return [childToAppend];
339
+ }
340
+ return [children as Node, childToAppend];
341
+ }
342
+
343
+ function getNodeLabel(node: Node): string {
344
+ if (isNodeNullOrUndefined(node)) return 'Empty';
345
+ if (isNodeString(node)) return node as string;
346
+ if (isNodeArray(node)) return 'Collection';
347
+ return (node as NodeData<NodeDefaultAttribute>).type ?? 'Node';
348
+ }
349
+
350
+ function findNodePath(root: Node, target: Node): Node[] {
351
+ if (root === null || root === undefined) return [];
352
+ if (root === target) return [root];
353
+ if (typeof root === 'string') return [];
354
+ if (Array.isArray(root)) {
355
+ for (const child of root) {
356
+ const childPath = findNodePath(child, target);
357
+ if (childPath.length) {
358
+ return childPath;
359
+ }
360
+ }
361
+ return [];
362
+ }
363
+
364
+ const nodeData = root as NodeData<NodeDefaultAttribute>;
365
+ const children = nodeData.children;
366
+ if (!children) return [];
367
+ const childList = Array.isArray(children) ? children : [children];
368
+ for (const child of childList) {
369
+ const childPath = findNodePath(child, target);
370
+ if (childPath.length) {
371
+ return [root, ...childPath];
372
+ }
373
+ }
374
+ return [];
375
+ }
@@ -0,0 +1,81 @@
1
+ import React, { useId } from 'react';
2
+
3
+ type CheckboxChangeHandler = (
4
+ checked: boolean,
5
+ event: React.ChangeEvent<HTMLInputElement>,
6
+ ) => void;
7
+
8
+ export type CheckboxProps = Omit<
9
+ React.InputHTMLAttributes<HTMLInputElement>,
10
+ 'type' | 'onChange' | 'className'
11
+ > & {
12
+ label?: React.ReactNode;
13
+ helperText?: React.ReactNode;
14
+ className?: string;
15
+ inputClassName?: string;
16
+ onChange?: CheckboxChangeHandler;
17
+ };
18
+
19
+ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
20
+ (
21
+ {
22
+ label,
23
+ helperText,
24
+ className,
25
+ inputClassName,
26
+ onChange,
27
+ id,
28
+ disabled,
29
+ ...rest
30
+ },
31
+ ref,
32
+ ) => {
33
+ const autoId = useId();
34
+ const inputId = id ?? autoId;
35
+ const helperId = helperText ? `${inputId}-helper` : undefined;
36
+
37
+ const wrapperClassName = [
38
+ 'builder-checkbox',
39
+ disabled ? 'builder-checkbox--disabled' : undefined,
40
+ className,
41
+ ]
42
+ .filter(Boolean)
43
+ .join(' ');
44
+
45
+ const nativeClassName = ['builder-checkbox__native', inputClassName]
46
+ .filter(Boolean)
47
+ .join(' ');
48
+
49
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
50
+ onChange?.(event.target.checked, event);
51
+ };
52
+
53
+ return (
54
+ <div className={wrapperClassName}>
55
+ <label htmlFor={inputId} className="builder-checkbox__label">
56
+ <input
57
+ {...rest}
58
+ ref={ref}
59
+ id={inputId}
60
+ type="checkbox"
61
+ className={nativeClassName}
62
+ onChange={handleChange}
63
+ disabled={disabled}
64
+ aria-describedby={helperId}
65
+ />
66
+ <span className="builder-checkbox__control" aria-hidden="true" />
67
+ {label ? (
68
+ <span className="builder-checkbox__text">{label}</span>
69
+ ) : null}
70
+ </label>
71
+ {helperText ? (
72
+ <span id={helperId} className="builder-checkbox__helper">
73
+ {helperText}
74
+ </span>
75
+ ) : null}
76
+ </div>
77
+ );
78
+ },
79
+ );
80
+
81
+ Checkbox.displayName = 'Checkbox';
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Device } from '../types/Device';
3
+ import androidIcon from '../assets/images/android.svg';
4
+ import iosIcon from '../assets/images/apple.svg';
5
+
6
+ const platformIcons: Record<string, string> = {
7
+ android: androidIcon,
8
+ ios: iosIcon,
9
+ };
10
+
11
+ type DeviceButtonProps = {
12
+ device: Device;
13
+ selectedDevice: Device | null;
14
+ onSelect: (device: Device) => void;
15
+ };
16
+
17
+ export function DeviceButton({
18
+ device,
19
+ selectedDevice,
20
+ onSelect,
21
+ }: DeviceButtonProps) {
22
+ const platformIcon = platformIcons[device.platform];
23
+
24
+ return (
25
+ <button
26
+ type="button"
27
+ className={`editor-device-button${
28
+ selectedDevice === device ? ' editor-device-button--selected' : ''
29
+ }`}
30
+ onClick={() => onSelect(device)}
31
+ >
32
+ {device.name} <br />
33
+ {device.width}x{device.height}
34
+ {platformIcon && <img src={platformIcon} alt="" aria-hidden="true" />}
35
+ </button>
36
+ );
37
+ }
38
+
39
+ export default DeviceButton;