@developer_tribe/react-builder 1.0.1 → 1.0.3

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