@developer_tribe/react-builder 1.2.8 → 1.2.9

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 (191) hide show
  1. package/dist/AttributesEditor.d.ts +2 -11
  2. package/dist/attribute-analyser/style/native/useExtractImageStyle.d.ts +10 -0
  3. package/dist/attribute-analyser/style/native/useExtractTextStyle.d.ts +9 -0
  4. package/dist/attribute-analyser/style/native/useExtractViewStyle.d.ts +8 -0
  5. package/dist/attribute-analyser/style/web/useExtractImageStyle.d.ts +4 -0
  6. package/dist/attribute-analyser/style/web/useExtractTextStyle.d.ts +4 -0
  7. package/dist/attribute-analyser/style/web/useExtractViewStyle.d.ts +4 -0
  8. package/dist/attributes-editor/AttributesEditorFields.d.ts +18 -0
  9. package/dist/attributes-editor/AttributesEditorView.d.ts +4 -0
  10. package/dist/attributes-editor/attributesEditorModelTypes.d.ts +67 -0
  11. package/dist/attributes-editor/attributesEditorUtils.d.ts +19 -0
  12. package/dist/attributes-editor/useAttributesEditorModel.d.ts +2 -0
  13. package/dist/build-components/BIcon/BIconProps.generated.d.ts +6 -6
  14. package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +3 -3
  15. package/dist/build-components/Button/ButtonProps.generated.d.ts +1 -1
  16. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +2 -2
  17. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +4 -4
  18. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +3 -3
  19. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +1 -1
  20. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +1 -1
  21. package/dist/build-components/Image/ImageProps.generated.d.ts +5 -3
  22. package/dist/build-components/Main/MainProps.generated.d.ts +2 -2
  23. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +1 -1
  24. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +9 -8
  25. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +11 -11
  26. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +15 -9
  27. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +10 -10
  28. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +8 -6
  29. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +6 -3
  30. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +5 -4
  31. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +3 -3
  32. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +3 -3
  33. package/dist/build-components/PaywallBackground/PaywallBackgroundProps.generated.d.ts +1 -1
  34. package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +6 -6
  35. package/dist/build-components/PaywallOptions/PaywallOptionsProps.generated.d.ts +1 -1
  36. package/dist/build-components/PaywallProvider/PaywallProviderProps.generated.d.ts +1 -1
  37. package/dist/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.d.ts +1 -1
  38. package/dist/build-components/RadioButton/RadioButtonProps.generated.d.ts +4 -4
  39. package/dist/build-components/Text/TextProps.generated.d.ts +3 -3
  40. package/dist/build-components/View/ViewProps.generated.d.ts +1 -1
  41. package/dist/build-components/patterns.generated.d.ts +2690 -5804
  42. package/dist/index.cjs.js +5 -5
  43. package/dist/index.cjs.js.map +1 -1
  44. package/dist/index.d.ts +4 -0
  45. package/dist/index.esm.js +5 -5
  46. package/dist/index.esm.js.map +1 -1
  47. package/dist/index.native.cjs.js +6 -4
  48. package/dist/index.native.cjs.js.map +1 -1
  49. package/dist/index.native.d.ts +5 -6
  50. package/dist/index.native.esm.js +6 -4
  51. package/dist/index.native.esm.js.map +1 -1
  52. package/dist/migrations/migratePipe.d.ts +1 -1
  53. package/dist/migrations/migrations/1.1.2_extract_component_attributes_from_style.d.ts +2 -0
  54. package/dist/mockOS/components/PermissionModal.d.ts +1 -2
  55. package/dist/styles.css +1 -1
  56. package/dist/types/PreviewConfig.d.ts +1 -5
  57. package/dist/utils/getMeta.d.ts +5 -0
  58. package/dist/utils/patterns.d.ts +12 -0
  59. package/package.json +2 -1
  60. package/scripts/prebuild/prebuild.js +14 -0
  61. package/scripts/prebuild/utils/createGeneratedProps.js +19 -13
  62. package/scripts/prebuild/utils/index.js +1 -0
  63. package/scripts/prebuild/utils/updateMetaJson.js +66 -0
  64. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +37 -3
  65. package/scripts/prebuild/utils/validatePatternJson.js +27 -2
  66. package/scripts/public/scripts/build/index.js +20 -3
  67. package/scripts/public/scripts/build/info.json +6 -0
  68. package/scripts/public/scripts/build/utils/createComponentsIndex.js +9 -3
  69. package/scripts/public/scripts/build/utils/createRenderNodeGenerated.js +66 -8
  70. package/src/AttributesEditor.tsx +8 -944
  71. package/src/assets/meta.json +4 -0
  72. package/src/assets/samples/carousel-sample.json +1 -1
  73. package/src/assets/samples/getSamples.ts +2 -0
  74. package/src/assets/samples/paywall-1.json +11 -7
  75. package/src/assets/samples/simple-1.json +3 -3
  76. package/src/assets/samples/simple-2.json +3 -3
  77. package/src/assets/samples/unmigrated-builder-1.1.1.json +87 -0
  78. package/src/assets/samples/unmigrated-builder1.json +1 -1
  79. package/src/assets/samples/unvalidated-builder1.json +3 -3
  80. package/src/assets/samples/unvalidated-crash1.json +1 -1
  81. package/src/assets/samples/unvalidated-crashcomponent1.json +1 -1
  82. package/src/assets/samples/vpn-onboard-1.json +1 -1
  83. package/src/assets/samples/vpn-onboard-2.json +1 -1
  84. package/src/assets/samples/vpn-onboard-3.json +1 -1
  85. package/src/assets/samples/vpn-onboard-4.json +1 -1
  86. package/src/assets/samples/vpn-onboard-5.json +1 -1
  87. package/src/assets/samples/vpn-onboard-6.json +1 -1
  88. package/src/attribute-analyser/style/native/useExtractImageStyle.ts +46 -0
  89. package/src/attribute-analyser/style/native/useExtractTextStyle.ts +50 -0
  90. package/src/attribute-analyser/style/native/useExtractViewStyle.ts +32 -0
  91. package/src/attribute-analyser/style/web/useExtractImageStyle.ts +20 -0
  92. package/src/attribute-analyser/style/web/useExtractTextStyle.ts +27 -0
  93. package/src/attribute-analyser/style/web/useExtractViewStyle.ts +20 -0
  94. package/src/attributes-editor/AttributesEditorFields.tsx +248 -0
  95. package/src/attributes-editor/AttributesEditorView.tsx +360 -0
  96. package/src/attributes-editor/attributesEditorModelTypes.ts +86 -0
  97. package/src/attributes-editor/attributesEditorUtils.ts +102 -0
  98. package/src/attributes-editor/useAttributesEditorModel.ts +477 -0
  99. package/src/build-components/BIcon/BIcon.tsx +4 -4
  100. package/src/build-components/BIcon/BIconProps.generated.ts +6 -6
  101. package/src/build-components/BIcon/pattern.json +5 -6
  102. package/src/build-components/BackgroundImage/BackgroundImage.tsx +3 -2
  103. package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +3 -3
  104. package/src/build-components/Button/Button.tsx +2 -2
  105. package/src/build-components/Button/ButtonProps.generated.ts +1 -1
  106. package/src/build-components/Button/pattern.json +17 -15
  107. package/src/build-components/Carousel/Carousel.tsx +1 -1
  108. package/src/build-components/Carousel/CarouselProps.generated.ts +2 -2
  109. package/src/build-components/CarouselButtons/CarouselButtons.tsx +4 -7
  110. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +7 -7
  111. package/src/build-components/CarouselButtons/pattern.json +2 -1
  112. package/src/build-components/CarouselDots/CarouselDots.tsx +2 -2
  113. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +9 -9
  114. package/src/build-components/CarouselItem/CarouselItem.tsx +1 -1
  115. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +1 -1
  116. package/src/build-components/CarouselProvider/CarouselProvider.tsx +1 -1
  117. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +1 -1
  118. package/src/build-components/Image/Image.tsx +1 -1
  119. package/src/build-components/Image/ImageProps.generated.ts +5 -3
  120. package/src/build-components/Image/pattern.json +10 -9
  121. package/src/build-components/Main/Main.tsx +2 -2
  122. package/src/build-components/Main/MainProps.generated.ts +2 -2
  123. package/src/build-components/Main/pattern.json +2 -1
  124. package/src/build-components/Onboard/OnboardProps.generated.ts +1 -1
  125. package/src/build-components/OnboardButton/OnboardButton.tsx +7 -6
  126. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +14 -13
  127. package/src/build-components/OnboardButton/pattern.json +9 -7
  128. package/src/build-components/OnboardButtons/OnboardButtons.tsx +31 -31
  129. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +14 -14
  130. package/src/build-components/OnboardButtons/pattern.json +9 -7
  131. package/src/build-components/OnboardDot/OnboardDot.tsx +7 -6
  132. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +26 -9
  133. package/src/build-components/OnboardFooter/OnboardFooter.tsx +17 -16
  134. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +10 -10
  135. package/src/build-components/OnboardFooter/pattern.json +16 -14
  136. package/src/build-components/OnboardImage/OnboardImage.tsx +8 -8
  137. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +8 -6
  138. package/src/build-components/OnboardImage/pattern.json +2 -1
  139. package/src/build-components/OnboardItem/OnboardItem.tsx +1 -1
  140. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +6 -3
  141. package/src/build-components/OnboardItem/pattern.json +2 -1
  142. package/src/build-components/OnboardProvider/OnboardProvider.tsx +1 -1
  143. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +5 -4
  144. package/src/build-components/OnboardProvider/pattern.json +2 -1
  145. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +3 -3
  146. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +3 -3
  147. package/src/build-components/PaywallBackground/PaywallBackground.tsx +1 -1
  148. package/src/build-components/PaywallBackground/PaywallBackgroundProps.generated.ts +1 -1
  149. package/src/build-components/PaywallCloseButton/PaywallCloseButton.tsx +5 -4
  150. package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +6 -6
  151. package/src/build-components/PaywallOptions/PaywallOptionButton.tsx +1 -1
  152. package/src/build-components/PaywallOptions/PaywallOptionsProps.generated.ts +1 -1
  153. package/src/build-components/PaywallProvider/PaywallProvider.tsx +1 -1
  154. package/src/build-components/PaywallProvider/PaywallProviderProps.generated.ts +1 -1
  155. package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButton.tsx +1 -1
  156. package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.ts +1 -1
  157. package/src/build-components/RadioButton/RadioButton.tsx +5 -5
  158. package/src/build-components/RadioButton/RadioButtonProps.generated.ts +4 -4
  159. package/src/build-components/RadioButton/pattern.json +9 -7
  160. package/src/build-components/Text/Text.tsx +6 -6
  161. package/src/build-components/Text/TextProps.generated.ts +3 -3
  162. package/src/build-components/Text/pattern.json +15 -11
  163. package/src/build-components/View/View.tsx +1 -1
  164. package/src/build-components/View/ViewProps.generated.ts +1 -1
  165. package/src/build-components/View/pattern.json +71 -66
  166. package/src/build-components/patterns.generated.ts +3059 -6008
  167. package/src/components/AttributesEditorPanel.tsx +2 -2
  168. package/src/index.native.ts +6 -9
  169. package/src/index.ts +4 -0
  170. package/src/migrations/migratePipe.ts +7 -3
  171. package/src/migrations/migrations/1.1.2_extract_component_attributes_from_style.ts +211 -0
  172. package/src/mockOS/components/MockOSRouter.tsx +3 -1
  173. package/src/mockOS/components/PermissionModal.tsx +20 -160
  174. package/src/mockOS/components/SubscriptionModal.tsx +41 -278
  175. package/src/pages/ProjectPage.tsx +12 -6
  176. package/src/styles/components/_attributes-editor.scss +122 -0
  177. package/src/styles/components/_mockos-router.scss +388 -0
  178. package/src/styles/components/_onboard.scss +23 -0
  179. package/src/styles/index.scss +1 -0
  180. package/src/types/PreviewConfig.ts +1 -5
  181. package/src/utils/analyseNodeByPatterns.ts +39 -4
  182. package/src/utils/extractTextStyle/extractTextStyle.ts +4 -1
  183. package/src/utils/getMeta.ts +15 -0
  184. package/src/utils/patterns.ts +47 -4
  185. package/dist/hooks/useExtractImageStyle.d.ts +0 -5
  186. package/dist/hooks/useExtractTextStyle.d.ts +0 -3
  187. package/dist/hooks/useExtractViewStyle.d.ts +0 -3
  188. package/src/hooks/useExtractImageStyle.ts +0 -30
  189. package/src/hooks/useExtractTextStyle.ts +0 -34
  190. package/src/hooks/useExtractViewStyle.ts +0 -30
  191. package/src/migrations/migrations/1.1.0_normalize_style_attributes.ts +0 -80
@@ -1,949 +1,13 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { Node, NodeData, NodeDefaultAttribute } from './types/Node';
3
- import type { ProjectColorTokenMap, ProjectColors } from './types/Project';
4
- import { isNodeString } from './utils/analyseNode';
1
+ import React from 'react';
2
+ import type { AttributesEditorProps } from './attributes-editor/attributesEditorModelTypes';
5
3
  import { useLogRender } from './utils/useLogRender';
6
- import {
7
- getAttributeMeta,
8
- getAttributeSchema,
9
- getPatternByType,
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 { toPreferredScale } from './attributes-editor/SizeField';
15
- import {
16
- LayoutContext,
17
- SchemaEntry,
18
- isBooleanFieldType,
19
- } from './attributes-editor/types';
20
- import { FieldInfoTooltip } from './attributes-editor/FieldInfoTooltip';
21
- import type { ViewPropsGenerated } from './build-components/View/ViewProps.generated';
22
- import useNode from './build-components/useNode';
23
- import { MockableFeatureModal } from './modals';
24
- import { Icon } from './components/Icon.generated';
25
- import { IconPickerModal } from './modals/IconPickerModal';
26
- import type { IconsType } from './types/Icons';
27
- import Modal from './modals/Modal';
28
- import { loadFontFamily } from './utils/loadFontFamily';
29
- import { fontsDebug } from './utils/fontsDebug';
4
+ import { AttributesEditorView } from './attributes-editor/AttributesEditorView';
5
+ import { useAttributesEditorModel } from './attributes-editor/useAttributesEditorModel';
30
6
 
31
- type AttributesEditorProps = {
32
- node: Node;
33
- onChange: (next: Node) => void;
34
- projectColors?: ProjectColors;
35
- };
36
-
37
- function isPlainObject(value: unknown): value is Record<string, unknown> {
38
- return typeof value === 'object' && value !== null && !Array.isArray(value);
39
- }
40
-
41
- type TabId = 'style' | 'container' | 'other';
42
-
43
- type TabConfig = {
44
- id: TabId;
45
- label: string;
46
- entries: SchemaEntry[];
47
- };
48
-
49
- type SpecialSection = {
50
- key: string;
51
- entries: SchemaEntry[];
52
- meta?: {
53
- label?: string;
54
- description?: string;
55
- sort?: number;
56
- };
57
- };
58
-
59
- type TabContentInfo = Record<
60
- TabId,
61
- {
62
- baseCount: number;
63
- specialCount: number;
64
- }
65
- >;
66
-
67
- export function AttributesEditor({
68
- node,
69
- onChange,
70
- projectColors,
71
- }: AttributesEditorProps) {
7
+ export function AttributesEditor(props: AttributesEditorProps) {
72
8
  useLogRender('AttributesEditor');
73
- if (!node || isNodeString(node)) return null;
74
-
75
- const baseData = node as NodeData<NodeDefaultAttribute>;
76
- const data = useNode(baseData);
77
- const {
78
- appConfig,
79
- fonts: projectFonts,
80
- loadedFonts,
81
- markFontLoaded,
82
- addError,
83
- } = useRenderStore((state) => ({
84
- appConfig: state.appConfig,
85
- fonts: state.fonts,
86
- loadedFonts: state.loadedFonts,
87
- markFontLoaded: state.markFontLoaded,
88
- addError: state.addError,
89
- }));
90
- const [activeFontField, setActiveFontField] = useState<string | null>(null);
91
- const [isFontLoading, setIsFontLoading] = useState(false);
92
- const [fontLoadError, setFontLoadError] = useState<string | null>(null);
93
- const projectColorsForPicker = useMemo<ProjectColors | undefined>(() => {
94
- if (projectColors) {
95
- return projectColors;
96
- }
97
-
98
- const addColor = (collection: Set<string>, value?: string) => {
99
- if (typeof value !== 'string') return;
100
- const trimmed = value.trim();
101
- if (!trimmed) return;
102
- collection.add(trimmed);
103
- };
104
-
105
- const fallback = new Set<string>();
106
- const styles = [
107
- appConfig?.screenStyle?.light,
108
- appConfig?.screenStyle?.dark,
109
- ].filter(Boolean) as Array<{
110
- backgroundColor?: string;
111
- color?: string;
112
- seperatorColor?: string;
113
- }>;
114
- styles.forEach((style) => {
115
- ['backgroundColor', 'color', 'seperatorColor'].forEach((key) => {
116
- const value = style?.[key as keyof typeof style];
117
- addColor(fallback, value);
118
- });
119
- });
120
-
121
- if (fallback.size === 0) {
122
- return undefined;
123
- }
124
-
125
- const fallbackRecord: ProjectColorTokenMap = {};
126
- Array.from(fallback).forEach((color, index) => {
127
- fallbackRecord[`FALLBACK_${index + 1}`] = color;
128
- });
129
-
130
- return { STATIC_COLORS: fallbackRecord };
131
- }, [appConfig, projectColors]);
132
-
133
- const schema = getAttributeSchema(data?.type) ?? {};
134
- const attributeMeta = getAttributeMeta(data?.type);
135
- const attributes = (data?.attributes ?? {}) as NodeDefaultAttribute;
136
- const styleAttributeKeys = useMemo(() => {
137
- // schemaVersion=2 stores style-like props under attributes.style; keep legacy support for flat attrs.
138
- const viewSchema = getAttributeSchema('View') ?? {};
139
- const textSchema = getAttributeSchema('Text') ?? {};
140
- return new Set([...Object.keys(viewSchema), ...Object.keys(textSchema)]);
141
- }, []);
142
- const getAttributeValue = useCallback(
143
- (name: string) => {
144
- const direct = (attributes as Record<string, unknown>)?.[name];
145
- if (direct !== undefined && direct !== null) return direct;
146
- const styleBag = (attributes as any)?.style as
147
- | Record<string, unknown>
148
- | undefined;
149
- return styleBag?.[name];
150
- },
151
- [attributes],
152
- );
153
- const viewAttributes = useMemo<
154
- Partial<ViewPropsGenerated['attributes']> | undefined
155
- >(
156
- () =>
157
- data?.type === 'View'
158
- ? (attributes as ViewPropsGenerated['attributes'])
159
- : undefined,
160
- [attributes, data?.type],
161
- );
162
-
163
- const layoutContext = useMemo<LayoutContext>(
164
- () => ({
165
- flexDirection:
166
- attributes?.flexDirection as LayoutContext['flexDirection'],
167
- alignItems: attributes?.alignItems as LayoutContext['alignItems'],
168
- justifyContent:
169
- attributes?.justifyContent as LayoutContext['justifyContent'],
170
- }),
171
- [
172
- attributes?.flexDirection,
173
- attributes?.alignItems,
174
- attributes?.justifyContent,
175
- ],
176
- );
177
-
178
- const patternForType = useMemo(
179
- () => (data?.type ? getPatternByType(data.type) : undefined),
180
- [data?.type],
181
- );
182
-
183
- const componentMeta = patternForType?.meta;
184
-
185
- const entries = useMemo(
186
- () =>
187
- Object.entries(schema).filter(([, type]) =>
188
- typeof type === 'string' ? type !== 'never' : true,
189
- ),
190
- [schema],
191
- );
192
-
193
- const visibleEntries = useMemo(() => {
194
- if (!componentMeta?.hideAllAttributes) return entries;
195
- return entries.filter(([name]) => {
196
- const meta = attributeMeta?.[name];
197
- return meta?.forceVisible === true || meta?.override === true;
198
- });
199
- }, [attributeMeta, componentMeta?.hideAllAttributes, entries]);
200
-
201
- const componentTitle = componentMeta?.label ?? data?.type ?? 'Component';
202
- const componentDescription = componentMeta?.description;
203
-
204
- const mockableFeatureKeys = useMemo(() => {
205
- const mockable = componentMeta?.mockableFeatures;
206
- if (!mockable || typeof mockable !== 'object') return [];
207
- return Object.entries(mockable)
208
- .filter(([, enabled]) => enabled === true)
209
- .map(([key]) => key)
210
- .filter((key) => typeof key === 'string' && key.trim().length > 0)
211
- .sort((a, b) => a.localeCompare(b));
212
- }, [componentMeta?.mockableFeatures]);
213
-
214
- const [activeMockableFeature, setActiveMockableFeature] = useState<
215
- string | null
216
- >(null);
217
-
218
- const [activeIconField, setActiveIconField] = useState<string | null>(null);
219
-
220
- const headerSection = (
221
- <div className="attributes-editor__component-meta">
222
- <p className="attributes-editor__component-title">{componentTitle}</p>
223
- {componentDescription ? (
224
- <p className="attributes-editor__component-description">
225
- {componentDescription}
226
- </p>
227
- ) : null}
228
- </div>
229
- );
230
-
231
- const mockableSection =
232
- mockableFeatureKeys.length > 0 ? (
233
- <section className="attributes-editor__mockable">
234
- <p className="attributes-editor__mockable-title">Mockable</p>
235
- <table className="attributes-editor__mockable-table">
236
- <tbody>
237
- {mockableFeatureKeys.map((key) => (
238
- <tr key={key} className="attributes-editor__mockable-row">
239
- <td className="attributes-editor__mockable-name">{key}</td>
240
- <td className="attributes-editor__mockable-action">
241
- <button
242
- type="button"
243
- className="editor-button"
244
- onClick={() => setActiveMockableFeature(key)}
245
- >
246
- {key}
247
- </button>
248
- </td>
249
- </tr>
250
- ))}
251
- </tbody>
252
- </table>
253
- </section>
254
- ) : null;
255
-
256
- const { grouped, specialGroups } = useMemo(() => {
257
- const groups: Record<TabId, SchemaEntry[]> = {
258
- style: [],
259
- container: [],
260
- other: [],
261
- };
262
- const specials: Record<string, SchemaEntry[]> = {};
263
-
264
- const getSortOrder = (name: string) =>
265
- attributeMeta?.[name]?.sort ?? Number.MAX_SAFE_INTEGER;
266
- const compareEntries = (a: SchemaEntry, b: SchemaEntry) => {
267
- const order = getSortOrder(a.name) - getSortOrder(b.name);
268
- return order !== 0 ? order : a.name.localeCompare(b.name);
269
- };
270
-
271
- visibleEntries.forEach(([name, type]) => {
272
- const meta = attributeMeta?.[name];
273
- const specialCategory = meta?.specialCategory;
274
- if (typeof specialCategory === 'string') {
275
- const normalizedSpecialCategory = specialCategory.trim();
276
- if (normalizedSpecialCategory) {
277
- if (!specials[normalizedSpecialCategory]) {
278
- specials[normalizedSpecialCategory] = [];
279
- }
280
- specials[normalizedSpecialCategory].push({ name, type });
281
- return;
282
- }
283
- }
284
-
285
- const metaCategory = meta?.category;
286
- const normalized =
287
- metaCategory === 'style'
288
- ? 'style'
289
- : metaCategory === 'container'
290
- ? 'container'
291
- : 'other';
292
- groups[normalized].push({ name, type });
293
- });
294
-
295
- Object.values(groups).forEach((list) => {
296
- list.sort(compareEntries);
297
- });
298
-
299
- Object.values(specials).forEach((list) => {
300
- list.sort(compareEntries);
301
- });
302
-
303
- return { grouped: groups, specialGroups: specials };
304
- }, [attributeMeta, visibleEntries]);
305
-
306
- const specialSectionsByTab = useMemo<Record<TabId, SpecialSection[]>>(() => {
307
- const buckets: Record<TabId, SpecialSection[]> = {
308
- style: [],
309
- container: [],
310
- other: [],
311
- };
312
-
313
- const compareSections = (a: SpecialSection, b: SpecialSection) => {
314
- const aSort = a.meta?.sort ?? Number.MAX_SAFE_INTEGER;
315
- const bSort = b.meta?.sort ?? Number.MAX_SAFE_INTEGER;
316
- const order = aSort - bSort;
317
- return order !== 0 ? order : a.key.localeCompare(b.key);
318
- };
319
-
320
- Object.entries(specialGroups).forEach(([categoryKey, categoryEntries]) => {
321
- if (!categoryEntries.length) return;
322
- const metaForCategory = componentMeta?.specialCategories?.[categoryKey];
323
- const targetCategory = (
324
- metaForCategory?.category === 'style'
325
- ? 'style'
326
- : metaForCategory?.category === 'container'
327
- ? 'container'
328
- : 'other'
329
- ) as TabId;
330
- buckets[targetCategory].push({
331
- key: categoryKey,
332
- entries: categoryEntries,
333
- meta: metaForCategory,
334
- });
335
- });
336
-
337
- (Object.keys(buckets) as TabId[]).forEach((tabId) => {
338
- buckets[tabId].sort(compareSections);
339
- });
340
-
341
- return buckets;
342
- }, [componentMeta?.specialCategories, specialGroups]);
343
-
344
- const handleAttributeChange = useCallback(
345
- (name: string, val: unknown) => {
346
- const prevAttrs =
347
- ((baseData?.attributes ?? {}) as Record<string, unknown>) ?? {};
348
- const isStyleKey = styleAttributeKeys.has(name);
349
- const nextAttrs: Record<string, unknown> = { ...prevAttrs };
350
- if (isStyleKey) {
351
- const prevStyle = (
352
- isPlainObject(nextAttrs.style) ? nextAttrs.style : {}
353
- ) as Record<string, unknown>;
354
- nextAttrs.style = { ...prevStyle, [name]: val };
355
- // Normalize away legacy flat style keys once edited.
356
- if (name in nextAttrs) delete nextAttrs[name];
357
- } else {
358
- nextAttrs[name] = val;
359
- }
360
- const next: NodeData<NodeDefaultAttribute> = {
361
- ...baseData,
362
- attributes: nextAttrs as NodeDefaultAttribute,
363
- };
364
- onChange(next);
365
- },
366
- [baseData, onChange, styleAttributeKeys],
367
- );
368
-
369
- const renderIconTypeField = useCallback(
370
- (name: string, currentValue: unknown) => {
371
- const normalized =
372
- typeof currentValue === 'string'
373
- ? (currentValue as IconsType)
374
- : undefined;
375
- return (
376
- <>
377
- <button
378
- type="button"
379
- onClick={() => setActiveIconField(name)}
380
- style={{
381
- width: '100%',
382
- display: 'flex',
383
- alignItems: 'center',
384
- justifyContent: 'space-between',
385
- gap: 8,
386
- borderRadius: 6,
387
- border: '1px solid #ddd',
388
- padding: '8px 10px',
389
- background: 'hsl(var(--card, var(--rb-card, 0 0% 100%)))',
390
- cursor: 'pointer',
391
- }}
392
- >
393
- <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
394
- {normalized ? <Icon iconType={normalized} size={18} /> : null}
395
- <span style={{ fontWeight: 500 }}>
396
- {normalized ?? 'Select icon'}
397
- </span>
398
- </span>
399
- <span style={{ fontSize: 12, color: '#666' }}>Open</span>
400
- </button>
401
-
402
- {activeIconField === name ? (
403
- <IconPickerModal
404
- value={normalized}
405
- onSelect={(iconName) => {
406
- handleAttributeChange(name, iconName);
407
- setActiveIconField(null);
408
- }}
409
- onClose={() => setActiveIconField(null)}
410
- onClear={() => {
411
- handleAttributeChange(name, undefined);
412
- setActiveIconField(null);
413
- }}
414
- />
415
- ) : null}
416
- </>
417
- );
418
- },
419
- [activeIconField, handleAttributeChange],
420
- );
421
-
422
- const renderFontFamilyField = useCallback(
423
- (name: string, currentValue: unknown) => {
424
- const normalized =
425
- typeof currentValue === 'string' && currentValue.trim().length > 0
426
- ? currentValue.trim()
427
- : undefined;
428
- const fontsList = Array.isArray(projectFonts) ? projectFonts : [];
429
- const loaded = Array.isArray(loadedFonts) ? loadedFonts : [];
430
- const isOpen = activeFontField === name;
431
- return (
432
- <>
433
- <button
434
- type="button"
435
- onClick={() => {
436
- setFontLoadError(null);
437
- setActiveFontField(name);
438
- }}
439
- style={{
440
- width: '100%',
441
- display: 'flex',
442
- alignItems: 'center',
443
- justifyContent: 'space-between',
444
- gap: 8,
445
- borderRadius: 6,
446
- border: '1px solid #ddd',
447
- padding: '8px 10px',
448
- background: 'hsl(var(--card, var(--rb-card, 0 0% 100%)))',
449
- cursor: 'pointer',
450
- }}
451
- >
452
- <span style={{ flex: 1, textAlign: 'left', fontWeight: 500 }}>
453
- {normalized ?? 'Select font'}
454
- </span>
455
- <span style={{ fontSize: 12, color: '#666' }}>Open</span>
456
- </button>
457
-
458
- {isOpen ? (
459
- <Modal
460
- ariaLabelledBy="font-family-picker-title"
461
- onClose={() => {
462
- if (isFontLoading) return;
463
- setActiveFontField(null);
464
- }}
465
- closeOnOverlayClick={!isFontLoading}
466
- closeOnEsc={!isFontLoading}
467
- >
468
- <div style={{ display: 'grid', gap: 12 }}>
469
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
470
- <p
471
- id="font-family-picker-title"
472
- style={{ margin: 0, fontWeight: 700 }}
473
- >
474
- Select Font
475
- </p>
476
- <div style={{ flex: 1 }} />
477
- <button
478
- type="button"
479
- className="editor-button"
480
- onClick={() => {
481
- handleAttributeChange(name, undefined);
482
- setFontLoadError(null);
483
- setActiveFontField(null);
484
- }}
485
- disabled={isFontLoading}
486
- >
487
- Clear
488
- </button>
489
- <button
490
- type="button"
491
- className="editor-button"
492
- onClick={() => setActiveFontField(null)}
493
- disabled={isFontLoading}
494
- >
495
- Close
496
- </button>
497
- </div>
498
-
499
- {isFontLoading ? (
500
- <div style={{ fontSize: 12, color: '#666' }}>
501
- Loading font…
502
- </div>
503
- ) : null}
504
- {fontLoadError ? (
505
- <div style={{ fontSize: 12, color: '#b00020' }}>
506
- {fontLoadError}
507
- </div>
508
- ) : null}
509
-
510
- <div style={{ fontSize: 12, color: '#666' }}>
511
- {fontsList.length} fonts
512
- </div>
513
-
514
- <div
515
- style={{
516
- display: 'grid',
517
- gridTemplateColumns:
518
- 'repeat(auto-fill, minmax(180px, 1fr))',
519
- gap: 8,
520
- maxHeight: '60vh',
521
- overflow: 'auto',
522
- paddingRight: 4,
523
- }}
524
- >
525
- {fontsList.map((font) => {
526
- const fontName = font?.name;
527
- if (typeof fontName !== 'string' || !fontName.trim()) {
528
- return null;
529
- }
530
- const familyName = fontName.trim();
531
- const isActive = normalized === familyName;
532
- const isLoaded = loaded.includes(familyName);
533
- return (
534
- <button
535
- key={familyName}
536
- type="button"
537
- disabled={isFontLoading}
538
- onClick={async () => {
539
- fontsDebug.info(
540
- 'AttributesEditor: select fontFamily',
541
- {
542
- field: name,
543
- familyName,
544
- wasLoaded: isLoaded,
545
- },
546
- );
547
- setFontLoadError(null);
548
- handleAttributeChange(name, familyName);
549
- if (isLoaded) return;
550
- setIsFontLoading(true);
551
- try {
552
- fontsDebug.info(
553
- 'AttributesEditor: loadFontFamily start',
554
- { familyName },
555
- );
556
- await loadFontFamily(fontsList, familyName, {
557
- forceFetch: true,
558
- });
559
- markFontLoaded(familyName);
560
- fontsDebug.info(
561
- 'AttributesEditor: loadFontFamily success',
562
- { familyName },
563
- );
564
- } catch (e) {
565
- const msg =
566
- e instanceof Error ? e.message : String(e);
567
- setFontLoadError(
568
- `Failed to load "${familyName}": ${msg}`,
569
- );
570
- addError(
571
- `Failed to load font "${familyName}": ${msg}`,
572
- );
573
- fontsDebug.compactError(
574
- 'AttributesEditor: loadFontFamily failed',
575
- e,
576
- { familyName },
577
- );
578
- } finally {
579
- setIsFontLoading(false);
580
- }
581
- }}
582
- style={{
583
- display: 'flex',
584
- alignItems: 'center',
585
- justifyContent: 'space-between',
586
- gap: 8,
587
- borderRadius: 8,
588
- border: isActive
589
- ? '2px solid #222'
590
- : '1px solid #ddd',
591
- padding: '8px 10px',
592
- background:
593
- 'hsl(var(--card, var(--rb-card, 0 0% 100%)))',
594
- cursor: isFontLoading ? 'not-allowed' : 'pointer',
595
- }}
596
- aria-label={`Select font ${familyName}`}
597
- >
598
- <span
599
- style={{
600
- fontSize: 12,
601
- fontWeight: isActive ? 700 : 500,
602
- textAlign: 'left',
603
- overflow: 'hidden',
604
- textOverflow: 'ellipsis',
605
- whiteSpace: 'nowrap',
606
- }}
607
- title={familyName}
608
- >
609
- {familyName}
610
- </span>
611
- <span style={{ fontSize: 11, color: '#666' }}>
612
- {isLoaded ? 'Loaded' : 'Not loaded'}
613
- </span>
614
- </button>
615
- );
616
- })}
617
- </div>
618
- </div>
619
- </Modal>
620
- ) : null}
621
- </>
622
- );
623
- },
624
- [
625
- activeFontField,
626
- addError,
627
- handleAttributeChange,
628
- isFontLoading,
629
- loadedFonts,
630
- markFontLoaded,
631
- projectFonts,
632
- fontLoadError,
633
- ],
634
- );
635
-
636
- const handleChildrenChange = useCallback(
637
- (val: string) => {
638
- const next: NodeData<NodeDefaultAttribute> = {
639
- ...baseData,
640
- children: val,
641
- };
642
- onChange(next);
643
- },
644
- [baseData, onChange],
645
- );
646
-
647
- const renderSpecialSection = useCallback(
648
- ({ key, entries: sectionEntries, meta }: SpecialSection) => {
649
- if (key === 'size') {
650
- const normalizedTitle =
651
- meta?.label && meta.label.trim().length > 0
652
- ? meta.label
653
- : key.charAt(0).toUpperCase() + key.slice(1);
654
- const normalizedDescription = meta?.description;
655
- const orderedEntries = [...sectionEntries].sort((a, b) => {
656
- const preferredOrder = [
657
- 'width',
658
- 'height',
659
- 'minWidth',
660
- 'minHeight',
661
- 'maxWidth',
662
- 'maxHeight',
663
- ];
664
- const aIndex = preferredOrder.indexOf(a.name);
665
- const bIndex = preferredOrder.indexOf(b.name);
666
- const aRank = aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex;
667
- const bRank = bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex;
668
- return aRank !== bRank ? aRank - bRank : a.name.localeCompare(b.name);
669
- });
670
- return (
671
- <section key={key} className="special-category-section">
672
- <div className="special-category-section__header">
673
- <p className="special-category-section__title">
674
- {normalizedTitle}
675
- </p>
676
- </div>
677
- {normalizedDescription ? (
678
- <p className="special-category-section__description">
679
- {normalizedDescription}
680
- </p>
681
- ) : null}
682
- <div className="attributes-editor__size-grid">
683
- {orderedEntries.map(({ name, type }) => {
684
- const label = attributeMeta?.[name]?.label ?? name;
685
- const description = attributeMeta?.[name]?.description;
686
- const preferredScale = toPreferredScale(
687
- attributeMeta?.[name]?.preferedScale,
688
- );
689
- const currentValue = getAttributeValue(name);
690
- const isBoolean = isBooleanFieldType(type);
691
- const wrapperClassNames = [
692
- 'attributes-editor__field-wrapper',
693
- isBoolean ? 'attributes-editor__field-wrapper--boolean' : '',
694
- ]
695
- .filter(Boolean)
696
- .join(' ');
697
- return (
698
- <FieldInfoTooltip key={name} description={description}>
699
- <div
700
- className={`${wrapperClassNames} attributes-editor__size-grid-item`}
701
- >
702
- {!isBoolean ? (
703
- <p className="attributes-editor__field-label">
704
- {label}
705
- </p>
706
- ) : null}
707
- {type === 'iconType' ? (
708
- renderIconTypeField(name, currentValue)
709
- ) : type === 'fontFamily' ? (
710
- renderFontFamilyField(name, currentValue)
711
- ) : (
712
- <Field
713
- name={name}
714
- type={type}
715
- value={currentValue}
716
- onChange={(val) => handleAttributeChange(name, val)}
717
- componentType={data?.type}
718
- projectColors={projectColorsForPicker}
719
- layoutContext={layoutContext}
720
- viewAttributes={viewAttributes}
721
- label={isBoolean ? label : undefined}
722
- preferredScale={preferredScale}
723
- />
724
- )}
725
- </div>
726
- </FieldInfoTooltip>
727
- );
728
- })}
729
- </div>
730
- </section>
731
- );
732
- }
733
- return (
734
- <SpecialCategorySection
735
- key={key}
736
- category={key}
737
- entries={sectionEntries}
738
- attributeMeta={attributeMeta}
739
- attributes={attributes}
740
- onAttributeChange={handleAttributeChange}
741
- componentType={data?.type}
742
- projectColors={projectColorsForPicker}
743
- layoutContext={layoutContext}
744
- viewAttributes={viewAttributes}
745
- meta={meta}
746
- />
747
- );
748
- },
749
- [
750
- attributeMeta,
751
- attributes,
752
- data?.type,
753
- handleAttributeChange,
754
- layoutContext,
755
- projectColorsForPicker,
756
- renderIconTypeField,
757
- viewAttributes,
758
- ],
759
- );
760
-
761
- const tabs = useMemo<TabConfig[]>(
762
- () => [
763
- { id: 'container', label: 'Container', entries: grouped.container },
764
- { id: 'style', label: 'Styles', entries: grouped.style },
765
- { id: 'other', label: 'Others', entries: grouped.other },
766
- ],
767
- [grouped],
768
- );
769
-
770
- const tabContentInfo = useMemo<TabContentInfo>(() => {
771
- const info: TabContentInfo = {
772
- style: { baseCount: 0, specialCount: 0 },
773
- container: { baseCount: 0, specialCount: 0 },
774
- other: { baseCount: 0, specialCount: 0 },
775
- };
776
-
777
- tabs.forEach((tab) => {
778
- info[tab.id].baseCount = tab.entries.length;
779
- });
780
-
781
- (Object.keys(specialSectionsByTab) as TabId[]).forEach((tabId) => {
782
- info[tabId].specialCount = specialSectionsByTab[tabId].reduce(
783
- (sum, section) => sum + section.entries.length,
784
- 0,
785
- );
786
- });
787
-
788
- return info;
789
- }, [specialSectionsByTab, tabs]);
790
-
791
- const firstAvailableTab = useMemo<TabId>(
792
- () =>
793
- tabs.find((tab) => {
794
- const counts = tabContentInfo[tab.id];
795
- return counts.baseCount + counts.specialCount > 0;
796
- })?.id ?? 'other',
797
- [tabContentInfo, tabs],
798
- );
799
-
800
- const [activeTab, setActiveTab] = useState<TabId>(firstAvailableTab);
801
-
802
- useEffect(() => {
803
- setActiveTab((prev) => {
804
- const counts = tabContentInfo[prev];
805
- if (counts && counts.baseCount + counts.specialCount > 0) {
806
- return prev;
807
- }
808
- return firstAvailableTab;
809
- });
810
- }, [firstAvailableTab, tabContentInfo]);
811
-
812
- const activeEntries =
813
- tabs.find((tab) => tab.id === activeTab)?.entries ??
814
- tabs.find((tab) => tab.id === firstAvailableTab)?.entries ??
815
- [];
816
-
817
- const activeSpecialSections = specialSectionsByTab[activeTab] ?? [];
818
-
819
- const hasStringChildren =
820
- !!patternForType?.pattern &&
821
- (patternForType.pattern as { children?: unknown }).children === 'string';
822
-
823
- const childrenValue =
824
- typeof (baseData.children as unknown) === 'string'
825
- ? (baseData.children as string)
826
- : '';
827
-
828
- const childrenSection = hasStringChildren ? (
829
- <div className="attributes-editor__field-wrapper attributes-editor__field-wrapper--children">
830
- <p className="attributes-editor__field-label">Text</p>
831
- <input
832
- type="text"
833
- className="attributes-editor__text-input"
834
- value={childrenValue}
835
- onChange={(e) => handleChildrenChange(e.target.value)}
836
- />
837
- </div>
838
- ) : null;
839
-
840
- const tabsSection = (
841
- <div className="attributes-editor__tabs">
842
- {tabs.map((tab) => {
843
- const isActive = tab.id === activeTab;
844
- const counts = tabContentInfo[tab.id];
845
- const totalCount = counts.baseCount + counts.specialCount;
846
- const disabled = totalCount === 0;
847
- const buttonClassNames = [
848
- 'attributes-editor__tab-button',
849
- isActive ? 'attributes-editor__tab-button--active' : '',
850
- ]
851
- .filter(Boolean)
852
- .join(' ');
853
- return (
854
- <button
855
- key={tab.id}
856
- type="button"
857
- onClick={() => !disabled && setActiveTab(tab.id)}
858
- disabled={disabled}
859
- className={buttonClassNames}
860
- >
861
- {tab.label}
862
- {totalCount > 0 ? ` (${totalCount})` : ''}
863
- </button>
864
- );
865
- })}
866
- </div>
867
- );
868
-
869
- if (visibleEntries.length === 0) {
870
- return (
871
- <div className="attributes-editor">
872
- {headerSection}
873
- {mockableSection}
874
- {tabsSection}
875
- {childrenSection}
876
- {activeSpecialSections.map(renderSpecialSection)}
877
- <div className="attributes-editor__empty-state">
878
- No editable attributes
879
- </div>
880
- {activeMockableFeature ? (
881
- <MockableFeatureModal
882
- featureKey={activeMockableFeature}
883
- onClose={() => setActiveMockableFeature(null)}
884
- />
885
- ) : null}
886
- </div>
887
- );
888
- }
889
-
890
- return (
891
- <div className="attributes-editor">
892
- {headerSection}
893
- {mockableSection}
894
- {tabsSection}
895
- {childrenSection}
896
- {activeSpecialSections.map(renderSpecialSection)}
897
-
898
- {activeEntries.map(({ name, type }) => {
899
- const label = attributeMeta?.[name]?.label ?? name;
900
- const description = attributeMeta?.[name]?.description;
901
- const preferredScale = toPreferredScale(
902
- attributeMeta?.[name]?.preferedScale,
903
- );
904
- const isBoolean = isBooleanFieldType(type);
905
- const wrapperClassNames = [
906
- 'attributes-editor__field-wrapper',
907
- isBoolean ? 'attributes-editor__field-wrapper--boolean' : '',
908
- ]
909
- .filter(Boolean)
910
- .join(' ');
911
- return (
912
- <FieldInfoTooltip key={name} description={description}>
913
- <div className={wrapperClassNames}>
914
- {!isBoolean ? (
915
- <p className="attributes-editor__field-label">{label}</p>
916
- ) : null}
917
- {type === 'iconType' ? (
918
- renderIconTypeField(name, getAttributeValue(name))
919
- ) : type === 'fontFamily' ? (
920
- renderFontFamilyField(name, getAttributeValue(name))
921
- ) : (
922
- <Field
923
- name={name}
924
- type={type}
925
- value={getAttributeValue(name)}
926
- onChange={(val) => handleAttributeChange(name, val)}
927
- componentType={data?.type}
928
- projectColors={projectColorsForPicker}
929
- layoutContext={layoutContext}
930
- viewAttributes={viewAttributes}
931
- label={isBoolean ? label : undefined}
932
- preferredScale={preferredScale}
933
- />
934
- )}
935
- </div>
936
- </FieldInfoTooltip>
937
- );
938
- })}
939
- {activeMockableFeature ? (
940
- <MockableFeatureModal
941
- featureKey={activeMockableFeature}
942
- onClose={() => setActiveMockableFeature(null)}
943
- />
944
- ) : null}
945
- </div>
946
- );
9
+ const model = useAttributesEditorModel(props);
10
+ return <AttributesEditorView {...model} />;
947
11
  }
948
12
 
949
- export default React.memo(AttributesEditor);
13
+ //Optimzation trade off by readability: split into model/view/utils modules to keep each file small and semantically focused.