@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
@@ -1,347 +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
+ import { useLogRender } from './utils/useLogRender';
4
5
  import {
6
+ getAttributeMeta,
5
7
  getAttributeSchema,
6
- getTypeSchema,
7
- getArrayItemType,
8
- isPrimitiveType,
8
+ getPatternByType,
9
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';
10
21
 
11
22
  type AttributesEditorProps = {
12
23
  node: Node;
13
24
  onChange: (next: Node) => void;
14
25
  };
15
26
 
16
- function Field({
17
- name,
18
- type,
19
- value,
20
- onChange,
21
- componentType,
22
- }: {
23
- name: string;
24
- type: string | string[];
25
- value: any;
26
- onChange: (v: any) => void;
27
- // The current node's component type is needed to resolve custom type schemas
28
- componentType?: string;
29
- }) {
30
- // Render enum selector
31
- if (Array.isArray(type)) {
32
- return (
33
- <select
34
- value={value ?? ''}
35
- onChange={(e) => onChange(e.target.value)}
36
- className="input"
37
- >
38
- <option value="">(none)</option>
39
- {type.map((opt) => (
40
- <option key={opt} value={opt}>
41
- {opt}
42
- </option>
43
- ))}
44
- </select>
45
- );
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;
46
50
  }
51
+ >;
52
+
53
+ export function AttributesEditor({ node, onChange }: AttributesEditorProps) {
54
+ useLogRender('AttributesEditor');
55
+ if (!node || isNodeString(node)) return null;
47
56
 
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 : [];
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]);
52
80
 
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>
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>
110
251
  </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
- : '',
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
123
264
  ];
124
- onChange(next);
125
- }}
126
- >
127
- add
128
- </button>
129
- </div>
130
- </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
+ />
131
316
  );
132
- }
317
+ },
318
+ [
319
+ attributeMeta,
320
+ attributes,
321
+ data?.type,
322
+ handleAttributeChange,
323
+ layoutContext,
324
+ projectColors,
325
+ viewAttributes,
326
+ ],
327
+ );
133
328
 
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
- }
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
+ );
199
337
 
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>
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,
225
353
  );
226
- }
227
- }
354
+ });
228
355
 
229
- if (type === 'number') {
230
- return (
231
- <input
232
- type="number"
233
- value={value ?? ''}
234
- onChange={(e) =>
235
- onChange(e.target.value === '' ? undefined : Number(e.target.value))
236
- }
237
- className="input"
238
- />
239
- );
240
- }
241
- if (type === 'boolean') {
242
- return (
243
- <input
244
- type="checkbox"
245
- checked={Boolean(value)}
246
- onChange={(e) => onChange(e.target.checked)}
247
- />
248
- );
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>
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 (
283
401
  <button
402
+ key={tab.id}
284
403
  type="button"
285
- onClick={() => {
286
- const next = [...arr, ''];
287
- onChange(next);
288
- }}
404
+ onClick={() => !disabled && setActiveTab(tab.id)}
405
+ disabled={disabled}
406
+ className={buttonClassNames}
289
407
  >
290
- add
408
+ {tab.label}
409
+ {totalCount > 0 ? ` (${totalCount})` : ''}
291
410
  </button>
292
- </div>
293
- </div>
294
- );
295
- }
296
- return (
297
- <input
298
- type="text"
299
- value={value ?? ''}
300
- onChange={(e) =>
301
- onChange(e.target.value === '' ? undefined : e.target.value)
302
- }
303
- className="input"
304
- />
411
+ );
412
+ })}
413
+ </div>
305
414
  );
306
- }
307
-
308
- export function AttributesEditor({ node, onChange }: AttributesEditorProps) {
309
- if (!node || isNodeString(node)) return null;
310
- const data = node as NodeData<NodeDefaultAttribute>;
311
- const schema = getAttributeSchema(data?.type) ?? {};
312
- const attributes = (data?.attributes ?? {}) as NodeDefaultAttribute;
313
415
 
314
- const entries = Object.entries(schema);
315
416
  if (entries.length === 0) {
316
417
  return (
317
- <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>
318
426
  );
319
427
  }
320
428
 
321
429
  return (
322
- <div style={{}}>
323
- {entries.map(([name, type]) => (
324
- <React.Fragment key={name}>
325
- <p style={{ alignSelf: 'center', marginBottom: 4, fontWeight: 700 }}>
326
- {name}
327
- </p>
328
- <div style={{ marginBottom: 16 }}>
329
- <Field
330
- name={name}
331
- type={type}
332
- value={attributes?.[name]}
333
- onChange={(val) => {
334
- const next: NodeData<NodeDefaultAttribute> = {
335
- ...data,
336
- attributes: { ...(attributes ?? {}), [name]: val },
337
- };
338
- onChange(next);
339
- }}
340
- componentType={data?.type}
341
- />
342
- </div>
343
- </React.Fragment>
344
- ))}
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
+ })}
345
468
  </div>
346
469
  );
347
470
  }