@developer_tribe/react-builder 1.2.8 → 1.2.10

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 (192) 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 +1 -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 +4 -2
  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 +1 -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/dist/migrations/migrations/1.1.0_normalize_style_attributes.d.ts +0 -2
  189. package/src/hooks/useExtractImageStyle.ts +0 -30
  190. package/src/hooks/useExtractTextStyle.ts +0 -34
  191. package/src/hooks/useExtractViewStyle.ts +0 -30
  192. package/src/migrations/migrations/1.1.0_normalize_style_attributes.ts +0 -80
@@ -0,0 +1,102 @@
1
+ import type { NodeDefaultAttribute } from '../types/Node';
2
+ import type { SchemaEntry } from './types';
3
+
4
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+
8
+ export type StyleBagSource = 'styles' | 'style' | 'none';
9
+
10
+ export function getStyleBagSourceAndValue(attributes: NodeDefaultAttribute): {
11
+ source: StyleBagSource;
12
+ styleBag?: Record<string, unknown>;
13
+ } {
14
+ const record = (attributes ?? {}) as Record<string, unknown>;
15
+ const maybeStyles = record.styles;
16
+ if (isPlainObject(maybeStyles)) {
17
+ return { source: 'styles', styleBag: maybeStyles };
18
+ }
19
+ const maybeStyle = record.style;
20
+ if (isPlainObject(maybeStyle)) {
21
+ return { source: 'style', styleBag: maybeStyle };
22
+ }
23
+ return { source: 'none', styleBag: undefined };
24
+ }
25
+
26
+ type AttributeTypeSpec = SchemaEntry['type'];
27
+
28
+ function isAttributeTypeSpec(value: unknown): value is AttributeTypeSpec {
29
+ return typeof value === 'string' || Array.isArray(value);
30
+ }
31
+
32
+ function getNestedStyleSchema(
33
+ schema: Record<string, unknown>,
34
+ ): Record<string, AttributeTypeSpec> | undefined {
35
+ const maybe = schema.styles ?? schema.style;
36
+ if (!isPlainObject(maybe)) return undefined;
37
+ const out: Record<string, AttributeTypeSpec> = {};
38
+ for (const [k, v] of Object.entries(maybe)) {
39
+ if (isAttributeTypeSpec(v)) out[k] = v;
40
+ }
41
+ return Object.keys(out).length ? out : undefined;
42
+ }
43
+
44
+ /**
45
+ * Flattens schemaVersion=2 nested style schemas (under schema.style or schema.styles)
46
+ * into plain entries so the editor can list them as individual fields.
47
+ *
48
+ * Note: we intentionally exclude the raw `style` / `styles` keys themselves, because
49
+ * Field.tsx does not render object specs (---not-implemented----).
50
+ */
51
+ export function buildAttributesEditorEntries(
52
+ schema: Record<string, unknown>,
53
+ attributeMeta?: Record<string, { category?: string }>,
54
+ ): SchemaEntry[] {
55
+ const nestedStyleSchema = getNestedStyleSchema(schema);
56
+ const metaKeys = Object.keys(attributeMeta ?? {});
57
+ const topLevelSchemaKeys = Object.keys(schema).filter(
58
+ (k) => k !== 'style' && k !== 'styles',
59
+ );
60
+ const nestedStyleKeys = Object.keys(nestedStyleSchema ?? {});
61
+ const keySet = new Set<string>([
62
+ ...metaKeys,
63
+ ...topLevelSchemaKeys,
64
+ ...nestedStyleKeys,
65
+ ]);
66
+
67
+ const keys = Array.from(keySet).filter(
68
+ (k) => k !== 'style' && k !== 'styles',
69
+ );
70
+
71
+ const entries: SchemaEntry[] = [];
72
+ keys.forEach((name) => {
73
+ const top = (schema as Record<string, unknown>)[name];
74
+ const nested = nestedStyleSchema?.[name];
75
+ const spec = isAttributeTypeSpec(top) ? top : nested;
76
+ if (!spec) return;
77
+ entries.push({ name, type: spec });
78
+ });
79
+ return entries;
80
+ }
81
+
82
+ export function isValidStyleBagValue(
83
+ attributes: NodeDefaultAttribute,
84
+ ): boolean {
85
+ const record = attributes as Record<string, unknown>;
86
+ const maybeStyles = record?.styles;
87
+ const maybeStyle = record?.style;
88
+ return !(
89
+ (maybeStyles != null && !isPlainObject(maybeStyles)) ||
90
+ (maybeStyle != null && !isPlainObject(maybeStyle))
91
+ );
92
+ }
93
+
94
+ export function findLegacyFlatStyleKeys(
95
+ attributes: NodeDefaultAttribute,
96
+ isStyleKeyForWrite: (name: string) => boolean,
97
+ ): string[] {
98
+ if (!isPlainObject(attributes)) return [];
99
+ return Object.keys(attributes).filter(
100
+ (k) => k !== 'style' && k !== 'styles' && isStyleKeyForWrite(k),
101
+ );
102
+ }
@@ -0,0 +1,477 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import type { NodeData, NodeDefaultAttribute } from '../types/Node';
3
+ import type { ProjectColorTokenMap, ProjectColors } from '../types/Project';
4
+ import type { ViewPropsGenerated } from '../build-components/View/ViewProps.generated';
5
+ import useNode from '../build-components/useNode';
6
+ import { isNodeString } from '../utils/analyseNode';
7
+ import { logger } from '../utils/logger';
8
+ import {
9
+ getAttributeMeta,
10
+ getAttributeSchema,
11
+ getPatternByType,
12
+ getStyleAttributeKeySet,
13
+ } from '../utils/patterns';
14
+ import { useRenderStore } from '../store';
15
+ import type { LayoutContext, SchemaEntry } from './types';
16
+ import {
17
+ buildAttributesEditorEntries,
18
+ findLegacyFlatStyleKeys,
19
+ getStyleBagSourceAndValue,
20
+ isValidStyleBagValue,
21
+ } from './attributesEditorUtils';
22
+ import type {
23
+ AttributesEditorModel,
24
+ AttributesEditorProps,
25
+ AttributesEditorSpecialSection,
26
+ AttributesEditorTabConfig,
27
+ TabContentInfo,
28
+ TabId,
29
+ } from './attributesEditorModelTypes';
30
+
31
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
32
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
33
+ }
34
+
35
+ function buildProjectColorsFallback(appConfig: any): ProjectColors | undefined {
36
+ const addColor = (collection: Set<string>, value?: string) => {
37
+ if (typeof value !== 'string') return;
38
+ const trimmed = value.trim();
39
+ if (!trimmed) return;
40
+ collection.add(trimmed);
41
+ };
42
+
43
+ const fallback = new Set<string>();
44
+ const styles = [
45
+ appConfig?.screenStyle?.light,
46
+ appConfig?.screenStyle?.dark,
47
+ ].filter(Boolean) as Array<{
48
+ backgroundColor?: string;
49
+ color?: string;
50
+ seperatorColor?: string;
51
+ }>;
52
+ styles.forEach((style) => {
53
+ ['backgroundColor', 'color', 'seperatorColor'].forEach((key) => {
54
+ const value = style?.[key as keyof typeof style];
55
+ addColor(fallback, value);
56
+ });
57
+ });
58
+
59
+ if (fallback.size === 0) {
60
+ return undefined;
61
+ }
62
+
63
+ const fallbackRecord: ProjectColorTokenMap = {};
64
+ Array.from(fallback).forEach((color, index) => {
65
+ fallbackRecord[`FALLBACK_${index + 1}`] = color;
66
+ });
67
+
68
+ return { STATIC_COLORS: fallbackRecord };
69
+ }
70
+
71
+ export function useAttributesEditorModel({
72
+ node,
73
+ onChange,
74
+ projectColors,
75
+ }: AttributesEditorProps): AttributesEditorModel {
76
+ const isInvalidNode = !node || isNodeString(node);
77
+ const baseData = isInvalidNode
78
+ ? ({
79
+ type: 'View',
80
+ children: null,
81
+ attributes: {},
82
+ } as unknown as NodeData<NodeDefaultAttribute>)
83
+ : (node as NodeData<NodeDefaultAttribute>);
84
+
85
+ const data = useNode(baseData);
86
+ const {
87
+ appConfig,
88
+ fonts: projectFonts,
89
+ loadedFonts,
90
+ markFontLoaded,
91
+ addError,
92
+ } = useRenderStore((state) => ({
93
+ appConfig: state.appConfig,
94
+ fonts: state.fonts,
95
+ loadedFonts: state.loadedFonts,
96
+ markFontLoaded: state.markFontLoaded,
97
+ addError: state.addError,
98
+ }));
99
+
100
+ const schema = (getAttributeSchema(data?.type) ?? {}) as Record<
101
+ string,
102
+ unknown
103
+ >;
104
+ const attributeMeta = getAttributeMeta(data?.type);
105
+ const attributes = (data?.attributes ?? {}) as NodeDefaultAttribute;
106
+
107
+ const styleAttributeKeys = useMemo(() => getStyleAttributeKeySet(), []);
108
+ const styleBagInfo = useMemo(
109
+ () => getStyleBagSourceAndValue(attributes),
110
+ [attributes],
111
+ );
112
+ const styleBag = styleBagInfo.styleBag;
113
+
114
+ const isStyleKeyForWrite = useCallback(
115
+ (name: string) => {
116
+ const metaCategory = attributeMeta?.[name]?.category;
117
+ if (metaCategory === 'style') return true;
118
+ return styleAttributeKeys.has(name);
119
+ },
120
+ [attributeMeta, styleAttributeKeys],
121
+ );
122
+ const isStyleKeyForRead = useCallback(
123
+ (name: string) => {
124
+ if (isStyleKeyForWrite(name)) return true;
125
+ return !!(
126
+ styleBag && Object.prototype.hasOwnProperty.call(styleBag, name)
127
+ );
128
+ },
129
+ [isStyleKeyForWrite, styleBag],
130
+ );
131
+
132
+ const legacyFlatStyleKeys = useMemo(
133
+ () => findLegacyFlatStyleKeys(attributes, isStyleKeyForWrite),
134
+ [attributes, isStyleKeyForWrite],
135
+ );
136
+ const hasInvalidStyleBag = useMemo(
137
+ () => !isValidStyleBagValue(attributes),
138
+ [attributes],
139
+ );
140
+
141
+ useEffect(() => {
142
+ const hasLegacyStyleKey = styleBagInfo.source === 'style';
143
+ if (
144
+ legacyFlatStyleKeys.length === 0 &&
145
+ !hasInvalidStyleBag &&
146
+ !hasLegacyStyleKey
147
+ ) {
148
+ return;
149
+ }
150
+ logger.warn(
151
+ 'AttributesEditor',
152
+ 'Legacy/unmigrated attributes detected. This editor expects schemaVersion=2 (style nested under attributes.styles). Please run migrations.',
153
+ {
154
+ componentType: data?.type,
155
+ nodeKey: (baseData as any)?.key,
156
+ legacyFlatStyleKeys,
157
+ hasInvalidStyleBag,
158
+ styleBagSource: styleBagInfo.source,
159
+ },
160
+ );
161
+ }, [
162
+ baseData,
163
+ data?.type,
164
+ hasInvalidStyleBag,
165
+ legacyFlatStyleKeys,
166
+ styleBagInfo.source,
167
+ ]);
168
+
169
+ const getAttributeValue = useCallback(
170
+ (name: string) => {
171
+ if (isStyleKeyForRead(name)) {
172
+ return styleBag?.[name];
173
+ }
174
+ return (attributes as Record<string, unknown>)?.[name];
175
+ },
176
+ [attributes, isStyleKeyForRead, styleBag],
177
+ );
178
+
179
+ const projectColorsForPicker = useMemo<ProjectColors | undefined>(() => {
180
+ if (projectColors) return projectColors;
181
+ return buildProjectColorsFallback(appConfig);
182
+ }, [appConfig, projectColors]);
183
+
184
+ const viewAttributes = useMemo<
185
+ Partial<ViewPropsGenerated['attributes']> | undefined
186
+ >(
187
+ () =>
188
+ data?.type === 'View'
189
+ ? (attributes as ViewPropsGenerated['attributes'])
190
+ : undefined,
191
+ [attributes, data?.type],
192
+ );
193
+
194
+ const layoutContext = useMemo<LayoutContext>(
195
+ () => ({
196
+ flexDirection: styleBag?.flexDirection as LayoutContext['flexDirection'],
197
+ alignItems: styleBag?.alignItems as LayoutContext['alignItems'],
198
+ justifyContent:
199
+ styleBag?.justifyContent as LayoutContext['justifyContent'],
200
+ }),
201
+ [styleBag?.flexDirection, styleBag?.alignItems, styleBag?.justifyContent],
202
+ );
203
+
204
+ const patternForType = useMemo(
205
+ () => (data?.type ? getPatternByType(data.type) : undefined),
206
+ [data?.type],
207
+ );
208
+ const componentMeta = patternForType?.meta;
209
+
210
+ const componentTitle = componentMeta?.label ?? data?.type ?? 'Component';
211
+ const componentDescription = componentMeta?.description;
212
+
213
+ const entries = useMemo(() => {
214
+ return buildAttributesEditorEntries(schema, attributeMeta).filter(
215
+ ({ type }) => (typeof type === 'string' ? type !== 'never' : true),
216
+ );
217
+ }, [attributeMeta, schema]);
218
+
219
+ const visibleEntries = useMemo(() => {
220
+ if (!componentMeta?.hideAllAttributes) return entries;
221
+ return entries.filter(({ name }) => {
222
+ const meta = attributeMeta?.[name];
223
+ return meta?.forceVisible === true || meta?.override === true;
224
+ });
225
+ }, [attributeMeta, componentMeta?.hideAllAttributes, entries]);
226
+
227
+ const mockableFeatureKeys = useMemo(() => {
228
+ const mockable = componentMeta?.mockableFeatures;
229
+ if (!mockable || typeof mockable !== 'object') return [];
230
+ return Object.entries(mockable)
231
+ .filter(([, enabled]) => enabled === true)
232
+ .map(([key]) => key)
233
+ .filter((key) => typeof key === 'string' && key.trim().length > 0)
234
+ .sort((a, b) => a.localeCompare(b));
235
+ }, [componentMeta?.mockableFeatures]);
236
+
237
+ const [activeMockableFeature, setActiveMockableFeature] = useState<
238
+ string | null
239
+ >(null);
240
+
241
+ const groupedAndSpecialGroups = useMemo(() => {
242
+ const groups: Record<TabId, SchemaEntry[]> = {
243
+ style: [],
244
+ container: [],
245
+ other: [],
246
+ };
247
+ const specials: Record<string, SchemaEntry[]> = {};
248
+
249
+ const getSortOrder = (name: string) =>
250
+ attributeMeta?.[name]?.sort ?? Number.MAX_SAFE_INTEGER;
251
+ const compareEntries = (a: SchemaEntry, b: SchemaEntry) => {
252
+ const order = getSortOrder(a.name) - getSortOrder(b.name);
253
+ return order !== 0 ? order : a.name.localeCompare(b.name);
254
+ };
255
+
256
+ visibleEntries.forEach(({ name, type }) => {
257
+ const meta = attributeMeta?.[name];
258
+ const specialCategory = meta?.specialCategory;
259
+ if (typeof specialCategory === 'string') {
260
+ const normalizedSpecialCategory = specialCategory.trim();
261
+ if (normalizedSpecialCategory) {
262
+ if (!specials[normalizedSpecialCategory]) {
263
+ specials[normalizedSpecialCategory] = [];
264
+ }
265
+ specials[normalizedSpecialCategory].push({ name, type });
266
+ return;
267
+ }
268
+ }
269
+
270
+ const metaCategory = meta?.category;
271
+ const normalized =
272
+ metaCategory === 'style'
273
+ ? 'style'
274
+ : metaCategory === 'container'
275
+ ? 'container'
276
+ : 'other';
277
+ groups[normalized].push({ name, type });
278
+ });
279
+
280
+ Object.values(groups).forEach((list) => list.sort(compareEntries));
281
+ Object.values(specials).forEach((list) => list.sort(compareEntries));
282
+ return { grouped: groups, specialGroups: specials };
283
+ }, [attributeMeta, visibleEntries]);
284
+
285
+ const { grouped, specialGroups } = groupedAndSpecialGroups;
286
+
287
+ const specialSectionsByTab = useMemo<
288
+ Record<TabId, AttributesEditorSpecialSection[]>
289
+ >(() => {
290
+ const buckets: Record<TabId, AttributesEditorSpecialSection[]> = {
291
+ style: [],
292
+ container: [],
293
+ other: [],
294
+ };
295
+
296
+ const compareSections = (
297
+ a: AttributesEditorSpecialSection,
298
+ b: AttributesEditorSpecialSection,
299
+ ) => {
300
+ const aSort = a.meta?.sort ?? Number.MAX_SAFE_INTEGER;
301
+ const bSort = b.meta?.sort ?? Number.MAX_SAFE_INTEGER;
302
+ const order = aSort - bSort;
303
+ return order !== 0 ? order : a.key.localeCompare(b.key);
304
+ };
305
+
306
+ Object.entries(specialGroups).forEach(([categoryKey, categoryEntries]) => {
307
+ if (!categoryEntries.length) return;
308
+ const metaForCategory = componentMeta?.specialCategories?.[categoryKey];
309
+ const targetCategory = (
310
+ metaForCategory?.category === 'style'
311
+ ? 'style'
312
+ : metaForCategory?.category === 'container'
313
+ ? 'container'
314
+ : 'other'
315
+ ) as TabId;
316
+ buckets[targetCategory].push({
317
+ key: categoryKey,
318
+ entries: categoryEntries,
319
+ meta: metaForCategory,
320
+ });
321
+ });
322
+
323
+ (Object.keys(buckets) as TabId[]).forEach((tabId) => {
324
+ buckets[tabId].sort(compareSections);
325
+ });
326
+
327
+ return buckets;
328
+ }, [componentMeta?.specialCategories, specialGroups]);
329
+
330
+ const tabs = useMemo<AttributesEditorTabConfig[]>(
331
+ () => [
332
+ { id: 'container', label: 'Container', entries: grouped.container },
333
+ { id: 'style', label: 'Styles', entries: grouped.style },
334
+ { id: 'other', label: 'Others', entries: grouped.other },
335
+ ],
336
+ [grouped],
337
+ );
338
+
339
+ const tabContentInfo = useMemo<TabContentInfo>(() => {
340
+ const info: TabContentInfo = {
341
+ style: { baseCount: 0, specialCount: 0 },
342
+ container: { baseCount: 0, specialCount: 0 },
343
+ other: { baseCount: 0, specialCount: 0 },
344
+ };
345
+
346
+ tabs.forEach((tab) => {
347
+ info[tab.id].baseCount = tab.entries.length;
348
+ });
349
+
350
+ (Object.keys(specialSectionsByTab) as TabId[]).forEach((tabId) => {
351
+ info[tabId].specialCount = specialSectionsByTab[tabId].reduce(
352
+ (sum, section) => sum + section.entries.length,
353
+ 0,
354
+ );
355
+ });
356
+
357
+ return info;
358
+ }, [specialSectionsByTab, tabs]);
359
+
360
+ const firstAvailableTab = useMemo<TabId>(() => {
361
+ return (
362
+ tabs.find((tab) => {
363
+ const counts = tabContentInfo[tab.id];
364
+ return counts.baseCount + counts.specialCount > 0;
365
+ })?.id ?? 'other'
366
+ );
367
+ }, [tabContentInfo, tabs]);
368
+
369
+ const [activeTab, setActiveTab] = useState<TabId>(firstAvailableTab);
370
+
371
+ useEffect(() => {
372
+ setActiveTab((prev) => {
373
+ const counts = tabContentInfo[prev];
374
+ if (counts && counts.baseCount + counts.specialCount > 0) {
375
+ return prev;
376
+ }
377
+ return firstAvailableTab;
378
+ });
379
+ }, [firstAvailableTab, tabContentInfo]);
380
+
381
+ const activeEntries =
382
+ tabs.find((tab) => tab.id === activeTab)?.entries ??
383
+ tabs.find((tab) => tab.id === firstAvailableTab)?.entries ??
384
+ [];
385
+
386
+ const activeSpecialSections = specialSectionsByTab[activeTab] ?? [];
387
+
388
+ const handleAttributeChange = useCallback(
389
+ (name: string, val: unknown) => {
390
+ const prevAttrs =
391
+ ((baseData?.attributes ?? {}) as Record<string, unknown>) ?? {};
392
+ const isStyleKey = isStyleKeyForWrite(name);
393
+ const nextAttrs: Record<string, unknown> = { ...prevAttrs };
394
+ if (isStyleKey) {
395
+ const prevStyle = (
396
+ isPlainObject(nextAttrs.styles)
397
+ ? nextAttrs.styles
398
+ : isPlainObject(nextAttrs.style)
399
+ ? nextAttrs.style
400
+ : {}
401
+ ) as Record<string, unknown>;
402
+ const nextStyle = { ...prevStyle, [name]: val };
403
+ // Keep both keys in sync:
404
+ // - `styles` is the canonical store for the editor (latest migration).
405
+ // - `style` is kept for back-compat because build-components currently read `attributes.style`.
406
+ nextAttrs.styles = nextStyle;
407
+ nextAttrs.style = nextStyle;
408
+ if (name in nextAttrs) delete nextAttrs[name];
409
+ } else {
410
+ nextAttrs[name] = val;
411
+ }
412
+ const next: NodeData<NodeDefaultAttribute> = {
413
+ ...baseData,
414
+ attributes: nextAttrs as NodeDefaultAttribute,
415
+ };
416
+ onChange(next);
417
+ },
418
+ [baseData, isStyleKeyForWrite, onChange],
419
+ );
420
+
421
+ const handleChildrenChange = useCallback(
422
+ (val: string) => {
423
+ const next: NodeData<NodeDefaultAttribute> = {
424
+ ...baseData,
425
+ children: val,
426
+ };
427
+ onChange(next);
428
+ },
429
+ [baseData, onChange],
430
+ );
431
+
432
+ const hasStringChildren =
433
+ !!patternForType?.pattern &&
434
+ (patternForType.pattern as { children?: unknown }).children === 'string';
435
+ const childrenValue =
436
+ typeof (baseData.children as unknown) === 'string'
437
+ ? (baseData.children as string)
438
+ : '';
439
+
440
+ return {
441
+ isInvalidNode,
442
+ baseData,
443
+ data,
444
+ appConfig,
445
+ projectFonts,
446
+ loadedFonts,
447
+ markFontLoaded,
448
+ addError,
449
+ schema,
450
+ attributeMeta,
451
+ componentTitle,
452
+ componentDescription,
453
+ patternForType,
454
+ componentMeta,
455
+ attributes,
456
+ styleBag,
457
+ projectColorsForPicker,
458
+ viewAttributes,
459
+ layoutContext,
460
+ getAttributeValue,
461
+ handleAttributeChange,
462
+ handleChildrenChange,
463
+ tabs,
464
+ tabContentInfo,
465
+ firstAvailableTab,
466
+ activeTab,
467
+ setActiveTab,
468
+ activeEntries,
469
+ specialSectionsByTab,
470
+ activeSpecialSections,
471
+ mockableFeatureKeys,
472
+ activeMockableFeature,
473
+ setActiveMockableFeature,
474
+ hasStringChildren,
475
+ childrenValue,
476
+ };
477
+ }
@@ -7,7 +7,7 @@ import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
7
7
  import { useMergedStyle } from '../../utils/useMergedStyle';
8
8
  import { Icon } from '../../components/Icon.generated';
9
9
  import { IconsType } from '../../types/Icons';
10
- import { useExtractTextStyle } from '../../hooks/useExtractTextStyle';
10
+ import { useExtractTextStyle } from '../../attribute-analyser/style/web/useExtractTextStyle';
11
11
 
12
12
  function BIcon({ node }: BIconComponentProps) {
13
13
  useLogRender('BIcon');
@@ -28,9 +28,9 @@ function BIcon({ node }: BIconComponentProps) {
28
28
  baseStyle,
29
29
  isSelected ? SELECTED_OUTLINE_STYLE : undefined,
30
30
  );
31
- const styleBag = node.attributes?.style;
32
- const iconType = styleBag?.iconType ?? 'activity';
33
- const strokeWidth = styleBag?.strokeWidth;
31
+ const attrs = node.attributes;
32
+ const iconType = attrs?.iconType ?? 'activity';
33
+ const strokeWidth = attrs?.strokeWidth;
34
34
 
35
35
  return (
36
36
  <Icon
@@ -25,9 +25,6 @@ export interface BIconStyleGenerated {
25
25
  fontFamily?: string;
26
26
  fontWeight?: string;
27
27
  textAlign?: TextAlignOptionType;
28
- adjustsFontSizeToFit?: boolean;
29
- showEllipsis?: boolean;
30
- scrollable?: boolean;
31
28
  flexDirection?: FlexDirectionOptionType;
32
29
  alignItems?: AlignItemsOptionType;
33
30
  justifyContent?: JustifyContentOptionType;
@@ -61,15 +58,18 @@ export interface BIconStyleGenerated {
61
58
  left?: string;
62
59
  right?: string;
63
60
  zIndex?: number;
64
- iconType?: string;
65
- size?: number;
66
- strokeWidth?: number;
67
61
  }
68
62
 
69
63
  export interface BIconPropsGenerated {
70
64
  child: string;
71
65
  attributes: {
72
66
  style?: BIconStyleGenerated;
67
+ adjustsFontSizeToFit?: boolean;
68
+ showEllipsis?: boolean;
69
+ scrollable?: boolean;
70
+ iconType?: string;
71
+ size?: number;
72
+ strokeWidth?: number;
73
73
  };
74
74
  }
75
75
 
@@ -11,12 +11,11 @@
11
11
  }
12
12
  },
13
13
  "meta": {
14
- "desiredParent": [
15
- "all"
16
- ],
14
+ "desiredParent": ["all"],
17
15
  "label": "BIcon",
18
16
  "description": "Renders an icon from the icon set.",
19
- "styles": {
17
+ "styles": {},
18
+ "attributes": {
20
19
  "iconType": {
21
20
  "label": "Icon",
22
21
  "description": "Which icon to render.",
@@ -27,14 +26,14 @@
27
26
  "size": {
28
27
  "label": "Size",
29
28
  "description": "Icon size (px).",
30
- "category": "style",
29
+ "category": "other",
31
30
  "specialCategory": null,
32
31
  "sort": 2
33
32
  },
34
33
  "strokeWidth": {
35
34
  "label": "Stroke Width",
36
35
  "description": "SVG stroke width override (applied to the icon paths).",
37
- "category": "style",
36
+ "category": "other",
38
37
  "specialCategory": null,
39
38
  "sort": 3
40
39
  }
@@ -2,7 +2,7 @@ import React, { useId, useMemo } from 'react';
2
2
  import type { BackgroundImageComponentProps } from './BackgroundImageProps.generated';
3
3
  import useNode from '../useNode';
4
4
  import { useBuilderParams } from '../../components/BuilderProvider';
5
- import { useExtractViewStyle } from '../../hooks/useExtractViewStyle';
5
+ import { useExtractViewStyle } from '../../attribute-analyser/style/web/useExtractViewStyle';
6
6
  import { resolveImageSrc } from '../../utils/getImage';
7
7
  import { useLogRender } from '../../utils/useLogRender';
8
8
  import { isNodeSelected, SELECTED_OUTLINE_STYLE } from '../../utils/selection';
@@ -32,7 +32,8 @@ function BackgroundImage({ node }: BackgroundImageComponentProps) {
32
32
  if (resolved) style.backgroundImage = `url(${resolved})`;
33
33
  }
34
34
 
35
- switch (styleBag?.resizeMode) {
35
+ const resizeMode = attrs?.resizeMode ?? (styleBag as any)?.resizeMode;
36
+ switch (resizeMode) {
36
37
  case 'cover':
37
38
  style.backgroundSize = 'cover';
38
39
  break;