@developer_tribe/react-builder 1.0.5 → 1.0.6
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.
- package/dist/build-components/index.d.ts +1 -2
- package/dist/build-components/patterns.generated.d.ts +56 -439
- package/dist/components/AttributesEditorPanel.d.ts +2 -2
- package/dist/components/BottomBar.d.ts +8 -0
- package/dist/components/Checkbox.d.ts +1 -1
- package/dist/components/LoadingComponent.d.ts +1 -0
- package/dist/components/MobilePanelToggleButton.d.ts +8 -0
- package/dist/hooks/useMinimumDelay.d.ts +7 -0
- package/dist/hooks/useMobileEditorPanels.d.ts +12 -0
- package/dist/hooks/useSyncProjectPageStore.d.ts +15 -0
- package/dist/index.cjs.js +3 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +3 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.native.cjs.js +1 -1
- package/dist/index.native.cjs.js.map +1 -1
- package/dist/index.native.esm.js +4 -4
- package/dist/index.native.esm.js.map +1 -1
- package/dist/modals/ScreenColorsModal.d.ts +8 -0
- package/dist/modals/index.d.ts +1 -0
- package/dist/pages/tabs/BuilderPanel.d.ts +2 -2
- package/dist/store.d.ts +6 -0
- package/dist/styles.css +1 -1
- package/dist/utils/nodeTree.d.ts +5 -0
- package/package.json +1 -1
- package/src/RenderPage.tsx +4 -1
- package/src/assets/samples/carousel-sample.json +99 -81
- package/src/assets/samples/simple-1.json +8 -2
- package/src/assets/samples/simple-2.json +36 -9
- package/src/assets/samples/vpn-onboard-1.json +27 -23
- package/src/assets/samples/vpn-onboard-2.json +279 -275
- package/src/assets/samples/vpn-onboard-3.json +247 -246
- package/src/assets/samples/vpn-onboard-4.json +247 -246
- package/src/assets/samples/vpn-onboard-5.json +375 -369
- package/src/assets/samples/vpn-onboard-6.json +252 -248
- package/src/build-components/RenderNode.generated.tsx +0 -7
- package/src/build-components/View/pattern.json +2 -2
- package/src/build-components/index.ts +0 -5
- package/src/build-components/patterns.generated.ts +56 -455
- package/src/components/AttributesEditorPanel.tsx +12 -8
- package/src/components/BottomBar.tsx +236 -0
- package/src/components/EditorHeader.tsx +11 -4
- package/src/components/LoadingComponent.tsx +10 -0
- package/src/components/MobilePanelToggleButton.tsx +39 -0
- package/src/hooks/useMinimumDelay.ts +20 -0
- package/src/hooks/useMobileEditorPanels.ts +56 -0
- package/src/hooks/useSyncProjectPageStore.ts +40 -0
- package/src/modals/ScreenColorsModal.tsx +115 -0
- package/src/modals/index.ts +1 -0
- package/src/pages/ProjectPage.tsx +53 -243
- package/src/pages/tabs/BuilderPanel.tsx +14 -8
- package/src/store.ts +10 -6
- package/src/styles/base/_global.scss +12 -4
- package/src/styles/components/_attributes-editor.scss +9 -1
- package/src/styles/components/_bottom-bar.scss +113 -0
- package/src/styles/components/_editor-shell.scss +0 -19
- package/src/styles/index.scss +1 -0
- package/src/utils/analyseNodeByPatterns.ts +15 -0
- package/src/utils/nodeTree.ts +99 -0
- package/dist/build-components/PaywallSubscriButton/PaywallSubscriButton.d.ts +0 -5
- package/dist/build-components/PaywallSubscriButton/PaywallSubscriButtonProps.generated.d.ts +0 -50
- package/dist/pages/tabs/SideTool.d.ts +0 -8
- package/src/build-components/PaywallSubscriButton/PaywallSubscriButton.tsx +0 -10
- package/src/build-components/PaywallSubscriButton/PaywallSubscriButtonProps.generated.ts +0 -77
- package/src/build-components/PaywallSubscriButton/pattern.json +0 -27
- package/src/pages/tabs/SideTool.tsx +0 -253
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { useCallback, useEffect
|
|
2
|
-
import type { Node
|
|
1
|
+
import { useCallback, useEffect } from 'react';
|
|
2
|
+
import type { Node } from '../types/Node';
|
|
3
3
|
import type { Project, ProjectColors } from '../types/Project';
|
|
4
4
|
import { RenderPage } from '../RenderPage';
|
|
5
5
|
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 { SideTool } from './tabs/SideTool';
|
|
10
9
|
import { AppConfig, defaultAppConfig } from '../types/PreviewConfig';
|
|
11
10
|
import { useRenderStore } from '../store';
|
|
12
11
|
import { logger } from '../utils/logger';
|
|
@@ -15,8 +14,18 @@ import type { LogLevel } from '../types/Project';
|
|
|
15
14
|
import { analyseAndProccess } from '../utils/analyseNode';
|
|
16
15
|
import backgroundImage from '../assets/images/background.jpg';
|
|
17
16
|
import type { PaywallBenefits } from '../paywall/types/benefits';
|
|
18
|
-
import
|
|
19
|
-
import
|
|
17
|
+
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
|
+
import {
|
|
24
|
+
deleteNodeFromTree,
|
|
25
|
+
findNodeByKey,
|
|
26
|
+
isNodeRecord,
|
|
27
|
+
nodeHasChild,
|
|
28
|
+
} from '../utils/nodeTree';
|
|
20
29
|
export type ProjectPageProps = {
|
|
21
30
|
project: Project;
|
|
22
31
|
onSaveProject: (project: Project) => void;
|
|
@@ -26,8 +35,6 @@ export type ProjectPageProps = {
|
|
|
26
35
|
name: string;
|
|
27
36
|
};
|
|
28
37
|
|
|
29
|
-
const MOBILE_BREAKPOINT = 1000;
|
|
30
|
-
|
|
31
38
|
export function ProjectPage({
|
|
32
39
|
project,
|
|
33
40
|
appConfig = defaultAppConfig,
|
|
@@ -41,27 +48,41 @@ export function ProjectPage({
|
|
|
41
48
|
const {
|
|
42
49
|
current,
|
|
43
50
|
setCurrent,
|
|
51
|
+
setAppConfig,
|
|
44
52
|
setProjectColors,
|
|
45
53
|
setProjectName,
|
|
54
|
+
editorData,
|
|
55
|
+
setEditorData,
|
|
46
56
|
products,
|
|
47
57
|
benefits,
|
|
48
58
|
} = useRenderStore((s) => ({
|
|
49
59
|
current: s.current,
|
|
50
60
|
setCurrent: s.setCurrent,
|
|
61
|
+
setAppConfig: s.setAppConfig,
|
|
51
62
|
setProjectColors: s.setProjectColors,
|
|
52
63
|
setProjectName: s.setProjectName,
|
|
64
|
+
editorData: s.editorData,
|
|
65
|
+
setEditorData: s.setEditorData,
|
|
53
66
|
products: s.products,
|
|
54
67
|
benefits: s.benefits,
|
|
55
68
|
}));
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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,
|
|
65
86
|
});
|
|
66
87
|
|
|
67
88
|
const handleDeleteNode = useCallback(
|
|
@@ -73,13 +94,10 @@ export function ProjectPage({
|
|
|
73
94
|
);
|
|
74
95
|
if (!shouldDeleteRoot) return;
|
|
75
96
|
setEditorData(null);
|
|
76
|
-
setCurrent(null);
|
|
77
97
|
return;
|
|
78
98
|
}
|
|
79
99
|
const updated: Node = deleteNodeFromTree(editorData, nodeToDelete);
|
|
80
|
-
//@ts-ignore
|
|
81
100
|
setEditorData(updated);
|
|
82
|
-
|
|
83
101
|
if (current === nodeToDelete) {
|
|
84
102
|
setCurrent(updated);
|
|
85
103
|
return;
|
|
@@ -100,76 +118,30 @@ export function ProjectPage({
|
|
|
100
118
|
|
|
101
119
|
useEffect(() => {
|
|
102
120
|
logger.info('ProjectPage', 'mount', { projectName: project.name });
|
|
103
|
-
useRenderStore.getState().setAppConfig(appConfig);
|
|
104
|
-
logger.verbose('ProjectPage', 'appConfig applied', appConfig);
|
|
105
121
|
return () => {
|
|
106
122
|
logger.info('ProjectPage', 'unmount');
|
|
107
123
|
};
|
|
108
|
-
}, [
|
|
109
|
-
|
|
110
|
-
useEffect(() => {
|
|
111
|
-
setProjectName(name);
|
|
112
|
-
}, [name, setProjectName]);
|
|
113
|
-
|
|
114
|
-
useEffect(() => {
|
|
115
|
-
setProjectColors(resolvedProjectColors);
|
|
116
|
-
return () => setProjectColors(undefined);
|
|
117
|
-
}, [resolvedProjectColors, setProjectColors]);
|
|
124
|
+
}, [project.name]);
|
|
118
125
|
|
|
119
126
|
useEffect(() => {
|
|
120
127
|
if (!logLevel) return;
|
|
121
128
|
logger.setLevel(logLevel);
|
|
122
129
|
}, [logLevel]);
|
|
123
130
|
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
function handleResize() {
|
|
126
|
-
setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
handleResize();
|
|
130
|
-
window.addEventListener('resize', handleResize);
|
|
131
|
-
return () => window.removeEventListener('resize', handleResize);
|
|
132
|
-
}, []);
|
|
133
|
-
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
setMobilePanel(null);
|
|
136
|
-
}, [isMobile]);
|
|
137
|
-
|
|
138
|
-
const toggleMobilePanel = (panel: 'builder' | 'attributes') => {
|
|
139
|
-
setMobilePanel((prev) => (prev === panel ? null : panel));
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const closeMobilePanels = () => {
|
|
143
|
-
setMobilePanel(null);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const leftPanelIsOpen = !isMobile || mobilePanel === 'builder';
|
|
147
|
-
const attributesPanelIsOpen = !isMobile || mobilePanel === 'attributes';
|
|
148
|
-
|
|
149
|
-
useEffect(() => {
|
|
150
|
-
setMinLoadingDelayDone(false);
|
|
151
|
-
const timer = setTimeout(() => setMinLoadingDelayDone(true), 1000);
|
|
152
|
-
return () => clearTimeout(timer);
|
|
153
|
-
}, [project.data]);
|
|
154
|
-
|
|
155
131
|
useEffect(() => {
|
|
156
132
|
try {
|
|
157
133
|
// Reset to "loading" immediately on project change so the loader is shown
|
|
158
134
|
// until a valid node is available (and for at least 2 seconds).
|
|
159
135
|
setEditorData(null);
|
|
160
|
-
setCurrent(null);
|
|
161
136
|
const processed = analyseAndProccess(project.data);
|
|
162
137
|
if (!processed) {
|
|
163
138
|
setEditorData(null);
|
|
164
|
-
setCurrent(null);
|
|
165
139
|
return;
|
|
166
140
|
}
|
|
167
141
|
setEditorData(processed);
|
|
168
|
-
setCurrent(processed);
|
|
169
142
|
} catch (error) {
|
|
170
143
|
console.error(error);
|
|
171
144
|
setEditorData(null);
|
|
172
|
-
setCurrent(null);
|
|
173
145
|
}
|
|
174
146
|
}, [project.data]);
|
|
175
147
|
|
|
@@ -190,9 +162,6 @@ export function ProjectPage({
|
|
|
190
162
|
setEditorData(project.data);
|
|
191
163
|
setCurrent(project.data);
|
|
192
164
|
}}
|
|
193
|
-
current={current}
|
|
194
|
-
editorData={editorData}
|
|
195
|
-
setEditorData={setEditorData}
|
|
196
165
|
/>
|
|
197
166
|
{isMobile && (
|
|
198
167
|
<div
|
|
@@ -200,48 +169,20 @@ export function ProjectPage({
|
|
|
200
169
|
role="group"
|
|
201
170
|
aria-label="Editor panels"
|
|
202
171
|
>
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
aria-controls="split-left-panel"
|
|
172
|
+
<MobilePanelToggleButton
|
|
173
|
+
label="Builder"
|
|
174
|
+
isActive={mobilePanel === 'builder'}
|
|
175
|
+
ariaLabel="Toggle builder panel"
|
|
176
|
+
ariaControls="split-left-panel"
|
|
209
177
|
onClick={() => toggleMobilePanel('builder')}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
strokeWidth="2"
|
|
217
|
-
strokeLinecap="round"
|
|
218
|
-
fill="none"
|
|
219
|
-
/>
|
|
220
|
-
</svg>
|
|
221
|
-
</span>
|
|
222
|
-
<span className="mobile-panel-toggle__label">Builder</span>
|
|
223
|
-
</button>
|
|
224
|
-
<button
|
|
225
|
-
type="button"
|
|
226
|
-
className={`mobile-panel-toggle__button${mobilePanel === 'attributes' ? ' mobile-panel-toggle__button--active' : ''}`}
|
|
227
|
-
aria-label="Toggle attributes panel"
|
|
228
|
-
aria-expanded={mobilePanel === 'attributes'}
|
|
229
|
-
aria-controls="split-attributes-panel"
|
|
178
|
+
/>
|
|
179
|
+
<MobilePanelToggleButton
|
|
180
|
+
label="Attributes"
|
|
181
|
+
isActive={mobilePanel === 'attributes'}
|
|
182
|
+
ariaLabel="Toggle attributes panel"
|
|
183
|
+
ariaControls="split-attributes-panel"
|
|
230
184
|
onClick={() => toggleMobilePanel('attributes')}
|
|
231
|
-
|
|
232
|
-
<span className="mobile-panel-toggle__icon" aria-hidden="true">
|
|
233
|
-
<svg viewBox="0 0 16 12" role="presentation" focusable="false">
|
|
234
|
-
<path
|
|
235
|
-
d="M1 1h14M1 6h14M1 11h14"
|
|
236
|
-
stroke="currentColor"
|
|
237
|
-
strokeWidth="2"
|
|
238
|
-
strokeLinecap="round"
|
|
239
|
-
fill="none"
|
|
240
|
-
/>
|
|
241
|
-
</svg>
|
|
242
|
-
</span>
|
|
243
|
-
<span className="mobile-panel-toggle__label">Attributes</span>
|
|
244
|
-
</button>
|
|
185
|
+
/>
|
|
245
186
|
</div>
|
|
246
187
|
)}
|
|
247
188
|
<div className="editor-container">
|
|
@@ -261,19 +202,13 @@ export function ProjectPage({
|
|
|
261
202
|
</button>
|
|
262
203
|
)}
|
|
263
204
|
<div>
|
|
264
|
-
<BuilderPanel
|
|
265
|
-
data={editorData}
|
|
266
|
-
setData={setEditorData}
|
|
267
|
-
onDeleteNode={handleDeleteNode}
|
|
268
|
-
/>
|
|
205
|
+
<BuilderPanel onDeleteNode={handleDeleteNode} />
|
|
269
206
|
</div>
|
|
270
207
|
</div>
|
|
271
208
|
<div
|
|
272
209
|
style={{ backgroundImage: `url(${backgroundImage})` }}
|
|
273
210
|
className="split-right"
|
|
274
211
|
>
|
|
275
|
-
<SideTool data={editorData} setData={setEditorData} />
|
|
276
|
-
|
|
277
212
|
{showLoading && (
|
|
278
213
|
<div className="rb-loading-overlay" aria-busy="true">
|
|
279
214
|
<LoadingComponent />
|
|
@@ -291,6 +226,7 @@ export function ProjectPage({
|
|
|
291
226
|
}}
|
|
292
227
|
>
|
|
293
228
|
<RenderPage data={editorData} name={project.name} />
|
|
229
|
+
<BottomBar />
|
|
294
230
|
</BuilderProvider>
|
|
295
231
|
)}
|
|
296
232
|
</div>
|
|
@@ -309,27 +245,7 @@ export function ProjectPage({
|
|
|
309
245
|
Close
|
|
310
246
|
</button>
|
|
311
247
|
)}
|
|
312
|
-
<AttributesEditorPanel
|
|
313
|
-
attributes={editorData}
|
|
314
|
-
projectColors={resolvedProjectColors}
|
|
315
|
-
onChange={(data) => {
|
|
316
|
-
setEditorData(data);
|
|
317
|
-
let nodeKey: string | undefined = undefined;
|
|
318
|
-
if (
|
|
319
|
-
data &&
|
|
320
|
-
typeof data === 'object' &&
|
|
321
|
-
!Array.isArray(data) &&
|
|
322
|
-
'key' in (data as any)
|
|
323
|
-
) {
|
|
324
|
-
nodeKey = (data as any).key as string | undefined;
|
|
325
|
-
}
|
|
326
|
-
logger.verbose(
|
|
327
|
-
'ProjectPage',
|
|
328
|
-
'attributes change',
|
|
329
|
-
nodeKey ? { nodeKey } : undefined,
|
|
330
|
-
);
|
|
331
|
-
}}
|
|
332
|
-
/>
|
|
248
|
+
<AttributesEditorPanel projectColors={resolvedProjectColors} />
|
|
333
249
|
</div>
|
|
334
250
|
{isMobile && mobilePanel && (
|
|
335
251
|
<button
|
|
@@ -343,109 +259,3 @@ export function ProjectPage({
|
|
|
343
259
|
</div>
|
|
344
260
|
);
|
|
345
261
|
}
|
|
346
|
-
|
|
347
|
-
function LoadingComponent() {
|
|
348
|
-
return (
|
|
349
|
-
<div className="rb-loading">
|
|
350
|
-
<Lottie animationData={loadingAnimation as any} loop autoplay />
|
|
351
|
-
</div>
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function deleteNodeFromTree(root: Node, target: Node): Node {
|
|
356
|
-
if (root === null || root === undefined) return root;
|
|
357
|
-
if (typeof root === 'string') return root;
|
|
358
|
-
|
|
359
|
-
if (Array.isArray(root)) {
|
|
360
|
-
let changed = false;
|
|
361
|
-
const nextChildren: Node[] = [];
|
|
362
|
-
for (const child of root) {
|
|
363
|
-
if (child === target) {
|
|
364
|
-
changed = true;
|
|
365
|
-
continue;
|
|
366
|
-
}
|
|
367
|
-
const nextChild = deleteNodeFromTree(child, target);
|
|
368
|
-
if (nextChild !== child) changed = true;
|
|
369
|
-
nextChildren.push(nextChild);
|
|
370
|
-
}
|
|
371
|
-
return changed ? nextChildren : root;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const data = root as any;
|
|
375
|
-
if ('children' in data) {
|
|
376
|
-
const prev = data.children as Node;
|
|
377
|
-
if (!prev) return root;
|
|
378
|
-
|
|
379
|
-
if (Array.isArray(prev)) {
|
|
380
|
-
let changed = false;
|
|
381
|
-
const nextChildren: Node[] = [];
|
|
382
|
-
for (const child of prev) {
|
|
383
|
-
if (child === target) {
|
|
384
|
-
changed = true;
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
const nextChild = deleteNodeFromTree(child, target);
|
|
388
|
-
if (nextChild !== child) changed = true;
|
|
389
|
-
nextChildren.push(nextChild);
|
|
390
|
-
}
|
|
391
|
-
if (changed) {
|
|
392
|
-
return { ...data, children: nextChildren } as Node;
|
|
393
|
-
}
|
|
394
|
-
return root;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (prev === target) {
|
|
398
|
-
return { ...data, children: '' } as Node;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const nextChild = deleteNodeFromTree(prev, target);
|
|
402
|
-
if (nextChild !== prev) {
|
|
403
|
-
return { ...data, children: nextChild } as Node;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return root;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function isNodeRecord(node: Node): node is NodeData {
|
|
411
|
-
return (
|
|
412
|
-
node !== null &&
|
|
413
|
-
node !== undefined &&
|
|
414
|
-
typeof node === 'object' &&
|
|
415
|
-
!Array.isArray(node)
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function nodeHasChild(parent: NodeData, potentialChild: Node): boolean {
|
|
420
|
-
const { children } = parent;
|
|
421
|
-
if (!children) return false;
|
|
422
|
-
if (Array.isArray(children)) {
|
|
423
|
-
return children.some((child) => child === potentialChild);
|
|
424
|
-
}
|
|
425
|
-
return children === potentialChild;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function findNodeByKey(root: Node, key?: string): Node | null {
|
|
429
|
-
if (!key) return null;
|
|
430
|
-
if (root === null || root === undefined) return null;
|
|
431
|
-
if (typeof root === 'string') return null;
|
|
432
|
-
|
|
433
|
-
if (Array.isArray(root)) {
|
|
434
|
-
for (const child of root) {
|
|
435
|
-
const found = findNodeByKey(child, key);
|
|
436
|
-
if (found) return found;
|
|
437
|
-
}
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const nodeData = root as NodeData;
|
|
442
|
-
if (nodeData.key === key) {
|
|
443
|
-
return nodeData;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (nodeData.children) {
|
|
447
|
-
return findNodeByKey(nodeData.children as Node, key);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
return null;
|
|
451
|
-
}
|
|
@@ -4,8 +4,8 @@ import { Builder } from '../../components/Builder';
|
|
|
4
4
|
import { useRenderStore } from '../../store';
|
|
5
5
|
|
|
6
6
|
type BuilderPanelProps = {
|
|
7
|
-
data
|
|
8
|
-
setData
|
|
7
|
+
data?: Node;
|
|
8
|
+
setData?: (data: Node) => void;
|
|
9
9
|
onDeleteNode: (node: Node) => void;
|
|
10
10
|
};
|
|
11
11
|
|
|
@@ -15,10 +15,16 @@ export function BuilderPanel({
|
|
|
15
15
|
onDeleteNode,
|
|
16
16
|
}: BuilderPanelProps) {
|
|
17
17
|
useLogRender('BuilderPanel');
|
|
18
|
-
const { current, setCurrent } = useRenderStore(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
22
28
|
return (
|
|
23
29
|
<div
|
|
24
30
|
role="region"
|
|
@@ -26,8 +32,8 @@ export function BuilderPanel({
|
|
|
26
32
|
aria-hidden={false}
|
|
27
33
|
>
|
|
28
34
|
<Builder
|
|
29
|
-
data={
|
|
30
|
-
setData={
|
|
35
|
+
data={effectiveData}
|
|
36
|
+
setData={effectiveSetData}
|
|
31
37
|
current={current}
|
|
32
38
|
setCurrent={setCurrent}
|
|
33
39
|
onDeleteNode={onDeleteNode}
|
package/src/store.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { createWithEqualityFn } from 'zustand/traditional';
|
|
2
2
|
import { shallow } from 'zustand/shallow';
|
|
3
3
|
import type { Device } from './types/Device';
|
|
4
|
-
import {
|
|
5
|
-
defaultAppConfig,
|
|
6
|
-
type AppConfig,
|
|
7
|
-
type Localication,
|
|
8
|
-
} from './types/PreviewConfig';
|
|
4
|
+
import { defaultAppConfig, type AppConfig } from './types/PreviewConfig';
|
|
9
5
|
import { getDefaultDevice } from './utils/getDevices';
|
|
10
|
-
import { ScreenStyle } from './RenderPage';
|
|
11
6
|
import { createJSONStorage, persist } from 'zustand/middleware';
|
|
12
7
|
import { Node } from './types/Node';
|
|
13
8
|
import type { LogEntry, LogLevel, ProjectColors } from './types/Project';
|
|
@@ -20,6 +15,12 @@ import type {
|
|
|
20
15
|
type RenderStore = {
|
|
21
16
|
projectName: string;
|
|
22
17
|
setProjectName: (name: string) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Root node tree being edited (a.k.a. "editor data").
|
|
20
|
+
* Not persisted: it comes from the currently opened project.
|
|
21
|
+
*/
|
|
22
|
+
editorData: Node | null;
|
|
23
|
+
setEditorData: (data: Node | null) => void;
|
|
23
24
|
copiedNode: Node | null;
|
|
24
25
|
setCopiedNode: (node: Node | null) => void;
|
|
25
26
|
current: Node | null;
|
|
@@ -66,6 +67,8 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
|
|
|
66
67
|
(set) => ({
|
|
67
68
|
projectName: '',
|
|
68
69
|
setProjectName: (name) => set({ projectName: name }),
|
|
70
|
+
editorData: null,
|
|
71
|
+
setEditorData: (data) => set({ editorData: data, current: data }),
|
|
69
72
|
copiedNode: null,
|
|
70
73
|
setCopiedNode: (node) => set({ copiedNode: node }),
|
|
71
74
|
current: null,
|
|
@@ -193,6 +196,7 @@ export const useRenderStore = createWithEqualityFn<RenderStore>()(
|
|
|
193
196
|
name: 'render-store',
|
|
194
197
|
partialize: (state) => ({
|
|
195
198
|
// Explicitly DO NOT persist projectName (it comes from ProjectPage props)
|
|
199
|
+
// Explicitly DO NOT persist editorData/current (they come from the opened project)
|
|
196
200
|
copiedNode: state.copiedNode ?? null,
|
|
197
201
|
logLevel: state.logLevel,
|
|
198
202
|
products: state.products,
|
|
@@ -165,6 +165,7 @@ body,
|
|
|
165
165
|
display: flex;
|
|
166
166
|
align-items: center;
|
|
167
167
|
gap: sizes.$spaceCozy;
|
|
168
|
+
flex-wrap: wrap;
|
|
168
169
|
font-size: 12px;
|
|
169
170
|
color: colors.$mutedTextColor;
|
|
170
171
|
}
|
|
@@ -200,11 +201,15 @@ body,
|
|
|
200
201
|
display: flex;
|
|
201
202
|
align-items: center;
|
|
202
203
|
gap: sizes.$spaceCompact;
|
|
204
|
+
flex-wrap: wrap;
|
|
205
|
+
row-gap: sizes.$spaceTight;
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
.breadcrumb__item {
|
|
206
209
|
display: inline-flex;
|
|
207
210
|
align-items: center;
|
|
211
|
+
max-width: 100%;
|
|
212
|
+
overflow-wrap: anywhere;
|
|
208
213
|
&--clickable {
|
|
209
214
|
cursor: pointer;
|
|
210
215
|
}
|
|
@@ -212,7 +217,7 @@ body,
|
|
|
212
217
|
|
|
213
218
|
.breadcrumb__separator {
|
|
214
219
|
color: colors.$borderColor;
|
|
215
|
-
margin: 0
|
|
220
|
+
margin: 0;
|
|
216
221
|
}
|
|
217
222
|
|
|
218
223
|
.breadcrumb__link {
|
|
@@ -384,9 +389,6 @@ body,
|
|
|
384
389
|
top: sizes.$spaceCozy;
|
|
385
390
|
z-index: 5;
|
|
386
391
|
padding-bottom: sizes.$spaceCozy;
|
|
387
|
-
.side-tool {
|
|
388
|
-
width: 100%;
|
|
389
|
-
}
|
|
390
392
|
}
|
|
391
393
|
}
|
|
392
394
|
.screen-preview {
|
|
@@ -395,6 +397,12 @@ body,
|
|
|
395
397
|
height: 100%;
|
|
396
398
|
}
|
|
397
399
|
|
|
400
|
+
// Custom blue/glow cursor while preview mode is enabled
|
|
401
|
+
.screen-preview--preview {
|
|
402
|
+
cursor: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='32'%20height='32'%20viewBox='0%200%2032%2032'%3E%3Cdefs%3E%3Cfilter%20id='g'%20x='-50%25'%20y='-50%25'%20width='200%25'%20height='200%25'%3E%3CfeDropShadow%20dx='0'%20dy='0'%20stdDeviation='1.6'%20flood-color='%2360a5fa'%20flood-opacity='0.95'/%3E%3C/filter%3E%3C/defs%3E%3Cpath%20filter='url(%23g)'%20d='M6%202%20L6%2022%20L11%2017%20L15%2027%20L18%2026%20L14%2016%20L21%2016%20Z'%20fill='%2360a5fa'%20stroke='%231d4ed8'%20stroke-width='1'/%3E%3C/svg%3E") 6 2,
|
|
403
|
+
pointer;
|
|
404
|
+
}
|
|
405
|
+
|
|
398
406
|
/* ProjectPage loading overlay */
|
|
399
407
|
.rb-loading-overlay {
|
|
400
408
|
position: absolute;
|
|
@@ -190,8 +190,16 @@
|
|
|
190
190
|
border-radius: sizes.$radiusSoft;
|
|
191
191
|
font-size: sizes.$fontSizeSm;
|
|
192
192
|
line-height: 1.4;
|
|
193
|
-
|
|
193
|
+
// Prevent long descriptions (e.g. URLs or long tokens) from overflowing the tooltip bubble.
|
|
194
|
+
width: min(
|
|
195
|
+
sizes.$dimensionSizeGridMax,
|
|
196
|
+
calc(100vw - (2 * sizes.$spaceComfy))
|
|
197
|
+
);
|
|
194
198
|
text-align: left;
|
|
199
|
+
white-space: normal;
|
|
200
|
+
overflow-wrap: anywhere;
|
|
201
|
+
word-break: break-word;
|
|
202
|
+
hyphens: auto;
|
|
195
203
|
box-shadow: sizes.$shadowTooltip;
|
|
196
204
|
z-index: sizes.$zIndexRaised;
|
|
197
205
|
opacity: 0;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
@use '../foundation/colors';
|
|
2
|
+
|
|
3
|
+
.rb-bottom-bar {
|
|
4
|
+
position: fixed;
|
|
5
|
+
left: 50%;
|
|
6
|
+
transform: translateX(-66%);
|
|
7
|
+
bottom: 10px;
|
|
8
|
+
|
|
9
|
+
height: 50px;
|
|
10
|
+
border-radius: 24px;
|
|
11
|
+
|
|
12
|
+
background-color: #fff;
|
|
13
|
+
color: colors.$textColor;
|
|
14
|
+
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: row;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: flex-start;
|
|
19
|
+
|
|
20
|
+
padding: 0 10px;
|
|
21
|
+
gap: 10px;
|
|
22
|
+
|
|
23
|
+
border: 1px solid rgba(colors.$textColor, 0.06);
|
|
24
|
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.rb-bottom-bar__button {
|
|
28
|
+
appearance: none;
|
|
29
|
+
border: none;
|
|
30
|
+
background: transparent;
|
|
31
|
+
padding: 8px;
|
|
32
|
+
/* lighter icon color */
|
|
33
|
+
color: colors.$mutedTextColor;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.rb-bottom-bar__button.is-active {
|
|
42
|
+
color: colors.$accentColor;
|
|
43
|
+
background: rgba(colors.$accentColor, 0.08);
|
|
44
|
+
box-shadow: 0 0 18px rgba(colors.$accentColor, 0.28);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.rb-bottom-bar__button--preview.is-active {
|
|
48
|
+
// make preview cursor extra obvious
|
|
49
|
+
box-shadow:
|
|
50
|
+
0 0 0 3px rgba(colors.$accentColor, 0.18),
|
|
51
|
+
0 0 26px rgba(colors.$accentColor, 0.35);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.rb-bottom-bar__button:focus-visible {
|
|
55
|
+
outline: none;
|
|
56
|
+
box-shadow:
|
|
57
|
+
0 0 0 3px rgba(colors.$accentColor, 0.18),
|
|
58
|
+
0 0 18px rgba(colors.$accentColor, 0.22);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* border between each tool button */
|
|
62
|
+
.rb-bottom-bar__button + .rb-bottom-bar__button {
|
|
63
|
+
border-left: 1px solid rgba(colors.$textColor, 0.06);
|
|
64
|
+
padding-left: 14px;
|
|
65
|
+
margin-left: 4px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.rb-bottom-bar__button--rtl {
|
|
69
|
+
gap: 6px;
|
|
70
|
+
padding-right: 10px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.rb-bottom-bar__rtl-text {
|
|
74
|
+
font-size: 12px;
|
|
75
|
+
font-weight: 700;
|
|
76
|
+
letter-spacing: 0.3px;
|
|
77
|
+
color: colors.$textColor;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.rb-bottom-bar__spacer {
|
|
81
|
+
flex: 1 1 auto;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.rb-bottom-bar__langs {
|
|
85
|
+
display: inline-flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 0;
|
|
88
|
+
color: colors.$textColor;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.rb-bottom-bar__lang {
|
|
92
|
+
appearance: none;
|
|
93
|
+
border: none;
|
|
94
|
+
background: transparent;
|
|
95
|
+
padding: 6px 6px;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
font-weight: 700;
|
|
99
|
+
text-transform: lowercase;
|
|
100
|
+
color: inherit;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.rb-bottom-bar__lang.is-active {
|
|
104
|
+
color: colors.$accentColor;
|
|
105
|
+
text-shadow: 0 0 12px rgba(colors.$accentColor, 0.25);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* border between each language button */
|
|
109
|
+
.rb-bottom-bar__lang + .rb-bottom-bar__lang {
|
|
110
|
+
border-left: 1px solid rgba(colors.$textColor, 0.06);
|
|
111
|
+
margin-left: 8px;
|
|
112
|
+
padding-left: 14px;
|
|
113
|
+
}
|