@developer_tribe/react-builder 0.1.17 → 0.1.19

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.
@@ -1,4 +1,4 @@
1
- export type NodeDefaultAttribute = Record<string, boolean | string | number | null | undefined | string[]>;
1
+ export type NodeDefaultAttribute = Record<string, boolean | string | number | null | undefined | string[] | Record<string, unknown> | Array<Record<string, unknown>>>;
2
2
  export type Node<T = NodeDefaultAttribute> = null | undefined | string | {} | Node<T>[] | NodeData<T>;
3
3
  export interface NodeData<T = Record<string, unknown>> {
4
4
  type: string;
@@ -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.17",
3
+ "version": "0.1.19",
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,21 +247,50 @@ function Field({
56
247
  />
57
248
  );
58
249
  }
250
+ // Legacy support: string[]
59
251
  if (type === 'string[]') {
60
- const textValue = Array.isArray(value) ? value.join('\n') : '';
252
+ const arr: string[] = Array.isArray(value) ? value : [];
61
253
  return (
62
- <textarea
63
- rows={4}
64
- value={textValue}
65
- onChange={(e) => {
66
- const lines = e.target.value
67
- .split('\n')
68
- .map((s) => s.trim())
69
- .filter((s) => s.length > 0);
70
- onChange(lines.length > 0 ? lines : undefined);
71
- }}
72
- className="input"
73
- />
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>
74
294
  );
75
295
  }
76
296
  return (
@@ -114,6 +334,7 @@ export function AttributesEditor({ node, onChange }: AttributesEditorProps) {
114
334
  };
115
335
  onChange(next);
116
336
  }}
337
+ componentType={data?.type}
117
338
  />
118
339
  </React.Fragment>
119
340
  ))}
@@ -2,15 +2,27 @@
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: {
8
14
  labelKey?: string;
9
15
  button_text_color?: string;
16
+ animation?:
17
+ | 'simple-animation'
18
+ | 'line-animation'
19
+ | 'blur'
20
+ | 'blur-animation'
21
+ | 'blur-line-animation';
10
22
  button_background_color?: string;
11
23
  flex?: number;
12
24
  targetIndex?: number;
13
- events?: string;
25
+ events?: EventObjectGenerated[];
14
26
  };
15
27
  }
16
28
 
@@ -7,10 +7,24 @@
7
7
  "attributes": {
8
8
  "labelKey": "string",
9
9
  "button_text_color": "string",
10
+ "animation": [
11
+ "simple-animation",
12
+ "line-animation",
13
+ "blur",
14
+ "blur-animation",
15
+ "blur-line-animation"
16
+ ],
10
17
  "button_background_color": "string",
11
18
  "flex": "number",
12
19
  "targetIndex": "number",
13
- "events": "string"
20
+ "events": "EventObject[]"
21
+ }
22
+ },
23
+ "types": {
24
+ "EventObject": {
25
+ "type": ["Permission", "Navigate"],
26
+ "permission": "string",
27
+ "next_page_key": "string"
14
28
  }
15
29
  }
16
30
  }
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> =
@@ -129,6 +129,13 @@ function buildCarouselItem(
129
129
  const labelKey = attrs?.button_text_localization_key || '';
130
130
  const buttonTextColor = attrs?.button_text_color;
131
131
  const buttonBackgroundColor = attrs?.button_background_color;
132
+ const animation = attrs?.animation as
133
+ | 'simple-animation'
134
+ | 'line-animation'
135
+ | 'blur'
136
+ | 'blur-animation'
137
+ | 'blur-line-animation'
138
+ | undefined;
132
139
  const flex = attrs?.flex ? Number(attrs.flex) : undefined;
133
140
 
134
141
  // Find first Navigate event and map to target index
@@ -171,6 +178,7 @@ function buildCarouselItem(
171
178
  ...(typeof buttonTextColor === 'string'
172
179
  ? { button_text_color: buttonTextColor }
173
180
  : {}),
181
+ ...(typeof animation === 'string' ? { animation } : {}),
174
182
  ...(typeof buttonBackgroundColor === 'string'
175
183
  ? { button_background_color: buttonBackgroundColor }
176
184
  : {}),
@@ -26,6 +26,10 @@ type Pattern = {
26
26
  children: unknown;
27
27
  attributes: Record<string, string | string[]>;
28
28
  };
29
+ // Optional custom complex types referenced by attributes like "X[]" or "X"
30
+ // Each entry maps a type name (e.g., "EventObject") to its field schema
31
+ // where the inner record maps fieldName -> primitive type or enum options
32
+ types?: Record<string, Record<string, string | string[]>>;
29
33
  };
30
34
 
31
35
  const patterns: Pattern[] = [
@@ -61,3 +65,35 @@ export function getAttributeSchema(
61
65
  const p = getPatternByType(type);
62
66
  return p?.pattern.attributes;
63
67
  }
68
+
69
+ /**
70
+ * Returns the schema of a custom complex type declared under a component pattern's `types` block.
71
+ * For example, OnboardButton.pattern.types.EventObject
72
+ */
73
+ export function getTypeSchema(
74
+ componentType?: string | null,
75
+ typeName?: string | null,
76
+ ): Record<string, string | string[]> | undefined {
77
+ if (!componentType || !typeName) return undefined;
78
+ const p = getPatternByType(componentType);
79
+ // Some JSON imports may not type-check "types" so we fallback to any access
80
+ const types: Record<string, Record<string, string | string[]>> | undefined = (
81
+ p as unknown as {
82
+ types?: Record<string, Record<string, string | string[]>>;
83
+ }
84
+ )?.types;
85
+ return types?.[typeName];
86
+ }
87
+
88
+ /** Utility: returns true if the type name refers to a primitive scalar */
89
+ export function isPrimitiveType(typeName: string): boolean {
90
+ return (
91
+ typeName === 'string' || typeName === 'number' || typeName === 'boolean'
92
+ );
93
+ }
94
+
95
+ /** Utility: parse `X[]` forms and return the item type if present */
96
+ export function getArrayItemType(typeName: string): string | null {
97
+ if (typeof typeName !== 'string') return null;
98
+ return typeName.endsWith('[]') ? typeName.slice(0, -2) : null;
99
+ }