@developer_tribe/react-builder 1.2.23 → 1.2.25

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 (150) hide show
  1. package/dist/attribute-analyser/style/native/useExtractImageStyle.d.ts +5 -5
  2. package/dist/attribute-analyser/style/native/useExtractTextStyle.d.ts +6 -4
  3. package/dist/attribute-analyser/style/native/useExtractViewStyle.d.ts +5 -3
  4. package/dist/attributes-editor/SpecialCategorySection.d.ts +2 -1
  5. package/dist/attributes-editor/attributesEditorModelTypes.d.ts +2 -0
  6. package/dist/build-components/BIcon/BIconProps.generated.d.ts +0 -2
  7. package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +0 -2
  8. package/dist/build-components/Button/ButtonProps.generated.d.ts +0 -2
  9. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +0 -2
  10. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +0 -2
  11. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +0 -2
  12. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +0 -2
  13. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +0 -2
  14. package/dist/build-components/CountDown/CountDownProps.generated.d.ts +0 -2
  15. package/dist/build-components/Counter/CounterProps.generated.d.ts +0 -2
  16. package/dist/build-components/Image/ImageProps.generated.d.ts +0 -2
  17. package/dist/build-components/Main/MainProps.generated.d.ts +0 -2
  18. package/dist/build-components/NavigationBarColor/NavigationBarColorProps.generated.d.ts +0 -2
  19. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +0 -2
  20. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +0 -2
  21. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +0 -2
  22. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +0 -2
  23. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +0 -2
  24. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +0 -2
  25. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +0 -2
  26. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +0 -2
  27. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +0 -2
  28. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +0 -2
  29. package/dist/build-components/PaywallBackground/PaywallBackgroundProps.generated.d.ts +0 -2
  30. package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +0 -2
  31. package/dist/build-components/PaywallCounter/PaywallCounterProps.generated.d.ts +0 -2
  32. package/dist/build-components/PaywallOptions/PaywallOptionsProps.generated.d.ts +0 -2
  33. package/dist/build-components/PaywallProvider/PaywallProviderProps.generated.d.ts +0 -2
  34. package/dist/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.d.ts +0 -2
  35. package/dist/build-components/RadioButton/RadioButtonProps.generated.d.ts +0 -2
  36. package/dist/build-components/Separator/SeparatorProps.generated.d.ts +0 -2
  37. package/dist/build-components/StatusBarColor/StatusBarColorProps.generated.d.ts +0 -2
  38. package/dist/build-components/Text/TextProps.generated.d.ts +0 -2
  39. package/dist/build-components/patterns.generated.d.ts +80 -66
  40. package/dist/index.cjs.js +2 -2
  41. package/dist/index.cjs.js.map +1 -1
  42. package/dist/index.d.ts +3 -1
  43. package/dist/index.esm.js +2 -2
  44. package/dist/index.esm.js.map +1 -1
  45. package/dist/index.web.cjs.js +3 -3
  46. package/dist/index.web.cjs.js.map +1 -1
  47. package/dist/index.web.esm.js +3 -3
  48. package/dist/index.web.esm.js.map +1 -1
  49. package/dist/pages/ProjectPage.d.ts +2 -2
  50. package/dist/pages/projectPageUtils.d.ts +7 -1
  51. package/dist/types/Project.d.ts +6 -0
  52. package/dist/utils/attributeStyle.d.ts +12 -0
  53. package/dist/utils/patterns.d.ts +2 -0
  54. package/package.json +6 -1
  55. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +11 -2
  56. package/src/AttributesEditor.tsx +15 -4
  57. package/src/assets/meta.json +1 -1
  58. package/src/assets/samples/paywall-1.json +5 -5
  59. package/src/assets/samples/paywall-2.json +5 -5
  60. package/src/assets/samples/paywall-app-delete-offer.json +0 -1
  61. package/src/assets/samples/paywall-app-open-offer.json +0 -1
  62. package/src/assets/samples/paywall-back-offer.json +0 -1
  63. package/src/assets/samples/paywall-notification-offer.json +0 -1
  64. package/src/assets/samples/simple-2.json +0 -1
  65. package/src/attribute-analyser/style/native/useExtractImageStyle.ts +19 -15
  66. package/src/attribute-analyser/style/native/useExtractTextStyle.ts +25 -15
  67. package/src/attribute-analyser/style/native/useExtractViewStyle.ts +19 -21
  68. package/src/attributes-editor/AttributesEditorView.tsx +43 -36
  69. package/src/attributes-editor/SpecialCategorySection.tsx +5 -3
  70. package/src/attributes-editor/attributesEditorModelTypes.ts +2 -0
  71. package/src/attributes-editor/useAttributesEditorModel.ts +6 -0
  72. package/src/build-components/BIcon/BIconProps.generated.ts +0 -2
  73. package/src/build-components/BIcon/pattern.json +5 -3
  74. package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +0 -2
  75. package/src/build-components/BackgroundImage/pattern.json +12 -4
  76. package/src/build-components/Button/ButtonProps.generated.ts +0 -2
  77. package/src/build-components/Button/pattern.json +5 -3
  78. package/src/build-components/Carousel/CarouselProps.generated.ts +0 -2
  79. package/src/build-components/Carousel/pattern.json +11 -5
  80. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +0 -2
  81. package/src/build-components/CarouselButtons/pattern.json +11 -4
  82. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +0 -2
  83. package/src/build-components/CarouselDots/pattern.json +5 -3
  84. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +0 -2
  85. package/src/build-components/CarouselItem/pattern.json +6 -6
  86. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +0 -2
  87. package/src/build-components/CarouselProvider/pattern.json +6 -5
  88. package/src/build-components/CountDown/CountDownProps.generated.ts +0 -2
  89. package/src/build-components/CountDown/pattern.json +6 -3
  90. package/src/build-components/Counter/CounterProps.generated.ts +0 -2
  91. package/src/build-components/Counter/pattern.json +5 -1
  92. package/src/build-components/Image/ImageProps.generated.ts +0 -2
  93. package/src/build-components/Image/pattern.json +7 -2
  94. package/src/build-components/Main/MainProps.generated.ts +0 -2
  95. package/src/build-components/Main/pattern.json +5 -3
  96. package/src/build-components/NavigationBarColor/NavigationBarColorProps.generated.ts +0 -2
  97. package/src/build-components/NavigationBarColor/pattern.json +5 -3
  98. package/src/build-components/Onboard/OnboardProps.generated.ts +0 -2
  99. package/src/build-components/Onboard/pattern.json +9 -7
  100. package/src/build-components/OnboardButton/OnboardButton.tsx +19 -5
  101. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +0 -2
  102. package/src/build-components/OnboardButton/pattern.json +16 -5
  103. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +0 -2
  104. package/src/build-components/OnboardButtons/pattern.json +17 -6
  105. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +0 -2
  106. package/src/build-components/OnboardDot/pattern.json +5 -3
  107. package/src/build-components/OnboardFooter/OnboardFooter.tsx +15 -4
  108. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +0 -2
  109. package/src/build-components/OnboardFooter/pattern.json +5 -3
  110. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +0 -2
  111. package/src/build-components/OnboardImage/pattern.json +7 -3
  112. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +0 -2
  113. package/src/build-components/OnboardItem/pattern.json +13 -5
  114. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +0 -2
  115. package/src/build-components/OnboardProvider/pattern.json +10 -4
  116. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +0 -2
  117. package/src/build-components/OnboardSubtitle/pattern.json +7 -6
  118. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +0 -2
  119. package/src/build-components/OnboardTitle/pattern.json +7 -6
  120. package/src/build-components/PaywallBackground/PaywallBackgroundProps.generated.ts +0 -2
  121. package/src/build-components/PaywallBackground/pattern.json +5 -5
  122. package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +0 -2
  123. package/src/build-components/PaywallCloseButton/pattern.json +6 -6
  124. package/src/build-components/PaywallCounter/PaywallCounterProps.generated.ts +0 -2
  125. package/src/build-components/PaywallCounter/pattern.json +6 -3
  126. package/src/build-components/PaywallOptions/PaywallOptionsProps.generated.ts +0 -2
  127. package/src/build-components/PaywallOptions/pattern.json +6 -6
  128. package/src/build-components/PaywallProvider/PaywallProviderProps.generated.ts +0 -2
  129. package/src/build-components/PaywallProvider/pattern.json +5 -3
  130. package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.ts +0 -2
  131. package/src/build-components/PaywallSubscribeButton/pattern.json +6 -6
  132. package/src/build-components/RadioButton/RadioButtonProps.generated.ts +0 -2
  133. package/src/build-components/RadioButton/pattern.json +5 -3
  134. package/src/build-components/Separator/SeparatorProps.generated.ts +0 -2
  135. package/src/build-components/Separator/pattern.json +5 -3
  136. package/src/build-components/StatusBarColor/StatusBarColorProps.generated.ts +0 -2
  137. package/src/build-components/StatusBarColor/pattern.json +5 -3
  138. package/src/build-components/Text/TextProps.generated.ts +0 -2
  139. package/src/build-components/Text/pattern.json +11 -5
  140. package/src/build-components/View/pattern.json +18 -4
  141. package/src/build-components/patterns.generated.ts +72 -66
  142. package/src/components/AttributesEditorPanel.tsx +48 -32
  143. package/src/components/Builder.tsx +4 -1
  144. package/src/components/BuilderProvider.tsx +6 -6
  145. package/src/index.ts +4 -1
  146. package/src/pages/ProjectPage.tsx +45 -22
  147. package/src/pages/projectPageUtils.ts +15 -1
  148. package/src/types/Project.ts +7 -0
  149. package/src/utils/attributeStyle.ts +78 -0
  150. package/src/utils/patterns.ts +2 -0
@@ -1,3 +1,4 @@
1
+ import { useCallback, useRef } from 'react';
1
2
  import { AttributesEditor } from '../AttributesEditor';
2
3
  import type { Node, NodeData } from '../types/Node';
3
4
  import type { ProjectColors } from '../types/Project';
@@ -11,6 +12,32 @@ interface AttributesEditorPanelProps {
11
12
  projectColors?: ProjectColors;
12
13
  }
13
14
 
15
+ function replaceNode(root: Node, target: Node, next: Node): Node {
16
+ if (root === target) return next;
17
+ if (root === null || root === undefined) return root;
18
+ if (typeof root === 'string') return root;
19
+ if (Array.isArray(root)) {
20
+ let changed = false;
21
+ const arr = root.map((item) => {
22
+ const r = replaceNode(item, target, next);
23
+ if (r !== item) changed = true;
24
+ return r;
25
+ });
26
+ return changed ? arr : root;
27
+ }
28
+ const data = root as NodeData;
29
+ if ('children' in data) {
30
+ const prev = data.children;
31
+ const replaced = Array.isArray(prev)
32
+ ? prev.map((c: Node) => replaceNode(c, target, next))
33
+ : replaceNode(prev as Node, target, next);
34
+ if (replaced !== prev) {
35
+ return { ...data, children: replaced } as Node;
36
+ }
37
+ }
38
+ return root;
39
+ }
40
+
14
41
  export function AttributesEditorPanel({
15
42
  attributes,
16
43
  onChange,
@@ -21,6 +48,27 @@ export function AttributesEditorPanel({
21
48
  current: s.current,
22
49
  setCurrent: s.setCurrent,
23
50
  }));
51
+
52
+ // Stable refs so the onChange callback doesn't change identity every render.
53
+ const attributesRef = useRef(attributes);
54
+ attributesRef.current = attributes;
55
+ const currentRef = useRef(current);
56
+ currentRef.current = current;
57
+ const onChangeRef = useRef(onChange);
58
+ onChangeRef.current = onChange;
59
+
60
+ const handleAttributesChange = useCallback(
61
+ (next: Node) => {
62
+ const root = attributesRef.current as Node;
63
+ const target = currentRef.current;
64
+ if (!target) return;
65
+ const updated = replaceNode(root, target, next);
66
+ onChangeRef.current(updated);
67
+ setCurrent(next);
68
+ },
69
+ [setCurrent],
70
+ );
71
+
24
72
  if (!current) return null;
25
73
 
26
74
  const currentKey =
@@ -33,38 +81,6 @@ export function AttributesEditorPanel({
33
81
  : null;
34
82
  const nodeForEditor = resolvedCurrent ?? current;
35
83
 
36
- function replaceNode(root: Node, target: Node, next: Node): Node {
37
- if (root === target) return next;
38
- if (root === null || root === undefined) return root;
39
- if (typeof root === 'string') return root;
40
- if (Array.isArray(root)) {
41
- let changed = false;
42
- const arr = root.map((item) => {
43
- const r = replaceNode(item, target, next);
44
- if (r !== item) changed = true;
45
- return r;
46
- });
47
- return changed ? arr : root;
48
- }
49
- const data = root as NodeData;
50
- if ('children' in data) {
51
- const prev = data.children;
52
- const replaced = Array.isArray(prev)
53
- ? prev.map((c: Node) => replaceNode(c, target, next))
54
- : replaceNode(prev as Node, target, next);
55
- if (replaced !== prev) {
56
- return { ...data, children: replaced } as Node;
57
- }
58
- }
59
- return root;
60
- }
61
- const handleAttributesChange = (next: Node) => {
62
- const root = attributes as Node;
63
- const updated = replaceNode(root, current, next);
64
- onChange(updated);
65
- setCurrent(next);
66
- };
67
-
68
84
  return (
69
85
  <div className="attributes-editor-panel">
70
86
  <AttributesEditor
@@ -603,7 +603,10 @@ function getNodeLabel(node: Node): string {
603
603
  if (isNodeNullOrUndefined(node)) return 'Empty';
604
604
  if (isNodeString(node)) return node as string;
605
605
  if (isNodeArray(node)) return 'Collection';
606
- return (node as NodeData<NodeDefaultAttribute>).type ?? 'Node';
606
+ const nodeData = node as NodeData<NodeDefaultAttribute>;
607
+ const title = (nodeData.attributes as Record<string, unknown>)?.title;
608
+ if (typeof title === 'string' && title.trim().length > 0) return title;
609
+ return nodeData.type ?? 'Node';
607
610
  }
608
611
 
609
612
  function findNodePath(root: Node, target: Node): Node[] {
@@ -43,7 +43,7 @@ type BuilderProviderProps = {
43
43
  children: React.ReactNode;
44
44
  };
45
45
 
46
- const BuilderContext = createContext<BuilderProviderParams | undefined>(
46
+ const builderContext = createContext<BuilderProviderParams | undefined>(
47
47
  undefined,
48
48
  );
49
49
 
@@ -91,7 +91,7 @@ export function BuilderProvider({ params, children }: BuilderProviderProps) {
91
91
  );
92
92
 
93
93
  return (
94
- <BuilderContext.Provider value={value}>{children}</BuilderContext.Provider>
94
+ <builderContext.Provider value={value}>{children}</builderContext.Provider>
95
95
  );
96
96
  }
97
97
 
@@ -119,10 +119,10 @@ const defaultProjectColors: Readonly<ProjectColors> = {
119
119
  FOOTER_TEXT: '#81838F',
120
120
  },
121
121
  dark: {
122
- TEXT: '#161827',
123
- BACKGROUND: '#F4F5FF',
122
+ TEXT: '#E9EBF9',
123
+ BACKGROUND: '#080A17',
124
124
  ICON: '#0450E2',
125
- LINE: '#E9EBF9',
125
+ LINE: '#161827',
126
126
  ONBOARD_TITLE: '#FDFDFD',
127
127
  ONBOARD_SUBTITLE: '#C7C7C7',
128
128
  BUTTON_SECONDARY_TEXT: '#A9AAAC',
@@ -133,7 +133,7 @@ const defaultProjectColors: Readonly<ProjectColors> = {
133
133
 
134
134
  export function useBuilderParams(): Readonly<BuilderProviderParams> {
135
135
  return (
136
- useContext(BuilderContext) ?? {
136
+ useContext(builderContext) ?? {
137
137
  products: [],
138
138
  benefits: {},
139
139
  platform: 'web',
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  // Types
14
14
  export type { TargetedScreenSize } from './types/TargetedScreenSize';
15
15
  export type { Node, NodeData, NodeDefaultAttribute } from './types/Node';
16
- export type { Project, ProjectColors } from './types/Project';
16
+ export type { Project, ProjectColors, ProjectMeta } from './types/Project';
17
17
  export type { Device } from './types/Device';
18
18
  export type { AppConfig, Localication } from './types/PreviewConfig';
19
19
  export { defaultAppConfig } from './types/PreviewConfig';
@@ -91,3 +91,6 @@ export {
91
91
  } from './assets/samples/getSamples';
92
92
  export { getDefaultProject } from './utils/getDefaultProject';
93
93
  export type { EventObjectGenerated } from './build-components/OnboardButton/OnboardButtonProps.generated';
94
+
95
+ export { parseColor } from './utils/parseColor';
96
+ export type { ParseColorOptions } from './utils/parseColor';
@@ -1,6 +1,6 @@
1
- import { useCallback, useEffect, useState } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import type { Node, NodeData } from '../types/Node';
3
- import type { Project, ProjectColors } from '../types/Project';
3
+ import type { Project, ProjectColors, ProjectMeta } from '../types/Project';
4
4
  import { ToastContainer, toast } from 'react-toastify';
5
5
  import { RenderPage } from '../RenderPage';
6
6
  import { EditorHeader } from '../components/EditorHeader';
@@ -31,16 +31,16 @@ import {
31
31
  deleteNodeFromTree,
32
32
  findNodeByKey,
33
33
  isNodeRecord,
34
- nodeHasChild,
35
34
  } from '../utils/nodeTree';
36
35
  import type { Fonts } from '../types/Fonts';
37
36
  import { useProjectFonts } from '../hooks/useProjectFonts';
38
- import { resolveProjectForSave } from './projectPageUtils';
37
+ import { resolveProjectForSave, toProjectMeta } from './projectPageUtils';
39
38
  import { getDefaultProject } from '../utils/getDefaultProject';
40
39
  import { CURRENT_PROJECT_VERSION } from '../migrations/migratePipe';
41
40
  export type ProjectPageProps = {
42
41
  project: Project;
43
- onSaveProject: (project: Project) => void;
42
+ // TODO: Tüm onSaveProject call-site'ları toProjectMeta kullanacak şekilde migrate et
43
+ onSaveProject: (project: ProjectMeta) => void;
44
44
  appConfig?: AppConfig;
45
45
  logLevel?: LogLevel;
46
46
  projectColors?: ProjectColors;
@@ -151,14 +151,15 @@ export function ProjectPage({
151
151
  return;
152
152
  }
153
153
 
154
- if (isNodeRecord(current) && nodeHasChild(current, nodeToDelete)) {
155
- const currentKey = current.key;
156
- if (currentKey) {
157
- const nextCurrent = findNodeByKey(updated, currentKey);
158
- setCurrent(nextCurrent ?? updated);
159
- } else {
160
- setCurrent(updated);
161
- }
154
+ // Refresh current's reference so the Builder re-renders the updated subtree.
155
+ // deleteNodeFromTree creates new objects for ancestors of the removed node,
156
+ // so the old `current` reference becomes stale when the deleted node was a
157
+ // descendant of `current`.
158
+ if (isNodeRecord(current) && current.key) {
159
+ const nextCurrent = findNodeByKey(updated, current.key);
160
+ setCurrent(nextCurrent ?? updated);
161
+ } else {
162
+ setCurrent(updated);
162
163
  }
163
164
  },
164
165
  [editorData, current],
@@ -226,8 +227,26 @@ export function ProjectPage({
226
227
  return () => clearTimeout(timer);
227
228
  }, [activeProject.data]);
228
229
 
230
+ // Ref for the full project (used inside effect for migration check etc.)
231
+ const activeProjectRef = useRef(activeProject);
232
+ activeProjectRef.current = activeProject;
233
+ // Ref for current editorData so the effect can compare without depending on it.
234
+ const editorDataRef = useRef(editorData);
235
+ editorDataRef.current = editorData;
236
+
229
237
  useEffect(() => {
230
238
  try {
239
+ const currentProject = activeProjectRef.current;
240
+ const projectData = currentProject.data;
241
+
242
+ // Guard: skip reinit when the incoming project data is the same reference
243
+ // we already hold in editorData. After a save round-trip the parent pushes
244
+ // a new project object whose .data is the very same reference as editorData.
245
+ // Re-processing it would flash a loading state and discard in-flight changes.
246
+ if (projectData != null && projectData === editorDataRef.current) {
247
+ return;
248
+ }
249
+
231
250
  // Reset to "loading" immediately on project change so the loader is shown
232
251
  // until a valid node is available (and for at least 2 seconds).
233
252
  if (!isEmptyProjectData) {
@@ -237,7 +256,7 @@ export function ProjectPage({
237
256
  setValidationError(null);
238
257
  setValidationErrorStack(null);
239
258
  // Version gate: if project is older than the current schema, show migration UI.
240
- const pipe = getMigrationPipe(activeProject);
259
+ const pipe = getMigrationPipe(currentProject);
241
260
  if (!bypassValidation && pipe.required) {
242
261
  setMigrationGate(pipe);
243
262
  setEditorData(null);
@@ -248,8 +267,8 @@ export function ProjectPage({
248
267
  if (bypassValidation) {
249
268
  // Best-effort: let the user continue with the raw data even if invalid.
250
269
  // This may still crash the preview, but it unblocks users for debugging.
251
- setEditorData(activeProject.data as Node);
252
- setCurrent(activeProject.data as Node);
270
+ setEditorData(currentProject.data as Node);
271
+ setCurrent(currentProject.data as Node);
253
272
  return;
254
273
  }
255
274
  if (isEmptyProjectData) {
@@ -258,7 +277,7 @@ export function ProjectPage({
258
277
  return;
259
278
  }
260
279
 
261
- const inputNode: Node = activeProject.data as Node;
280
+ const inputNode: Node = currentProject.data as Node;
262
281
 
263
282
  const processed = analyseAndProccess(inputNode);
264
283
  if (!processed) return;
@@ -275,7 +294,9 @@ export function ProjectPage({
275
294
  setEditorData(null);
276
295
  setCurrent(null);
277
296
  }
278
- }, [activeProject, activeProject.data, bypassValidation, setCurrent]);
297
+ // Note: depend on activeProject.data (not activeProject object) to avoid
298
+ // reinit when the project wrapper changes but data is the same reference.
299
+ }, [activeProject.data, isEmptyProjectData, bypassValidation, setCurrent]);
279
300
 
280
301
  const showLoading =
281
302
  !isEmptyProjectData && (editorData === null || !minLoadingDelayDone);
@@ -300,13 +321,15 @@ export function ProjectPage({
300
321
  if (onSaveProjectColors && resolvedProjectColors) {
301
322
  onSaveProjectColors(resolvedProjectColors);
302
323
  }
303
- onSaveProject(
324
+ const projectMeta = toProjectMeta(
304
325
  resolveProjectForSave({
305
326
  project,
306
327
  overrideProject,
307
328
  data: editorData,
308
329
  }),
309
330
  );
331
+ logger.info('ProjectPage', 'saving project meta', projectMeta);
332
+ onSaveProject(projectMeta);
310
333
  toast.success('Saved');
311
334
  } catch (e) {
312
335
  logger.error('ProjectPage', 'save project failed', e);
@@ -377,7 +400,7 @@ export function ProjectPage({
377
400
 
378
401
  const { project: migratedProject } =
379
402
  runProjectMigrations(projectForMigration);
380
- onSaveProject(migratedProject);
403
+ onSaveProject(toProjectMeta(migratedProject));
381
404
  setOverrideProject(migratedProject);
382
405
  setBypassValidation(true);
383
406
  setMigrationGate(null);
@@ -430,7 +453,7 @@ export function ProjectPage({
430
453
 
431
454
  // This action only fixes project metadata. It intentionally does NOT
432
455
  // validate/normalize node data (it might still be invalid).
433
- onSaveProject(fixedProject);
456
+ onSaveProject(toProjectMeta(fixedProject));
434
457
  setOverrideProject(fixedProject);
435
458
  setBypassValidation(false);
436
459
  setMigrationGate(null);
@@ -488,7 +511,7 @@ export function ProjectPage({
488
511
  data: nodeCandidate as Project['data'],
489
512
  };
490
513
 
491
- onSaveProject(nextProject);
514
+ onSaveProject(toProjectMeta(nextProject));
492
515
  setOverrideProject(nextProject);
493
516
  setBypassValidation(false);
494
517
  setValidationError(null);
@@ -1,4 +1,4 @@
1
- import type { Project } from '../types/Project';
1
+ import type { Project, ProjectMeta } from '../types/Project';
2
2
  import type { Node } from '../types/Node';
3
3
  import { getDefaultProject } from '../utils/getDefaultProject';
4
4
 
@@ -13,3 +13,17 @@ export function resolveProjectForSave(args: {
13
13
  data: args.data,
14
14
  });
15
15
  }
16
+
17
+ /**
18
+ * Strips a full Project down to its essential persistence fields.
19
+ * Use before handing the project to onSaveProject so consumers only
20
+ * receive the canonical metadata (name, version, type, data).
21
+ */
22
+ export function toProjectMeta(project: Project): ProjectMeta {
23
+ return {
24
+ name: project.name,
25
+ version: project.version,
26
+ ...(project.type ? { type: project.type } : {}),
27
+ data: project.data,
28
+ };
29
+ }
@@ -29,6 +29,13 @@ export interface ProjectBase<T> {
29
29
 
30
30
  export interface Project extends ProjectBase<Node> {}
31
31
 
32
+ /**
33
+ * Lightweight subset of Project containing only the essential metadata
34
+ * needed for persistence (name, version, type, data).
35
+ * Excludes runtime/editor-only fields like appConfig and projectColors.
36
+ */
37
+ export type ProjectMeta = Pick<Project, 'name' | 'version' | 'type' | 'data'>;
38
+
32
39
  export type LogLevel = 'NONE' | 'ERROR' | 'WARN' | 'INFO' | 'VERBOSE';
33
40
 
34
41
  export type LogSource = string;
@@ -24,3 +24,81 @@ export function toAttributeRecord(
24
24
  function isPlainObject(value: unknown): value is Record<string, unknown> {
25
25
  return typeof value === 'object' && value !== null && !Array.isArray(value);
26
26
  }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Style-key filtering – separates visual style keys from non-style attributes
30
+ // ---------------------------------------------------------------------------
31
+
32
+ /**
33
+ * All attribute keys that represent visual style properties.
34
+ * Used to separate style keys from non-style (behavioral/content) keys.
35
+ *
36
+ * Keep in sync with ViewStyleGenerated, TextStyleGenerated, and ImageStyleGenerated.
37
+ */
38
+ const STYLE_ATTR_KEYS_LIST = [
39
+ // Style bag containers
40
+ 'style',
41
+ 'styles',
42
+ // Layout
43
+ 'flexDirection',
44
+ 'flexWrap',
45
+ 'alignItems',
46
+ 'justifyContent',
47
+ 'gap',
48
+ // Padding
49
+ 'padding',
50
+ 'paddingHorizontal',
51
+ 'paddingVertical',
52
+ 'paddingTop',
53
+ 'paddingBottom',
54
+ 'paddingLeft',
55
+ 'paddingRight',
56
+ // Margin
57
+ 'margin',
58
+ 'marginHorizontal',
59
+ 'marginVertical',
60
+ 'marginTop',
61
+ 'marginBottom',
62
+ 'marginLeft',
63
+ 'marginRight',
64
+ // Background & border
65
+ 'backgroundColor',
66
+ 'borderRadius',
67
+ // Sizing
68
+ 'width',
69
+ 'minWidth',
70
+ 'maxWidth',
71
+ 'height',
72
+ 'minHeight',
73
+ 'maxHeight',
74
+ // Flex & position
75
+ 'flex',
76
+ 'position',
77
+ 'top',
78
+ 'bottom',
79
+ 'left',
80
+ 'right',
81
+ 'zIndex',
82
+ // Text
83
+ 'color',
84
+ 'fontSize',
85
+ 'fontFamily',
86
+ 'fontWeight',
87
+ 'textAlign',
88
+ // Image
89
+ 'resizeMode',
90
+ ] as const;
91
+
92
+ /** Type-level union of all style attribute keys. Use with `Omit<T, StyleAttrKey>`. */
93
+ export type StyleAttrKey = (typeof STYLE_ATTR_KEYS_LIST)[number];
94
+
95
+ const STYLE_ATTR_KEYS: ReadonlySet<string> = new Set(STYLE_ATTR_KEYS_LIST);
96
+
97
+ /** Strips all visual-style keys from an attributes record, returning only non-style keys. */
98
+ export function stripStyleKeys(
99
+ attrs: Record<string, unknown>,
100
+ ): Record<string, unknown> {
101
+ return Object.fromEntries(
102
+ Object.entries(attrs).filter(([key]) => !STYLE_ATTR_KEYS.has(key)),
103
+ );
104
+ }
@@ -48,6 +48,8 @@ type Pattern = {
48
48
  schemaVersion: number;
49
49
  pattern: {
50
50
  type: string;
51
+ title?: string;
52
+ description?: string;
51
53
  children: unknown;
52
54
  attributes?: Record<string, string | string[]>;
53
55
  };