@developer_tribe/react-builder 1.0.7 → 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 (217) 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/BottomBar.tsx +45 -45
  162. package/src/components/Builder.tsx +20 -6
  163. package/src/components/BuilderButton.tsx +75 -38
  164. package/src/components/BuilderProvider.tsx +22 -2
  165. package/src/components/DeviceButton.tsx +12 -5
  166. package/src/components/EditorHeader.tsx +296 -38
  167. package/src/components/ParamsProvider.tsx +7 -0
  168. package/src/components/RenderErrorBoundary.tsx +200 -0
  169. package/src/hooks/useParams.ts +5 -1
  170. package/src/hooks/useSyncHtmlThemeClass.ts +19 -0
  171. package/src/index.native.ts +7 -0
  172. package/src/index.ts +8 -0
  173. package/src/migrations/migratePipe.ts +59 -0
  174. package/src/migrations/migrations/1.1.0_normalize_style_attributes.ts +80 -0
  175. package/src/migrations/semver.ts +24 -0
  176. package/src/migrations/types.ts +9 -0
  177. package/src/mockOS/components/PermissionModal.tsx +3 -2
  178. package/src/mockOS/components/SubscriptionModal.tsx +400 -0
  179. package/src/mockOS/context/MockOSContext.tsx +61 -10
  180. package/src/mockOS/context/MockOSContextBase.ts +1 -0
  181. package/src/mockOS/hooks/useMockIap.ts +11 -0
  182. package/src/mockOS/index.ts +7 -0
  183. package/src/mockOS/managers/mockOSIapManager.ts +10 -0
  184. package/src/mockOS/managers/subscriptionManager.ts +36 -0
  185. package/src/modals/IconPickerModal.tsx +1 -1
  186. package/src/pages/ProjectDebug.tsx +331 -0
  187. package/src/pages/ProjectMigrationPage.tsx +92 -0
  188. package/src/pages/ProjectPage.tsx +318 -166
  189. package/src/pages/ProjectValidationPage.tsx +54 -0
  190. package/src/styles/base/_global.scss +58 -11
  191. package/src/styles/components/_attributes-editor.scss +1 -1
  192. package/src/styles/components/_bottom-bar.scss +7 -4
  193. package/src/styles/components/_editor-shell.scss +126 -4
  194. package/src/styles/components/_mockos-router.scss +3 -2
  195. package/src/styles/components/_ui-components.scss +10 -5
  196. package/src/styles/foundation/_colors.scss +78 -11
  197. package/src/styles/foundation/_mixins.scss +4 -1
  198. package/src/styles/foundation/_sizes.scss +4 -2
  199. package/src/styles/index.scss +1 -0
  200. package/src/styles/layout/_builder.scss +61 -0
  201. package/src/styles/layout/_project-validation.scss +214 -0
  202. package/src/styles/modals/_add-component.scss +4 -2
  203. package/src/styles/modals/_color-modal.scss +4 -2
  204. package/src/styles/modals/_modal-shell.scss +3 -1
  205. package/src/types/Device.ts +5 -0
  206. package/src/utils/__special_exceptions.ts +88 -0
  207. package/src/utils/analyseNode.ts +8 -2
  208. package/src/utils/analyseNodeByPatterns.ts +43 -9
  209. package/src/utils/extractTextStyle.ts +19 -6
  210. package/src/utils/extractViewStyle.ts +68 -59
  211. package/src/utils/getImage.ts +76 -0
  212. package/src/utils/novaToJson.ts +2 -1
  213. package/src/utils/pasteNode.ts +172 -0
  214. package/src/utils/patterns.ts +4 -3
  215. package/dist/android.svg +0 -43
  216. package/dist/apple.svg +0 -16
  217. package/dist/background.jpg +0 -0
@@ -1,4 +1,4 @@
1
- import { useState } from 'react';
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import type { Device } from '../types/Device';
3
3
  import type { Node } from '../types/Node';
4
4
  import { copyNode } from '../utils/copyNode';
@@ -7,6 +7,9 @@ import { useRenderStore } from '../store';
7
7
  import { useLogRender } from '../utils/useLogRender';
8
8
  import { DeviceButton } from './DeviceButton';
9
9
  import { DeviceSelectorModal } from '../modals/DeviceSelectorModal';
10
+ import { toast } from 'react-toastify';
11
+ import { getSamples } from '../assets/samples/getSamples';
12
+ import type { Project } from '../types/Project';
10
13
 
11
14
  const devices = getDevices();
12
15
  interface EditorHeaderProps {
@@ -26,17 +29,67 @@ export function EditorHeader({
26
29
  }: EditorHeaderProps) {
27
30
  useLogRender('EditorHeader');
28
31
  const [isDevicesModalOpen, setIsDevicesModalOpen] = useState(false);
32
+ const [isActionsOpen, setIsActionsOpen] = useState(false);
33
+ const actionsRef = useRef<HTMLDivElement | null>(null);
29
34
  const copiedNode = useRenderStore((s) => s.copiedNode);
30
35
  const {
31
36
  device: selectedDevice,
32
37
  setDevice,
33
38
  setCurrent,
39
+ setAppConfig,
40
+ setProjectColors,
34
41
  } = useRenderStore((s) => ({
35
42
  device: s.device,
36
43
  setDevice: s.setDevice,
37
44
  setCurrent: s.setCurrent,
45
+ setAppConfig: s.setAppConfig,
46
+ setProjectColors: s.setProjectColors,
38
47
  }));
39
48
 
49
+ const sortedSamples = useMemo(() => {
50
+ const weight = (t?: Project['type']) => {
51
+ if (t === 'paywall') return 0;
52
+ if (t === 'onboard') return 1;
53
+ return 2;
54
+ };
55
+ return getSamples()
56
+ .slice()
57
+ .sort((a, b) => {
58
+ const w = weight(a.type) - weight(b.type);
59
+ if (w !== 0) return w;
60
+ return a.name.localeCompare(b.name);
61
+ });
62
+ }, []);
63
+
64
+ const actionsMenuId = useMemo(
65
+ () => `editor-actions-menu-${Math.random().toString(36).slice(2, 9)}`,
66
+ [],
67
+ );
68
+
69
+ useEffect(() => {
70
+ if (!isActionsOpen) return;
71
+
72
+ const handlePointerDown = (e: MouseEvent | TouchEvent) => {
73
+ const el = actionsRef.current;
74
+ if (!el) return;
75
+ if (e.target instanceof Element && el.contains(e.target)) return;
76
+ setIsActionsOpen(false);
77
+ };
78
+
79
+ const handleKeyDown = (e: KeyboardEvent) => {
80
+ if (e.key === 'Escape') setIsActionsOpen(false);
81
+ };
82
+
83
+ document.addEventListener('mousedown', handlePointerDown);
84
+ document.addEventListener('touchstart', handlePointerDown);
85
+ document.addEventListener('keydown', handleKeyDown);
86
+ return () => {
87
+ document.removeEventListener('mousedown', handlePointerDown);
88
+ document.removeEventListener('touchstart', handlePointerDown);
89
+ document.removeEventListener('keydown', handleKeyDown);
90
+ };
91
+ }, [isActionsOpen]);
92
+
40
93
  function replaceNode(root: Node, target: Node, next: Node): Node {
41
94
  if (root === target) return next;
42
95
  if (root === null || root === undefined) return root;
@@ -50,16 +103,13 @@ export function EditorHeader({
50
103
  });
51
104
  return changed ? arr : root;
52
105
  }
53
- const data = root as any;
106
+ const data = root as Record<string, unknown>;
54
107
  if ('children' in data) {
55
- const prev = data.children;
108
+ const prev = data.children as Node;
56
109
  const replaced = Array.isArray(prev)
57
- ? prev.map((c: Node) => replaceNode(c, target, next))
58
- : replaceNode(prev as Node, target, next);
59
- if (replaced !== prev) {
60
- data.children = replaced;
61
- return { ...data, children: replaced } as Node;
62
- }
110
+ ? prev.map((c) => replaceNode(c, target, next))
111
+ : replaceNode(prev, target, next);
112
+ if (replaced !== prev) return { ...data, children: replaced } as Node;
63
113
  }
64
114
  return root;
65
115
  }
@@ -81,6 +131,104 @@ export function EditorHeader({
81
131
  // in sync with what’s rendered/edited.
82
132
  setCurrent(cloned);
83
133
  };
134
+
135
+ const cloneNode = (node: Node): Node =>
136
+ JSON.parse(JSON.stringify(node)) as Node;
137
+
138
+ const handleReplaceFromSample = (sample: Project) => {
139
+ if (!setEditorData) return;
140
+ const next = cloneNode(sample.data);
141
+ setEditorData(next);
142
+ setCurrent(next);
143
+ if (sample.appConfig) setAppConfig(sample.appConfig);
144
+ setProjectColors(sample.projectColors);
145
+ toast.success(`Replaced with "${sample.name}"`);
146
+ };
147
+
148
+ const handlePasteFromSample = (sample: Project) => {
149
+ if (!current || !editorData || !setEditorData) return;
150
+ const incoming = cloneNode(sample.data);
151
+
152
+ const isRecord = (v: unknown): v is Record<string, unknown> =>
153
+ typeof v === 'object' && v !== null && !Array.isArray(v);
154
+
155
+ const isNodeData = (v: unknown): v is Record<string, unknown> =>
156
+ isRecord(v) && 'type' in v && 'children' in v;
157
+
158
+ const getPasteNodes = (node: Node): Node[] => {
159
+ if (isNodeData(node)) {
160
+ const t = node.type;
161
+ const isMainLike = t === 'main' || node.isMain === true;
162
+ if (isMainLike) {
163
+ const ch = node.children as Node;
164
+ if (!ch) return [];
165
+ return Array.isArray(ch) ? ch : [ch];
166
+ }
167
+ }
168
+ return [node];
169
+ };
170
+
171
+ // "Paste from sample" should ADD the sample into the selected node (not replace it).
172
+ // We do this by appending the sample's root node into current.children.
173
+ if (!isNodeData(current)) {
174
+ toast.error('Select a component node to paste into');
175
+ return;
176
+ }
177
+
178
+ const prevChildren = current.children as Node;
179
+ const pasteNodes = getPasteNodes(incoming);
180
+ if (pasteNodes.length === 0) {
181
+ toast.error('Sample has no children to paste');
182
+ return;
183
+ }
184
+ let nextChildren: Node;
185
+ if (!prevChildren) {
186
+ nextChildren = pasteNodes.length === 1 ? pasteNodes[0] : pasteNodes;
187
+ } else if (Array.isArray(prevChildren)) {
188
+ nextChildren = [...prevChildren, ...pasteNodes];
189
+ } else {
190
+ nextChildren = [prevChildren, ...pasteNodes];
191
+ }
192
+
193
+ const nextNode: Node = { ...current, children: nextChildren } as Node;
194
+
195
+ const updated = replaceNode(editorData, current, nextNode);
196
+ setEditorData(updated);
197
+ setCurrent(nextNode);
198
+ toast.success(`Pasted from "${sample.name}"`);
199
+ };
200
+
201
+ const closeActions = () => setIsActionsOpen(false);
202
+ const handleSaveProject = () => {
203
+ onSaveProject?.();
204
+ closeActions();
205
+ };
206
+ const handleRestoreProject = () => {
207
+ try {
208
+ onRestoreProject?.();
209
+ toast.info('Restored');
210
+ } catch {
211
+ toast.error('Restore failed');
212
+ } finally {
213
+ closeActions();
214
+ }
215
+ };
216
+ const handleCopyAndClose = () => {
217
+ if (!canCopy) return;
218
+ handleCopy();
219
+ toast.info('Copied');
220
+ closeActions();
221
+ };
222
+ const handlePasteAndClose = () => {
223
+ if (!canPaste) return;
224
+ handlePaste();
225
+ toast.success('Pasted');
226
+ closeActions();
227
+ };
228
+
229
+ const canCopy = !!current;
230
+ const canPaste = !!current && !!editorData && !!setEditorData && !!copiedNode;
231
+
84
232
  return (
85
233
  <div
86
234
  className="editor-header"
@@ -105,38 +253,148 @@ export function EditorHeader({
105
253
  </button>
106
254
  </div>
107
255
  <div className="editor-header__actions">
108
- <button
109
- className="editor-button editor-save-button"
110
- aria-label="Save project data"
111
- onClick={() => onSaveProject && onSaveProject()}
112
- >
113
- Save
114
- </button>
115
- {onRestoreProject && (
116
- <button
117
- className="editor-button editor-save-previewconfig-button"
118
- aria-label="Restore project data"
119
- onClick={() => onRestoreProject()}
120
- >
121
- Restore
122
- </button>
123
- )}
124
- <button
125
- className="editor-button"
126
- aria-label="Copy node"
127
- onClick={handleCopy}
128
- >
129
- Copy
130
- </button>
131
- {copiedNode && (
256
+ <div className="editor-actions-dropdown" ref={actionsRef}>
132
257
  <button
133
- className="editor-button"
134
- aria-label="Paste node"
135
- onClick={handlePaste}
258
+ className="editor-button editor-actions-dropdown__trigger"
259
+ aria-label="Open actions menu"
260
+ aria-haspopup="menu"
261
+ aria-expanded={isActionsOpen}
262
+ aria-controls={actionsMenuId}
263
+ onClick={() => setIsActionsOpen((v) => !v)}
136
264
  >
137
- Paste
265
+ Actions
266
+ <span className="editor-actions-dropdown__caret" aria-hidden="true">
267
+
268
+ </span>
138
269
  </button>
139
- )}
270
+ {isActionsOpen && (
271
+ <ul
272
+ id={actionsMenuId}
273
+ className="editor-actions-dropdown__menu"
274
+ role="menu"
275
+ aria-label="Editor actions"
276
+ >
277
+ <li role="none" className="editor-actions-dropdown__submenu-root">
278
+ <button
279
+ className="editor-actions-dropdown__item"
280
+ role="menuitem"
281
+ aria-haspopup="menu"
282
+ aria-label="Replace from samples"
283
+ onClick={(e) => e.preventDefault()}
284
+ >
285
+ Replace from samples
286
+ <span
287
+ className="editor-actions-dropdown__submenu-caret"
288
+ aria-hidden="true"
289
+ >
290
+
291
+ </span>
292
+ </button>
293
+ <ul
294
+ className="editor-actions-dropdown__submenu"
295
+ role="menu"
296
+ aria-label="Replace from sample"
297
+ >
298
+ {sortedSamples.map((s) => (
299
+ <li role="none" key={`replace-${s.name}`}>
300
+ <button
301
+ className="editor-actions-dropdown__item editor-actions-dropdown__item--compact"
302
+ role="menuitem"
303
+ onClick={() => {
304
+ handleReplaceFromSample(s);
305
+ closeActions();
306
+ }}
307
+ >
308
+ {s.name}
309
+ </button>
310
+ </li>
311
+ ))}
312
+ </ul>
313
+ </li>
314
+ <li role="none" className="editor-actions-dropdown__submenu-root">
315
+ <button
316
+ className="editor-actions-dropdown__item"
317
+ role="menuitem"
318
+ aria-haspopup="menu"
319
+ aria-label="Paste from samples"
320
+ onClick={(e) => e.preventDefault()}
321
+ disabled={!current}
322
+ title={!current ? 'Select a node first' : undefined}
323
+ >
324
+ Paste from samples
325
+ <span
326
+ className="editor-actions-dropdown__submenu-caret"
327
+ aria-hidden="true"
328
+ >
329
+
330
+ </span>
331
+ </button>
332
+ <ul
333
+ className="editor-actions-dropdown__submenu"
334
+ role="menu"
335
+ aria-label="Paste from sample"
336
+ >
337
+ {sortedSamples.map((s) => (
338
+ <li role="none" key={`paste-${s.name}`}>
339
+ <button
340
+ className="editor-actions-dropdown__item editor-actions-dropdown__item--compact"
341
+ role="menuitem"
342
+ onClick={() => {
343
+ handlePasteFromSample(s);
344
+ closeActions();
345
+ }}
346
+ disabled={!current}
347
+ >
348
+ {s.name}
349
+ </button>
350
+ </li>
351
+ ))}
352
+ </ul>
353
+ </li>
354
+ <li role="none">
355
+ <button
356
+ className="editor-actions-dropdown__item editor-save-button"
357
+ role="menuitem"
358
+ onClick={handleSaveProject}
359
+ disabled={!onSaveProject}
360
+ >
361
+ Save
362
+ </button>
363
+ </li>
364
+ {onRestoreProject && (
365
+ <li role="none">
366
+ <button
367
+ className="editor-actions-dropdown__item editor-save-previewconfig-button"
368
+ role="menuitem"
369
+ onClick={handleRestoreProject}
370
+ >
371
+ Restore
372
+ </button>
373
+ </li>
374
+ )}
375
+ <li role="none">
376
+ <button
377
+ className="editor-actions-dropdown__item"
378
+ role="menuitem"
379
+ onClick={handleCopyAndClose}
380
+ disabled={!canCopy}
381
+ >
382
+ Copy
383
+ </button>
384
+ </li>
385
+ <li role="none">
386
+ <button
387
+ className="editor-actions-dropdown__item"
388
+ role="menuitem"
389
+ onClick={handlePasteAndClose}
390
+ disabled={!canPaste}
391
+ >
392
+ Paste
393
+ </button>
394
+ </li>
395
+ </ul>
396
+ )}
397
+ </div>
140
398
  </div>
141
399
  {isDevicesModalOpen && (
142
400
  <DeviceSelectorModal
@@ -6,6 +6,11 @@ export type OtherParams = Record<string, string | boolean | number>;
6
6
  export type ParamsContextValue = {
7
7
  localizationParams: LocalizationParams;
8
8
  otherParams: OtherParams;
9
+ /**
10
+ * `true` when the value comes from an actual `<ParamsProvider />` in the tree.
11
+ * Used to detect nesting vs the default context value.
12
+ */
13
+ isProvided: boolean;
9
14
  };
10
15
 
11
16
  export type ParamsProviderProps = {
@@ -17,6 +22,7 @@ export type ParamsProviderProps = {
17
22
  export const ParamsContext = createContext<ParamsContextValue>({
18
23
  localizationParams: {},
19
24
  otherParams: {},
25
+ isProvided: false,
20
26
  });
21
27
 
22
28
  export function ParamsProvider({
@@ -28,6 +34,7 @@ export function ParamsProvider({
28
34
  () => ({
29
35
  localizationParams: localizationParams ?? {},
30
36
  otherParams: otherParams ?? {},
37
+ isProvided: true,
31
38
  }),
32
39
  [localizationParams, otherParams],
33
40
  );
@@ -0,0 +1,200 @@
1
+ import React from 'react';
2
+
3
+ export type RenderErrorBoundaryProps = {
4
+ children: React.ReactNode;
5
+ /**
6
+ * Optional label to show under the title, to help identify which preview crashed
7
+ * (e.g. "caught by BuilderProvider" vs "caught by ProjectDebug").
8
+ */
9
+ subtitle?: string;
10
+ /** Optional callback invoked when an error is caught. */
11
+ onError?: (error: Error, componentStack?: string) => void;
12
+ };
13
+
14
+ type RenderErrorBoundaryState = {
15
+ error: Error | null;
16
+ errorStack?: string;
17
+ componentStack?: string;
18
+ copied?: boolean;
19
+ copyError?: string | null;
20
+ };
21
+
22
+ export class RenderErrorBoundary extends React.Component<
23
+ RenderErrorBoundaryProps,
24
+ RenderErrorBoundaryState
25
+ > {
26
+ state: RenderErrorBoundaryState = {
27
+ error: null,
28
+ errorStack: undefined,
29
+ componentStack: undefined,
30
+ copied: false,
31
+ copyError: null,
32
+ };
33
+
34
+ static getDerivedStateFromError(
35
+ error: Error,
36
+ ): Partial<RenderErrorBoundaryState> {
37
+ return {
38
+ error,
39
+ errorStack: error?.stack,
40
+ copied: false,
41
+ copyError: null,
42
+ };
43
+ }
44
+
45
+ componentDidCatch(error: Error, info: React.ErrorInfo) {
46
+ const componentStack =
47
+ (info as unknown as { componentStack?: string })?.componentStack ??
48
+ undefined;
49
+
50
+ this.setState({
51
+ error,
52
+ errorStack: error?.stack,
53
+ componentStack,
54
+ });
55
+
56
+ this.props.onError?.(error, componentStack);
57
+ }
58
+
59
+ private buildDetails(): string {
60
+ const { error, errorStack, componentStack } = this.state;
61
+ return [
62
+ `Error: ${error?.message ?? ''}`,
63
+ '',
64
+ errorStack ? `Stack:\n${errorStack}` : 'Stack: (missing)',
65
+ '',
66
+ componentStack
67
+ ? `React component stack:\n${componentStack}`
68
+ : 'React component stack: (missing)',
69
+ ].join('\n');
70
+ }
71
+
72
+ private copyDetails = async () => {
73
+ const details = this.buildDetails();
74
+
75
+ try {
76
+ if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
77
+ await navigator.clipboard.writeText(details);
78
+ this.setState({ copied: true, copyError: null });
79
+ window.setTimeout(() => this.setState({ copied: false }), 900);
80
+ return;
81
+ }
82
+ } catch {
83
+ // fall through to legacy method
84
+ }
85
+
86
+ try {
87
+ if (typeof document === 'undefined') {
88
+ this.setState({ copied: false, copyError: 'Copy failed' });
89
+ return;
90
+ }
91
+ const el = document.createElement('textarea');
92
+ el.value = details;
93
+ el.setAttribute('readonly', 'true');
94
+ el.style.position = 'fixed';
95
+ el.style.opacity = '0';
96
+ document.body.appendChild(el);
97
+ el.select();
98
+ const ok = document.execCommand('copy');
99
+ document.body.removeChild(el);
100
+ if (!ok) {
101
+ this.setState({ copied: false, copyError: 'Copy failed' });
102
+ return;
103
+ }
104
+ this.setState({ copied: true, copyError: null });
105
+ window.setTimeout(() => this.setState({ copied: false }), 900);
106
+ } catch (e) {
107
+ this.setState({
108
+ copied: false,
109
+ copyError: e instanceof Error ? e.message : 'Copy failed',
110
+ });
111
+ }
112
+ };
113
+
114
+ private reset = () => {
115
+ this.setState({
116
+ error: null,
117
+ errorStack: undefined,
118
+ componentStack: undefined,
119
+ copied: false,
120
+ copyError: null,
121
+ });
122
+ };
123
+
124
+ render() {
125
+ const { error, copied, copyError } = this.state;
126
+ if (!error) return this.props.children;
127
+
128
+ const details = this.buildDetails();
129
+
130
+ return (
131
+ <div
132
+ role="alert"
133
+ style={{
134
+ padding: 16,
135
+ borderRadius: 12,
136
+ background: 'hsl(var(--card, var(--rb-card, 0 0% 100%)))',
137
+ color:
138
+ 'hsl(var(--card-foreground, var(--rb-card-foreground, 220.9 39.3% 11%)))',
139
+ border:
140
+ '1px solid hsl(var(--border, var(--rb-border, 220 13% 91%)) / 0.9)',
141
+ }}
142
+ >
143
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
144
+ <div style={{ fontWeight: 700 }}>Preview crashed</div>
145
+ {this.props.subtitle && (
146
+ <div style={{ opacity: 0.8, fontSize: 12 }}>
147
+ ({this.props.subtitle})
148
+ </div>
149
+ )}
150
+ <div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
151
+ <button
152
+ type="button"
153
+ className="editor-button"
154
+ onClick={this.copyDetails}
155
+ >
156
+ {copied ? 'Copied' : 'Copy details'}
157
+ </button>
158
+ <button
159
+ type="button"
160
+ className="editor-button"
161
+ onClick={this.reset}
162
+ >
163
+ Try again
164
+ </button>
165
+ </div>
166
+ </div>
167
+
168
+ {copyError && (
169
+ <div
170
+ style={{
171
+ marginTop: 8,
172
+ color:
173
+ 'hsl(var(--destructive, var(--rb-destructive, 0 84.2% 60.2%)))',
174
+ fontSize: 12,
175
+ }}
176
+ >
177
+ {copyError}
178
+ </div>
179
+ )}
180
+
181
+ <pre
182
+ style={{
183
+ marginTop: 12,
184
+ whiteSpace: 'pre-wrap',
185
+ wordBreak: 'break-word',
186
+ fontSize: 12,
187
+ lineHeight: 1.4,
188
+ background: 'hsl(var(--muted, var(--rb-muted, 220 14.3% 95.9%)))',
189
+ padding: 12,
190
+ borderRadius: 10,
191
+ overflow: 'auto',
192
+ maxHeight: 360,
193
+ }}
194
+ >
195
+ {details}
196
+ </pre>
197
+ </div>
198
+ );
199
+ }
200
+ }
@@ -3,6 +3,10 @@ import { ParamsContext } from '../components/ParamsProvider';
3
3
 
4
4
  export function useParams() {
5
5
  return (
6
- useContext(ParamsContext) ?? { localizationParams: {}, otherParams: {} }
6
+ useContext(ParamsContext) ?? {
7
+ localizationParams: {},
8
+ otherParams: {},
9
+ isProvided: false,
10
+ }
7
11
  );
8
12
  }
@@ -0,0 +1,19 @@
1
+ import { useEffect } from 'react';
2
+ import { useRenderStore } from '../store';
3
+
4
+ /**
5
+ * Syncs the builder's `appConfig.theme` to the DOM so CSS can react to `.dark`.
6
+ *
7
+ * - Uses `html.dark` as the toggle (shadcn/tailwind convention).
8
+ * - Also toggles `html.light` so system dark can be overridden back to light.
9
+ */
10
+ export function useSyncHtmlThemeClass() {
11
+ const theme = useRenderStore((s) => s.appConfig.theme);
12
+
13
+ useEffect(() => {
14
+ if (typeof document === 'undefined') return;
15
+ const root = document.documentElement;
16
+ root.classList.toggle('dark', theme === 'dark');
17
+ root.classList.toggle('light', theme === 'light');
18
+ }, [theme]);
19
+ }
@@ -57,6 +57,13 @@ export {
57
57
  export { novaToJson } from './utils/novaToJson';
58
58
  export { getDevices, getDefaultDevice } from './utils/getDevices';
59
59
  export { querySelector } from './utils/querySelector';
60
+ export {
61
+ getImage,
62
+ TRIBE_ASSETS_BASE_URL,
63
+ TribeAssetName,
64
+ parseTribeAssetName,
65
+ resolveImageSrc,
66
+ } from './utils/getImage';
60
67
  export {
61
68
  getPatternByType,
62
69
  getAttributeSchema,
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export {
8
8
  } from './components/BuilderProvider';
9
9
  export { ParamsProvider } from './components/ParamsProvider';
10
10
  export { LocalizationParamsProvider } from './components/LocalizationParamsProvider';
11
+ export { RenderErrorBoundary } from './components/RenderErrorBoundary';
11
12
  export { useParams } from './hooks/useParams';
12
13
  export { useLocalizationParams } from './hooks/useLocalizationParams';
13
14
  export { useLocalize } from './hooks/useLocalize';
@@ -39,6 +40,13 @@ export { querySelector } from './utils/querySelector';
39
40
  export { extractViewStyle } from './utils/extractViewStyle';
40
41
  export { extractImageStyle } from './utils/extractImageStyle';
41
42
  export { extractTextStyle } from './utils/extractTextStyle';
43
+ export {
44
+ getImage,
45
+ TRIBE_ASSETS_BASE_URL,
46
+ TribeAssetName,
47
+ parseTribeAssetName,
48
+ resolveImageSrc,
49
+ } from './utils/getImage';
42
50
  export { ProjectPage } from './pages/ProjectPage';
43
51
  export type { ProjectPageProps } from './pages/ProjectPage';
44
52
  export { copyNode } from './utils/copyNode';