@developer_tribe/react-builder 1.0.6 → 1.0.7

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 (63) hide show
  1. package/dist/build-components/patterns.generated.d.ts +56 -56
  2. package/dist/components/AttributesEditorPanel.d.ts +2 -2
  3. package/dist/components/BottomBar.d.ts +6 -2
  4. package/dist/components/Checkbox.d.ts +1 -1
  5. package/dist/index.cjs.js +3 -3
  6. package/dist/index.cjs.js.map +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.esm.js +3 -3
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.native.cjs.js +1 -1
  11. package/dist/index.native.cjs.js.map +1 -1
  12. package/dist/index.native.esm.js +4 -4
  13. package/dist/index.native.esm.js.map +1 -1
  14. package/dist/modals/ScreenColorsModal.d.ts +1 -1
  15. package/dist/pages/ProjectPage.d.ts +3 -2
  16. package/dist/pages/tabs/BuilderPanel.d.ts +2 -2
  17. package/dist/pages/tabs/SideTool.d.ts +8 -0
  18. package/dist/store.d.ts +0 -6
  19. package/dist/styles.css +1 -1
  20. package/package.json +5 -2
  21. package/src/RenderPage.tsx +1 -4
  22. package/src/assets/samples/carousel-sample.json +81 -99
  23. package/src/assets/samples/simple-1.json +2 -8
  24. package/src/assets/samples/simple-2.json +9 -36
  25. package/src/assets/samples/vpn-onboard-1.json +23 -27
  26. package/src/assets/samples/vpn-onboard-2.json +275 -279
  27. package/src/assets/samples/vpn-onboard-3.json +246 -247
  28. package/src/assets/samples/vpn-onboard-4.json +246 -247
  29. package/src/assets/samples/vpn-onboard-5.json +369 -375
  30. package/src/assets/samples/vpn-onboard-6.json +248 -252
  31. package/src/build-components/View/pattern.json +2 -2
  32. package/src/build-components/patterns.generated.ts +56 -56
  33. package/src/components/AttributesEditorPanel.tsx +8 -12
  34. package/src/components/BottomBar.tsx +75 -69
  35. package/src/components/EditorHeader.tsx +4 -11
  36. package/src/index.ts +2 -1
  37. package/src/modals/ScreenColorsModal.tsx +57 -51
  38. package/src/pages/ProjectPage.tsx +168 -69
  39. package/src/pages/tabs/BuilderPanel.tsx +8 -14
  40. package/src/pages/tabs/SideTool.tsx +253 -0
  41. package/src/store.ts +6 -10
  42. package/src/styles/base/_global.scss +29 -32
  43. package/src/styles/components/_attributes-editor.scss +27 -33
  44. package/src/styles/components/_bottom-bar.scss +11 -23
  45. package/src/styles/components/_editor-shell.scss +38 -18
  46. package/src/styles/components/_mockos-router.scss +16 -14
  47. package/src/styles/components/_ui-components.scss +14 -15
  48. package/src/styles/foundation/_colors.scss +28 -8
  49. package/src/styles/foundation/_mixins.scss +1 -1
  50. package/src/styles/foundation/_sizes.scss +4 -2
  51. package/src/styles/layout/_builder.scss +1 -1
  52. package/src/styles/modals/_add-component.scss +2 -2
  53. package/src/styles/modals/_color-modal.scss +2 -2
  54. package/src/styles/modals/_modal-shell.scss +1 -1
  55. package/src/utils/analyseNodeByPatterns.ts +0 -15
  56. package/dist/components/MobilePanelToggleButton.d.ts +0 -8
  57. package/dist/hooks/useMinimumDelay.d.ts +0 -7
  58. package/dist/hooks/useMobileEditorPanels.d.ts +0 -12
  59. package/dist/hooks/useSyncProjectPageStore.d.ts +0 -15
  60. package/src/components/MobilePanelToggleButton.tsx +0 -39
  61. package/src/hooks/useMinimumDelay.ts +0 -20
  62. package/src/hooks/useMobileEditorPanels.ts +0 -56
  63. package/src/hooks/useSyncProjectPageStore.ts +0 -40
@@ -1,90 +1,100 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import type { AppConfig } from '../types/PreviewConfig';
3
+ import { defaultAppConfig } from '../types/PreviewConfig';
3
4
  import Modal from './Modal';
4
5
 
5
- const screenStyleDefaults = {
6
- light: { backgroundColor: '#FDFDFD', color: '#161827' },
7
- dark: { backgroundColor: '#12131A', color: '#E9EBF9' },
8
- } as const;
6
+ type ScreenMode = 'light' | 'dark';
7
+ type ScreenColorKey = keyof AppConfig['screenStyle']['light'];
9
8
 
10
- type ScreenMode = keyof typeof screenStyleDefaults;
11
- type ScreenColorKey = keyof (typeof screenStyleDefaults)['light'];
9
+ type ScreenColorsModalProps = {
10
+ appConfig: AppConfig;
11
+ onChange: (next: AppConfig) => void;
12
+ onClose: () => void;
13
+ };
12
14
 
13
- const colorFields = [
15
+ type ColorField = {
16
+ id: string;
17
+ label: string;
18
+ mode: ScreenMode;
19
+ key: ScreenColorKey;
20
+ };
21
+
22
+ const colorFields: ColorField[] = [
14
23
  {
15
24
  id: 'light-bg',
16
- label: 'Light Background Color',
17
- mode: 'light' as ScreenMode,
18
- key: 'backgroundColor' as ScreenColorKey,
25
+ label: 'Light Background',
26
+ mode: 'light',
27
+ key: 'backgroundColor',
19
28
  },
20
29
  {
21
30
  id: 'light-color',
22
- label: 'Light Color',
23
- mode: 'light' as ScreenMode,
24
- key: 'color' as ScreenColorKey,
31
+ label: 'Light Text',
32
+ mode: 'light',
33
+ key: 'color',
25
34
  },
26
35
  {
27
36
  id: 'dark-bg',
28
- label: 'Dark Background Color',
29
- mode: 'dark' as ScreenMode,
30
- key: 'backgroundColor' as ScreenColorKey,
37
+ label: 'Dark Background',
38
+ mode: 'dark',
39
+ key: 'backgroundColor',
31
40
  },
32
41
  {
33
42
  id: 'dark-color',
34
- label: 'Dark Color',
35
- mode: 'dark' as ScreenMode,
36
- key: 'color' as ScreenColorKey,
43
+ label: 'Dark Text',
44
+ mode: 'dark',
45
+ key: 'color',
37
46
  },
38
- ] as const;
39
-
40
- type ScreenColorsModalProps = {
41
- appConfig: AppConfig;
42
- onChange: (next: AppConfig) => void;
43
- onClose: () => void;
44
- };
47
+ ];
45
48
 
46
49
  export function ScreenColorsModal({
47
50
  appConfig,
48
51
  onChange,
49
52
  onClose,
50
53
  }: ScreenColorsModalProps) {
51
- const getScreenColorValue = (mode: ScreenMode, key: ScreenColorKey) =>
52
- appConfig.screenStyle?.[mode]?.[key] ?? screenStyleDefaults[mode][key];
54
+ const defaults = defaultAppConfig.screenStyle;
53
55
 
54
- const handleScreenStyleChange = (
55
- mode: ScreenMode,
56
- key: ScreenColorKey,
57
- value: string,
58
- ) => {
56
+ const getValue = (mode: ScreenMode, key: ScreenColorKey) =>
57
+ appConfig.screenStyle?.[mode]?.[key] ?? defaults[mode][key];
58
+
59
+ const handleChange = (mode: ScreenMode, key: ScreenColorKey, value: string) =>
59
60
  onChange({
60
61
  ...appConfig,
61
62
  screenStyle: {
62
- ...screenStyleDefaults,
63
+ ...defaults,
63
64
  ...appConfig.screenStyle,
64
65
  [mode]: {
65
- ...screenStyleDefaults[mode],
66
+ ...defaults[mode],
66
67
  ...appConfig.screenStyle?.[mode],
67
68
  [key]: value,
68
69
  },
69
70
  },
70
71
  });
71
- };
72
+
73
+ const headerDescription = useMemo(
74
+ () =>
75
+ 'Edit light/dark screen background & text colors used in the preview.',
76
+ [],
77
+ );
72
78
 
73
79
  return (
74
80
  <Modal
75
81
  onClose={onClose}
76
- ariaLabelledBy="screen-colors-modal-title"
77
- className="modal--large modal--scrollable"
82
+ ariaLabelledBy="screen-colors-title"
83
+ contentClassName="localication-modal__content"
78
84
  >
79
- <div className="modal__header">
80
- <h3 id="screen-colors-modal-title" className="modal__title">
81
- Screen Colors
82
- </h3>
85
+ <div className="modal__header localication-modal__header">
86
+ <div className="localication-modal__header-main">
87
+ <h3 id="screen-colors-title" className="modal__title">
88
+ Screen Colors
89
+ </h3>
90
+ <p className="localication-modal__description">{headerDescription}</p>
91
+ </div>
83
92
  <button type="button" className="editor-button" onClick={onClose}>
84
93
  Close
85
94
  </button>
86
95
  </div>
87
- <div className="modal__body">
96
+
97
+ <div className="localication-modal__body">
88
98
  <div
89
99
  style={{
90
100
  display: 'grid',
@@ -94,15 +104,13 @@ export function ScreenColorsModal({
94
104
  >
95
105
  {colorFields.map(({ id, label, mode, key }) => (
96
106
  <React.Fragment key={id}>
97
- <div>{label}</div>
107
+ <div style={{ alignSelf: 'center' }}>{label}</div>
98
108
  <input
99
109
  id={id}
100
110
  type="color"
101
111
  className="input input--color"
102
- value={getScreenColorValue(mode, key)}
103
- onChange={(e) =>
104
- handleScreenStyleChange(mode, key, e.target.value)
105
- }
112
+ value={String(getValue(mode, key))}
113
+ onChange={(e) => handleChange(mode, key, e.target.value)}
106
114
  />
107
115
  </React.Fragment>
108
116
  ))}
@@ -111,5 +119,3 @@ export function ScreenColorsModal({
111
119
  </Modal>
112
120
  );
113
121
  }
114
-
115
- export default ScreenColorsModal;
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect } from 'react';
1
+ import { useCallback, useEffect, useState } from 'react';
2
2
  import type { Node } from '../types/Node';
3
3
  import type { Project, ProjectColors } from '../types/Project';
4
4
  import { RenderPage } from '../RenderPage';
@@ -6,6 +6,7 @@ import { EditorHeader } from '../components/EditorHeader';
6
6
  import { AttributesEditorPanel } from '../components/AttributesEditorPanel';
7
7
  import { BuilderProvider } from '../components/BuilderProvider';
8
8
  import { BuilderPanel } from './tabs/BuilderPanel';
9
+ import { BottomBar } from '../components/BottomBar';
9
10
  import { AppConfig, defaultAppConfig } from '../types/PreviewConfig';
10
11
  import { useRenderStore } from '../store';
11
12
  import { logger } from '../utils/logger';
@@ -15,11 +16,6 @@ import { analyseAndProccess } from '../utils/analyseNode';
15
16
  import backgroundImage from '../assets/images/background.jpg';
16
17
  import type { PaywallBenefits } from '../paywall/types/benefits';
17
18
  import { LoadingComponent } from '../components/LoadingComponent';
18
- import { MobilePanelToggleButton } from '../components/MobilePanelToggleButton';
19
- import { BottomBar } from '../components/BottomBar';
20
- import { useMobileEditorPanels } from '../hooks/useMobileEditorPanels';
21
- import { useSyncProjectPageStore } from '../hooks/useSyncProjectPageStore';
22
- import { useMinimumDelay } from '../hooks/useMinimumDelay';
23
19
  import {
24
20
  deleteNodeFromTree,
25
21
  findNodeByKey,
@@ -32,57 +28,48 @@ export type ProjectPageProps = {
32
28
  appConfig?: AppConfig;
33
29
  logLevel?: LogLevel;
34
30
  projectColors?: ProjectColors;
35
- name: string;
31
+ onSaveProjectColors?: (colors: ProjectColors) => void;
32
+ name?: string;
36
33
  };
37
34
 
35
+ const MOBILE_BREAKPOINT = 1000;
36
+
38
37
  export function ProjectPage({
39
38
  project,
40
39
  appConfig = defaultAppConfig,
41
40
  onSaveProject,
42
41
  logLevel,
43
42
  projectColors,
43
+ onSaveProjectColors,
44
44
  name,
45
45
  }: ProjectPageProps) {
46
46
  useLogRender('ProjectPage');
47
+ const resolvedName = name ?? project.name;
47
48
  const resolvedProjectColors = projectColors ?? project.projectColors;
48
49
  const {
49
50
  current,
50
51
  setCurrent,
51
- setAppConfig,
52
52
  setProjectColors,
53
53
  setProjectName,
54
- editorData,
55
- setEditorData,
56
54
  products,
57
55
  benefits,
58
- } = useRenderStore((s) => ({
56
+ } = useRenderStore(s => ({
59
57
  current: s.current,
60
58
  setCurrent: s.setCurrent,
61
- setAppConfig: s.setAppConfig,
62
59
  setProjectColors: s.setProjectColors,
63
60
  setProjectName: s.setProjectName,
64
- editorData: s.editorData,
65
- setEditorData: s.setEditorData,
66
61
  products: s.products,
67
62
  benefits: s.benefits,
68
63
  }));
69
- const minLoadingDelayDone = useMinimumDelay(1000, [project.data]);
70
- const {
71
- isMobile,
72
- mobilePanel,
73
- toggleMobilePanel,
74
- closeMobilePanels,
75
- leftPanelIsOpen,
76
- attributesPanelIsOpen,
77
- } = useMobileEditorPanels();
78
-
79
- useSyncProjectPageStore({
80
- appConfig,
81
- name,
82
- projectColors: resolvedProjectColors,
83
- setAppConfig,
84
- setProjectName,
85
- setProjectColors,
64
+ const [editorData, setEditorData] = useState<Node>(null);
65
+ const [minLoadingDelayDone, setMinLoadingDelayDone] =
66
+ useState<boolean>(false);
67
+ const [mobilePanel, setMobilePanel] = useState<
68
+ 'builder' | 'attributes' | null
69
+ >(null);
70
+ const [isMobile, setIsMobile] = useState<boolean>(() => {
71
+ if (typeof window === 'undefined') return false;
72
+ return window.innerWidth <= MOBILE_BREAKPOINT;
86
73
  });
87
74
 
88
75
  const handleDeleteNode = useCallback(
@@ -90,14 +77,17 @@ export function ProjectPage({
90
77
  // Extra warning for deleting the root node (editorData)
91
78
  if (nodeToDelete === editorData) {
92
79
  const shouldDeleteRoot = window.confirm(
93
- 'You are about to delete the root component. This will clear the entire screen. Continue?',
80
+ 'You are about to delete the root component. This will clear the entire screen. Continue?'
94
81
  );
95
82
  if (!shouldDeleteRoot) return;
96
83
  setEditorData(null);
84
+ setCurrent(null);
97
85
  return;
98
86
  }
99
87
  const updated: Node = deleteNodeFromTree(editorData, nodeToDelete);
88
+ //@ts-ignore
100
89
  setEditorData(updated);
90
+
101
91
  if (current === nodeToDelete) {
102
92
  setCurrent(updated);
103
93
  return;
@@ -113,45 +103,94 @@ export function ProjectPage({
113
103
  }
114
104
  }
115
105
  },
116
- [editorData, current],
106
+ [editorData, current]
117
107
  );
118
108
 
119
109
  useEffect(() => {
120
110
  logger.info('ProjectPage', 'mount', { projectName: project.name });
111
+ useRenderStore.getState().setAppConfig(appConfig);
112
+ logger.verbose('ProjectPage', 'appConfig applied', appConfig);
121
113
  return () => {
122
114
  logger.info('ProjectPage', 'unmount');
123
115
  };
124
- }, [project.name]);
116
+ }, [appConfig, project.name]);
117
+
118
+ useEffect(() => {
119
+ setProjectName(resolvedName);
120
+ }, [resolvedName, setProjectName]);
121
+
122
+ useEffect(() => {
123
+ setProjectColors(resolvedProjectColors);
124
+ return () => setProjectColors(undefined);
125
+ }, [resolvedProjectColors, setProjectColors]);
125
126
 
126
127
  useEffect(() => {
127
128
  if (!logLevel) return;
128
129
  logger.setLevel(logLevel);
129
130
  }, [logLevel]);
130
131
 
132
+ useEffect(() => {
133
+ function handleResize() {
134
+ setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT);
135
+ }
136
+
137
+ handleResize();
138
+ window.addEventListener('resize', handleResize);
139
+ return () => window.removeEventListener('resize', handleResize);
140
+ }, []);
141
+
142
+ useEffect(() => {
143
+ setMobilePanel(null);
144
+ }, [isMobile]);
145
+
146
+ const toggleMobilePanel = (panel: 'builder' | 'attributes') => {
147
+ setMobilePanel(prev => (prev === panel ? null : panel));
148
+ };
149
+
150
+ const closeMobilePanels = () => {
151
+ setMobilePanel(null);
152
+ };
153
+
154
+ const leftPanelIsOpen = !isMobile || mobilePanel === 'builder';
155
+ const attributesPanelIsOpen = !isMobile || mobilePanel === 'attributes';
156
+
157
+ useEffect(() => {
158
+ setMinLoadingDelayDone(false);
159
+ const timer = setTimeout(() => setMinLoadingDelayDone(true), 1000);
160
+ return () => clearTimeout(timer);
161
+ }, [project.data]);
162
+
131
163
  useEffect(() => {
132
164
  try {
133
165
  // Reset to "loading" immediately on project change so the loader is shown
134
166
  // until a valid node is available (and for at least 2 seconds).
135
167
  setEditorData(null);
168
+ setCurrent(null);
136
169
  const processed = analyseAndProccess(project.data);
137
170
  if (!processed) {
138
171
  setEditorData(null);
172
+ setCurrent(null);
139
173
  return;
140
174
  }
141
175
  setEditorData(processed);
176
+ setCurrent(processed);
142
177
  } catch (error) {
143
178
  console.error(error);
144
179
  setEditorData(null);
180
+ setCurrent(null);
145
181
  }
146
182
  }, [project.data]);
147
183
 
148
184
  const showLoading = editorData === null || !minLoadingDelayDone;
149
185
 
150
186
  return (
151
- <div className="container-full">
187
+ <div className='container-full'>
152
188
  <EditorHeader
153
189
  onSaveProject={() => {
154
190
  logger.info('ProjectPage', 'save project', { name: project.name });
191
+ if (onSaveProjectColors && resolvedProjectColors) {
192
+ onSaveProjectColors(resolvedProjectColors);
193
+ }
155
194
  onSaveProject({
156
195
  ...project,
157
196
  data: editorData,
@@ -162,55 +201,94 @@ export function ProjectPage({
162
201
  setEditorData(project.data);
163
202
  setCurrent(project.data);
164
203
  }}
204
+ current={current}
205
+ editorData={editorData}
206
+ setEditorData={setEditorData}
165
207
  />
166
208
  {isMobile && (
167
209
  <div
168
- className="mobile-panel-toggle"
169
- role="group"
170
- aria-label="Editor panels"
210
+ className='mobile-panel-toggle'
211
+ role='group'
212
+ aria-label='Editor panels'
171
213
  >
172
- <MobilePanelToggleButton
173
- label="Builder"
174
- isActive={mobilePanel === 'builder'}
175
- ariaLabel="Toggle builder panel"
176
- ariaControls="split-left-panel"
214
+ <button
215
+ type='button'
216
+ className={`mobile-panel-toggle__button${mobilePanel === 'builder' ? ' mobile-panel-toggle__button--active' : ''}`}
217
+ aria-label='Toggle builder panel'
218
+ aria-expanded={mobilePanel === 'builder'}
219
+ aria-controls='split-left-panel'
177
220
  onClick={() => toggleMobilePanel('builder')}
178
- />
179
- <MobilePanelToggleButton
180
- label="Attributes"
181
- isActive={mobilePanel === 'attributes'}
182
- ariaLabel="Toggle attributes panel"
183
- ariaControls="split-attributes-panel"
221
+ >
222
+ <span className='mobile-panel-toggle__icon' aria-hidden='true'>
223
+ <svg viewBox='0 0 16 12' role='presentation' focusable='false'>
224
+ <path
225
+ d='M1 1h14M1 6h14M1 11h14'
226
+ stroke='currentColor'
227
+ strokeWidth='2'
228
+ strokeLinecap='round'
229
+ fill='none'
230
+ />
231
+ </svg>
232
+ </span>
233
+ <span className='mobile-panel-toggle__label'>Builder</span>
234
+ </button>
235
+ <button
236
+ type='button'
237
+ className={`mobile-panel-toggle__button${mobilePanel === 'attributes' ? ' mobile-panel-toggle__button--active' : ''}`}
238
+ aria-label='Toggle attributes panel'
239
+ aria-expanded={mobilePanel === 'attributes'}
240
+ aria-controls='split-attributes-panel'
184
241
  onClick={() => toggleMobilePanel('attributes')}
185
- />
242
+ >
243
+ <span className='mobile-panel-toggle__icon' aria-hidden='true'>
244
+ <svg viewBox='0 0 16 12' role='presentation' focusable='false'>
245
+ <path
246
+ d='M1 1h14M1 6h14M1 11h14'
247
+ stroke='currentColor'
248
+ strokeWidth='2'
249
+ strokeLinecap='round'
250
+ fill='none'
251
+ />
252
+ </svg>
253
+ </span>
254
+ <span className='mobile-panel-toggle__label'>Attributes</span>
255
+ </button>
186
256
  </div>
187
257
  )}
188
- <div className="editor-container">
258
+ <div className='editor-container'>
189
259
  <div
190
- id="split-left-panel"
260
+ id='split-left-panel'
191
261
  className={`split-left${leftPanelIsOpen ? ' is-open' : ''}`}
192
262
  aria-hidden={isMobile && !leftPanelIsOpen}
193
263
  >
194
264
  {isMobile && (
195
265
  <button
196
- type="button"
197
- className="split-panel__close"
198
- aria-label="Close builder panel"
266
+ type='button'
267
+ className='split-panel__close'
268
+ aria-label='Close builder panel'
199
269
  onClick={closeMobilePanels}
200
270
  >
201
271
  Close
202
272
  </button>
203
273
  )}
204
274
  <div>
205
- <BuilderPanel onDeleteNode={handleDeleteNode} />
275
+ <BuilderPanel
276
+ data={editorData}
277
+ setData={setEditorData}
278
+ onDeleteNode={handleDeleteNode}
279
+ />
206
280
  </div>
207
281
  </div>
208
282
  <div
209
- style={{ backgroundImage: `url(${backgroundImage})` }}
210
- className="split-right"
283
+ style={{
284
+ // Set as a CSS variable so `.dark .split-right` can override it.
285
+ // eslint-disable-next-line @typescript-eslint/naming-convention
286
+ ['--rb-canvas-bg' as any]: `url(${backgroundImage})`,
287
+ }}
288
+ className='split-right'
211
289
  >
212
290
  {showLoading && (
213
- <div className="rb-loading-overlay" aria-busy="true">
291
+ <div className='rb-loading-overlay' aria-busy='true'>
214
292
  <LoadingComponent />
215
293
  </div>
216
294
  )}
@@ -225,33 +303,54 @@ export function ProjectPage({
225
303
  : {},
226
304
  }}
227
305
  >
228
- <RenderPage data={editorData} name={project.name} />
229
- <BottomBar />
306
+ <RenderPage data={editorData} name={resolvedName} />
230
307
  </BuilderProvider>
231
308
  )}
232
309
  </div>
310
+ {/* BOTOM BAR */}
311
+ <BottomBar data={editorData} setData={setEditorData} />
233
312
  <div
234
- id="split-attributes-panel"
313
+ id='split-attributes-panel'
235
314
  className={`split-third${attributesPanelIsOpen ? ' is-open' : ''}`}
236
315
  aria-hidden={isMobile && !attributesPanelIsOpen}
237
316
  >
238
317
  {isMobile && (
239
318
  <button
240
- type="button"
241
- className="split-panel__close"
242
- aria-label="Close attributes panel"
319
+ type='button'
320
+ className='split-panel__close'
321
+ aria-label='Close attributes panel'
243
322
  onClick={closeMobilePanels}
244
323
  >
245
324
  Close
246
325
  </button>
247
326
  )}
248
- <AttributesEditorPanel projectColors={resolvedProjectColors} />
327
+ <AttributesEditorPanel
328
+ attributes={editorData}
329
+ projectColors={resolvedProjectColors}
330
+ onChange={data => {
331
+ setEditorData(data);
332
+ let nodeKey: string | undefined = undefined;
333
+ if (
334
+ data &&
335
+ typeof data === 'object' &&
336
+ !Array.isArray(data) &&
337
+ 'key' in (data as any)
338
+ ) {
339
+ nodeKey = (data as any).key as string | undefined;
340
+ }
341
+ logger.verbose(
342
+ 'ProjectPage',
343
+ 'attributes change',
344
+ nodeKey ? { nodeKey } : undefined
345
+ );
346
+ }}
347
+ />
249
348
  </div>
250
349
  {isMobile && mobilePanel && (
251
350
  <button
252
- type="button"
253
- className="editor-container__overlay"
254
- aria-label="Close active panel"
351
+ type='button'
352
+ className='editor-container__overlay'
353
+ aria-label='Close active panel'
255
354
  onClick={closeMobilePanels}
256
355
  />
257
356
  )}
@@ -4,8 +4,8 @@ import { Builder } from '../../components/Builder';
4
4
  import { useRenderStore } from '../../store';
5
5
 
6
6
  type BuilderPanelProps = {
7
- data?: Node;
8
- setData?: (data: Node) => void;
7
+ data: Node;
8
+ setData: (data: Node) => void;
9
9
  onDeleteNode: (node: Node) => void;
10
10
  };
11
11
 
@@ -15,16 +15,10 @@ export function BuilderPanel({
15
15
  onDeleteNode,
16
16
  }: BuilderPanelProps) {
17
17
  useLogRender('BuilderPanel');
18
- const { current, setCurrent, editorData, setEditorData } = useRenderStore(
19
- (s) => ({
20
- current: s.current,
21
- setCurrent: s.setCurrent,
22
- editorData: s.editorData,
23
- setEditorData: s.setEditorData,
24
- }),
25
- );
26
- const effectiveData = data ?? editorData;
27
- const effectiveSetData = setData ?? setEditorData;
18
+ const { current, setCurrent } = useRenderStore((s) => ({
19
+ current: s.current,
20
+ setCurrent: s.setCurrent,
21
+ }));
28
22
  return (
29
23
  <div
30
24
  role="region"
@@ -32,8 +26,8 @@ export function BuilderPanel({
32
26
  aria-hidden={false}
33
27
  >
34
28
  <Builder
35
- data={effectiveData}
36
- setData={effectiveSetData}
29
+ data={data}
30
+ setData={setData}
37
31
  current={current}
38
32
  setCurrent={setCurrent}
39
33
  onDeleteNode={onDeleteNode}