@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
@@ -0,0 +1,248 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import type { Fonts } from '../types/Fonts';
3
+ import { Icon } from '../components/Icon.generated';
4
+ import { IconPickerModal } from '../modals/IconPickerModal';
5
+ import Modal from '../modals/Modal';
6
+ import { loadFontFamily } from '../utils/loadFontFamily';
7
+ import { fontsDebug } from '../utils/fontsDebug';
8
+ import type { IconsType } from '../types/Icons';
9
+
10
+ type IconTypePickerFieldProps = {
11
+ name: string;
12
+ value: unknown;
13
+ onChange: (next: unknown) => void;
14
+ };
15
+
16
+ export function IconTypePickerField({
17
+ name,
18
+ value,
19
+ onChange,
20
+ }: IconTypePickerFieldProps) {
21
+ const [isOpen, setIsOpen] = useState(false);
22
+ const normalized =
23
+ typeof value === 'string' ? (value as IconsType) : undefined;
24
+
25
+ return (
26
+ <>
27
+ <button
28
+ type="button"
29
+ onClick={() => setIsOpen(true)}
30
+ className="attributes-editor__picker-button"
31
+ >
32
+ <span className="attributes-editor__picker-button-left">
33
+ {normalized ? <Icon iconType={normalized} size={18} /> : null}
34
+ <span className="attributes-editor__picker-button-label">
35
+ {normalized ?? 'Select icon'}
36
+ </span>
37
+ </span>
38
+ <span className="attributes-editor__picker-button-open">Open</span>
39
+ </button>
40
+
41
+ {isOpen ? (
42
+ <IconPickerModal
43
+ value={normalized}
44
+ onSelect={(iconName) => {
45
+ onChange(iconName);
46
+ setIsOpen(false);
47
+ }}
48
+ onClose={() => setIsOpen(false)}
49
+ onClear={() => {
50
+ onChange(undefined);
51
+ setIsOpen(false);
52
+ }}
53
+ />
54
+ ) : null}
55
+ </>
56
+ );
57
+ }
58
+
59
+ type FontFamilyPickerFieldProps = {
60
+ name: string;
61
+ value: unknown;
62
+ onChange: (next: unknown) => void;
63
+ fonts: Fonts;
64
+ loadedFonts: string[] | undefined;
65
+ markFontLoaded: (fontFamily: string) => void;
66
+ addError: (message: string) => void;
67
+ };
68
+
69
+ export function FontFamilyPickerField({
70
+ name,
71
+ value,
72
+ onChange,
73
+ fonts,
74
+ loadedFonts,
75
+ markFontLoaded,
76
+ addError,
77
+ }: FontFamilyPickerFieldProps) {
78
+ const [isOpen, setIsOpen] = useState(false);
79
+ const [isFontLoading, setIsFontLoading] = useState(false);
80
+ const [fontLoadError, setFontLoadError] = useState<string | null>(null);
81
+
82
+ const normalized =
83
+ typeof value === 'string' && value.trim().length > 0
84
+ ? value.trim()
85
+ : undefined;
86
+
87
+ const fontsList = Array.isArray(fonts) ? fonts : [];
88
+ const loaded = Array.isArray(loadedFonts) ? loadedFonts : [];
89
+
90
+ const fontCountLabel = useMemo(
91
+ () => `${fontsList.length} fonts`,
92
+ [fontsList.length],
93
+ );
94
+
95
+ return (
96
+ <>
97
+ <button
98
+ type="button"
99
+ onClick={() => {
100
+ setFontLoadError(null);
101
+ setIsOpen(true);
102
+ }}
103
+ className="attributes-editor__picker-button"
104
+ >
105
+ <span className="attributes-editor__picker-button-label attributes-editor__picker-button-label--fill">
106
+ {normalized ?? 'Select font'}
107
+ </span>
108
+ <span className="attributes-editor__picker-button-open">Open</span>
109
+ </button>
110
+
111
+ {isOpen ? (
112
+ <Modal
113
+ ariaLabelledBy="font-family-picker-title"
114
+ onClose={() => {
115
+ if (isFontLoading) return;
116
+ setIsOpen(false);
117
+ }}
118
+ closeOnOverlayClick={!isFontLoading}
119
+ closeOnEsc={!isFontLoading}
120
+ >
121
+ <div className="attributes-editor__font-modal">
122
+ <div className="attributes-editor__font-modal-header">
123
+ <p
124
+ id="font-family-picker-title"
125
+ className="attributes-editor__font-modal-title"
126
+ >
127
+ Select Font
128
+ </p>
129
+ <div className="attributes-editor__font-modal-spacer" />
130
+ <button
131
+ type="button"
132
+ className="editor-button"
133
+ onClick={() => {
134
+ onChange(undefined);
135
+ setFontLoadError(null);
136
+ setIsOpen(false);
137
+ }}
138
+ disabled={isFontLoading}
139
+ >
140
+ Clear
141
+ </button>
142
+ <button
143
+ type="button"
144
+ className="editor-button"
145
+ onClick={() => setIsOpen(false)}
146
+ disabled={isFontLoading}
147
+ >
148
+ Close
149
+ </button>
150
+ </div>
151
+
152
+ {isFontLoading ? (
153
+ <div className="attributes-editor__font-modal-note">
154
+ Loading font…
155
+ </div>
156
+ ) : null}
157
+ {fontLoadError ? (
158
+ <div className="attributes-editor__font-modal-error">
159
+ {fontLoadError}
160
+ </div>
161
+ ) : null}
162
+
163
+ <div className="attributes-editor__font-modal-note">
164
+ {fontCountLabel}
165
+ </div>
166
+
167
+ <div className="attributes-editor__font-grid">
168
+ {fontsList.map((font) => {
169
+ const fontName = font?.name;
170
+ if (typeof fontName !== 'string' || !fontName.trim()) {
171
+ return null;
172
+ }
173
+ const familyName = fontName.trim();
174
+ const isActive = normalized === familyName;
175
+ const isLoaded = loaded.includes(familyName);
176
+ const optionClassName = [
177
+ 'attributes-editor__font-option',
178
+ isActive ? 'attributes-editor__font-option--active' : '',
179
+ ]
180
+ .filter(Boolean)
181
+ .join(' ');
182
+
183
+ return (
184
+ <button
185
+ key={familyName}
186
+ type="button"
187
+ disabled={isFontLoading}
188
+ onClick={async () => {
189
+ fontsDebug.info('AttributesEditor: select fontFamily', {
190
+ field: name,
191
+ familyName,
192
+ wasLoaded: isLoaded,
193
+ });
194
+ setFontLoadError(null);
195
+ onChange(familyName);
196
+ if (isLoaded) return;
197
+
198
+ setIsFontLoading(true);
199
+ try {
200
+ fontsDebug.info(
201
+ 'AttributesEditor: loadFontFamily start',
202
+ { familyName },
203
+ );
204
+ await loadFontFamily(fontsList, familyName, {
205
+ forceFetch: true,
206
+ });
207
+ markFontLoaded(familyName);
208
+ fontsDebug.info(
209
+ 'AttributesEditor: loadFontFamily success',
210
+ { familyName },
211
+ );
212
+ } catch (e) {
213
+ const msg = e instanceof Error ? e.message : String(e);
214
+ setFontLoadError(
215
+ `Failed to load "${familyName}": ${msg}`,
216
+ );
217
+ addError(`Failed to load font "${familyName}": ${msg}`);
218
+ fontsDebug.compactError(
219
+ 'AttributesEditor: loadFontFamily failed',
220
+ e,
221
+ { familyName },
222
+ );
223
+ } finally {
224
+ setIsFontLoading(false);
225
+ }
226
+ }}
227
+ className={optionClassName}
228
+ aria-label={`Select font ${familyName}`}
229
+ >
230
+ <span
231
+ className="attributes-editor__font-option-name"
232
+ title={familyName}
233
+ >
234
+ {familyName}
235
+ </span>
236
+ <span className="attributes-editor__font-option-status">
237
+ {isLoaded ? 'Loaded' : 'Not loaded'}
238
+ </span>
239
+ </button>
240
+ );
241
+ })}
242
+ </div>
243
+ </div>
244
+ </Modal>
245
+ ) : null}
246
+ </>
247
+ );
248
+ }
@@ -0,0 +1,360 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Field } from './Field';
3
+ import { SpecialCategorySection } from './SpecialCategorySection';
4
+ import { FieldInfoTooltip } from './FieldInfoTooltip';
5
+ import { toPreferredScale } from './SizeField';
6
+ import { isBooleanFieldType, type SchemaEntry } from './types';
7
+ import { MockableFeatureModal } from '../modals';
8
+ import {
9
+ FontFamilyPickerField,
10
+ IconTypePickerField,
11
+ } from './AttributesEditorFields';
12
+ import type {
13
+ AttributesEditorModel,
14
+ AttributesEditorSpecialSection,
15
+ } from './attributesEditorModelTypes';
16
+ import type { NodeDefaultAttribute } from '../types/Node';
17
+
18
+ type AttributesEditorViewProps = AttributesEditorModel;
19
+
20
+ function getPreferredOrderedSizeEntries(entries: SchemaEntry[]): SchemaEntry[] {
21
+ const preferredOrder = [
22
+ 'width',
23
+ 'height',
24
+ 'minWidth',
25
+ 'minHeight',
26
+ 'maxWidth',
27
+ 'maxHeight',
28
+ ];
29
+ return [...entries].sort((a, b) => {
30
+ const aIndex = preferredOrder.indexOf(a.name);
31
+ const bIndex = preferredOrder.indexOf(b.name);
32
+ const aRank = aIndex === -1 ? Number.MAX_SAFE_INTEGER : aIndex;
33
+ const bRank = bIndex === -1 ? Number.MAX_SAFE_INTEGER : bIndex;
34
+ return aRank !== bRank ? aRank - bRank : a.name.localeCompare(b.name);
35
+ });
36
+ }
37
+
38
+ function getSpecialSectionTitle(section: AttributesEditorSpecialSection): {
39
+ title: string;
40
+ description?: string;
41
+ } {
42
+ const key = section.key;
43
+ const meta = section.meta;
44
+ const title =
45
+ meta?.label && meta.label.trim().length > 0
46
+ ? meta.label
47
+ : key.charAt(0).toUpperCase() + key.slice(1);
48
+ return { title, description: meta?.description };
49
+ }
50
+
51
+ export function AttributesEditorView(props: AttributesEditorViewProps) {
52
+ const {
53
+ isInvalidNode,
54
+ componentTitle,
55
+ componentDescription,
56
+ mockableFeatureKeys,
57
+ activeMockableFeature,
58
+ setActiveMockableFeature,
59
+ tabs,
60
+ tabContentInfo,
61
+ activeTab,
62
+ setActiveTab,
63
+ firstAvailableTab,
64
+ activeEntries,
65
+ activeSpecialSections,
66
+ hasStringChildren,
67
+ childrenValue,
68
+ handleChildrenChange,
69
+ attributeMeta,
70
+ data,
71
+ getAttributeValue,
72
+ handleAttributeChange,
73
+ projectColorsForPicker,
74
+ layoutContext,
75
+ viewAttributes,
76
+ styleBag,
77
+ attributes,
78
+ projectFonts,
79
+ loadedFonts,
80
+ markFontLoaded,
81
+ addError,
82
+ } = props;
83
+
84
+ const headerSection = (
85
+ <div className="attributes-editor__component-meta">
86
+ <p className="attributes-editor__component-title">{componentTitle}</p>
87
+ {componentDescription ? (
88
+ <p className="attributes-editor__component-description">
89
+ {componentDescription}
90
+ </p>
91
+ ) : null}
92
+ </div>
93
+ );
94
+
95
+ const mockableSection =
96
+ mockableFeatureKeys.length > 0 ? (
97
+ <section className="attributes-editor__mockable">
98
+ <p className="attributes-editor__mockable-title">Mockable</p>
99
+ <table className="attributes-editor__mockable-table">
100
+ <tbody>
101
+ {mockableFeatureKeys.map((key) => (
102
+ <tr key={key} className="attributes-editor__mockable-row">
103
+ <td className="attributes-editor__mockable-name">{key}</td>
104
+ <td className="attributes-editor__mockable-action">
105
+ <button
106
+ type="button"
107
+ className="editor-button"
108
+ onClick={() => setActiveMockableFeature(key)}
109
+ >
110
+ {key}
111
+ </button>
112
+ </td>
113
+ </tr>
114
+ ))}
115
+ </tbody>
116
+ </table>
117
+ </section>
118
+ ) : null;
119
+
120
+ const tabsSection = (
121
+ <div className="attributes-editor__tabs">
122
+ {tabs.map((tab) => {
123
+ const isActive = tab.id === activeTab;
124
+ const counts = tabContentInfo[tab.id];
125
+ const totalCount = counts.baseCount + counts.specialCount;
126
+ const disabled = totalCount === 0;
127
+ const buttonClassNames = [
128
+ 'attributes-editor__tab-button',
129
+ isActive ? 'attributes-editor__tab-button--active' : '',
130
+ ]
131
+ .filter(Boolean)
132
+ .join(' ');
133
+
134
+ return (
135
+ <button
136
+ key={tab.id}
137
+ type="button"
138
+ onClick={() => !disabled && setActiveTab(tab.id)}
139
+ disabled={disabled}
140
+ className={buttonClassNames}
141
+ >
142
+ {tab.label}
143
+ {totalCount > 0 ? ` (${totalCount})` : ''}
144
+ </button>
145
+ );
146
+ })}
147
+ </div>
148
+ );
149
+
150
+ const childrenSection = hasStringChildren ? (
151
+ <div className="attributes-editor__field-wrapper attributes-editor__field-wrapper--children">
152
+ <p className="attributes-editor__field-label">Text</p>
153
+ <input
154
+ type="text"
155
+ className="attributes-editor__text-input"
156
+ value={childrenValue}
157
+ onChange={(e) => handleChildrenChange(e.target.value)}
158
+ />
159
+ </div>
160
+ ) : null;
161
+
162
+ const hasAnyContent = useMemo(() => {
163
+ const counts = tabContentInfo[firstAvailableTab];
164
+ return (
165
+ (counts?.baseCount ?? 0) + (counts?.specialCount ?? 0) > 0 ||
166
+ (hasStringChildren && childrenValue !== undefined)
167
+ );
168
+ }, [childrenValue, firstAvailableTab, hasStringChildren, tabContentInfo]);
169
+
170
+ function renderEntry(entry: SchemaEntry) {
171
+ const name = entry.name;
172
+ const type = entry.type;
173
+ const label = attributeMeta?.[name]?.label ?? name;
174
+ const description = attributeMeta?.[name]?.description;
175
+ const preferredScale = toPreferredScale(
176
+ attributeMeta?.[name]?.preferedScale,
177
+ );
178
+ const isBoolean = isBooleanFieldType(type);
179
+ const wrapperClassNames = [
180
+ 'attributes-editor__field-wrapper',
181
+ isBoolean ? 'attributes-editor__field-wrapper--boolean' : '',
182
+ ]
183
+ .filter(Boolean)
184
+ .join(' ');
185
+
186
+ const value = getAttributeValue(name);
187
+
188
+ return (
189
+ <FieldInfoTooltip key={name} description={description}>
190
+ <div className={wrapperClassNames}>
191
+ {!isBoolean ? (
192
+ <p className="attributes-editor__field-label">{label}</p>
193
+ ) : null}
194
+
195
+ {type === 'iconType' ? (
196
+ <IconTypePickerField
197
+ name={name}
198
+ value={value}
199
+ onChange={(next) => handleAttributeChange(name, next)}
200
+ />
201
+ ) : type === 'fontFamily' ? (
202
+ <FontFamilyPickerField
203
+ name={name}
204
+ value={value}
205
+ onChange={(next) => handleAttributeChange(name, next)}
206
+ fonts={projectFonts}
207
+ loadedFonts={loadedFonts}
208
+ markFontLoaded={markFontLoaded}
209
+ addError={addError}
210
+ />
211
+ ) : (
212
+ <Field
213
+ name={name}
214
+ type={type}
215
+ value={value}
216
+ onChange={(val) => handleAttributeChange(name, val)}
217
+ componentType={data?.type}
218
+ projectColors={projectColorsForPicker}
219
+ layoutContext={layoutContext}
220
+ viewAttributes={viewAttributes}
221
+ label={isBoolean ? label : undefined}
222
+ preferredScale={preferredScale}
223
+ />
224
+ )}
225
+ </div>
226
+ </FieldInfoTooltip>
227
+ );
228
+ }
229
+
230
+ function renderSpecialSection(section: AttributesEditorSpecialSection) {
231
+ if (section.key === 'size') {
232
+ const { title, description } = getSpecialSectionTitle(section);
233
+ const orderedEntries = getPreferredOrderedSizeEntries(section.entries);
234
+ return (
235
+ <section key={section.key} className="special-category-section">
236
+ <div className="special-category-section__header">
237
+ <p className="special-category-section__title">{title}</p>
238
+ </div>
239
+ {description ? (
240
+ <p className="special-category-section__description">
241
+ {description}
242
+ </p>
243
+ ) : null}
244
+ <div className="attributes-editor__size-grid">
245
+ {orderedEntries.map((entry) => {
246
+ const label = attributeMeta?.[entry.name]?.label ?? entry.name;
247
+ const description = attributeMeta?.[entry.name]?.description;
248
+ const preferredScale = toPreferredScale(
249
+ attributeMeta?.[entry.name]?.preferedScale,
250
+ );
251
+ const currentValue = getAttributeValue(entry.name);
252
+ const isBoolean = isBooleanFieldType(entry.type);
253
+ const wrapperClassNames = [
254
+ 'attributes-editor__field-wrapper',
255
+ isBoolean ? 'attributes-editor__field-wrapper--boolean' : '',
256
+ ]
257
+ .filter(Boolean)
258
+ .join(' ');
259
+ return (
260
+ <FieldInfoTooltip key={entry.name} description={description}>
261
+ <div
262
+ className={`${wrapperClassNames} attributes-editor__size-grid-item`}
263
+ >
264
+ {!isBoolean ? (
265
+ <p className="attributes-editor__field-label">{label}</p>
266
+ ) : null}
267
+ {entry.type === 'iconType' ? (
268
+ <IconTypePickerField
269
+ name={entry.name}
270
+ value={currentValue}
271
+ onChange={(next) =>
272
+ handleAttributeChange(entry.name, next)
273
+ }
274
+ />
275
+ ) : entry.type === 'fontFamily' ? (
276
+ <FontFamilyPickerField
277
+ name={entry.name}
278
+ value={currentValue}
279
+ onChange={(next) =>
280
+ handleAttributeChange(entry.name, next)
281
+ }
282
+ fonts={projectFonts}
283
+ loadedFonts={loadedFonts}
284
+ markFontLoaded={markFontLoaded}
285
+ addError={addError}
286
+ />
287
+ ) : (
288
+ <Field
289
+ name={entry.name}
290
+ type={entry.type}
291
+ value={currentValue}
292
+ onChange={(val) =>
293
+ handleAttributeChange(entry.name, val)
294
+ }
295
+ componentType={data?.type}
296
+ projectColors={projectColorsForPicker}
297
+ layoutContext={layoutContext}
298
+ viewAttributes={viewAttributes}
299
+ label={isBoolean ? label : undefined}
300
+ preferredScale={preferredScale}
301
+ />
302
+ )}
303
+ </div>
304
+ </FieldInfoTooltip>
305
+ );
306
+ })}
307
+ </div>
308
+ </section>
309
+ );
310
+ }
311
+
312
+ const sectionAttributes =
313
+ section.meta?.category === 'style'
314
+ ? ((styleBag ?? {}) as unknown as NodeDefaultAttribute)
315
+ : attributes;
316
+
317
+ return (
318
+ <SpecialCategorySection
319
+ key={section.key}
320
+ category={section.key}
321
+ entries={section.entries}
322
+ attributeMeta={attributeMeta}
323
+ attributes={sectionAttributes}
324
+ onAttributeChange={handleAttributeChange}
325
+ componentType={data?.type}
326
+ projectColors={projectColorsForPicker}
327
+ layoutContext={layoutContext}
328
+ viewAttributes={viewAttributes}
329
+ meta={section.meta}
330
+ />
331
+ );
332
+ }
333
+
334
+ if (isInvalidNode) return null;
335
+
336
+ return (
337
+ <div className="attributes-editor">
338
+ {headerSection}
339
+ {mockableSection}
340
+ {tabsSection}
341
+ {childrenSection}
342
+ {activeSpecialSections.map(renderSpecialSection)}
343
+
344
+ {activeEntries.map(renderEntry)}
345
+
346
+ {!hasAnyContent ? (
347
+ <div className="attributes-editor__empty-state">
348
+ No editable attributes
349
+ </div>
350
+ ) : null}
351
+
352
+ {activeMockableFeature ? (
353
+ <MockableFeatureModal
354
+ featureKey={activeMockableFeature}
355
+ onClose={() => setActiveMockableFeature(null)}
356
+ />
357
+ ) : null}
358
+ </div>
359
+ );
360
+ }
@@ -0,0 +1,86 @@
1
+ import type { Node, NodeData, NodeDefaultAttribute } from '../types/Node';
2
+ import type { ProjectColors } from '../types/Project';
3
+ import type { ViewPropsGenerated } from '../build-components/View/ViewProps.generated';
4
+ import type { Fonts } from '../types/Fonts';
5
+ import type { LayoutContext, SchemaEntry } from './types';
6
+
7
+ export type TabId = 'style' | 'container' | 'other';
8
+
9
+ export type AttributesEditorProps = {
10
+ node: Node;
11
+ onChange: (next: Node) => void;
12
+ projectColors?: ProjectColors;
13
+ };
14
+
15
+ export type AttributesEditorTabConfig = {
16
+ id: TabId;
17
+ label: string;
18
+ entries: SchemaEntry[];
19
+ };
20
+
21
+ export type AttributesEditorSpecialSection = {
22
+ key: string;
23
+ entries: SchemaEntry[];
24
+ meta?: {
25
+ label?: string;
26
+ description?: string;
27
+ sort?: number;
28
+ category?: string;
29
+ };
30
+ };
31
+
32
+ export type TabContentInfo = Record<
33
+ TabId,
34
+ {
35
+ baseCount: number;
36
+ specialCount: number;
37
+ }
38
+ >;
39
+
40
+ export type AttributesEditorModel = {
41
+ isInvalidNode: boolean;
42
+ baseData: NodeData<NodeDefaultAttribute>;
43
+ data: NodeData<NodeDefaultAttribute>;
44
+
45
+ // store-derived
46
+ appConfig: unknown;
47
+ projectFonts: Fonts;
48
+ loadedFonts: string[] | undefined;
49
+ markFontLoaded: (fontFamily: string) => void;
50
+ addError: (message: string) => void;
51
+
52
+ // patterns/meta/schema
53
+ schema: Record<string, unknown>;
54
+ attributeMeta?: Record<string, any>;
55
+ componentTitle: string;
56
+ componentDescription?: string;
57
+ patternForType?: any;
58
+ componentMeta?: any;
59
+
60
+ // attributes/style
61
+ attributes: NodeDefaultAttribute;
62
+ styleBag?: Record<string, unknown>;
63
+ projectColorsForPicker?: ProjectColors;
64
+ viewAttributes?: Partial<ViewPropsGenerated['attributes']>;
65
+ layoutContext: LayoutContext;
66
+ getAttributeValue: (name: string) => unknown;
67
+ handleAttributeChange: (name: string, val: unknown) => void;
68
+ handleChildrenChange: (val: string) => void;
69
+
70
+ // tabs/entries
71
+ tabs: AttributesEditorTabConfig[];
72
+ tabContentInfo: TabContentInfo;
73
+ firstAvailableTab: TabId;
74
+ activeTab: TabId;
75
+ setActiveTab: (next: TabId) => void;
76
+ activeEntries: SchemaEntry[];
77
+ specialSectionsByTab: Record<TabId, AttributesEditorSpecialSection[]>;
78
+ activeSpecialSections: AttributesEditorSpecialSection[];
79
+
80
+ // misc UI
81
+ mockableFeatureKeys: string[];
82
+ activeMockableFeature: string | null;
83
+ setActiveMockableFeature: (next: string | null) => void;
84
+ hasStringChildren: boolean;
85
+ childrenValue: string;
86
+ };