@developer_tribe/react-builder 0.1.31 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/DeviceMockFrame.d.ts +1 -17
  2. package/dist/RenderPage.d.ts +1 -9
  3. package/dist/build-components/Button/ButtonProps.generated.d.ts +2 -1
  4. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +2 -1
  5. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +2 -1
  6. package/dist/build-components/Image/ImageProps.generated.d.ts +2 -1
  7. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +8 -4
  8. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +6 -3
  9. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +2 -1
  10. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +10 -5
  11. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +4 -1
  12. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +4 -2
  13. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +10 -5
  14. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +10 -5
  15. package/dist/build-components/Text/TextProps.generated.d.ts +10 -5
  16. package/dist/build-components/View/ViewProps.generated.d.ts +6 -3
  17. package/dist/build-components/index.d.ts +1 -0
  18. package/dist/build-components/patterns.generated.d.ts +7 -2
  19. package/dist/components/AttributesEditorPanel.d.ts +9 -0
  20. package/dist/components/Breadcrumb.d.ts +13 -0
  21. package/dist/components/Builder.d.ts +9 -0
  22. package/dist/components/EditorHeader.d.ts +15 -0
  23. package/dist/index.cjs.js +7 -4
  24. package/dist/index.cjs.js.map +1 -0
  25. package/dist/index.d.ts +8 -4
  26. package/dist/index.esm.js +7 -4
  27. package/dist/index.esm.js.map +1 -0
  28. package/dist/pages/ProjectPage.d.ts +11 -0
  29. package/dist/pages/tabs/BuilderTab.d.ts +9 -0
  30. package/dist/pages/tabs/DebugTab.d.ts +7 -0
  31. package/dist/pages/tabs/PreviewTab.d.ts +3 -0
  32. package/dist/store.d.ts +17 -18
  33. package/dist/styles.css +1 -1
  34. package/dist/types/PreviewConfig.d.ts +6 -3
  35. package/dist/types/Project.d.ts +12 -2
  36. package/dist/utils/copyNode.d.ts +2 -0
  37. package/dist/utils/logger.d.ts +11 -0
  38. package/dist/utils/useLogRender.d.ts +1 -0
  39. package/package.json +17 -9
  40. package/scripts/prebuild/utils/createBuildComponentsIndex.js +15 -1
  41. package/scripts/prebuild/utils/createGeneratedProps.js +64 -5
  42. package/src/AttributesEditor.tsx +2 -0
  43. package/src/DeviceMockFrame.tsx +22 -31
  44. package/src/RenderPage.tsx +5 -42
  45. package/src/assets/images/android.svg +43 -0
  46. package/src/assets/images/apple.svg +16 -0
  47. package/src/assets/images/background.jpg +0 -0
  48. package/src/assets/samples/carousel-sample.json +2 -3
  49. package/src/assets/samples/getSamples.ts +51 -8
  50. package/src/assets/samples/simple-1.json +1 -2
  51. package/src/assets/samples/simple-2.json +1 -2
  52. package/src/assets/samples/vpn-onboard-1.json +1 -2
  53. package/src/assets/samples/vpn-onboard-2.json +1 -2
  54. package/src/assets/samples/vpn-onboard-3.json +1 -2
  55. package/src/assets/samples/vpn-onboard-4.json +1 -2
  56. package/src/assets/samples/vpn-onboard-5.json +1024 -0
  57. package/src/assets/samples/vpn-onboard-6.json +708 -0
  58. package/src/build-components/Button/Button.tsx +2 -0
  59. package/src/build-components/Button/ButtonProps.generated.ts +14 -12
  60. package/src/build-components/Carousel/Carousel.tsx +2 -0
  61. package/src/build-components/CarouselButtons/CarouselButtons.tsx +2 -0
  62. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +6 -1
  63. package/src/build-components/CarouselDots/CarouselDots.tsx +2 -0
  64. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +9 -7
  65. package/src/build-components/CarouselItem/CarouselItem.tsx +2 -0
  66. package/src/build-components/Image/Image.tsx +2 -0
  67. package/src/build-components/Image/ImageProps.generated.ts +3 -1
  68. package/src/build-components/Onboard/Onboard.tsx +2 -0
  69. package/src/build-components/OnboardButton/OnboardButton.tsx +7 -4
  70. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +14 -9
  71. package/src/build-components/OnboardButton/pattern.json +3 -2
  72. package/src/build-components/OnboardButtons/OnboardButtons.tsx +7 -7
  73. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +10 -3
  74. package/src/build-components/OnboardDot/OnboardDot.tsx +2 -0
  75. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +9 -7
  76. package/src/build-components/OnboardFooter/OnboardFooter.tsx +5 -3
  77. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +33 -22
  78. package/src/build-components/OnboardImage/OnboardImage.tsx +26 -1
  79. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +5 -1
  80. package/src/build-components/OnboardImage/pattern.json +3 -5
  81. package/src/build-components/OnboardItem/OnboardItem.tsx +2 -0
  82. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +5 -2
  83. package/src/build-components/OnboardProvider/OnboardProvider.tsx +2 -0
  84. package/src/build-components/OnboardSubtitle/OnboardSubtitle.tsx +2 -0
  85. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +33 -22
  86. package/src/build-components/OnboardTitle/OnboardTitle.tsx +2 -0
  87. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +33 -22
  88. package/src/build-components/Text/Text.tsx +5 -3
  89. package/src/build-components/Text/TextProps.generated.ts +33 -22
  90. package/src/build-components/View/View.tsx +2 -0
  91. package/src/build-components/View/ViewProps.generated.ts +18 -9
  92. package/src/build-components/index.ts +22 -0
  93. package/src/build-components/patterns.generated.ts +7 -2
  94. package/src/components/AttributesEditorPanel.tsx +112 -0
  95. package/src/components/Breadcrumb.tsx +48 -0
  96. package/src/components/Builder.tsx +272 -0
  97. package/src/components/EditorHeader.tsx +186 -0
  98. package/src/index.ts +8 -4
  99. package/src/pages/ProjectPage.tsx +152 -0
  100. package/src/pages/tabs/BuilderTab.tsx +33 -0
  101. package/src/pages/tabs/DebugTab.tsx +23 -0
  102. package/src/pages/tabs/PreviewTab.tsx +194 -0
  103. package/src/size-matters/index.ts +5 -1
  104. package/src/store.ts +60 -38
  105. package/src/styles/_mixins.scss +21 -0
  106. package/src/styles/_variables.scss +27 -0
  107. package/src/styles/builder.scss +60 -0
  108. package/src/styles/components.scss +88 -0
  109. package/src/styles/editor.scss +174 -0
  110. package/src/styles/global.scss +200 -0
  111. package/src/styles/index.scss +7 -0
  112. package/src/styles/pages.scss +2 -0
  113. package/src/types/PreviewConfig.ts +14 -5
  114. package/src/types/Project.ts +15 -2
  115. package/src/utils/copyNode.ts +7 -0
  116. package/src/utils/extractTextStyle.ts +4 -2
  117. package/src/utils/getDevices.ts +1 -0
  118. package/src/utils/logger.ts +76 -0
  119. package/src/utils/novaToJson.ts +5 -0
  120. package/src/utils/useLogRender.ts +13 -0
@@ -2,34 +2,45 @@
2
2
 
3
3
  import type { NodeData } from '../../types/Node';
4
4
 
5
+ export type FontWeightOptionType =
6
+ | 'normal'
7
+ | 'bold'
8
+ | '100'
9
+ | '200'
10
+ | '300'
11
+ | '400'
12
+ | '500'
13
+ | '600'
14
+ | '700'
15
+ | '800'
16
+ | '900';
17
+ export type TextAlignOptionType = 'left' | 'center' | 'right' | 'justify';
18
+ export type FlexDirectionOptionType = 'row' | 'column';
19
+ export type AlignItemsOptionType =
20
+ | 'flex-start'
21
+ | 'center'
22
+ | 'flex-end'
23
+ | 'stretch'
24
+ | 'baseline';
25
+ export type JustifyContentOptionType =
26
+ | 'flex-start'
27
+ | 'center'
28
+ | 'flex-end'
29
+ | 'space-between'
30
+ | 'space-around'
31
+ | 'space-evenly';
32
+
5
33
  export interface OnboardTitlePropsGenerated {
6
34
  child: string;
7
35
  attributes: {
8
36
  color?: string;
9
37
  fontSize?: number;
10
- fontWeight?:
11
- | 'normal'
12
- | 'bold'
13
- | '100'
14
- | '200'
15
- | '300'
16
- | '400'
17
- | '500'
18
- | '600'
19
- | '700'
20
- | '800'
21
- | '900';
22
- textAlign?: 'left' | 'center' | 'right' | 'justify';
38
+ fontWeight?: FontWeightOptionType;
39
+ textAlign?: TextAlignOptionType;
23
40
  scrollable?: boolean;
24
- flexDirection?: 'row' | 'column';
25
- alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline';
26
- justifyContent?:
27
- | 'flex-start'
28
- | 'center'
29
- | 'flex-end'
30
- | 'space-between'
31
- | 'space-around'
32
- | 'space-evenly';
41
+ flexDirection?: FlexDirectionOptionType;
42
+ alignItems?: AlignItemsOptionType;
43
+ justifyContent?: JustifyContentOptionType;
33
44
  gap?: string;
34
45
  padding?: number;
35
46
  paddingHorizontal?: string;
@@ -3,13 +3,15 @@ import type { TextComponentProps } from './TextProps.generated';
3
3
  import useNode from '../useNode';
4
4
  import { useRenderStore } from '../../store';
5
5
  import { extractTextStyle } from '../../utils/extractTextStyle';
6
+ import { useLogRender } from '../../utils/useLogRender';
6
7
 
7
8
  function Text({ node }: TextComponentProps) {
9
+ useLogRender('Text');
8
10
  node = useNode(node);
9
- const { defaultLanguage, localication } = useRenderStore((s) => ({
10
- defaultLanguage: s.defaultLanguage,
11
- localication: s.localication,
11
+ const { appConfig } = useRenderStore((s) => ({
12
+ appConfig: s.appConfig,
12
13
  }));
14
+ const { defaultLanguage, localication } = appConfig;
13
15
  const keyOrText: string = node.children as string;
14
16
  const style = extractTextStyle(node);
15
17
 
@@ -2,19 +2,41 @@
2
2
 
3
3
  import type { NodeData } from '../../types/Node';
4
4
 
5
+ export type FlexDirectionOptionType = 'row' | 'column';
6
+ export type AlignItemsOptionType =
7
+ | 'flex-start'
8
+ | 'center'
9
+ | 'flex-end'
10
+ | 'stretch'
11
+ | 'baseline';
12
+ export type JustifyContentOptionType =
13
+ | 'flex-start'
14
+ | 'center'
15
+ | 'flex-end'
16
+ | 'space-between'
17
+ | 'space-around'
18
+ | 'space-evenly';
19
+ export type FontWeightOptionType =
20
+ | 'normal'
21
+ | 'bold'
22
+ | '100'
23
+ | '200'
24
+ | '300'
25
+ | '400'
26
+ | '500'
27
+ | '600'
28
+ | '700'
29
+ | '800'
30
+ | '900';
31
+ export type TextAlignOptionType = 'left' | 'center' | 'right' | 'justify';
32
+
5
33
  export interface TextPropsGenerated {
6
34
  child: string;
7
35
  attributes: {
8
36
  scrollable?: boolean;
9
- flexDirection?: 'row' | 'column';
10
- alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline';
11
- justifyContent?:
12
- | 'flex-start'
13
- | 'center'
14
- | 'flex-end'
15
- | 'space-between'
16
- | 'space-around'
17
- | 'space-evenly';
37
+ flexDirection?: FlexDirectionOptionType;
38
+ alignItems?: AlignItemsOptionType;
39
+ justifyContent?: JustifyContentOptionType;
18
40
  gap?: string;
19
41
  padding?: number;
20
42
  paddingHorizontal?: string;
@@ -36,19 +58,8 @@ export interface TextPropsGenerated {
36
58
  height?: number;
37
59
  color?: string;
38
60
  fontSize?: number;
39
- fontWeight?:
40
- | 'normal'
41
- | 'bold'
42
- | '100'
43
- | '200'
44
- | '300'
45
- | '400'
46
- | '500'
47
- | '600'
48
- | '700'
49
- | '800'
50
- | '900';
51
- textAlign?: 'left' | 'center' | 'right' | 'justify';
61
+ fontWeight?: FontWeightOptionType;
62
+ textAlign?: TextAlignOptionType;
52
63
  };
53
64
  }
54
65
 
@@ -7,8 +7,10 @@ import RenderNode from '../RenderNode.generated';
7
7
  import { Node } from '../../types/Node';
8
8
  import useNode from '../useNode';
9
9
  import { extractViewStyle } from '../../utils/extractViewStyle';
10
+ import { useLogRender } from '../../utils/useLogRender';
10
11
 
11
12
  export function View({ node }: ViewComponentProps) {
13
+ useLogRender('View');
12
14
  node = useNode(node);
13
15
  return (
14
16
  <div style={extractViewStyle(node)} className="scroll-container">
@@ -2,19 +2,28 @@
2
2
 
3
3
  import type { NodeData } from '../../types/Node';
4
4
 
5
+ export type FlexDirectionOptionType = 'row' | 'column';
6
+ export type AlignItemsOptionType =
7
+ | 'flex-start'
8
+ | 'center'
9
+ | 'flex-end'
10
+ | 'stretch'
11
+ | 'baseline';
12
+ export type JustifyContentOptionType =
13
+ | 'flex-start'
14
+ | 'center'
15
+ | 'flex-end'
16
+ | 'space-between'
17
+ | 'space-around'
18
+ | 'space-evenly';
19
+
5
20
  export interface ViewPropsGenerated {
6
21
  child: string;
7
22
  attributes: {
8
23
  scrollable?: boolean;
9
- flexDirection?: 'row' | 'column';
10
- alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch' | 'baseline';
11
- justifyContent?:
12
- | 'flex-start'
13
- | 'center'
14
- | 'flex-end'
15
- | 'space-between'
16
- | 'space-around'
17
- | 'space-evenly';
24
+ flexDirection?: FlexDirectionOptionType;
25
+ alignItems?: AlignItemsOptionType;
26
+ justifyContent?: JustifyContentOptionType;
18
27
  gap?: string;
19
28
  padding?: number;
20
29
  paddingHorizontal?: string;
@@ -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,
@@ -117,8 +117,9 @@ export const patterns = [
117
117
  types: {
118
118
  EventObject: {
119
119
  type: ['Permission', 'Navigate'],
120
- permission: ['att', 'notification', 'rating', 'null'],
121
- navigate_to: ['string', 'null'],
120
+ permission: ['att', 'notification', 'rating', 'GDPR', 'null'],
121
+ navigate_to: 'string',
122
+ targetIndex: 'number',
122
123
  },
123
124
  },
124
125
  },
@@ -239,8 +240,12 @@ export const patterns = [
239
240
  height: 'number',
240
241
  resizeMode: ['cover', 'contain', 'stretch', 'center'],
241
242
  borderRadius: 'number',
243
+ video_url: 'string',
244
+ lottie: 'string',
242
245
  },
243
246
  },
247
+ types: {},
248
+ defaults: {},
244
249
  },
245
250
  {
246
251
  schemaVersion: 1,
@@ -0,0 +1,112 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { AttributesEditor, Node, NodeData } from '..';
3
+ import { useLogRender } from '../utils/useLogRender';
4
+
5
+ interface AttributesEditorPanelProps {
6
+ current: Node;
7
+ attributes: any;
8
+ onChange: (data: Node) => void;
9
+ setCurrent: (current: Node) => void;
10
+ }
11
+
12
+ export function AttributesEditorPanel({
13
+ current,
14
+ attributes,
15
+ onChange,
16
+ setCurrent,
17
+ }: AttributesEditorPanelProps) {
18
+ useLogRender('AttributesEditorPanel');
19
+ if (!current) return null;
20
+
21
+ function replaceNode(root: Node, target: Node, next: Node): Node {
22
+ if (root === target) return next;
23
+ if (root === null || root === undefined) return root;
24
+ if (typeof root === 'string') return root;
25
+ if (Array.isArray(root)) {
26
+ let changed = false;
27
+ const arr = root.map((item) => {
28
+ const r = replaceNode(item, target, next);
29
+ if (r !== item) changed = true;
30
+ return r;
31
+ });
32
+ return changed ? arr : root;
33
+ }
34
+ const data = root as any;
35
+ if ('children' in data) {
36
+ const prev = data.children;
37
+ const replaced = Array.isArray(prev)
38
+ ? prev.map((c: Node) => replaceNode(c, target, next))
39
+ : replaceNode(prev as Node, target, next);
40
+ if (replaced !== prev) {
41
+ data.children = replaced;
42
+ return { ...data, children: replaced } as Node;
43
+ }
44
+ }
45
+ return root;
46
+ }
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]);
66
+
67
+ return (
68
+ <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
+ />
110
+ </div>
111
+ );
112
+ }
@@ -0,0 +1,48 @@
1
+ import { ReactNode } from 'react';
2
+ import { useLogRender } from '../utils/useLogRender';
3
+ export type BreadcrumbItem = {
4
+ label: string;
5
+ to?: string;
6
+ onClick?: () => void;
7
+ };
8
+
9
+ type BreadcrumbProps = {
10
+ items: BreadcrumbItem[];
11
+ separator?: ReactNode;
12
+ ariaLabel?: string;
13
+ };
14
+
15
+ export function Breadcrumb({
16
+ items,
17
+ separator = '/',
18
+ ariaLabel = 'Breadcrumb',
19
+ }: BreadcrumbProps) {
20
+ useLogRender('Breadcrumb');
21
+ return (
22
+ <nav className="breadcrumb" aria-label={ariaLabel}>
23
+ <ol className="breadcrumb__list">
24
+ {items.map((item, index) => {
25
+ const isLast = index === items.length - 1;
26
+ return (
27
+ <li className="breadcrumb__item" key={`${item.label}-${index}`}>
28
+ {index > 0 && (
29
+ <span className="breadcrumb__separator" aria-hidden>
30
+ {separator}
31
+ </span>
32
+ )}
33
+ {isLast || !item.to ? (
34
+ <span className="breadcrumb__current" aria-current="page">
35
+ {item.label}
36
+ </span>
37
+ ) : (
38
+ <p onClick={item.onClick} className="breadcrumb__link">
39
+ {item.label}
40
+ </p>
41
+ )}
42
+ </li>
43
+ );
44
+ })}
45
+ </ol>
46
+ </nav>
47
+ );
48
+ }
@@ -0,0 +1,272 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ isNodeArray,
4
+ isNodeNullOrUndefined,
5
+ isNodeString,
6
+ Node,
7
+ NodeData,
8
+ NodeDefaultAttribute,
9
+ allcomponentNames,
10
+ } from '..';
11
+ import { Breadcrumb } from './Breadcrumb';
12
+ import { useLogRender } from '../utils/useLogRender';
13
+ import { getDefaultsForType, getPatternByType } from '../utils/patterns';
14
+
15
+ type BuilderEditorProps = {
16
+ data: Node;
17
+ setData: (data: Node) => void;
18
+ current: Node;
19
+ setCurrent: (current: Node) => void;
20
+ };
21
+
22
+ interface BuilderEditorComponentProps {
23
+ node: Node;
24
+ onClick: (node: Node) => void;
25
+ }
26
+
27
+ function BuilderButton({ node, onClick }: { node: Node; onClick: () => void }) {
28
+ if (isNodeNullOrUndefined(node)) {
29
+ return <div className="builder__placeholder">Null or undefined</div>;
30
+ }
31
+ if (isNodeString(node)) {
32
+ return <div className="builder__text">{node as string}</div>;
33
+ }
34
+ const nodeData = node as NodeData<NodeDefaultAttribute>;
35
+
36
+ let extra = '';
37
+ if (nodeData.attributes?.condition) {
38
+ extra = ` (${nodeData.attributes.condition} ${nodeData.attributes.conditionVariable})`;
39
+ }
40
+ return (
41
+ <a onClick={onClick} className="builder__button">
42
+ {nodeData.type} {extra}
43
+ </a>
44
+ );
45
+ }
46
+
47
+ function BuilderComponent({ node, onClick }: BuilderEditorComponentProps) {
48
+ if (isNodeNullOrUndefined(node)) {
49
+ return <div className="builder__placeholder">Null or undefined</div>;
50
+ }
51
+ if (isNodeString(node)) {
52
+ return (
53
+ <div className="builder__text">
54
+ {node as string} (Please define a node)
55
+ </div>
56
+ );
57
+ }
58
+
59
+ if (isNodeArray(node)) {
60
+ return (
61
+ <div className="builder__list">
62
+ {(node as Node[]).map((item, index) => (
63
+ <BuilderButton
64
+ onClick={() => {
65
+ onClick(item);
66
+ }}
67
+ key={index}
68
+ node={item}
69
+ />
70
+ ))}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ const nodeData = node as NodeData<NodeDefaultAttribute>;
76
+ const children = nodeData.children
77
+ ? isNodeArray(nodeData.children)
78
+ ? (nodeData.children as Node[])
79
+ : [nodeData.children]
80
+ : null;
81
+
82
+ return (
83
+ <div className="builder__node">
84
+ <p className="builder__node-type">{nodeData.type}</p>
85
+ <div className="builder__children">
86
+ {children &&
87
+ children.map((child, index) => (
88
+ <BuilderButton
89
+ onClick={() => {
90
+ onClick(child);
91
+ }}
92
+ key={index}
93
+ node={child}
94
+ />
95
+ ))}
96
+ </div>
97
+ </div>
98
+ );
99
+ }
100
+
101
+ export function Builder({
102
+ data,
103
+ setData,
104
+ current,
105
+ setCurrent,
106
+ }: BuilderEditorProps) {
107
+ useLogRender('Builder');
108
+ const [crumbs, setCrumbs] = useState<string[]>(['root']);
109
+ const breadcrumbItems = useMemo(
110
+ () => crumbs.map((c, idx) => ({ label: c })),
111
+ [crumbs],
112
+ );
113
+
114
+ function replaceNode(root: Node, target: Node, next: Node): Node {
115
+ if (root === target) return next;
116
+ if (root === null || root === undefined) return root;
117
+ if (typeof root === 'string') return root;
118
+ if (Array.isArray(root)) {
119
+ let changed = false;
120
+ const arr = root.map((item) => {
121
+ const r = replaceNode(item, target, next);
122
+ if (r !== item) changed = true;
123
+ return r;
124
+ });
125
+ return changed ? arr : root;
126
+ }
127
+ const data = root as any;
128
+ if ('children' in data) {
129
+ const prev = data.children;
130
+ const replaced = Array.isArray(prev)
131
+ ? prev.map((c: Node) => replaceNode(c, target, next))
132
+ : replaceNode(prev as Node, target, next);
133
+ if (replaced !== prev) {
134
+ data.children = replaced;
135
+ return { ...data, children: replaced } as Node;
136
+ }
137
+ }
138
+ return root;
139
+ }
140
+
141
+ function createDefaultNode(type: string): NodeData<NodeDefaultAttribute> {
142
+ const pattern = getPatternByType(type)?.pattern;
143
+ const defaults = getDefaultsForType(type) ?? {};
144
+ let children: Node = '';
145
+ const childrenSchema = pattern?.children as unknown;
146
+ if (childrenSchema === 'never') {
147
+ children = '';
148
+ } else if (childrenSchema === 'string') {
149
+ children = '';
150
+ } else if (
151
+ childrenSchema === 'node' ||
152
+ (Array.isArray(childrenSchema) && childrenSchema.includes('node'))
153
+ ) {
154
+ children = [];
155
+ } else if (typeof childrenSchema === 'string') {
156
+ // Specific child type like 'carouselItem' – initialize as empty array to allow multiple
157
+ children = [];
158
+ } else {
159
+ children = '';
160
+ }
161
+ return {
162
+ type,
163
+ children,
164
+ attributes: { ...defaults },
165
+ } as NodeData<NodeDefaultAttribute>;
166
+ }
167
+
168
+ function getAllowedChildTypes(parent: Node): string[] {
169
+ if (
170
+ isNodeNullOrUndefined(parent) ||
171
+ isNodeString(parent) ||
172
+ isNodeArray(parent)
173
+ )
174
+ return [];
175
+ const parentData = parent as NodeData;
176
+ const parentType = parentData.type;
177
+ // Special rule: limit OnboardButtons to OnboardButton only
178
+ if (parentType === 'OnboardButtons') return ['OnboardButton'];
179
+ const childrenSchema = getPatternByType(parentType)?.pattern
180
+ ?.children as unknown;
181
+ if (!childrenSchema) return [];
182
+ if (childrenSchema === 'never' || childrenSchema === 'string') return [];
183
+ if (
184
+ childrenSchema === 'node' ||
185
+ (Array.isArray(childrenSchema) && childrenSchema.includes('node'))
186
+ ) {
187
+ return [...allcomponentNames];
188
+ }
189
+ if (typeof childrenSchema === 'string') {
190
+ return [childrenSchema];
191
+ }
192
+ return [];
193
+ }
194
+
195
+ return (
196
+ <div className="builder">
197
+ <Breadcrumb items={breadcrumbItems} />
198
+
199
+ <div className="builder__current">
200
+ {crumbs[crumbs.length - 1] + ' ( ' + crumbs.length + '. level )'}
201
+ </div>
202
+ <BuilderComponent
203
+ onClick={(node: Node) => {
204
+ setCurrent(node);
205
+ setCrumbs((crumbs) => [
206
+ ...crumbs,
207
+ typeof node === 'string'
208
+ ? node
209
+ : (node as NodeData<NodeDefaultAttribute>).type,
210
+ ]);
211
+ }}
212
+ node={current}
213
+ />
214
+ {!isNodeNullOrUndefined(current) &&
215
+ !isNodeString(current) &&
216
+ !isNodeArray(current) &&
217
+ (() => {
218
+ const allowed = getAllowedChildTypes(current);
219
+ if (allowed.length === 0) return null;
220
+ return (
221
+ <div
222
+ style={{
223
+ display: 'flex',
224
+ flexWrap: 'wrap',
225
+ gap: 8,
226
+ paddingTop: 12,
227
+ }}
228
+ >
229
+ {allowed.map((t) => (
230
+ <button
231
+ key={t}
232
+ className="editor-button"
233
+ onClick={() => {
234
+ const parent = current as NodeData<NodeDefaultAttribute>;
235
+ const nextChild = createDefaultNode(t);
236
+ let nextChildren: Node;
237
+ if (Array.isArray(parent.children)) {
238
+ nextChildren = [
239
+ ...(parent.children as Node[]),
240
+ nextChild,
241
+ ];
242
+ } else if (
243
+ parent.children === null ||
244
+ parent.children === undefined ||
245
+ typeof parent.children === 'string'
246
+ ) {
247
+ nextChildren = [nextChild];
248
+ } else {
249
+ nextChildren = [parent.children as Node, nextChild];
250
+ }
251
+ const updatedParent: NodeData<NodeDefaultAttribute> = {
252
+ ...parent,
253
+ children: nextChildren,
254
+ };
255
+ const updatedRoot = replaceNode(
256
+ data,
257
+ current,
258
+ updatedParent,
259
+ );
260
+ setData(updatedRoot);
261
+ setCurrent(updatedParent);
262
+ }}
263
+ >
264
+ Add {t}
265
+ </button>
266
+ ))}
267
+ </div>
268
+ );
269
+ })()}
270
+ </div>
271
+ );
272
+ }