@developer_tribe/react-builder 0.1.32 → 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 (218) hide show
  1. package/dist/DeviceMockFrame.d.ts +2 -17
  2. package/dist/RenderPage.d.ts +4 -11
  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/index.d.ts +1 -0
  31. package/dist/build-components/patterns.generated.d.ts +4855 -132
  32. package/dist/components/AttributesEditorPanel.d.ts +9 -0
  33. package/dist/components/Breadcrumb.d.ts +15 -0
  34. package/dist/components/Builder.d.ts +9 -0
  35. package/dist/components/Checkbox.d.ts +17 -0
  36. package/dist/components/DeviceButton.d.ts +8 -0
  37. package/dist/components/DeviceNavigationBar.d.ts +10 -0
  38. package/dist/components/DeviceStatusBar.d.ts +9 -0
  39. package/dist/components/EditorHeader.d.ts +10 -0
  40. package/dist/index.cjs.js +6 -5
  41. package/dist/index.cjs.js.map +1 -0
  42. package/dist/index.d.ts +8 -4
  43. package/dist/index.esm.js +6 -5
  44. package/dist/index.esm.js.map +1 -0
  45. package/dist/mockOS/components/MockLaunchScreenComponent.d.ts +6 -0
  46. package/dist/mockOS/components/MockOSRouter.d.ts +8 -0
  47. package/dist/mockOS/components/PermissionModal.d.ts +9 -0
  48. package/dist/mockOS/context/MockOSContext.d.ts +36 -0
  49. package/dist/mockOS/hooks/useMockNavigation.d.ts +3 -0
  50. package/dist/mockOS/hooks/useMockPermission.d.ts +3 -0
  51. package/dist/mockOS/index.d.ts +9 -0
  52. package/dist/mockOS/managers/mockPermissionManager.d.ts +10 -0
  53. package/dist/mockOS/managers/navigationManager.d.ts +17 -0
  54. package/dist/modals/AddComponentModal.d.ts +8 -0
  55. package/dist/modals/ColorModal.d.ts +9 -0
  56. package/dist/modals/DeviceSelectorModal.d.ts +9 -0
  57. package/dist/modals/LocalicationModal.d.ts +8 -0
  58. package/dist/modals/Modal.d.ts +12 -0
  59. package/dist/modals/index.d.ts +5 -0
  60. package/dist/pages/ProjectPage.d.ts +11 -0
  61. package/dist/pages/tabs/BuilderTab.d.ts +9 -0
  62. package/dist/pages/tabs/DebugTab.d.ts +7 -0
  63. package/dist/pages/tabs/PreviewTab.d.ts +3 -0
  64. package/dist/store.d.ts +15 -18
  65. package/dist/styles.css +1 -1
  66. package/dist/types/PreviewConfig.d.ts +6 -3
  67. package/dist/types/Project.d.ts +12 -2
  68. package/dist/utils/copyNode.d.ts +2 -0
  69. package/dist/utils/logger.d.ts +11 -0
  70. package/dist/utils/patterns.d.ts +24 -0
  71. package/dist/utils/useLogRender.d.ts +1 -0
  72. package/package.json +17 -9
  73. package/scripts/prebuild/utils/createBuildComponentsIndex.js +15 -1
  74. package/scripts/prebuild/utils/createGeneratedProps.js +11 -3
  75. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +45 -6
  76. package/scripts/prebuild/utils/validatePatternJson.js +13 -5
  77. package/src/AttributesEditor.tsx +434 -311
  78. package/src/DeviceMockFrame.tsx +42 -67
  79. package/src/RenderPage.tsx +8 -44
  80. package/src/assets/images/android.svg +43 -0
  81. package/src/assets/images/apple.svg +16 -0
  82. package/src/assets/images/background.jpg +0 -0
  83. package/src/assets/samples/carousel-sample.json +2 -3
  84. package/src/assets/samples/getSamples.ts +49 -12
  85. package/src/assets/samples/simple-1.json +1 -2
  86. package/src/assets/samples/simple-2.json +1 -2
  87. package/src/assets/samples/vpn-onboard-1.json +1 -2
  88. package/src/assets/samples/vpn-onboard-2.json +1 -2
  89. package/src/assets/samples/vpn-onboard-3.json +1 -2
  90. package/src/assets/samples/vpn-onboard-4.json +1 -2
  91. package/src/assets/samples/vpn-onboard-5.json +1 -2
  92. package/src/assets/samples/vpn-onboard-6.json +1 -2
  93. package/src/attributes-editor/Field.tsx +662 -0
  94. package/src/attributes-editor/FieldInfoTooltip.tsx +49 -0
  95. package/src/attributes-editor/LayoutPreviewPicker.tsx +199 -0
  96. package/src/attributes-editor/SpecialCategorySection.tsx +284 -0
  97. package/src/attributes-editor/types.ts +30 -0
  98. package/src/build-components/Button/Button.tsx +12 -2
  99. package/src/build-components/Button/ButtonProps.generated.ts +37 -1
  100. package/src/build-components/Button/pattern.json +31 -2
  101. package/src/build-components/Carousel/Carousel.tsx +17 -2
  102. package/src/build-components/Carousel/CarouselProps.generated.ts +39 -1
  103. package/src/build-components/Carousel/pattern.json +10 -0
  104. package/src/build-components/CarouselButtons/CarouselButtons.tsx +8 -2
  105. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +36 -0
  106. package/src/build-components/CarouselButtons/pattern.json +22 -0
  107. package/src/build-components/CarouselDots/CarouselDots.tsx +42 -8
  108. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +36 -0
  109. package/src/build-components/CarouselDots/pattern.json +15 -0
  110. package/src/build-components/CarouselItem/CarouselItem.tsx +7 -2
  111. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +39 -1
  112. package/src/build-components/CarouselItem/pattern.json +7 -0
  113. package/src/build-components/CarouselProvider/CarouselProvider.tsx +10 -2
  114. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +39 -1
  115. package/src/build-components/CarouselProvider/pattern.json +7 -0
  116. package/src/build-components/Image/Image.tsx +10 -2
  117. package/src/build-components/Image/ImageProps.generated.ts +36 -3
  118. package/src/build-components/Image/pattern.json +46 -3
  119. package/src/build-components/Onboard/Onboard.tsx +8 -1
  120. package/src/build-components/Onboard/OnboardProps.generated.ts +39 -1
  121. package/src/build-components/Onboard/pattern.json +11 -0
  122. package/src/build-components/OnboardButton/OnboardButton.tsx +53 -9
  123. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +36 -0
  124. package/src/build-components/OnboardButton/pattern.json +71 -5
  125. package/src/build-components/OnboardButtons/OnboardButtons.tsx +27 -17
  126. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +36 -0
  127. package/src/build-components/OnboardButtons/pattern.json +70 -4
  128. package/src/build-components/OnboardDot/OnboardDot.tsx +106 -4
  129. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +22 -0
  130. package/src/build-components/OnboardDot/pattern.json +54 -1
  131. package/src/build-components/OnboardFooter/OnboardFooter.tsx +14 -6
  132. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +4 -5
  133. package/src/build-components/OnboardFooter/pattern.json +58 -2
  134. package/src/build-components/OnboardImage/OnboardImage.tsx +29 -5
  135. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +36 -3
  136. package/src/build-components/OnboardImage/pattern.json +21 -0
  137. package/src/build-components/OnboardItem/OnboardItem.tsx +8 -1
  138. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +35 -3
  139. package/src/build-components/OnboardItem/pattern.json +38 -2
  140. package/src/build-components/OnboardProvider/OnboardProvider.tsx +22 -8
  141. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +37 -4
  142. package/src/build-components/OnboardProvider/pattern.json +51 -4
  143. package/src/build-components/OnboardSubtitle/OnboardSubtitle.tsx +2 -0
  144. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +4 -5
  145. package/src/build-components/OnboardSubtitle/pattern.json +6 -0
  146. package/src/build-components/OnboardTitle/OnboardTitle.tsx +2 -0
  147. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +4 -5
  148. package/src/build-components/OnboardTitle/pattern.json +6 -0
  149. package/src/build-components/Text/Text.tsx +12 -6
  150. package/src/build-components/Text/TextProps.generated.ts +4 -5
  151. package/src/build-components/Text/pattern.json +38 -2
  152. package/src/build-components/View/View.tsx +11 -6
  153. package/src/build-components/View/ViewProps.generated.ts +3 -4
  154. package/src/build-components/View/pattern.json +227 -19
  155. package/src/build-components/index.ts +22 -0
  156. package/src/build-components/patterns.generated.ts +4905 -139
  157. package/src/components/AttributesEditorPanel.tsx +58 -0
  158. package/src/components/Breadcrumb.tsx +80 -0
  159. package/src/components/Builder.tsx +375 -0
  160. package/src/components/Checkbox.tsx +81 -0
  161. package/src/components/DeviceButton.tsx +39 -0
  162. package/src/components/DeviceNavigationBar.tsx +201 -0
  163. package/src/components/DeviceStatusBar.tsx +85 -0
  164. package/src/components/EditorHeader.tsx +138 -0
  165. package/src/index.ts +8 -4
  166. package/src/mockOS/components/MockLaunchScreenComponent.tsx +43 -0
  167. package/src/mockOS/components/MockOSRouter.tsx +115 -0
  168. package/src/mockOS/components/PermissionModal.tsx +270 -0
  169. package/src/mockOS/context/MockOSContext.tsx +179 -0
  170. package/src/mockOS/hooks/useMockNavigation.ts +11 -0
  171. package/src/mockOS/hooks/useMockPermission.ts +11 -0
  172. package/src/mockOS/index.ts +26 -0
  173. package/src/mockOS/managers/mockPermissionManager.ts +54 -0
  174. package/src/mockOS/managers/navigationManager.ts +91 -0
  175. package/src/modals/AddComponentModal.tsx +313 -0
  176. package/src/modals/ColorModal.tsx +268 -0
  177. package/src/modals/DeviceSelectorModal.tsx +57 -0
  178. package/src/modals/LocalicationModal.tsx +54 -0
  179. package/src/modals/Modal.tsx +57 -0
  180. package/src/modals/index.ts +5 -0
  181. package/src/pages/ProjectPage.tsx +150 -0
  182. package/src/pages/tabs/BuilderTab.tsx +33 -0
  183. package/src/pages/tabs/DebugTab.tsx +64 -0
  184. package/src/pages/tabs/PreviewTab.tsx +206 -0
  185. package/src/size-matters/index.ts +25 -5
  186. package/src/store.ts +56 -38
  187. package/src/styles/base/_global.scss +253 -0
  188. package/src/styles/components/_attributes-editor.scss +261 -0
  189. package/src/styles/components/_editor-shell.scss +189 -0
  190. package/src/styles/components/_mockos-router.scss +140 -0
  191. package/src/styles/components/_ui-components.scss +183 -0
  192. package/src/styles/foundation/_colors.scss +8 -0
  193. package/src/styles/foundation/_mixins.scss +22 -0
  194. package/src/styles/{_reset.scss → foundation/_reset.scss} +5 -2
  195. package/src/styles/foundation/_sizes.scss +37 -0
  196. package/src/styles/foundation/_typography.scss +4 -0
  197. package/src/styles/foundation/_variables.scss +3 -0
  198. package/src/styles/index.scss +22 -129
  199. package/src/styles/layout/_builder.scss +68 -0
  200. package/src/styles/layout/_pages.scss +3 -0
  201. package/src/styles/modals/_add-component.scss +122 -0
  202. package/src/styles/modals/_color-modal.scss +130 -0
  203. package/src/styles/modals/_device-selector.scss +18 -0
  204. package/src/styles/modals/_localication-modal.scss +68 -0
  205. package/src/styles/modals/_modal-shell.scss +46 -0
  206. package/src/styles/utilities/_carousel.scss +125 -0
  207. package/src/types/PreviewConfig.ts +14 -5
  208. package/src/types/Project.ts +15 -2
  209. package/src/types/images.d.ts +8 -0
  210. package/src/utils/copyNode.ts +7 -0
  211. package/src/utils/extractTextStyle.ts +8 -4
  212. package/src/utils/extractViewStyle.ts +51 -7
  213. package/src/utils/getDevices.ts +1 -0
  214. package/src/utils/logger.ts +76 -0
  215. package/src/utils/patterns.ts +33 -0
  216. package/src/utils/useLogRender.ts +13 -0
  217. package/dist/build-components/OnboardDot/OnboardExpandingDotProps.generated.d.ts +0 -10
  218. package/src/build-components/OnboardDot/OnboardExpandingDotProps.generated.ts +0 -20
@@ -0,0 +1,662 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import type { ViewPropsGenerated } from '../build-components/View/ViewProps.generated';
3
+ import { ColorModal } from '../modals/ColorModal';
4
+ import {
5
+ getArrayItemType,
6
+ getTypeSchema,
7
+ isPrimitiveType,
8
+ } from '../utils/patterns';
9
+ import { Checkbox } from '../components/Checkbox';
10
+ import { LayoutPreviewPicker } from './LayoutPreviewPicker';
11
+ import { LayoutContext, LayoutFieldName, isBooleanFieldType } from './types';
12
+
13
+ export type FieldProps = {
14
+ name: string;
15
+ type: string | string[];
16
+ value: any;
17
+ onChange: (v: any) => void;
18
+ componentType?: string;
19
+ projectColors?: string[];
20
+ layoutContext?: LayoutContext;
21
+ viewAttributes?: Partial<ViewPropsGenerated['attributes']>;
22
+ label?: React.ReactNode;
23
+ preferredScale?: string;
24
+ };
25
+
26
+ const layoutFieldNames: LayoutFieldName[] = [
27
+ 'flexDirection',
28
+ 'alignItems',
29
+ 'justifyContent',
30
+ ];
31
+
32
+ function isLayoutField(name: string): name is LayoutFieldName {
33
+ return layoutFieldNames.includes(name as LayoutFieldName);
34
+ }
35
+
36
+ export function Field({
37
+ name,
38
+ type,
39
+ value,
40
+ onChange,
41
+ componentType,
42
+ projectColors,
43
+ layoutContext,
44
+ viewAttributes,
45
+ label,
46
+ preferredScale,
47
+ }: FieldProps) {
48
+ if (Array.isArray(type)) {
49
+ if (isLayoutField(name)) {
50
+ const enumOptions = type.filter(
51
+ (opt): opt is string => typeof opt === 'string' && opt.length > 0,
52
+ );
53
+ return (
54
+ <LayoutPreviewPicker
55
+ mode={name}
56
+ options={enumOptions}
57
+ value={typeof value === 'string' ? value : undefined}
58
+ onChange={onChange}
59
+ layoutContext={layoutContext}
60
+ viewAttributes={viewAttributes}
61
+ />
62
+ );
63
+ }
64
+ return (
65
+ <select
66
+ value={value ?? ''}
67
+ onChange={(e) => onChange(e.target.value)}
68
+ className="input"
69
+ >
70
+ <option value="">(none)</option>
71
+ {type.map((opt) => (
72
+ <option key={opt} value={opt}>
73
+ {opt}
74
+ </option>
75
+ ))}
76
+ </select>
77
+ );
78
+ }
79
+
80
+ const itemType = typeof type === 'string' ? getArrayItemType(type) : null;
81
+ if (itemType) {
82
+ const arr: any[] = Array.isArray(value) ? value : [];
83
+
84
+ if (isPrimitiveType(itemType)) {
85
+ const renderPrimitiveArrayItem = (itemValue: any, idx: number) => {
86
+ switch (itemType) {
87
+ case 'number':
88
+ return (
89
+ <input
90
+ type="number"
91
+ value={itemValue ?? ''}
92
+ onChange={(e) => {
93
+ const next = [...arr];
94
+ next[idx] =
95
+ e.target.value === '' ? undefined : Number(e.target.value);
96
+ onChange(next);
97
+ }}
98
+ className="input"
99
+ style={{ flex: 1 }}
100
+ />
101
+ );
102
+ case 'boolean':
103
+ case 'bool':
104
+ return (
105
+ <Checkbox
106
+ checked={Boolean(itemValue)}
107
+ onChange={(checked) => {
108
+ const next = [...arr];
109
+ next[idx] = checked;
110
+ onChange(next);
111
+ }}
112
+ name={`${name}-${idx}`}
113
+ />
114
+ );
115
+ case 'string':
116
+ return (
117
+ <input
118
+ type="text"
119
+ value={itemValue ?? ''}
120
+ onChange={(e) => {
121
+ const next = [...arr];
122
+ next[idx] =
123
+ e.target.value === '' ? undefined : e.target.value;
124
+ onChange(next);
125
+ }}
126
+ className="input"
127
+ style={{ flex: 1 }}
128
+ />
129
+ );
130
+ case 'color':
131
+ return (
132
+ <ColorPickerButton
133
+ value={typeof itemValue === 'string' ? itemValue : undefined}
134
+ onChange={(hex) => {
135
+ const next = [...arr];
136
+ next[idx] = hex;
137
+ onChange(next);
138
+ }}
139
+ projectColors={projectColors}
140
+ />
141
+ );
142
+ default:
143
+ return <p>---not-implemented----</p>;
144
+ }
145
+ };
146
+
147
+ return (
148
+ <div style={{ display: 'grid', gap: 8 }}>
149
+ {arr.map((item, idx) => (
150
+ <div
151
+ key={idx}
152
+ style={{ display: 'flex', gap: 8, alignItems: 'center' }}
153
+ >
154
+ {renderPrimitiveArrayItem(item, idx)}
155
+ <button
156
+ type="button"
157
+ onClick={() => {
158
+ const next = arr.filter((_, i) => i !== idx);
159
+ onChange(next.length ? next : undefined);
160
+ }}
161
+ >
162
+ remove
163
+ </button>
164
+ </div>
165
+ ))}
166
+ <div>
167
+ <button
168
+ type="button"
169
+ onClick={() => {
170
+ const next = [
171
+ ...arr,
172
+ itemType === 'boolean'
173
+ ? false
174
+ : itemType === 'number'
175
+ ? 0
176
+ : '',
177
+ ];
178
+ onChange(next);
179
+ }}
180
+ >
181
+ add
182
+ </button>
183
+ </div>
184
+ </div>
185
+ );
186
+ }
187
+
188
+ const schema = getTypeSchema(componentType, itemType) ?? {};
189
+ return (
190
+ <div style={{ display: 'grid', gap: 8 }}>
191
+ {arr.map((item, idx) => {
192
+ const obj = (item ?? {}) as Record<string, unknown>;
193
+ return (
194
+ <div
195
+ key={idx}
196
+ style={{ border: '1px solid #ddd', borderRadius: 6, padding: 8 }}
197
+ >
198
+ <div
199
+ style={{
200
+ display: 'grid',
201
+ gridTemplateColumns: '1fr 1fr',
202
+ gap: 8,
203
+ }}
204
+ >
205
+ {Object.entries(schema).map(([fieldName, fieldType]) => {
206
+ if (isBooleanFieldType(fieldType)) {
207
+ return (
208
+ <div key={fieldName} style={{ gridColumn: '1 / -1' }}>
209
+ <Field
210
+ name={fieldName}
211
+ type={fieldType}
212
+ value={obj?.[fieldName as keyof typeof obj]}
213
+ onChange={(val) => {
214
+ const next = [...arr];
215
+ const nextObj = {
216
+ ...(obj ?? {}),
217
+ [fieldName]: val,
218
+ };
219
+ next[idx] = nextObj;
220
+ onChange(next);
221
+ }}
222
+ componentType={componentType}
223
+ projectColors={projectColors}
224
+ layoutContext={layoutContext}
225
+ viewAttributes={viewAttributes}
226
+ label={fieldName}
227
+ />
228
+ </div>
229
+ );
230
+ }
231
+ return (
232
+ <React.Fragment key={fieldName}>
233
+ <div style={{ alignSelf: 'center' }}>{fieldName}</div>
234
+ <Field
235
+ name={fieldName}
236
+ type={fieldType}
237
+ value={obj?.[fieldName as keyof typeof obj]}
238
+ onChange={(val) => {
239
+ const next = [...arr];
240
+ const nextObj = { ...(obj ?? {}), [fieldName]: val };
241
+ next[idx] = nextObj;
242
+ onChange(next);
243
+ }}
244
+ componentType={componentType}
245
+ projectColors={projectColors}
246
+ layoutContext={layoutContext}
247
+ viewAttributes={viewAttributes}
248
+ />
249
+ </React.Fragment>
250
+ );
251
+ })}
252
+ </div>
253
+ <div style={{ marginTop: 8 }}>
254
+ <button
255
+ type="button"
256
+ onClick={() => {
257
+ const next = arr.filter((_, i) => i !== idx);
258
+ onChange(next.length ? next : undefined);
259
+ }}
260
+ >
261
+ remove
262
+ </button>
263
+ </div>
264
+ </div>
265
+ );
266
+ })}
267
+ <div>
268
+ <button
269
+ type="button"
270
+ onClick={() => {
271
+ const empty: Record<string, unknown> = {};
272
+ const next = [...arr, empty];
273
+ onChange(next);
274
+ }}
275
+ >
276
+ add
277
+ </button>
278
+ </div>
279
+ </div>
280
+ );
281
+ }
282
+
283
+ if (typeof type === 'string' && !isPrimitiveType(type)) {
284
+ const schema = getTypeSchema(componentType, type);
285
+ if (schema) {
286
+ const obj = (value ?? {}) as Record<string, unknown>;
287
+ return (
288
+ <div
289
+ style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}
290
+ >
291
+ {Object.entries(schema).map(([fieldName, fieldType]) => {
292
+ if (isBooleanFieldType(fieldType)) {
293
+ return (
294
+ <div key={fieldName} style={{ gridColumn: '1 / -1' }}>
295
+ <Field
296
+ name={fieldName}
297
+ type={fieldType}
298
+ value={obj?.[fieldName as keyof typeof obj]}
299
+ onChange={(val) => {
300
+ const nextObj = { ...(obj ?? {}), [fieldName]: val };
301
+ onChange(nextObj);
302
+ }}
303
+ componentType={componentType}
304
+ projectColors={projectColors}
305
+ layoutContext={layoutContext}
306
+ viewAttributes={viewAttributes}
307
+ label={fieldName}
308
+ />
309
+ </div>
310
+ );
311
+ }
312
+ return (
313
+ <React.Fragment key={fieldName}>
314
+ <div style={{ alignSelf: 'center' }}>{fieldName}</div>
315
+ <Field
316
+ name={fieldName}
317
+ type={fieldType}
318
+ value={obj?.[fieldName as keyof typeof obj]}
319
+ onChange={(val) => {
320
+ const nextObj = { ...(obj ?? {}), [fieldName]: val };
321
+ onChange(nextObj);
322
+ }}
323
+ componentType={componentType}
324
+ projectColors={projectColors}
325
+ layoutContext={layoutContext}
326
+ viewAttributes={viewAttributes}
327
+ />
328
+ </React.Fragment>
329
+ );
330
+ })}
331
+ </div>
332
+ );
333
+ }
334
+ }
335
+
336
+ if (type === 'string[]') {
337
+ const arr: string[] = Array.isArray(value) ? value : [];
338
+ return (
339
+ <div style={{ display: 'grid', gap: 8 }}>
340
+ {arr.map((item, idx) => (
341
+ <div
342
+ key={idx}
343
+ style={{ display: 'flex', gap: 8, alignItems: 'center' }}
344
+ >
345
+ <input
346
+ type="text"
347
+ value={item ?? ''}
348
+ onChange={(e) => {
349
+ const next = [...arr];
350
+ next[idx] = e.target.value;
351
+ onChange(next);
352
+ }}
353
+ className="input"
354
+ style={{ flex: 1 }}
355
+ />
356
+ <button
357
+ type="button"
358
+ onClick={() => {
359
+ const next = arr.filter((_, i) => i !== idx);
360
+ onChange(next.length ? next : undefined);
361
+ }}
362
+ >
363
+ remove
364
+ </button>
365
+ </div>
366
+ ))}
367
+ <div>
368
+ <button
369
+ type="button"
370
+ onClick={() => {
371
+ const next = [...arr, ''];
372
+ onChange(next);
373
+ }}
374
+ >
375
+ add
376
+ </button>
377
+ </div>
378
+ </div>
379
+ );
380
+ }
381
+
382
+ if (typeof type !== 'string') {
383
+ return <p>---not-implemented----</p>;
384
+ }
385
+
386
+ switch (type) {
387
+ case 'size':
388
+ return (
389
+ <SizeField
390
+ value={value}
391
+ onChange={onChange}
392
+ preferredScale={preferredScale}
393
+ fieldName={name}
394
+ />
395
+ );
396
+ case 'number':
397
+ return (
398
+ <input
399
+ type="number"
400
+ value={value ?? ''}
401
+ onChange={(e) =>
402
+ onChange(e.target.value === '' ? undefined : Number(e.target.value))
403
+ }
404
+ className="input"
405
+ />
406
+ );
407
+ case 'boolean':
408
+ case 'bool':
409
+ return (
410
+ <Checkbox
411
+ label={label ?? name}
412
+ checked={Boolean(value)}
413
+ onChange={(checked) => onChange(checked)}
414
+ name={name}
415
+ />
416
+ );
417
+ case 'string':
418
+ return (
419
+ <input
420
+ type="text"
421
+ value={value ?? ''}
422
+ onChange={(e) =>
423
+ onChange(e.target.value === '' ? undefined : e.target.value)
424
+ }
425
+ className="input"
426
+ />
427
+ );
428
+ case 'color':
429
+ return (
430
+ <ColorPickerButton
431
+ value={typeof value === 'string' ? value : undefined}
432
+ onChange={(hex) => onChange(hex)}
433
+ projectColors={projectColors}
434
+ />
435
+ );
436
+ default:
437
+ return <p>---not-implemented----</p>;
438
+ }
439
+ }
440
+
441
+ type ColorPickerButtonProps = {
442
+ value?: string;
443
+ onChange: (color?: string) => void;
444
+ projectColors?: string[];
445
+ };
446
+
447
+ function ColorPickerButton({
448
+ value,
449
+ onChange,
450
+ projectColors = [],
451
+ }: ColorPickerButtonProps) {
452
+ const [isOpen, setIsOpen] = useState(false);
453
+ const normalizedValue = typeof value === 'string' ? value : undefined;
454
+
455
+ return (
456
+ <>
457
+ <button
458
+ type="button"
459
+ onClick={() => setIsOpen(true)}
460
+ style={{
461
+ width: '100%',
462
+ display: 'flex',
463
+ alignItems: 'center',
464
+ justifyContent: 'space-between',
465
+ gap: 8,
466
+ borderRadius: 6,
467
+ border: '1px solid #ddd',
468
+ padding: '8px 10px',
469
+ background: '#fff',
470
+ cursor: 'pointer',
471
+ }}
472
+ >
473
+ <span
474
+ aria-hidden
475
+ style={{
476
+ width: 32,
477
+ height: 32,
478
+ borderRadius: 6,
479
+ border: '1px solid rgba(0,0,0,0.1)',
480
+ background: normalizedValue ?? 'transparent',
481
+ }}
482
+ />
483
+ <span style={{ flex: 1, textAlign: 'left', fontWeight: 500 }}>
484
+ {normalizedValue ?? 'Select color'}
485
+ </span>
486
+ <span style={{ fontSize: 12, color: '#666' }}>Open</span>
487
+ </button>
488
+ {isOpen ? (
489
+ <ColorModal
490
+ value={normalizedValue}
491
+ projectColors={projectColors}
492
+ onSelect={(hex) => {
493
+ onChange(hex);
494
+ setIsOpen(false);
495
+ }}
496
+ onClose={() => setIsOpen(false)}
497
+ onClear={() => {
498
+ onChange(undefined);
499
+ setIsOpen(false);
500
+ }}
501
+ />
502
+ ) : null}
503
+ </>
504
+ );
505
+ }
506
+
507
+ type SizeUnit = '' | 'vs' | 's' | 'f' | '%';
508
+
509
+ function parseSizeValue(value: unknown): { amount: string; unit: SizeUnit } {
510
+ const empty = { amount: '', unit: '' as SizeUnit };
511
+ if (typeof value === 'number' && Number.isFinite(value)) {
512
+ return { amount: String(value), unit: '' };
513
+ }
514
+ if (typeof value !== 'string') return empty;
515
+ const trimmed = value.trim();
516
+ if (!trimmed) return empty;
517
+ if (trimmed.endsWith('%')) {
518
+ return { amount: trimmed.slice(0, -1), unit: '%' };
519
+ }
520
+ const lower = trimmed.toLowerCase();
521
+ if (lower.endsWith('@vs'))
522
+ return { amount: trimmed.slice(0, -3), unit: 'vs' };
523
+ if (lower.endsWith('vs')) return { amount: trimmed.slice(0, -2), unit: 'vs' };
524
+ if (lower.endsWith('@fs')) return { amount: trimmed.slice(0, -3), unit: 'f' };
525
+ if (lower.endsWith('@f')) return { amount: trimmed.slice(0, -2), unit: 'f' };
526
+ if (lower.endsWith('fs')) return { amount: trimmed.slice(0, -2), unit: 'f' };
527
+ if (lower.endsWith('f')) return { amount: trimmed.slice(0, -1), unit: 'f' };
528
+ if (lower.endsWith('@s')) return { amount: trimmed.slice(0, -2), unit: 's' };
529
+ if (lower.endsWith('s')) return { amount: trimmed.slice(0, -1), unit: 's' };
530
+ if (lower.endsWith('px')) return { amount: trimmed.slice(0, -2), unit: '' };
531
+ return { amount: trimmed, unit: '' };
532
+ }
533
+
534
+ function composeSizeValue(amount: string, unit: SizeUnit): string | number {
535
+ const trimmed = amount.trim();
536
+ if (unit === '%') {
537
+ return `${trimmed}%`;
538
+ }
539
+ if (unit === '') {
540
+ const numeric = Number(trimmed);
541
+ return Number.isFinite(numeric) ? numeric : trimmed;
542
+ }
543
+ if (unit === 'f') {
544
+ return `${trimmed}@fs`;
545
+ }
546
+ return `${trimmed}@${unit}`;
547
+ }
548
+
549
+ type SizeFieldProps = {
550
+ value: unknown;
551
+ onChange: (val: unknown) => void;
552
+ preferredScale?: string;
553
+ fieldName: string;
554
+ };
555
+
556
+ function normalizePreferredScale(
557
+ preferredScale: string | undefined,
558
+ fieldName: string,
559
+ ): SizeUnit {
560
+ const fallbackName = fieldName.trim().toLowerCase();
561
+ const fallback: SizeUnit =
562
+ fallbackName.includes('height') ||
563
+ fallbackName.includes('top') ||
564
+ fallbackName.includes('vertical')
565
+ ? 'vs'
566
+ : 's';
567
+ if (typeof preferredScale !== 'string') return fallback;
568
+ const normalized = preferredScale.trim().toLowerCase();
569
+ if (
570
+ normalized === 'vs' ||
571
+ normalized === 's' ||
572
+ normalized === 'f' ||
573
+ normalized === '%'
574
+ ) {
575
+ return normalized as SizeUnit;
576
+ }
577
+ return fallback;
578
+ }
579
+
580
+ function SizeField({
581
+ value,
582
+ onChange,
583
+ preferredScale,
584
+ fieldName,
585
+ }: SizeFieldProps) {
586
+ const parsed = useMemo(() => parseSizeValue(value), [value]);
587
+ const normalizedPreferred = useMemo(
588
+ () => normalizePreferredScale(preferredScale, fieldName),
589
+ [preferredScale, fieldName],
590
+ );
591
+ const [amount, setAmount] = useState(parsed.amount);
592
+ const [unit, setUnit] = useState<SizeUnit>(
593
+ parsed.unit || normalizedPreferred,
594
+ );
595
+
596
+ useEffect(() => {
597
+ setAmount(parsed.amount);
598
+ setUnit(parsed.unit || normalizedPreferred);
599
+ }, [parsed.amount, parsed.unit, normalizedPreferred]);
600
+
601
+ const emitValue = useCallback(
602
+ (nextAmount: string, nextUnit: SizeUnit) => {
603
+ const trimmed = nextAmount.trim();
604
+ if (!trimmed) {
605
+ onChange(undefined);
606
+ return;
607
+ }
608
+ onChange(composeSizeValue(trimmed, nextUnit));
609
+ },
610
+ [onChange],
611
+ );
612
+
613
+ const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
614
+ const nextAmount = e.target.value;
615
+ setAmount(nextAmount);
616
+ if (!nextAmount.trim()) {
617
+ onChange(undefined);
618
+ return;
619
+ }
620
+ emitValue(nextAmount, unit);
621
+ };
622
+
623
+ const handleUnitChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
624
+ const nextUnit = e.target.value as SizeUnit;
625
+ setUnit(nextUnit);
626
+ if (!amount.trim()) return;
627
+ emitValue(amount, nextUnit);
628
+ };
629
+
630
+ const unitPriority: SizeUnit[] = ['vs', 's', 'f', '%'];
631
+ const orderedUnits = [
632
+ normalizedPreferred,
633
+ ...unitPriority.filter((unit) => unit !== normalizedPreferred),
634
+ ];
635
+ const unitOptions: Array<{ value: SizeUnit; label: string }> =
636
+ orderedUnits.map((unit) => ({
637
+ value: unit,
638
+ label: unit === normalizedPreferred ? `${unit}` : unit,
639
+ }));
640
+
641
+ return (
642
+ <div className="attributes-editor__size-field">
643
+ <input
644
+ type="number"
645
+ className="input attributes-editor__size-field-input"
646
+ value={amount}
647
+ onChange={handleAmountChange}
648
+ />
649
+ <select
650
+ className="input attributes-editor__size-field-select"
651
+ value={unit}
652
+ onChange={handleUnitChange}
653
+ >
654
+ {unitOptions.map((opt) => (
655
+ <option key={opt.value} value={opt.value}>
656
+ {opt.label}
657
+ </option>
658
+ ))}
659
+ </select>
660
+ </div>
661
+ );
662
+ }
@@ -0,0 +1,49 @@
1
+ import React, { useState } from 'react';
2
+
3
+ type FieldInfoTooltipProps = {
4
+ description?: string;
5
+ children: React.ReactNode;
6
+ };
7
+
8
+ export function FieldInfoTooltip({
9
+ description,
10
+ children,
11
+ }: FieldInfoTooltipProps) {
12
+ const [isHovered, setIsHovered] = useState(false);
13
+
14
+ if (!description || !description.trim()) {
15
+ return <>{children}</>;
16
+ }
17
+
18
+ const bubbleClassName = [
19
+ 'field-info-tooltip__bubble',
20
+ isHovered ? 'field-info-tooltip__bubble--visible' : '',
21
+ ]
22
+ .filter(Boolean)
23
+ .join(' ');
24
+ const handleShow = () => setIsHovered(true);
25
+ const handleHide = () => setIsHovered(false);
26
+
27
+ return (
28
+ <div className="field-info-tooltip">
29
+ {children}
30
+ <button
31
+ className="field-info-tooltip__trigger"
32
+ onMouseEnter={handleShow}
33
+ onMouseLeave={handleHide}
34
+ onFocus={handleShow}
35
+ onBlur={handleHide}
36
+ type="button"
37
+ >
38
+ Show info
39
+ <span
40
+ className={bubbleClassName}
41
+ role="tooltip"
42
+ aria-hidden={!isHovered}
43
+ >
44
+ {description}
45
+ </span>
46
+ </button>
47
+ </div>
48
+ );
49
+ }