@developer_tribe/react-builder 0.1.16 → 0.1.18

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.
@@ -6,7 +6,17 @@ type Pattern = {
6
6
  children: unknown;
7
7
  attributes: Record<string, string | string[]>;
8
8
  };
9
+ types?: Record<string, Record<string, string | string[]>>;
9
10
  };
10
11
  export declare function getPatternByType(type?: string | null): Pattern | undefined;
11
12
  export declare function getAttributeSchema(type?: string | null): Record<string, string | string[]> | undefined;
13
+ /**
14
+ * Returns the schema of a custom complex type declared under a component pattern's `types` block.
15
+ * For example, OnboardButton.pattern.types.EventObject
16
+ */
17
+ export declare function getTypeSchema(componentType?: string | null, typeName?: string | null): Record<string, string | string[]> | undefined;
18
+ /** Utility: returns true if the type name refers to a primitive scalar */
19
+ export declare function isPrimitiveType(typeName: string): boolean;
20
+ /** Utility: parse `X[]` forms and return the item type if present */
21
+ export declare function getArrayItemType(typeName: string): string | null;
12
22
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@developer_tribe/react-builder",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "restricted": true,
6
6
  "main": "dist/index.cjs.js",
@@ -1,8 +1,15 @@
1
1
  import path from 'path';
2
2
  import { promises as fs } from 'fs';
3
3
  import { ensureDir } from './ensureDir.js';
4
- // inlined from tsTypeFromAttributeType.js
5
- function tsTypeFromAttributeType(attrType) {
4
+ import { formatWithPrettier } from './formatWithPrettier.js';
5
+
6
+ // Helpers
7
+ const isPrimitive = t => t === 'string' || t === 'number' || t === 'boolean';
8
+ const getArrayItem = t =>
9
+ typeof t === 'string' && t.endsWith('[]') ? t.slice(0, -2) : null;
10
+
11
+ // Convert attribute type spec to TS type. Supports enum arrays, primitives, CustomType, and CustomType[]
12
+ function tsTypeFromAttributeType(attrType, allTypes) {
6
13
  if (attrType === 'string') return 'string';
7
14
  if (attrType === 'number') return 'number';
8
15
  if (attrType === 'boolean') return 'boolean';
@@ -10,9 +17,19 @@ function tsTypeFromAttributeType(attrType) {
10
17
  const literals = attrType.map(v => JSON.stringify(v)).join(' | ');
11
18
  return literals.length > 0 ? literals : 'string';
12
19
  }
20
+ if (typeof attrType === 'string') {
21
+ const item = getArrayItem(attrType);
22
+ if (item) {
23
+ if (isPrimitive(item)) return `${item}[]`;
24
+ // Custom type array
25
+ return `${item}Generated[]`;
26
+ }
27
+ if (isPrimitive(attrType)) return attrType;
28
+ // Custom object type
29
+ return `${attrType}Generated`;
30
+ }
13
31
  return 'string';
14
32
  }
15
- import { formatWithPrettier } from './formatWithPrettier.js';
16
33
 
17
34
  export async function createGeneratedProps(
18
35
  componentDir,
@@ -25,8 +42,24 @@ export async function createGeneratedProps(
25
42
 
26
43
  const { pattern, allowUnknownAttributes } = patternJson;
27
44
  const attributes = pattern.attributes || {};
45
+ const allTypes = patternJson.types || {};
46
+
47
+ // Emit custom type interfaces if present
48
+ const customTypeEntries = Object.entries(allTypes);
49
+ const customTypeBlocks = customTypeEntries.map(([typeName, schema]) => {
50
+ const fields = Object.entries(schema).map(([k, t]) => {
51
+ const tsType = tsTypeFromAttributeType(t, allTypes);
52
+ return ` ${k}?: ${tsType};`;
53
+ });
54
+ return (
55
+ `export interface ${typeName}Generated {\n` +
56
+ (fields.length ? fields.join('\n') + '\n' : '') +
57
+ `}\n`
58
+ );
59
+ });
60
+
28
61
  const attributeLines = Object.entries(attributes).map(([key, t]) => {
29
- const tsType = tsTypeFromAttributeType(t);
62
+ const tsType = tsTypeFromAttributeType(t, allTypes);
30
63
  return ` ${key}?: ${tsType};`;
31
64
  });
32
65
 
@@ -48,6 +81,8 @@ export async function createGeneratedProps(
48
81
  // Re-export a component props helper to avoid repeating the local type in each component file
49
82
  `import type { NodeData } from '../../types/Node';\n` +
50
83
  `\n` +
84
+ (customTypeBlocks.length ? customTypeBlocks.join('\n') + '\n' : '') +
85
+ `\n` +
51
86
  `export interface ${componentName}PropsGenerated {\n` +
52
87
  ` child: ${normalizedChildTsType};\n` +
53
88
  ` attributes: {\n` +
@@ -116,19 +116,61 @@ async function validatePatternJson(componentDir, componentName) {
116
116
  );
117
117
  }
118
118
 
119
+ // Helpers for validating custom types
120
+ const isPrimitive = t => t === 'string' || t === 'number' || t === 'boolean';
121
+ const hasCustomTypes = typeof data.types === 'object' && data.types != null;
122
+ const isCustomType = t =>
123
+ hasCustomTypes &&
124
+ typeof data.types[t] === 'object' &&
125
+ data.types[t] != null;
126
+ const isEnumArray = v =>
127
+ Array.isArray(v) && v.every(x => typeof x === 'string');
128
+ const isValidTypeRef = t => {
129
+ if (typeof t !== 'string') return false;
130
+ if (isPrimitive(t)) return true;
131
+ // Support arrays like X[]
132
+ if (t.endsWith('[]')) {
133
+ const base = t.slice(0, -2);
134
+ return isPrimitive(base) || isCustomType(base);
135
+ }
136
+ // Custom type name
137
+ return isCustomType(t);
138
+ };
139
+
140
+ // Validate attributes accept primitive, enum array, or custom type refs (including X[])
119
141
  for (const [attrName, attrType] of Object.entries(pattern.attributes)) {
120
- const isValidType =
121
- typeof attrType === 'string' &&
122
- (attrType === 'string' || attrType === 'number' || attrType === 'boolean')
123
- ? true
124
- : Array.isArray(attrType) && attrType.every(v => typeof v === 'string');
125
- if (!isValidType) {
142
+ const ok =
143
+ (typeof attrType === 'string' && isValidTypeRef(attrType)) ||
144
+ isEnumArray(attrType);
145
+ if (!ok) {
126
146
  return fail(
127
- `[${componentName}] pattern.json -> 'pattern.attributes.${attrName}' must be 'string' | 'number' | 'boolean' | string[]`
147
+ `[${componentName}] pattern.json -> 'pattern.attributes.${attrName}' must be 'string' | 'number' | 'boolean' | string[] | CustomType | CustomType[]`
128
148
  );
129
149
  }
130
150
  }
131
151
 
152
+ // If types block exists, validate its shape (only primitives or enum arrays for fields)
153
+ if (hasCustomTypes) {
154
+ for (const [typeName, typeSchema] of Object.entries(data.types)) {
155
+ if (typeof typeSchema !== 'object' || typeSchema == null) {
156
+ return fail(
157
+ `[${componentName}] pattern.json -> 'types.${typeName}' must be an object`
158
+ );
159
+ }
160
+ for (const [fieldName, fieldType] of Object.entries(typeSchema)) {
161
+ const fieldOk =
162
+ (typeof fieldType === 'string' &&
163
+ (isPrimitive(fieldType) || isValidTypeRef(fieldType))) ||
164
+ isEnumArray(fieldType);
165
+ if (!fieldOk) {
166
+ return fail(
167
+ `[${componentName}] pattern.json -> 'types.${typeName}.${fieldName}' must be 'string' | 'number' | 'boolean' | string[] | CustomType | CustomType[]`
168
+ );
169
+ }
170
+ }
171
+ }
172
+ }
173
+
132
174
  return data;
133
175
  }
134
176
 
@@ -1,7 +1,12 @@
1
1
  import React from 'react';
2
2
  import { Node, NodeData, NodeDefaultAttribute } from './types/Node';
3
3
  import { isNodeString } from './utils/analyseNode';
4
- import { getAttributeSchema } from './utils/patterns';
4
+ import {
5
+ getAttributeSchema,
6
+ getTypeSchema,
7
+ getArrayItemType,
8
+ isPrimitiveType,
9
+ } from './utils/patterns';
5
10
 
6
11
  type AttributesEditorProps = {
7
12
  node: Node;
@@ -13,12 +18,16 @@ function Field({
13
18
  type,
14
19
  value,
15
20
  onChange,
21
+ componentType,
16
22
  }: {
17
23
  name: string;
18
24
  type: string | string[];
19
25
  value: any;
20
26
  onChange: (v: any) => void;
27
+ // The current node's component type is needed to resolve custom type schemas
28
+ componentType?: string;
21
29
  }) {
30
+ // Render enum selector
22
31
  if (Array.isArray(type)) {
23
32
  return (
24
33
  <select
@@ -35,6 +44,188 @@ function Field({
35
44
  </select>
36
45
  );
37
46
  }
47
+
48
+ // Arrays: detect X[] (including string[]/number[]/boolean[]/CustomType[])
49
+ const itemType = typeof type === 'string' ? getArrayItemType(type) : null;
50
+ if (itemType) {
51
+ const arr: any[] = Array.isArray(value) ? value : [];
52
+
53
+ // Primitive arrays with add/remove controls
54
+ if (isPrimitiveType(itemType)) {
55
+ return (
56
+ <div style={{ display: 'grid', gap: 8 }}>
57
+ {arr.map((item, idx) => (
58
+ <div
59
+ key={idx}
60
+ style={{ display: 'flex', gap: 8, alignItems: 'center' }}
61
+ >
62
+ {itemType === 'number' ? (
63
+ <input
64
+ type="number"
65
+ value={item ?? ''}
66
+ onChange={(e) => {
67
+ const next = [...arr];
68
+ next[idx] =
69
+ e.target.value === ''
70
+ ? undefined
71
+ : Number(e.target.value);
72
+ onChange(next);
73
+ }}
74
+ className="input"
75
+ style={{ flex: 1 }}
76
+ />
77
+ ) : itemType === 'boolean' ? (
78
+ <input
79
+ type="checkbox"
80
+ checked={Boolean(item)}
81
+ onChange={(e) => {
82
+ const next = [...arr];
83
+ next[idx] = e.target.checked;
84
+ onChange(next);
85
+ }}
86
+ />
87
+ ) : (
88
+ <input
89
+ type="text"
90
+ value={item ?? ''}
91
+ onChange={(e) => {
92
+ const next = [...arr];
93
+ next[idx] =
94
+ e.target.value === '' ? undefined : e.target.value;
95
+ onChange(next);
96
+ }}
97
+ className="input"
98
+ style={{ flex: 1 }}
99
+ />
100
+ )}
101
+ <button
102
+ type="button"
103
+ onClick={() => {
104
+ const next = arr.filter((_, i) => i !== idx);
105
+ onChange(next.length ? next : undefined);
106
+ }}
107
+ >
108
+ remove
109
+ </button>
110
+ </div>
111
+ ))}
112
+ <div>
113
+ <button
114
+ type="button"
115
+ onClick={() => {
116
+ const next = [
117
+ ...arr,
118
+ itemType === 'boolean'
119
+ ? false
120
+ : itemType === 'number'
121
+ ? 0
122
+ : '',
123
+ ];
124
+ onChange(next);
125
+ }}
126
+ >
127
+ add
128
+ </button>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ // Object arrays with nested editors
135
+ const schema = getTypeSchema(componentType, itemType) ?? {};
136
+ return (
137
+ <div style={{ display: 'grid', gap: 8 }}>
138
+ {arr.map((item, idx) => {
139
+ const obj = (item ?? {}) as Record<string, unknown>;
140
+ return (
141
+ <div
142
+ key={idx}
143
+ style={{ border: '1px solid #ddd', borderRadius: 6, padding: 8 }}
144
+ >
145
+ <div
146
+ style={{
147
+ display: 'grid',
148
+ gridTemplateColumns: '1fr 1fr',
149
+ gap: 8,
150
+ }}
151
+ >
152
+ {Object.entries(schema).map(([fieldName, fieldType]) => (
153
+ <React.Fragment key={fieldName}>
154
+ <div style={{ alignSelf: 'center' }}>{fieldName}</div>
155
+ <Field
156
+ name={fieldName}
157
+ type={fieldType}
158
+ value={obj?.[fieldName as keyof typeof obj]}
159
+ onChange={(val) => {
160
+ const next = [...arr];
161
+ const nextObj = { ...(obj ?? {}), [fieldName]: val };
162
+ next[idx] = nextObj;
163
+ onChange(next);
164
+ }}
165
+ componentType={componentType}
166
+ />
167
+ </React.Fragment>
168
+ ))}
169
+ </div>
170
+ <div style={{ marginTop: 8 }}>
171
+ <button
172
+ type="button"
173
+ onClick={() => {
174
+ const next = arr.filter((_, i) => i !== idx);
175
+ onChange(next.length ? next : undefined);
176
+ }}
177
+ >
178
+ remove
179
+ </button>
180
+ </div>
181
+ </div>
182
+ );
183
+ })}
184
+ <div>
185
+ <button
186
+ type="button"
187
+ onClick={() => {
188
+ const empty: Record<string, unknown> = {};
189
+ const next = [...arr, empty];
190
+ onChange(next);
191
+ }}
192
+ >
193
+ add
194
+ </button>
195
+ </div>
196
+ </div>
197
+ );
198
+ }
199
+
200
+ // Non-array complex object types defined under pattern `types`
201
+ if (typeof type === 'string' && !isPrimitiveType(type)) {
202
+ const schema = getTypeSchema(componentType, type);
203
+ if (schema) {
204
+ const obj = (value ?? {}) as Record<string, unknown>;
205
+ return (
206
+ <div
207
+ style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}
208
+ >
209
+ {Object.entries(schema).map(([fieldName, fieldType]) => (
210
+ <React.Fragment key={fieldName}>
211
+ <div style={{ alignSelf: 'center' }}>{fieldName}</div>
212
+ <Field
213
+ name={fieldName}
214
+ type={fieldType}
215
+ value={obj?.[fieldName as keyof typeof obj]}
216
+ onChange={(val) => {
217
+ const nextObj = { ...(obj ?? {}), [fieldName]: val };
218
+ onChange(nextObj);
219
+ }}
220
+ componentType={componentType}
221
+ />
222
+ </React.Fragment>
223
+ ))}
224
+ </div>
225
+ );
226
+ }
227
+ }
228
+
38
229
  if (type === 'number') {
39
230
  return (
40
231
  <input
@@ -56,6 +247,52 @@ function Field({
56
247
  />
57
248
  );
58
249
  }
250
+ // Legacy support: string[]
251
+ if (type === 'string[]') {
252
+ const arr: string[] = Array.isArray(value) ? value : [];
253
+ return (
254
+ <div style={{ display: 'grid', gap: 8 }}>
255
+ {arr.map((item, idx) => (
256
+ <div
257
+ key={idx}
258
+ style={{ display: 'flex', gap: 8, alignItems: 'center' }}
259
+ >
260
+ <input
261
+ type="text"
262
+ value={item ?? ''}
263
+ onChange={(e) => {
264
+ const next = [...arr];
265
+ next[idx] = e.target.value;
266
+ onChange(next);
267
+ }}
268
+ className="input"
269
+ style={{ flex: 1 }}
270
+ />
271
+ <button
272
+ type="button"
273
+ onClick={() => {
274
+ const next = arr.filter((_, i) => i !== idx);
275
+ onChange(next.length ? next : undefined);
276
+ }}
277
+ >
278
+ remove
279
+ </button>
280
+ </div>
281
+ ))}
282
+ <div>
283
+ <button
284
+ type="button"
285
+ onClick={() => {
286
+ const next = [...arr, ''];
287
+ onChange(next);
288
+ }}
289
+ >
290
+ add
291
+ </button>
292
+ </div>
293
+ </div>
294
+ );
295
+ }
59
296
  return (
60
297
  <input
61
298
  type="text"
@@ -97,6 +334,7 @@ export function AttributesEditor({ node, onChange }: AttributesEditorProps) {
97
334
  };
98
335
  onChange(next);
99
336
  }}
337
+ componentType={data?.type}
100
338
  />
101
339
  </React.Fragment>
102
340
  ))}
@@ -25,17 +25,23 @@ export function RenderPage({
25
25
  localication,
26
26
  defaultLanguage,
27
27
  }: RenderPageProps) {
28
+ const screenPreviewHeight = 800;
29
+ // The calculation is correct for maintaining the aspect ratio of the target screen size.
30
+ // It scales the width proportionally to a fixed preview height.
31
+ // width = (previewHeight * targetWidth) / targetHeight
32
+ const height = screenPreviewHeight;
33
+ const width = (height * screenSize.width) / screenSize.height;
28
34
  return (
29
35
  <div className="stage-wrapper" style={{ overflow: 'auto' }}>
30
36
  <div
31
37
  className="stage"
32
38
  style={{
33
- width: screenSize.width,
34
- height: screenSize.height,
35
- minWidth: screenSize.width,
36
- maxWidth: screenSize.width,
37
- minHeight: screenSize.height,
38
- maxHeight: screenSize.height,
39
+ width: width,
40
+ height: height,
41
+ minWidth: width,
42
+ maxWidth: width,
43
+ minHeight: height,
44
+ maxHeight: height,
39
45
  overflow: 'hidden',
40
46
  position: 'relative',
41
47
  padding: 4,
@@ -2,6 +2,12 @@
2
2
 
3
3
  import type { NodeData } from '../../types/Node';
4
4
 
5
+ export interface EventObjectGenerated {
6
+ type?: 'Permission' | 'Navigate';
7
+ permission?: string;
8
+ next_page_key?: string;
9
+ }
10
+
5
11
  export interface OnboardButtonPropsGenerated {
6
12
  child: string;
7
13
  attributes: {
@@ -10,7 +16,7 @@ export interface OnboardButtonPropsGenerated {
10
16
  button_background_color?: string;
11
17
  flex?: number;
12
18
  targetIndex?: number;
13
- events?: string;
19
+ events?: EventObjectGenerated[];
14
20
  };
15
21
  }
16
22
 
@@ -10,7 +10,14 @@
10
10
  "button_background_color": "string",
11
11
  "flex": "number",
12
12
  "targetIndex": "number",
13
- "events": "string"
13
+ "events": "EventObject[]"
14
+ }
15
+ },
16
+ "types": {
17
+ "EventObject": {
18
+ "type": ["Permission", "Navigate"],
19
+ "permission": "string",
20
+ "next_page_key": "string"
14
21
  }
15
22
  }
16
23
  }
@@ -1,8 +1,64 @@
1
- import React from 'react';
1
+ import React, { useContext } from 'react';
2
2
  import type { OnboardFooterComponentProps } from './OnboardFooterProps.generated';
3
+ import { mainNodeContext } from '../../RenderMainNode';
3
4
 
4
5
  function OnboardFooter({ node }: OnboardFooterComponentProps) {
5
- return String(node?.type ?? 'OnboardFooter');
6
+ const { localication, defaultLanguage } = useContext(mainNodeContext) ?? {};
7
+ const t = (key?: string) =>
8
+ key ? (localication?.[defaultLanguage ?? 'en']?.[key] ?? key) : '';
9
+
10
+ const text = t(node?.attributes?.textLocalizationKey);
11
+ const style: React.CSSProperties = {
12
+ display: 'flex',
13
+ flexDirection: (node?.attributes?.flexDirection as any) ?? 'column',
14
+ gap: typeof node?.attributes?.gap === 'number' ? node.attributes.gap : 0,
15
+ padding:
16
+ typeof node?.attributes?.padding === 'number'
17
+ ? node.attributes.padding
18
+ : undefined,
19
+ margin:
20
+ typeof node?.attributes?.margin === 'number'
21
+ ? node.attributes.margin
22
+ : undefined,
23
+ backgroundColor: node?.attributes?.backgroundColor,
24
+ borderRadius:
25
+ typeof node?.attributes?.borderRadius === 'number'
26
+ ? node.attributes.borderRadius
27
+ : undefined,
28
+ width:
29
+ typeof node?.attributes?.width === 'number'
30
+ ? node.attributes.width
31
+ : undefined,
32
+ height:
33
+ typeof node?.attributes?.height === 'number'
34
+ ? node.attributes.height
35
+ : undefined,
36
+ alignItems: node?.attributes?.alignItems as any,
37
+ justifyContent: node?.attributes?.justifyContent as any,
38
+ };
39
+
40
+ const linkStyle = (color?: string): React.CSSProperties => ({
41
+ color,
42
+ cursor: color ? 'pointer' : undefined,
43
+ });
44
+
45
+ return (
46
+ <div className="primitive primitive-footer" style={style}>
47
+ {!!text && <p style={{ color: node?.attributes?.textColor }}>{text}</p>}
48
+ <div style={{ display: 'flex', gap: 8 }}>
49
+ {node?.attributes?.linkedWordFirstLocalizationKey && (
50
+ <span style={linkStyle(node?.attributes?.linkedWordFirstColor)}>
51
+ {t(node?.attributes?.linkedWordFirstLocalizationKey)}
52
+ </span>
53
+ )}
54
+ {node?.attributes?.linkedWordSecondLocalizationKey && (
55
+ <span style={linkStyle(node?.attributes?.linkedWordSecondColor)}>
56
+ {t(node?.attributes?.linkedWordSecondLocalizationKey)}
57
+ </span>
58
+ )}
59
+ </div>
60
+ </div>
61
+ );
6
62
  }
7
63
 
8
64
  export default React.memo(OnboardFooter);
@@ -22,6 +22,14 @@ export interface OnboardFooterPropsGenerated {
22
22
  borderRadius?: number;
23
23
  width?: number;
24
24
  height?: number;
25
+ textLocalizationKey?: string;
26
+ textColor?: string;
27
+ linkedWordFirstLocalizationKey?: string;
28
+ linkedWordFirstColor?: string;
29
+ linkedWordFirstPage?: string;
30
+ linkedWordSecondLocalizationKey?: string;
31
+ linkedWordSecondColor?: string;
32
+ linkedWordSecondPage?: string;
25
33
  };
26
34
  }
27
35
 
@@ -22,7 +22,15 @@
22
22
  "backgroundColor": "string",
23
23
  "borderRadius": "number",
24
24
  "width": "number",
25
- "height": "number"
25
+ "height": "number",
26
+ "textLocalizationKey": "string",
27
+ "textColor": "string",
28
+ "linkedWordFirstLocalizationKey": "string",
29
+ "linkedWordFirstColor": "string",
30
+ "linkedWordFirstPage": "string",
31
+ "linkedWordSecondLocalizationKey": "string",
32
+ "linkedWordSecondColor": "string",
33
+ "linkedWordSecondPage": "string"
26
34
  }
27
35
  }
28
36
  }
@@ -112,3 +112,17 @@
112
112
  .embla__dot--selected:after {
113
113
  box-shadow: inset 0 0 0 0.2rem var(--text-body);
114
114
  }
115
+ .carousel-provider {
116
+ height: 100%;
117
+ }
118
+ .embla {
119
+ height: 100%;
120
+ }
121
+ .embla__viewport {
122
+ height: 100%;
123
+ display: flex;
124
+ flex-direction: column;
125
+ }
126
+ .embla__container {
127
+ flex: 1;
128
+ }
package/src/types/Node.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  export type NodeDefaultAttribute = Record<
2
2
  string,
3
- boolean | string | number | null | undefined | string[]
3
+ | boolean
4
+ | string
5
+ | number
6
+ | null
7
+ | undefined
8
+ | string[]
9
+ | Record<string, unknown>
10
+ | Array<Record<string, unknown>>
4
11
  >;
5
12
 
6
13
  export type Node<T = NodeDefaultAttribute> =