@developer_tribe/react-builder 1.0.8 → 1.0.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 (216) hide show
  1. package/dist/build-components/BIcon/BIconProps.generated.d.ts +3 -0
  2. package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +1 -0
  3. package/dist/build-components/Button/ButtonProps.generated.d.ts +1 -0
  4. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +5 -0
  5. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +1 -0
  6. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +1 -0
  7. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +1 -0
  8. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +1 -0
  9. package/dist/build-components/Image/ImageProps.generated.d.ts +1 -0
  10. package/dist/build-components/Main/MainProps.generated.d.ts +1 -1
  11. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +1 -0
  12. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +1 -0
  13. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +1 -0
  14. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +1 -0
  15. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +3 -0
  16. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +1 -0
  17. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +1 -0
  18. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +3 -0
  19. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +3 -0
  20. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +3 -0
  21. package/dist/build-components/PaywallBackground/PaywallBackgroundProps.generated.d.ts +1 -1
  22. package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +3 -1
  23. package/dist/build-components/PaywallOptions/PaywallOptionsProps.generated.d.ts +1 -1
  24. package/dist/build-components/PaywallProvider/PaywallContext.d.ts +12 -0
  25. package/dist/build-components/PaywallProvider/PaywallProviderProps.generated.d.ts +1 -1
  26. package/dist/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.d.ts +1 -0
  27. package/dist/build-components/RadioButton/RadioButtonProps.generated.d.ts +1 -1
  28. package/dist/build-components/Text/TextProps.generated.d.ts +3 -0
  29. package/dist/build-components/View/ViewProps.generated.d.ts +1 -0
  30. package/dist/build-components/patterns.generated.d.ts +372 -374
  31. package/dist/components/BuilderProvider.d.ts +2 -0
  32. package/dist/components/ParamsProvider.d.ts +5 -0
  33. package/dist/components/RenderErrorBoundary.d.ts +28 -0
  34. package/dist/hooks/useSyncHtmlThemeClass.d.ts +7 -0
  35. package/dist/index.cjs.js +5 -5
  36. package/dist/index.cjs.js.map +1 -1
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.esm.js +3 -3
  39. package/dist/index.esm.js.map +1 -1
  40. package/dist/index.native.cjs.js +4 -4
  41. package/dist/index.native.cjs.js.map +1 -1
  42. package/dist/index.native.d.ts +1 -0
  43. package/dist/index.native.esm.js +4 -4
  44. package/dist/index.native.esm.js.map +1 -1
  45. package/dist/migrations/migratePipe.d.ts +14 -0
  46. package/dist/migrations/migrations/1.1.0_normalize_style_attributes.d.ts +2 -0
  47. package/dist/migrations/semver.d.ts +8 -0
  48. package/dist/migrations/types.d.ts +8 -0
  49. package/dist/mockOS/components/SubscriptionModal.d.ts +7 -0
  50. package/dist/mockOS/context/MockOSContextBase.d.ts +1 -0
  51. package/dist/mockOS/hooks/useMockIap.d.ts +3 -0
  52. package/dist/mockOS/index.d.ts +4 -0
  53. package/dist/mockOS/managers/mockOSIapManager.d.ts +6 -0
  54. package/dist/mockOS/managers/subscriptionManager.d.ts +10 -0
  55. package/dist/pages/ProjectDebug.d.ts +14 -0
  56. package/dist/pages/ProjectMigrationPage.d.ts +23 -0
  57. package/dist/pages/ProjectValidationPage.d.ts +15 -0
  58. package/dist/styles.css +1 -1
  59. package/dist/types/Device.d.ts +5 -0
  60. package/dist/utils/__special_exceptions.d.ts +7 -0
  61. package/dist/utils/getImage.d.ts +23 -0
  62. package/dist/utils/pasteNode.d.ts +15 -0
  63. package/dist/utils/patterns.d.ts +1 -2
  64. package/package.json +6 -2
  65. package/scripts/migrate-patterns-to-v2.mjs +131 -0
  66. package/scripts/migrate-samples-to-current.ts +79 -0
  67. package/scripts/prebuild/utils/createGeneratedProps.js +4 -5
  68. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +32 -21
  69. package/scripts/prebuild/utils/validatePatternJson.js +12 -10
  70. package/src/.DS_Store +0 -0
  71. package/src/AttributesEditor.tsx +41 -11
  72. package/src/RenderPage.tsx +55 -0
  73. package/src/assets/.DS_Store +0 -0
  74. package/src/assets/devices.json +91 -0
  75. package/src/assets/samples/carousel-sample.json +141 -29
  76. package/src/assets/samples/getSamples.ts +9 -0
  77. package/src/assets/samples/paywall-1.json +119 -71
  78. package/src/assets/samples/simple-1.json +28 -16
  79. package/src/assets/samples/simple-2.json +157 -82
  80. package/src/assets/samples/unmigrated-builder1.json +42 -0
  81. package/src/assets/samples/unvalidated-builder1.json +49 -0
  82. package/src/assets/samples/unvalidated-crash1.json +19 -0
  83. package/src/assets/samples/unvalidated-crashcomponent1.json +16 -0
  84. package/src/assets/samples/vpn-onboard-1.json +91 -51
  85. package/src/assets/samples/vpn-onboard-2.json +318 -278
  86. package/src/assets/samples/vpn-onboard-3.json +286 -252
  87. package/src/assets/samples/vpn-onboard-4.json +286 -252
  88. package/src/assets/samples/vpn-onboard-5.json +434 -374
  89. package/src/assets/samples/vpn-onboard-6.json +290 -250
  90. package/src/attributes-editor/Field.tsx +1 -1
  91. package/src/attributes-editor/LayoutPreviewPicker.tsx +5 -2
  92. package/src/build-components/BIcon/BIconProps.generated.ts +3 -0
  93. package/src/build-components/BIcon/pattern.json +12 -9
  94. package/src/build-components/BackgroundImage/BackgroundImage.tsx +3 -1
  95. package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +1 -0
  96. package/src/build-components/BackgroundImage/pattern.json +25 -16
  97. package/src/build-components/Button/Button.tsx +26 -3
  98. package/src/build-components/Button/ButtonProps.generated.ts +1 -0
  99. package/src/build-components/Button/pattern.json +10 -6
  100. package/src/build-components/Carousel/CarouselProps.generated.ts +5 -0
  101. package/src/build-components/Carousel/pattern.json +19 -8
  102. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +1 -0
  103. package/src/build-components/CarouselButtons/pattern.json +11 -5
  104. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +1 -0
  105. package/src/build-components/CarouselDots/pattern.json +5 -4
  106. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +1 -0
  107. package/src/build-components/CarouselItem/pattern.json +5 -4
  108. package/src/build-components/CarouselProvider/CarouselProvider.tsx +44 -2
  109. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +1 -0
  110. package/src/build-components/Image/Image.tsx +2 -1
  111. package/src/build-components/Image/ImageProps.generated.ts +1 -0
  112. package/src/build-components/Image/pattern.json +11 -5
  113. package/src/build-components/Main/MainProps.generated.ts +1 -1
  114. package/src/build-components/Main/pattern.json +12 -9
  115. package/src/build-components/Onboard/OnboardProps.generated.ts +1 -0
  116. package/src/build-components/Onboard/pattern.json +14 -9
  117. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +1 -0
  118. package/src/build-components/OnboardButton/pattern.json +5 -4
  119. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +1 -0
  120. package/src/build-components/OnboardButtons/pattern.json +5 -4
  121. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +1 -0
  122. package/src/build-components/OnboardDot/pattern.json +5 -4
  123. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +3 -0
  124. package/src/build-components/OnboardFooter/pattern.json +8 -5
  125. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +1 -0
  126. package/src/build-components/OnboardImage/pattern.json +7 -4
  127. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +1 -0
  128. package/src/build-components/OnboardItem/pattern.json +18 -9
  129. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +3 -0
  130. package/src/build-components/OnboardProvider/pattern.json +21 -6
  131. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +3 -0
  132. package/src/build-components/OnboardSubtitle/pattern.json +10 -6
  133. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +3 -0
  134. package/src/build-components/OnboardTitle/pattern.json +11 -7
  135. package/src/build-components/PaywallBackground/PaywallBackgroundProps.generated.ts +1 -1
  136. package/src/build-components/PaywallBackground/pattern.json +5 -4
  137. package/src/build-components/PaywallCloseButton/PaywallCloseButton.tsx +6 -1
  138. package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +3 -1
  139. package/src/build-components/PaywallCloseButton/pattern.json +15 -12
  140. package/src/build-components/PaywallOptions/PaywallOptionButton.tsx +0 -1
  141. package/src/build-components/PaywallOptions/PaywallOptions.tsx +3 -2
  142. package/src/build-components/PaywallOptions/PaywallOptionsProps.generated.ts +1 -1
  143. package/src/build-components/PaywallOptions/pattern.json +14 -11
  144. package/src/build-components/PaywallProvider/PaywallContext.ts +25 -0
  145. package/src/build-components/PaywallProvider/PaywallProvider.tsx +102 -5
  146. package/src/build-components/PaywallProvider/PaywallProviderProps.generated.ts +1 -1
  147. package/src/build-components/PaywallProvider/pattern.json +11 -8
  148. package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButton.tsx +7 -0
  149. package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.ts +1 -0
  150. package/src/build-components/PaywallSubscribeButton/pattern.json +16 -13
  151. package/src/build-components/RadioButton/RadioButtonProps.generated.ts +1 -1
  152. package/src/build-components/RadioButton/pattern.json +5 -4
  153. package/src/build-components/Text/Text.tsx +107 -4
  154. package/src/build-components/Text/TextProps.generated.ts +3 -0
  155. package/src/build-components/Text/pattern.json +19 -4
  156. package/src/build-components/View/ViewProps.generated.ts +1 -0
  157. package/src/build-components/View/pattern.json +28 -13
  158. package/src/build-components/other.tsx +15 -0
  159. package/src/build-components/patterns.generated.ts +340 -235
  160. package/src/build-components/useNode.ts +22 -3
  161. package/src/components/Builder.tsx +20 -6
  162. package/src/components/BuilderButton.tsx +75 -38
  163. package/src/components/BuilderProvider.tsx +22 -2
  164. package/src/components/DeviceButton.tsx +12 -5
  165. package/src/components/EditorHeader.tsx +296 -38
  166. package/src/components/ParamsProvider.tsx +7 -0
  167. package/src/components/RenderErrorBoundary.tsx +200 -0
  168. package/src/hooks/useParams.ts +5 -1
  169. package/src/hooks/useSyncHtmlThemeClass.ts +19 -0
  170. package/src/index.native.ts +7 -0
  171. package/src/index.ts +8 -0
  172. package/src/migrations/migratePipe.ts +59 -0
  173. package/src/migrations/migrations/1.1.0_normalize_style_attributes.ts +80 -0
  174. package/src/migrations/semver.ts +24 -0
  175. package/src/migrations/types.ts +9 -0
  176. package/src/mockOS/components/PermissionModal.tsx +3 -2
  177. package/src/mockOS/components/SubscriptionModal.tsx +400 -0
  178. package/src/mockOS/context/MockOSContext.tsx +61 -10
  179. package/src/mockOS/context/MockOSContextBase.ts +1 -0
  180. package/src/mockOS/hooks/useMockIap.ts +11 -0
  181. package/src/mockOS/index.ts +7 -0
  182. package/src/mockOS/managers/mockOSIapManager.ts +10 -0
  183. package/src/mockOS/managers/subscriptionManager.ts +36 -0
  184. package/src/modals/IconPickerModal.tsx +1 -1
  185. package/src/pages/ProjectDebug.tsx +331 -0
  186. package/src/pages/ProjectMigrationPage.tsx +92 -0
  187. package/src/pages/ProjectPage.tsx +313 -161
  188. package/src/pages/ProjectValidationPage.tsx +54 -0
  189. package/src/styles/base/_global.scss +58 -11
  190. package/src/styles/components/_attributes-editor.scss +1 -1
  191. package/src/styles/components/_bottom-bar.scss +7 -4
  192. package/src/styles/components/_editor-shell.scss +126 -4
  193. package/src/styles/components/_mockos-router.scss +3 -2
  194. package/src/styles/components/_ui-components.scss +10 -5
  195. package/src/styles/foundation/_colors.scss +78 -11
  196. package/src/styles/foundation/_mixins.scss +4 -1
  197. package/src/styles/foundation/_sizes.scss +4 -2
  198. package/src/styles/index.scss +1 -0
  199. package/src/styles/layout/_builder.scss +61 -0
  200. package/src/styles/layout/_project-validation.scss +214 -0
  201. package/src/styles/modals/_add-component.scss +4 -2
  202. package/src/styles/modals/_color-modal.scss +4 -2
  203. package/src/styles/modals/_modal-shell.scss +3 -1
  204. package/src/types/Device.ts +5 -0
  205. package/src/utils/__special_exceptions.ts +88 -0
  206. package/src/utils/analyseNode.ts +8 -2
  207. package/src/utils/analyseNodeByPatterns.ts +43 -9
  208. package/src/utils/extractTextStyle.ts +19 -6
  209. package/src/utils/extractViewStyle.ts +68 -59
  210. package/src/utils/getImage.ts +76 -0
  211. package/src/utils/novaToJson.ts +2 -1
  212. package/src/utils/pasteNode.ts +172 -0
  213. package/src/utils/patterns.ts +4 -3
  214. package/dist/android.svg +0 -43
  215. package/dist/apple.svg +0 -16
  216. package/dist/background.jpg +0 -0
@@ -0,0 +1,79 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ // NOTE: Keep the `.js` extension so the compiled ESM output runs in Node.
4
+ import { runProjectMigrations } from '../src/migrations/migratePipe.js';
5
+
6
+ type JsonValue =
7
+ | null
8
+ | boolean
9
+ | number
10
+ | string
11
+ | JsonValue[]
12
+ | { [k: string]: JsonValue };
13
+
14
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
15
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
16
+ }
17
+
18
+ function readJsonFile(filePath: string): unknown {
19
+ const raw = fs.readFileSync(filePath, 'utf8');
20
+ return JSON.parse(raw);
21
+ }
22
+
23
+ function writeJsonFile(filePath: string, value: unknown): void {
24
+ const json = JSON.stringify(value, null, 2) + '\n';
25
+ fs.writeFileSync(filePath, json, 'utf8');
26
+ }
27
+
28
+ function main() {
29
+ // Resolve from current working directory (expected to be repo root when invoked).
30
+ const repoRoot = path.resolve(process.cwd());
31
+
32
+ const sampleDir = path.join(repoRoot, 'src', 'assets', 'samples');
33
+ const targets = [
34
+ 'carousel-sample.json',
35
+ 'paywall-1.json',
36
+ 'simple-1.json',
37
+ 'simple-2.json',
38
+ 'unvalidated-crash1.json',
39
+ 'unvalidated-crashcomponent1.json',
40
+ 'unvalidated-builder1.json',
41
+ 'vpn-onboard-1.json',
42
+ 'vpn-onboard-2.json',
43
+ 'vpn-onboard-3.json',
44
+ 'vpn-onboard-4.json',
45
+ 'vpn-onboard-5.json',
46
+ 'vpn-onboard-6.json',
47
+ ].map(f => path.join(sampleDir, f));
48
+
49
+ let changed = 0;
50
+ for (const filePath of targets) {
51
+ const before = readJsonFile(filePath);
52
+ if (!isPlainObject(before)) {
53
+ throw new Error(`Expected JSON object in ${filePath}`);
54
+ }
55
+
56
+ const { project: after, applied } = runProjectMigrations(before as any);
57
+
58
+ const beforeStr = JSON.stringify(before);
59
+ const afterStr = JSON.stringify(after as unknown as JsonValue);
60
+ if (beforeStr !== afterStr) {
61
+ changed++;
62
+ writeJsonFile(filePath, after);
63
+ // eslint-disable-next-line no-console
64
+ console.log(
65
+ `migrated ${path.relative(repoRoot, filePath)}: ${
66
+ applied.map(m => m.id).join(', ') || '(none)'
67
+ }`
68
+ );
69
+ } else {
70
+ // eslint-disable-next-line no-console
71
+ console.log(`up-to-date ${path.relative(repoRoot, filePath)}`);
72
+ }
73
+ }
74
+
75
+ // eslint-disable-next-line no-console
76
+ console.log(`done. changed: ${changed}/${targets.length}`);
77
+ }
78
+
79
+ main();
@@ -78,7 +78,7 @@ export async function createGeneratedProps(
78
78
  const fileName = `${componentName}Props.generated.ts`;
79
79
  const filePath = path.join(componentDir, fileName);
80
80
 
81
- const { pattern, allowUnknownAttributes } = patternJson;
81
+ const { pattern } = patternJson;
82
82
  const attributes = pattern.attributes || {};
83
83
  const allTypes = patternJson.types || {};
84
84
 
@@ -126,9 +126,8 @@ export async function createGeneratedProps(
126
126
  return ` ${key}?: ${tsType};`;
127
127
  });
128
128
 
129
- const indexSignature = allowUnknownAttributes
130
- ? ' [key: string]: string | number | boolean | undefined;\n'
131
- : '';
129
+ // In schema v2 we always allow a nested `style` object (for CSSProperties-like overrides/defaults).
130
+ const styleLine = ` style?: Record<string, unknown>;`;
132
131
 
133
132
  const childTsType =
134
133
  typeof pattern.children === 'string' ? pattern.children : 'string';
@@ -155,8 +154,8 @@ export async function createGeneratedProps(
155
154
  `export interface ${componentName}PropsGenerated {\n` +
156
155
  ` child: ${normalizedChildTsType};\n` +
157
156
  ` attributes: {\n` +
157
+ `${styleLine}\n` +
158
158
  (attributeLines.length ? attributeLines.join('\n') + '\n' : '') +
159
- indexSignature +
160
159
  ` };\n` +
161
160
  `}\n` +
162
161
  `\n` +
@@ -89,11 +89,6 @@ async function validatePatternJson(componentDir, componentName) {
89
89
  `[${componentName}] pattern.json -> 'schemaVersion' must be a number`
90
90
  );
91
91
  }
92
- if (typeof data.allowUnknownAttributes !== 'boolean') {
93
- return fail(
94
- `[${componentName}] pattern.json -> 'allowUnknownAttributes' must be a boolean`
95
- );
96
- }
97
92
  if (typeof data.pattern !== 'object' || data.pattern == null) {
98
93
  return fail(
99
94
  `[${componentName}] pattern.json -> 'pattern' must be an object`
@@ -110,12 +105,19 @@ async function validatePatternJson(componentDir, componentName) {
110
105
  `[${componentName}] pattern.json -> 'pattern.type' must match the component folder name '${componentName}' (PascalCase).`
111
106
  );
112
107
  }
113
- if (
114
- typeof pattern.children !== 'string' &&
115
- !Array.isArray(pattern.children)
116
- ) {
108
+ // children spec can only be: node | string | never
109
+ // - "node" means: Node | Node[]
110
+ // - "string" means: text node
111
+ // - "never" means: no children allowed
112
+ if (typeof pattern.children !== 'string') {
117
113
  return fail(
118
- `[${componentName}] pattern.json -> 'pattern.children' must be a string or an array`
114
+ `[${componentName}] pattern.json -> 'pattern.children' must be a string`
115
+ );
116
+ }
117
+ const allowedChildrenKinds = new Set(['node', 'string', 'never']);
118
+ if (!allowedChildrenKinds.has(pattern.children)) {
119
+ return fail(
120
+ `[${componentName}] pattern.json -> 'pattern.children' must be one of: node | string | never`
119
121
  );
120
122
  }
121
123
  if (typeof pattern.attributes !== 'object' || pattern.attributes == null) {
@@ -298,25 +300,35 @@ async function validatePatternJson(componentDir, componentName) {
298
300
  : parentData?.pattern?.children;
299
301
 
300
302
  const mergedTypes = { ...(parentData.types || {}), ...(data.types || {}) };
303
+ const parentDefaults = buildDefaultsBlock(parentData);
304
+ const childDefaults = buildDefaultsBlock(data);
301
305
  const mergedDefaults = {
302
- ...buildDefaultsBlock(parentData),
303
- ...buildDefaultsBlock(data),
306
+ ...parentDefaults,
307
+ ...childDefaults,
308
+ // Deep-merge `defaults.style` so parent style defaults aren't blown away.
309
+ style: {
310
+ ...(isPlainObject(parentDefaults?.style) ? parentDefaults.style : {}),
311
+ ...(isPlainObject(childDefaults?.style) ? childDefaults.style : {}),
312
+ },
304
313
  };
305
- const mergedAllowUnknown =
306
- typeof data.allowUnknownAttributes === 'boolean'
307
- ? data.allowUnknownAttributes
308
- : parentData.allowUnknownAttributes;
314
+ if (Object.keys(mergedDefaults.style || {}).length === 0) {
315
+ delete mergedDefaults.style;
316
+ }
309
317
 
310
- const mergedMetaAttributes = {
311
- ...(parentData?.meta?.attributes || {}),
312
- ...(data?.meta?.attributes || {}),
318
+ const parentMetaStyles =
319
+ (parentData?.meta?.styles || parentData?.meta?.attributes || {}) ?? {};
320
+ const childMetaStyles =
321
+ (data?.meta?.styles || data?.meta?.attributes || {}) ?? {};
322
+ const mergedMetaStyles = {
323
+ ...(isPlainObject(parentMetaStyles) ? parentMetaStyles : {}),
324
+ ...(isPlainObject(childMetaStyles) ? childMetaStyles : {}),
313
325
  };
314
326
  const mergedMeta =
315
327
  parentData.meta || data.meta
316
328
  ? {
317
329
  ...(parentData.meta || {}),
318
330
  ...(data.meta || {}),
319
- attributes: mergedMetaAttributes,
331
+ styles: mergedMetaStyles,
320
332
  }
321
333
  : undefined;
322
334
 
@@ -332,7 +344,6 @@ async function validatePatternJson(componentDir, componentName) {
332
344
  data = {
333
345
  ...parentData,
334
346
  ...data,
335
- allowUnknownAttributes: mergedAllowUnknown,
336
347
  types: mergedTypes,
337
348
  defaults: mergedDefaults,
338
349
  meta: mergedMeta,
@@ -30,11 +30,6 @@ export async function validatePatternJson(componentDir, componentName) {
30
30
  `[${componentName}] pattern.json -> 'schemaVersion' must be a number`
31
31
  );
32
32
  }
33
- if (typeof data.allowUnknownAttributes !== 'boolean') {
34
- return fail(
35
- `[${componentName}] pattern.json -> 'allowUnknownAttributes' must be a boolean`
36
- );
37
- }
38
33
  if (typeof data.pattern !== 'object' || data.pattern == null) {
39
34
  return fail(
40
35
  `[${componentName}] pattern.json -> 'pattern' must be an object`
@@ -46,12 +41,19 @@ export async function validatePatternJson(componentDir, componentName) {
46
41
  `[${componentName}] pattern.json -> 'pattern.type' must be a string`
47
42
  );
48
43
  }
49
- if (
50
- typeof pattern.children !== 'string' &&
51
- !Array.isArray(pattern.children)
52
- ) {
44
+ // children spec can only be: node | string | never
45
+ // - "node" means: Node | Node[]
46
+ // - "string" means: text node
47
+ // - "never" means: no children allowed
48
+ if (typeof pattern.children !== 'string') {
49
+ return fail(
50
+ `[${componentName}] pattern.json -> 'pattern.children' must be a string`
51
+ );
52
+ }
53
+ const allowedChildrenKinds = new Set(['node', 'string', 'never']);
54
+ if (!allowedChildrenKinds.has(pattern.children)) {
53
55
  return fail(
54
- `[${componentName}] pattern.json -> 'pattern.children' must be a string or an array`
56
+ `[${componentName}] pattern.json -> 'pattern.children' must be one of: node | string | never`
55
57
  );
56
58
  }
57
59
  if (typeof pattern.attributes !== 'object' || pattern.attributes == null) {
package/src/.DS_Store CHANGED
Binary file
@@ -31,6 +31,10 @@ type AttributesEditorProps = {
31
31
  projectColors?: ProjectColors;
32
32
  };
33
33
 
34
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
35
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
36
+ }
37
+
34
38
  type TabId = 'style' | 'container' | 'other';
35
39
 
36
40
  type TabConfig = {
@@ -111,6 +115,23 @@ export function AttributesEditor({
111
115
  const schema = getAttributeSchema(data?.type) ?? {};
112
116
  const attributeMeta = getAttributeMeta(data?.type);
113
117
  const attributes = (data?.attributes ?? {}) as NodeDefaultAttribute;
118
+ const styleAttributeKeys = useMemo(() => {
119
+ // schemaVersion=2 stores style-like props under attributes.style; keep legacy support for flat attrs.
120
+ const viewSchema = getAttributeSchema('View') ?? {};
121
+ const textSchema = getAttributeSchema('Text') ?? {};
122
+ return new Set([...Object.keys(viewSchema), ...Object.keys(textSchema)]);
123
+ }, []);
124
+ const getAttributeValue = useCallback(
125
+ (name: string) => {
126
+ const direct = (attributes as Record<string, unknown>)?.[name];
127
+ if (direct !== undefined && direct !== null) return direct;
128
+ const styleBag = (attributes as any)?.style as
129
+ | Record<string, unknown>
130
+ | undefined;
131
+ return styleBag?.[name];
132
+ },
133
+ [attributes],
134
+ );
114
135
  const viewAttributes = useMemo<
115
136
  Partial<ViewPropsGenerated['attributes']> | undefined
116
137
  >(
@@ -304,16 +325,27 @@ export function AttributesEditor({
304
325
 
305
326
  const handleAttributeChange = useCallback(
306
327
  (name: string, val: unknown) => {
328
+ const prevAttrs =
329
+ ((baseData?.attributes ?? {}) as Record<string, unknown>) ?? {};
330
+ const isStyleKey = styleAttributeKeys.has(name);
331
+ const nextAttrs: Record<string, unknown> = { ...prevAttrs };
332
+ if (isStyleKey) {
333
+ const prevStyle = (
334
+ isPlainObject(nextAttrs.style) ? nextAttrs.style : {}
335
+ ) as Record<string, unknown>;
336
+ nextAttrs.style = { ...prevStyle, [name]: val };
337
+ // Normalize away legacy flat style keys once edited.
338
+ if (name in nextAttrs) delete nextAttrs[name];
339
+ } else {
340
+ nextAttrs[name] = val;
341
+ }
307
342
  const next: NodeData<NodeDefaultAttribute> = {
308
343
  ...baseData,
309
- attributes: {
310
- ...((baseData?.attributes ?? {}) as NodeDefaultAttribute),
311
- [name]: val,
312
- },
344
+ attributes: nextAttrs as NodeDefaultAttribute,
313
345
  };
314
346
  onChange(next);
315
347
  },
316
- [baseData, onChange],
348
+ [baseData, onChange, styleAttributeKeys],
317
349
  );
318
350
 
319
351
  const renderIconTypeField = useCallback(
@@ -336,7 +368,7 @@ export function AttributesEditor({
336
368
  borderRadius: 6,
337
369
  border: '1px solid #ddd',
338
370
  padding: '8px 10px',
339
- background: '#fff',
371
+ background: 'hsl(var(--card, var(--rb-card, 0 0% 100%)))',
340
372
  cursor: 'pointer',
341
373
  }}
342
374
  >
@@ -422,9 +454,7 @@ export function AttributesEditor({
422
454
  const preferredScale = toPreferredScale(
423
455
  attributeMeta?.[name]?.preferedScale,
424
456
  );
425
- const currentValue = (attributes as Record<string, unknown>)[
426
- name
427
- ];
457
+ const currentValue = getAttributeValue(name);
428
458
  const isBoolean = isBooleanFieldType(type);
429
459
  const wrapperClassNames = [
430
460
  'attributes-editor__field-wrapper',
@@ -651,12 +681,12 @@ export function AttributesEditor({
651
681
  <p className="attributes-editor__field-label">{label}</p>
652
682
  ) : null}
653
683
  {type === 'iconType' ? (
654
- renderIconTypeField(name, attributes?.[name])
684
+ renderIconTypeField(name, getAttributeValue(name))
655
685
  ) : (
656
686
  <Field
657
687
  name={name}
658
688
  type={type}
659
- value={attributes?.[name]}
689
+ value={getAttributeValue(name)}
660
690
  onChange={(val) => handleAttributeChange(name, val)}
661
691
  componentType={data?.type}
662
692
  projectColors={projectColorsForPicker}
@@ -5,6 +5,7 @@ import { RenderNode } from './build-components';
5
5
  import { useRenderStore } from './store';
6
6
  import { useLogRender } from './utils/useLogRender';
7
7
  import { findNodeByKeyNested } from './utils/findNodeByKeyNested';
8
+ import type { NodeData } from './types/Node';
8
9
  export type ScreenStyle = {
9
10
  light: { backgroundColor: string; color: string; seperatorColor?: string };
10
11
  dark: { backgroundColor: string; color: string; seperatorColor?: string };
@@ -15,6 +16,49 @@ interface RenderPageProps {
15
16
  onSelectNode?: (node: Node | null) => void;
16
17
  }
17
18
 
19
+ function isNodeDataLike(value: unknown): value is NodeData {
20
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
21
+ return false;
22
+ }
23
+ const v = value as Record<string, unknown>;
24
+ return typeof v.type === 'string' && 'children' in v;
25
+ }
26
+
27
+ function isNullish(value: unknown): value is null | undefined {
28
+ return value === null || value === undefined;
29
+ }
30
+
31
+ function isString(value: unknown): value is string {
32
+ return typeof value === 'string';
33
+ }
34
+
35
+ function isArray(value: unknown): value is unknown[] {
36
+ return Array.isArray(value);
37
+ }
38
+
39
+ function isEmptyPlainObject(value: unknown): boolean {
40
+ return (
41
+ typeof value === 'object' &&
42
+ value !== null &&
43
+ !Array.isArray(value) &&
44
+ Object.keys(value as object).length === 0
45
+ );
46
+ }
47
+
48
+ function isEmptyPreview(node: Node): boolean {
49
+ if (isNullish(node) || isEmptyPlainObject(node)) return true;
50
+ if (isString(node)) return node.trim().length === 0;
51
+ if (isArray(node)) return node.length === 0;
52
+ if (!isNodeDataLike(node)) return true;
53
+
54
+ const children = node.children as Node;
55
+ if (isNullish(children) || isEmptyPlainObject(children)) return true;
56
+ if (isString(children)) return children.trim().length === 0;
57
+ if (isArray(children)) return children.length === 0;
58
+ // children is another node object
59
+ return false;
60
+ }
61
+
18
62
  export function RenderPage({ data, name, onSelectNode }: RenderPageProps) {
19
63
  useLogRender('RenderPage');
20
64
  const { previewMode, forceRender, setCurrent } = useRenderStore((s) => ({
@@ -23,6 +67,7 @@ export function RenderPage({ data, name, onSelectNode }: RenderPageProps) {
23
67
  setCurrent: s.setCurrent,
24
68
  }));
25
69
  const previewRootRef = useRef<HTMLDivElement | null>(null);
70
+ const showEmptyState = isEmptyPreview(data);
26
71
 
27
72
  useEffect(() => {
28
73
  if (!previewMode) {
@@ -64,6 +109,16 @@ export function RenderPage({ data, name, onSelectNode }: RenderPageProps) {
64
109
  <DeviceMockFrame appName={name}>
65
110
  <div className="screen-preview" ref={previewRootRef}>
66
111
  <RenderNode node={data} />
112
+ {showEmptyState && (
113
+ <div className="rb-empty-preview" aria-hidden="true">
114
+ <div className="rb-empty-preview__card">
115
+ <div className="rb-empty-preview__title">Empty screen</div>
116
+ <div className="rb-empty-preview__hint">
117
+ Add a component to start
118
+ </div>
119
+ </div>
120
+ </div>
121
+ )}
67
122
  </div>
68
123
  </DeviceMockFrame>
69
124
  );
Binary file