@developer_tribe/react-builder 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/DeviceMockFrame.d.ts +2 -1
  2. package/dist/RenderPage.d.ts +4 -3
  3. package/dist/attributes-editor/Field.d.ts +16 -0
  4. package/dist/attributes-editor/FieldInfoTooltip.d.ts +7 -0
  5. package/dist/attributes-editor/LayoutPreviewPicker.d.ts +12 -0
  6. package/dist/attributes-editor/SpecialCategorySection.d.ts +19 -0
  7. package/dist/attributes-editor/types.d.ts +14 -0
  8. package/dist/background.jpg +0 -0
  9. package/dist/build-components/Button/Button.d.ts +1 -1
  10. package/dist/build-components/Button/ButtonProps.generated.d.ts +26 -1
  11. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +27 -1
  12. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +25 -0
  13. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +25 -0
  14. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +27 -1
  15. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +27 -1
  16. package/dist/build-components/Image/ImageProps.generated.d.ts +25 -3
  17. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +27 -1
  18. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +25 -0
  19. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +25 -0
  20. package/dist/build-components/OnboardDot/OnboardDot.d.ts +1 -1
  21. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +22 -0
  22. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +4 -5
  23. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +25 -3
  24. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +24 -3
  25. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +25 -4
  26. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +4 -5
  27. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +4 -5
  28. package/dist/build-components/Text/TextProps.generated.d.ts +4 -5
  29. package/dist/build-components/View/ViewProps.generated.d.ts +3 -4
  30. package/dist/build-components/patterns.generated.d.ts +4855 -132
  31. package/dist/components/Breadcrumb.d.ts +3 -1
  32. package/dist/components/Checkbox.d.ts +17 -0
  33. package/dist/components/DeviceButton.d.ts +8 -0
  34. package/dist/components/DeviceNavigationBar.d.ts +10 -0
  35. package/dist/components/DeviceStatusBar.d.ts +9 -0
  36. package/dist/components/EditorHeader.d.ts +3 -8
  37. package/dist/index.cjs.js +5 -5
  38. package/dist/index.cjs.js.map +1 -1
  39. package/dist/index.esm.js +5 -5
  40. package/dist/index.esm.js.map +1 -1
  41. package/dist/mockOS/components/MockLaunchScreenComponent.d.ts +6 -0
  42. package/dist/mockOS/components/MockOSRouter.d.ts +8 -0
  43. package/dist/mockOS/components/PermissionModal.d.ts +9 -0
  44. package/dist/mockOS/context/MockOSContext.d.ts +36 -0
  45. package/dist/mockOS/hooks/useMockNavigation.d.ts +3 -0
  46. package/dist/mockOS/hooks/useMockPermission.d.ts +3 -0
  47. package/dist/mockOS/index.d.ts +9 -0
  48. package/dist/mockOS/managers/mockPermissionManager.d.ts +10 -0
  49. package/dist/mockOS/managers/navigationManager.d.ts +17 -0
  50. package/dist/modals/AddComponentModal.d.ts +8 -0
  51. package/dist/modals/ColorModal.d.ts +9 -0
  52. package/dist/modals/DeviceSelectorModal.d.ts +9 -0
  53. package/dist/modals/LocalicationModal.d.ts +8 -0
  54. package/dist/modals/Modal.d.ts +12 -0
  55. package/dist/modals/index.d.ts +5 -0
  56. package/dist/pages/ProjectPage.d.ts +1 -1
  57. package/dist/store.d.ts +0 -2
  58. package/dist/styles.css +1 -1
  59. package/dist/utils/patterns.d.ts +24 -0
  60. package/package.json +2 -1
  61. package/scripts/prebuild/utils/createGeneratedProps.js +11 -3
  62. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +45 -6
  63. package/scripts/prebuild/utils/validatePatternJson.js +13 -5
  64. package/src/AttributesEditor.tsx +433 -312
  65. package/src/DeviceMockFrame.tsx +21 -37
  66. package/src/RenderPage.tsx +5 -4
  67. package/src/assets/images/android.svg +42 -42
  68. package/src/assets/images/apple.svg +15 -15
  69. package/src/attributes-editor/Field.tsx +662 -0
  70. package/src/attributes-editor/FieldInfoTooltip.tsx +49 -0
  71. package/src/attributes-editor/LayoutPreviewPicker.tsx +199 -0
  72. package/src/attributes-editor/SpecialCategorySection.tsx +284 -0
  73. package/src/attributes-editor/types.ts +30 -0
  74. package/src/build-components/Button/Button.tsx +10 -2
  75. package/src/build-components/Button/ButtonProps.generated.ts +37 -1
  76. package/src/build-components/Button/pattern.json +31 -2
  77. package/src/build-components/Carousel/Carousel.tsx +15 -2
  78. package/src/build-components/Carousel/CarouselProps.generated.ts +39 -1
  79. package/src/build-components/Carousel/pattern.json +10 -0
  80. package/src/build-components/CarouselButtons/CarouselButtons.tsx +6 -2
  81. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +36 -0
  82. package/src/build-components/CarouselButtons/pattern.json +22 -0
  83. package/src/build-components/CarouselDots/CarouselDots.tsx +40 -8
  84. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +36 -0
  85. package/src/build-components/CarouselDots/pattern.json +15 -0
  86. package/src/build-components/CarouselItem/CarouselItem.tsx +5 -2
  87. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +39 -1
  88. package/src/build-components/CarouselItem/pattern.json +7 -0
  89. package/src/build-components/CarouselProvider/CarouselProvider.tsx +10 -2
  90. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +39 -1
  91. package/src/build-components/CarouselProvider/pattern.json +7 -0
  92. package/src/build-components/Image/Image.tsx +8 -2
  93. package/src/build-components/Image/ImageProps.generated.ts +36 -3
  94. package/src/build-components/Image/pattern.json +46 -3
  95. package/src/build-components/Onboard/Onboard.tsx +6 -1
  96. package/src/build-components/Onboard/OnboardProps.generated.ts +39 -1
  97. package/src/build-components/Onboard/pattern.json +11 -0
  98. package/src/build-components/OnboardButton/OnboardButton.tsx +46 -5
  99. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +36 -0
  100. package/src/build-components/OnboardButton/pattern.json +71 -5
  101. package/src/build-components/OnboardButtons/OnboardButtons.tsx +20 -10
  102. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +36 -0
  103. package/src/build-components/OnboardButtons/pattern.json +70 -4
  104. package/src/build-components/OnboardDot/OnboardDot.tsx +104 -4
  105. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +22 -0
  106. package/src/build-components/OnboardDot/pattern.json +54 -1
  107. package/src/build-components/OnboardFooter/OnboardFooter.tsx +9 -3
  108. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +4 -5
  109. package/src/build-components/OnboardFooter/pattern.json +58 -2
  110. package/src/build-components/OnboardImage/OnboardImage.tsx +27 -5
  111. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +36 -3
  112. package/src/build-components/OnboardImage/pattern.json +21 -0
  113. package/src/build-components/OnboardItem/OnboardItem.tsx +6 -1
  114. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +35 -3
  115. package/src/build-components/OnboardItem/pattern.json +38 -2
  116. package/src/build-components/OnboardProvider/OnboardProvider.tsx +20 -8
  117. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +37 -4
  118. package/src/build-components/OnboardProvider/pattern.json +51 -4
  119. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +4 -5
  120. package/src/build-components/OnboardSubtitle/pattern.json +6 -0
  121. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +4 -5
  122. package/src/build-components/OnboardTitle/pattern.json +6 -0
  123. package/src/build-components/Text/Text.tsx +7 -3
  124. package/src/build-components/Text/TextProps.generated.ts +4 -5
  125. package/src/build-components/Text/pattern.json +38 -2
  126. package/src/build-components/View/View.tsx +9 -6
  127. package/src/build-components/View/ViewProps.generated.ts +3 -4
  128. package/src/build-components/View/pattern.json +227 -19
  129. package/src/build-components/patterns.generated.ts +4905 -139
  130. package/src/components/AttributesEditorPanel.tsx +7 -61
  131. package/src/components/Breadcrumb.tsx +37 -5
  132. package/src/components/Builder.tsx +180 -77
  133. package/src/components/Checkbox.tsx +81 -0
  134. package/src/components/DeviceButton.tsx +39 -0
  135. package/src/components/DeviceNavigationBar.tsx +201 -0
  136. package/src/components/DeviceStatusBar.tsx +85 -0
  137. package/src/components/EditorHeader.tsx +26 -74
  138. package/src/mockOS/components/MockLaunchScreenComponent.tsx +43 -0
  139. package/src/mockOS/components/MockOSRouter.tsx +115 -0
  140. package/src/mockOS/components/PermissionModal.tsx +270 -0
  141. package/src/mockOS/context/MockOSContext.tsx +179 -0
  142. package/src/mockOS/hooks/useMockNavigation.ts +11 -0
  143. package/src/mockOS/hooks/useMockPermission.ts +11 -0
  144. package/src/mockOS/index.ts +26 -0
  145. package/src/mockOS/managers/mockPermissionManager.ts +54 -0
  146. package/src/mockOS/managers/navigationManager.ts +91 -0
  147. package/src/modals/AddComponentModal.tsx +313 -0
  148. package/src/modals/ColorModal.tsx +268 -0
  149. package/src/modals/DeviceSelectorModal.tsx +57 -0
  150. package/src/modals/LocalicationModal.tsx +54 -0
  151. package/src/modals/Modal.tsx +57 -0
  152. package/src/modals/index.ts +5 -0
  153. package/src/pages/ProjectPage.tsx +19 -21
  154. package/src/pages/tabs/DebugTab.tsx +50 -9
  155. package/src/pages/tabs/PreviewTab.tsx +52 -40
  156. package/src/size-matters/index.ts +21 -5
  157. package/src/store.ts +0 -4
  158. package/src/styles/{global.scss → base/_global.scss} +92 -39
  159. package/src/styles/components/_attributes-editor.scss +261 -0
  160. package/src/styles/{editor.scss → components/_editor-shell.scss} +72 -57
  161. package/src/styles/components/_mockos-router.scss +140 -0
  162. package/src/styles/components/_ui-components.scss +183 -0
  163. package/src/styles/foundation/_colors.scss +8 -0
  164. package/src/styles/{_mixins.scss → foundation/_mixins.scss} +5 -4
  165. package/src/styles/{_reset.scss → foundation/_reset.scss} +5 -2
  166. package/src/styles/foundation/_sizes.scss +37 -0
  167. package/src/styles/foundation/_typography.scss +4 -0
  168. package/src/styles/foundation/_variables.scss +3 -0
  169. package/src/styles/index.scss +22 -136
  170. package/src/styles/layout/_builder.scss +68 -0
  171. package/src/styles/layout/_pages.scss +3 -0
  172. package/src/styles/modals/_add-component.scss +122 -0
  173. package/src/styles/modals/_color-modal.scss +130 -0
  174. package/src/styles/modals/_device-selector.scss +18 -0
  175. package/src/styles/modals/_localication-modal.scss +68 -0
  176. package/src/styles/modals/_modal-shell.scss +46 -0
  177. package/src/styles/utilities/_carousel.scss +125 -0
  178. package/src/types/images.d.ts +8 -0
  179. package/src/utils/extractTextStyle.ts +4 -2
  180. package/src/utils/extractViewStyle.ts +51 -7
  181. package/src/utils/patterns.ts +33 -0
  182. package/dist/build-components/OnboardDot/OnboardExpandingDotProps.generated.d.ts +0 -10
  183. package/src/build-components/OnboardDot/OnboardExpandingDotProps.generated.ts +0 -20
  184. package/src/styles/_variables.scss +0 -27
  185. package/src/styles/builder.scss +0 -60
  186. package/src/styles/components.scss +0 -88
  187. package/src/styles/pages.scss +0 -2
@@ -1,349 +1,470 @@
1
- import React from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { Node, NodeData, NodeDefaultAttribute } from './types/Node';
3
3
  import { isNodeString } from './utils/analyseNode';
4
4
  import { useLogRender } from './utils/useLogRender';
5
5
  import {
6
+ getAttributeMeta,
6
7
  getAttributeSchema,
7
- getTypeSchema,
8
- getArrayItemType,
9
- isPrimitiveType,
8
+ getPatternByType,
10
9
  } from './utils/patterns';
10
+ import { useRenderStore } from './store';
11
+ import { Field } from './attributes-editor/Field';
12
+ import { SpecialCategorySection } from './attributes-editor/SpecialCategorySection';
13
+ import {
14
+ LayoutContext,
15
+ SchemaEntry,
16
+ isBooleanFieldType,
17
+ } from './attributes-editor/types';
18
+ import { FieldInfoTooltip } from './attributes-editor/FieldInfoTooltip';
19
+ import type { ViewPropsGenerated } from './build-components/View/ViewProps.generated';
20
+ import useNode from './build-components/useNode';
11
21
 
12
22
  type AttributesEditorProps = {
13
23
  node: Node;
14
24
  onChange: (next: Node) => void;
15
25
  };
16
26
 
17
- function Field({
18
- name,
19
- type,
20
- value,
21
- onChange,
22
- componentType,
23
- }: {
24
- name: string;
25
- type: string | string[];
26
- value: any;
27
- onChange: (v: any) => void;
28
- // The current node's component type is needed to resolve custom type schemas
29
- componentType?: string;
30
- }) {
31
- // Render enum selector
32
- if (Array.isArray(type)) {
33
- return (
34
- <select
35
- value={value ?? ''}
36
- onChange={(e) => onChange(e.target.value)}
37
- className="input"
38
- >
39
- <option value="">(none)</option>
40
- {type.map((opt) => (
41
- <option key={opt} value={opt}>
42
- {opt}
43
- </option>
44
- ))}
45
- </select>
46
- );
27
+ type TabId = 'style' | 'container' | 'other';
28
+
29
+ type TabConfig = {
30
+ id: TabId;
31
+ label: string;
32
+ entries: SchemaEntry[];
33
+ };
34
+
35
+ type SpecialSection = {
36
+ key: string;
37
+ entries: SchemaEntry[];
38
+ meta?: {
39
+ label?: string;
40
+ description?: string;
41
+ sort?: number;
42
+ };
43
+ };
44
+
45
+ type TabContentInfo = Record<
46
+ TabId,
47
+ {
48
+ baseCount: number;
49
+ specialCount: number;
47
50
  }
51
+ >;
48
52
 
49
- // Arrays: detect X[] (including string[]/number[]/boolean[]/CustomType[])
50
- const itemType = typeof type === 'string' ? getArrayItemType(type) : null;
51
- if (itemType) {
52
- const arr: any[] = Array.isArray(value) ? value : [];
53
+ export function AttributesEditor({ node, onChange }: AttributesEditorProps) {
54
+ useLogRender('AttributesEditor');
55
+ if (!node || isNodeString(node)) return null;
53
56
 
54
- // Primitive arrays with add/remove controls
55
- if (isPrimitiveType(itemType)) {
56
- return (
57
- <div style={{ display: 'grid', gap: 8 }}>
58
- {arr.map((item, idx) => (
59
- <div
60
- key={idx}
61
- style={{ display: 'flex', gap: 8, alignItems: 'center' }}
62
- >
63
- {itemType === 'number' ? (
64
- <input
65
- type="number"
66
- value={item ?? ''}
67
- onChange={(e) => {
68
- const next = [...arr];
69
- next[idx] =
70
- e.target.value === ''
71
- ? undefined
72
- : Number(e.target.value);
73
- onChange(next);
74
- }}
75
- className="input"
76
- style={{ flex: 1 }}
77
- />
78
- ) : itemType === 'boolean' ? (
79
- <input
80
- type="checkbox"
81
- checked={Boolean(item)}
82
- onChange={(e) => {
83
- const next = [...arr];
84
- next[idx] = e.target.checked;
85
- onChange(next);
86
- }}
87
- />
88
- ) : (
89
- <input
90
- type="text"
91
- value={item ?? ''}
92
- onChange={(e) => {
93
- const next = [...arr];
94
- next[idx] =
95
- e.target.value === '' ? undefined : e.target.value;
96
- onChange(next);
97
- }}
98
- className="input"
99
- style={{ flex: 1 }}
100
- />
101
- )}
102
- <button
103
- type="button"
104
- onClick={() => {
105
- const next = arr.filter((_, i) => i !== idx);
106
- onChange(next.length ? next : undefined);
107
- }}
108
- >
109
- remove
110
- </button>
57
+ const baseData = node as NodeData<NodeDefaultAttribute>;
58
+ const data = useNode(baseData);
59
+ const appConfig = useRenderStore((state) => state.appConfig);
60
+ const projectColors = useMemo(() => {
61
+ const colors = new Set<string>();
62
+ const styles = [
63
+ appConfig?.screenStyle?.light,
64
+ appConfig?.screenStyle?.dark,
65
+ ].filter(Boolean) as Array<{
66
+ backgroundColor?: string;
67
+ color?: string;
68
+ seperatorColor?: string;
69
+ }>;
70
+ styles.forEach((style) => {
71
+ ['backgroundColor', 'color', 'seperatorColor'].forEach((key) => {
72
+ const value = style?.[key as keyof typeof style];
73
+ if (typeof value === 'string' && value.trim()) {
74
+ colors.add(value.trim());
75
+ }
76
+ });
77
+ });
78
+ return Array.from(colors);
79
+ }, [appConfig]);
80
+
81
+ const schema = getAttributeSchema(data?.type) ?? {};
82
+ const attributeMeta = getAttributeMeta(data?.type);
83
+ const attributes = (data?.attributes ?? {}) as NodeDefaultAttribute;
84
+ const viewAttributes = useMemo<
85
+ Partial<ViewPropsGenerated['attributes']> | undefined
86
+ >(
87
+ () =>
88
+ data?.type === 'View'
89
+ ? (attributes as ViewPropsGenerated['attributes'])
90
+ : undefined,
91
+ [attributes, data?.type],
92
+ );
93
+
94
+ const layoutContext = useMemo<LayoutContext>(
95
+ () => ({
96
+ flexDirection:
97
+ attributes?.flexDirection as LayoutContext['flexDirection'],
98
+ alignItems: attributes?.alignItems as LayoutContext['alignItems'],
99
+ justifyContent:
100
+ attributes?.justifyContent as LayoutContext['justifyContent'],
101
+ }),
102
+ [
103
+ attributes?.flexDirection,
104
+ attributes?.alignItems,
105
+ attributes?.justifyContent,
106
+ ],
107
+ );
108
+
109
+ const entries = useMemo(
110
+ () =>
111
+ Object.entries(schema).filter(([, type]) =>
112
+ typeof type === 'string' ? type !== 'never' : true,
113
+ ),
114
+ [schema],
115
+ );
116
+
117
+ const componentMeta = useMemo(
118
+ () => (data?.type ? getPatternByType(data.type)?.meta : undefined),
119
+ [data?.type],
120
+ );
121
+
122
+ const componentTitle = componentMeta?.label ?? data?.type ?? 'Component';
123
+ const componentDescription = componentMeta?.description;
124
+
125
+ const headerSection = (
126
+ <div className="attributes-editor__component-meta">
127
+ <p className="attributes-editor__component-title">{componentTitle}</p>
128
+ {componentDescription ? (
129
+ <p className="attributes-editor__component-description">
130
+ {componentDescription}
131
+ </p>
132
+ ) : null}
133
+ </div>
134
+ );
135
+
136
+ const { grouped, specialGroups } = useMemo(() => {
137
+ const groups: Record<TabId, SchemaEntry[]> = {
138
+ style: [],
139
+ container: [],
140
+ other: [],
141
+ };
142
+ const specials: Record<string, SchemaEntry[]> = {};
143
+
144
+ const getSortOrder = (name: string) =>
145
+ attributeMeta?.[name]?.sort ?? Number.MAX_SAFE_INTEGER;
146
+ const compareEntries = (a: SchemaEntry, b: SchemaEntry) => {
147
+ const order = getSortOrder(a.name) - getSortOrder(b.name);
148
+ return order !== 0 ? order : a.name.localeCompare(b.name);
149
+ };
150
+
151
+ entries.forEach(([name, type]) => {
152
+ const meta = attributeMeta?.[name];
153
+ const specialCategory = meta?.specialCategory;
154
+ if (typeof specialCategory === 'string') {
155
+ const normalizedSpecialCategory = specialCategory.trim();
156
+ if (normalizedSpecialCategory) {
157
+ if (!specials[normalizedSpecialCategory]) {
158
+ specials[normalizedSpecialCategory] = [];
159
+ }
160
+ specials[normalizedSpecialCategory].push({ name, type });
161
+ return;
162
+ }
163
+ }
164
+
165
+ const metaCategory = meta?.category;
166
+ const normalized =
167
+ metaCategory === 'style'
168
+ ? 'style'
169
+ : metaCategory === 'container'
170
+ ? 'container'
171
+ : 'other';
172
+ groups[normalized].push({ name, type });
173
+ });
174
+
175
+ Object.values(groups).forEach((list) => {
176
+ list.sort(compareEntries);
177
+ });
178
+
179
+ Object.values(specials).forEach((list) => {
180
+ list.sort(compareEntries);
181
+ });
182
+
183
+ return { grouped: groups, specialGroups: specials };
184
+ }, [attributeMeta, entries]);
185
+
186
+ const specialSectionsByTab = useMemo<Record<TabId, SpecialSection[]>>(() => {
187
+ const buckets: Record<TabId, SpecialSection[]> = {
188
+ style: [],
189
+ container: [],
190
+ other: [],
191
+ };
192
+
193
+ const compareSections = (a: SpecialSection, b: SpecialSection) => {
194
+ const aSort = a.meta?.sort ?? Number.MAX_SAFE_INTEGER;
195
+ const bSort = b.meta?.sort ?? Number.MAX_SAFE_INTEGER;
196
+ const order = aSort - bSort;
197
+ return order !== 0 ? order : a.key.localeCompare(b.key);
198
+ };
199
+
200
+ Object.entries(specialGroups).forEach(([categoryKey, categoryEntries]) => {
201
+ if (!categoryEntries.length) return;
202
+ const metaForCategory = componentMeta?.specialCategories?.[categoryKey];
203
+ const targetCategory = (
204
+ metaForCategory?.category === 'style'
205
+ ? 'style'
206
+ : metaForCategory?.category === 'container'
207
+ ? 'container'
208
+ : 'other'
209
+ ) as TabId;
210
+ buckets[targetCategory].push({
211
+ key: categoryKey,
212
+ entries: categoryEntries,
213
+ meta: metaForCategory,
214
+ });
215
+ });
216
+
217
+ (Object.keys(buckets) as TabId[]).forEach((tabId) => {
218
+ buckets[tabId].sort(compareSections);
219
+ });
220
+
221
+ return buckets;
222
+ }, [componentMeta?.specialCategories, specialGroups]);
223
+ const handleAttributeChange = useCallback(
224
+ (name: string, val: unknown) => {
225
+ const next: NodeData<NodeDefaultAttribute> = {
226
+ ...baseData,
227
+ attributes: {
228
+ ...((baseData?.attributes ?? {}) as NodeDefaultAttribute),
229
+ [name]: val,
230
+ },
231
+ };
232
+ onChange(next);
233
+ },
234
+ [baseData, onChange],
235
+ );
236
+
237
+ const renderSpecialSection = useCallback(
238
+ ({ key, entries: sectionEntries, meta }: SpecialSection) => {
239
+ if (key === 'size') {
240
+ const normalizedTitle =
241
+ meta?.label && meta.label.trim().length > 0
242
+ ? meta.label
243
+ : key.charAt(0).toUpperCase() + key.slice(1);
244
+ const normalizedDescription = meta?.description;
245
+ return (
246
+ <section key={key} className="special-category-section">
247
+ <div className="special-category-section__header">
248
+ <p className="special-category-section__title">
249
+ {normalizedTitle}
250
+ </p>
111
251
  </div>
112
- ))}
113
- <div>
114
- <button
115
- type="button"
116
- onClick={() => {
117
- const next = [
118
- ...arr,
119
- itemType === 'boolean'
120
- ? false
121
- : itemType === 'number'
122
- ? 0
123
- : '',
252
+ {normalizedDescription ? (
253
+ <p className="special-category-section__description">
254
+ {normalizedDescription}
255
+ </p>
256
+ ) : null}
257
+ <div className="attributes-editor__size-grid">
258
+ {sectionEntries.map(({ name, type }) => {
259
+ const label = attributeMeta?.[name]?.label ?? name;
260
+ const description = attributeMeta?.[name]?.description;
261
+ const preferredScale = attributeMeta?.[name]?.preferedScale;
262
+ const currentValue = (attributes as Record<string, unknown>)[
263
+ name
124
264
  ];
125
- onChange(next);
126
- }}
127
- >
128
- add
129
- </button>
130
- </div>
131
- </div>
265
+ const isBoolean = isBooleanFieldType(type);
266
+ const wrapperClassNames = [
267
+ 'attributes-editor__field-wrapper',
268
+ isBoolean ? 'attributes-editor__field-wrapper--boolean' : '',
269
+ ]
270
+ .filter(Boolean)
271
+ .join(' ');
272
+ return (
273
+ <FieldInfoTooltip key={name} description={description}>
274
+ <div
275
+ className={`${wrapperClassNames} attributes-editor__size-grid-item`}
276
+ >
277
+ {!isBoolean ? (
278
+ <p className="attributes-editor__field-label">
279
+ {label}
280
+ </p>
281
+ ) : null}
282
+ <Field
283
+ name={name}
284
+ type={type}
285
+ value={currentValue}
286
+ onChange={(val) => handleAttributeChange(name, val)}
287
+ componentType={data?.type}
288
+ projectColors={projectColors}
289
+ layoutContext={layoutContext}
290
+ viewAttributes={viewAttributes}
291
+ label={isBoolean ? label : undefined}
292
+ preferredScale={preferredScale}
293
+ />
294
+ </div>
295
+ </FieldInfoTooltip>
296
+ );
297
+ })}
298
+ </div>
299
+ </section>
300
+ );
301
+ }
302
+ return (
303
+ <SpecialCategorySection
304
+ key={key}
305
+ category={key}
306
+ entries={sectionEntries}
307
+ attributeMeta={attributeMeta}
308
+ attributes={attributes}
309
+ onAttributeChange={handleAttributeChange}
310
+ componentType={data?.type}
311
+ projectColors={projectColors}
312
+ layoutContext={layoutContext}
313
+ viewAttributes={viewAttributes}
314
+ meta={meta}
315
+ />
132
316
  );
133
- }
317
+ },
318
+ [
319
+ attributeMeta,
320
+ attributes,
321
+ data?.type,
322
+ handleAttributeChange,
323
+ layoutContext,
324
+ projectColors,
325
+ viewAttributes,
326
+ ],
327
+ );
134
328
 
135
- // Object arrays with nested editors
136
- const schema = getTypeSchema(componentType, itemType) ?? {};
137
- return (
138
- <div style={{ display: 'grid', gap: 8 }}>
139
- {arr.map((item, idx) => {
140
- const obj = (item ?? {}) as Record<string, unknown>;
141
- return (
142
- <div
143
- key={idx}
144
- style={{ border: '1px solid #ddd', borderRadius: 6, padding: 8 }}
145
- >
146
- <div
147
- style={{
148
- display: 'grid',
149
- gridTemplateColumns: '1fr 1fr',
150
- gap: 8,
151
- }}
152
- >
153
- {Object.entries(schema).map(([fieldName, fieldType]) => (
154
- <React.Fragment key={fieldName}>
155
- <div style={{ alignSelf: 'center' }}>{fieldName}</div>
156
- <Field
157
- name={fieldName}
158
- type={fieldType}
159
- value={obj?.[fieldName as keyof typeof obj]}
160
- onChange={(val) => {
161
- const next = [...arr];
162
- const nextObj = { ...(obj ?? {}), [fieldName]: val };
163
- next[idx] = nextObj;
164
- onChange(next);
165
- }}
166
- componentType={componentType}
167
- />
168
- </React.Fragment>
169
- ))}
170
- </div>
171
- <div style={{ marginTop: 8 }}>
172
- <button
173
- type="button"
174
- onClick={() => {
175
- const next = arr.filter((_, i) => i !== idx);
176
- onChange(next.length ? next : undefined);
177
- }}
178
- >
179
- remove
180
- </button>
181
- </div>
182
- </div>
183
- );
184
- })}
185
- <div>
186
- <button
187
- type="button"
188
- onClick={() => {
189
- const empty: Record<string, unknown> = {};
190
- const next = [...arr, empty];
191
- onChange(next);
192
- }}
193
- >
194
- add
195
- </button>
196
- </div>
197
- </div>
198
- );
199
- }
329
+ const tabs = useMemo<TabConfig[]>(
330
+ () => [
331
+ { id: 'container', label: 'Container', entries: grouped.container },
332
+ { id: 'style', label: 'Styles', entries: grouped.style },
333
+ { id: 'other', label: 'Others', entries: grouped.other },
334
+ ],
335
+ [grouped],
336
+ );
200
337
 
201
- // Non-array complex object types defined under pattern `types`
202
- if (typeof type === 'string' && !isPrimitiveType(type)) {
203
- const schema = getTypeSchema(componentType, type);
204
- if (schema) {
205
- const obj = (value ?? {}) as Record<string, unknown>;
206
- return (
207
- <div
208
- style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}
209
- >
210
- {Object.entries(schema).map(([fieldName, fieldType]) => (
211
- <React.Fragment key={fieldName}>
212
- <div style={{ alignSelf: 'center' }}>{fieldName}</div>
213
- <Field
214
- name={fieldName}
215
- type={fieldType}
216
- value={obj?.[fieldName as keyof typeof obj]}
217
- onChange={(val) => {
218
- const nextObj = { ...(obj ?? {}), [fieldName]: val };
219
- onChange(nextObj);
220
- }}
221
- componentType={componentType}
222
- />
223
- </React.Fragment>
224
- ))}
225
- </div>
338
+ const tabContentInfo = useMemo<TabContentInfo>(() => {
339
+ const info: TabContentInfo = {
340
+ style: { baseCount: 0, specialCount: 0 },
341
+ container: { baseCount: 0, specialCount: 0 },
342
+ other: { baseCount: 0, specialCount: 0 },
343
+ };
344
+
345
+ tabs.forEach((tab) => {
346
+ info[tab.id].baseCount = tab.entries.length;
347
+ });
348
+
349
+ (Object.keys(specialSectionsByTab) as TabId[]).forEach((tabId) => {
350
+ info[tabId].specialCount = specialSectionsByTab[tabId].reduce(
351
+ (sum, section) => sum + section.entries.length,
352
+ 0,
226
353
  );
227
- }
228
- }
354
+ });
229
355
 
230
- if (type === 'number') {
231
- return (
232
- <input
233
- type="number"
234
- value={value ?? ''}
235
- onChange={(e) =>
236
- onChange(e.target.value === '' ? undefined : Number(e.target.value))
237
- }
238
- className="input"
239
- />
240
- );
241
- }
242
- if (type === 'boolean') {
243
- return (
244
- <input
245
- type="checkbox"
246
- checked={Boolean(value)}
247
- onChange={(e) => onChange(e.target.checked)}
248
- />
249
- );
250
- }
251
- // Legacy support: string[]
252
- if (type === 'string[]') {
253
- const arr: string[] = Array.isArray(value) ? value : [];
254
- return (
255
- <div style={{ display: 'grid', gap: 8 }}>
256
- {arr.map((item, idx) => (
257
- <div
258
- key={idx}
259
- style={{ display: 'flex', gap: 8, alignItems: 'center' }}
260
- >
261
- <input
262
- type="text"
263
- value={item ?? ''}
264
- onChange={(e) => {
265
- const next = [...arr];
266
- next[idx] = e.target.value;
267
- onChange(next);
268
- }}
269
- className="input"
270
- style={{ flex: 1 }}
271
- />
272
- <button
273
- type="button"
274
- onClick={() => {
275
- const next = arr.filter((_, i) => i !== idx);
276
- onChange(next.length ? next : undefined);
277
- }}
278
- >
279
- remove
280
- </button>
281
- </div>
282
- ))}
283
- <div>
356
+ return info;
357
+ }, [specialSectionsByTab, tabs]);
358
+
359
+ const firstAvailableTab = useMemo<TabId>(
360
+ () =>
361
+ tabs.find((tab) => {
362
+ const counts = tabContentInfo[tab.id];
363
+ return counts.baseCount + counts.specialCount > 0;
364
+ })?.id ?? 'other',
365
+ [tabContentInfo, tabs],
366
+ );
367
+
368
+ const [activeTab, setActiveTab] = useState<TabId>(firstAvailableTab);
369
+
370
+ useEffect(() => {
371
+ setActiveTab((prev) => {
372
+ const counts = tabContentInfo[prev];
373
+ if (counts && counts.baseCount + counts.specialCount > 0) {
374
+ return prev;
375
+ }
376
+ return firstAvailableTab;
377
+ });
378
+ }, [firstAvailableTab, tabContentInfo]);
379
+
380
+ const activeEntries =
381
+ tabs.find((tab) => tab.id === activeTab)?.entries ??
382
+ tabs.find((tab) => tab.id === firstAvailableTab)?.entries ??
383
+ [];
384
+
385
+ const activeSpecialSections = specialSectionsByTab[activeTab] ?? [];
386
+
387
+ const tabsSection = (
388
+ <div className="attributes-editor__tabs">
389
+ {tabs.map((tab) => {
390
+ const isActive = tab.id === activeTab;
391
+ const counts = tabContentInfo[tab.id];
392
+ const totalCount = counts.baseCount + counts.specialCount;
393
+ const disabled = totalCount === 0;
394
+ const buttonClassNames = [
395
+ 'attributes-editor__tab-button',
396
+ isActive ? 'attributes-editor__tab-button--active' : '',
397
+ ]
398
+ .filter(Boolean)
399
+ .join(' ');
400
+ return (
284
401
  <button
402
+ key={tab.id}
285
403
  type="button"
286
- onClick={() => {
287
- const next = [...arr, ''];
288
- onChange(next);
289
- }}
404
+ onClick={() => !disabled && setActiveTab(tab.id)}
405
+ disabled={disabled}
406
+ className={buttonClassNames}
290
407
  >
291
- add
408
+ {tab.label}
409
+ {totalCount > 0 ? ` (${totalCount})` : ''}
292
410
  </button>
293
- </div>
294
- </div>
295
- );
296
- }
297
- return (
298
- <input
299
- type="text"
300
- value={value ?? ''}
301
- onChange={(e) =>
302
- onChange(e.target.value === '' ? undefined : e.target.value)
303
- }
304
- className="input"
305
- />
411
+ );
412
+ })}
413
+ </div>
306
414
  );
307
- }
308
415
 
309
- export function AttributesEditor({ node, onChange }: AttributesEditorProps) {
310
- useLogRender('AttributesEditor');
311
- if (!node || isNodeString(node)) return null;
312
- const data = node as NodeData<NodeDefaultAttribute>;
313
- const schema = getAttributeSchema(data?.type) ?? {};
314
- const attributes = (data?.attributes ?? {}) as NodeDefaultAttribute;
315
-
316
- const entries = Object.entries(schema);
317
416
  if (entries.length === 0) {
318
417
  return (
319
- <div style={{ padding: 8, opacity: 0.7 }}>No editable attributes</div>
418
+ <div className="attributes-editor">
419
+ {headerSection}
420
+ {tabsSection}
421
+ {activeSpecialSections.map(renderSpecialSection)}
422
+ <div className="attributes-editor__empty-state">
423
+ No editable attributes
424
+ </div>
425
+ </div>
320
426
  );
321
427
  }
322
428
 
323
429
  return (
324
- <div style={{}}>
325
- {entries.map(([name, type]) => (
326
- <React.Fragment key={name}>
327
- <p style={{ alignSelf: 'center', marginBottom: 4, fontWeight: 700 }}>
328
- {name}
329
- </p>
330
- <div style={{ marginBottom: 16 }}>
331
- <Field
332
- name={name}
333
- type={type}
334
- value={attributes?.[name]}
335
- onChange={(val) => {
336
- const next: NodeData<NodeDefaultAttribute> = {
337
- ...data,
338
- attributes: { ...(attributes ?? {}), [name]: val },
339
- };
340
- onChange(next);
341
- }}
342
- componentType={data?.type}
343
- />
344
- </div>
345
- </React.Fragment>
346
- ))}
430
+ <div className="attributes-editor">
431
+ {headerSection}
432
+ {tabsSection}
433
+ {activeSpecialSections.map(renderSpecialSection)}
434
+
435
+ {activeEntries.map(({ name, type }) => {
436
+ const label = attributeMeta?.[name]?.label ?? name;
437
+ const description = attributeMeta?.[name]?.description;
438
+ const preferredScale = attributeMeta?.[name]?.preferedScale;
439
+ const isBoolean = isBooleanFieldType(type);
440
+ const wrapperClassNames = [
441
+ 'attributes-editor__field-wrapper',
442
+ isBoolean ? 'attributes-editor__field-wrapper--boolean' : '',
443
+ ]
444
+ .filter(Boolean)
445
+ .join(' ');
446
+ return (
447
+ <FieldInfoTooltip key={name} description={description}>
448
+ <div className={wrapperClassNames}>
449
+ {!isBoolean ? (
450
+ <p className="attributes-editor__field-label">{label}</p>
451
+ ) : null}
452
+ <Field
453
+ name={name}
454
+ type={type}
455
+ value={attributes?.[name]}
456
+ onChange={(val) => handleAttributeChange(name, val)}
457
+ componentType={data?.type}
458
+ projectColors={projectColors}
459
+ layoutContext={layoutContext}
460
+ viewAttributes={viewAttributes}
461
+ label={isBoolean ? label : undefined}
462
+ preferredScale={preferredScale}
463
+ />
464
+ </div>
465
+ </FieldInfoTooltip>
466
+ );
467
+ })}
347
468
  </div>
348
469
  );
349
470
  }