@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
|
@@ -5,8 +5,8 @@ import { useLogRender } from '../utils/useLogRender';
|
|
|
5
5
|
import { useRenderStore } from '../store';
|
|
6
6
|
|
|
7
7
|
interface AttributesEditorPanelProps {
|
|
8
|
-
attributes
|
|
9
|
-
onChange
|
|
8
|
+
attributes?: any;
|
|
9
|
+
onChange?: (data: Node) => void;
|
|
10
10
|
projectColors?: ProjectColors;
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -16,10 +16,14 @@ export function AttributesEditorPanel({
|
|
|
16
16
|
projectColors,
|
|
17
17
|
}: AttributesEditorPanelProps) {
|
|
18
18
|
useLogRender('AttributesEditorPanel');
|
|
19
|
-
const { current, setCurrent } = useRenderStore(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const { current, setCurrent, editorData, setEditorData } = useRenderStore(
|
|
20
|
+
(s) => ({
|
|
21
|
+
current: s.current,
|
|
22
|
+
setCurrent: s.setCurrent,
|
|
23
|
+
editorData: s.editorData,
|
|
24
|
+
setEditorData: s.setEditorData,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
23
27
|
if (!current) return null;
|
|
24
28
|
|
|
25
29
|
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
@@ -49,9 +53,9 @@ export function AttributesEditorPanel({
|
|
|
49
53
|
return root;
|
|
50
54
|
}
|
|
51
55
|
const handleAttributesChange = (next: Node) => {
|
|
52
|
-
const root = attributes as Node;
|
|
56
|
+
const root = (attributes ?? editorData) as Node;
|
|
53
57
|
const updated = replaceNode(root, current, next);
|
|
54
|
-
onChange(updated);
|
|
58
|
+
(onChange ?? setEditorData)(updated);
|
|
55
59
|
setCurrent(next);
|
|
56
60
|
};
|
|
57
61
|
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { Icon } from './Icon.generated';
|
|
3
|
+
import type { IconsType } from '../types/Icons';
|
|
4
|
+
import { useRenderStore } from '../store';
|
|
5
|
+
import { Checkbox } from './Checkbox';
|
|
6
|
+
import type { Localication } from '../types/PreviewConfig';
|
|
7
|
+
import { LocalicationModal, Modal, ScreenColorsModal } from '../modals';
|
|
8
|
+
import { JsonTextEditor } from './JsonTextEditor';
|
|
9
|
+
import type { Node } from '../types/Node';
|
|
10
|
+
import { analyseAndProccess } from '../utils/analyseNode';
|
|
11
|
+
|
|
12
|
+
type BottomBarProps = {
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Empty placeholder bottom bar (Figma-like). We'll complete later.
|
|
18
|
+
*/
|
|
19
|
+
export function BottomBar({ className }: BottomBarProps) {
|
|
20
|
+
const rtlIcon: IconsType = 'translate';
|
|
21
|
+
const magicCursorIcon: IconsType = 'magicpen';
|
|
22
|
+
const debugIcon: IconsType = 'speedometer-03';
|
|
23
|
+
const localizationIcon: IconsType = 'globe-01';
|
|
24
|
+
const colorIcon: IconsType = 'colors';
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
appConfig,
|
|
28
|
+
setAppConfig,
|
|
29
|
+
previewMode,
|
|
30
|
+
setPreviewMode,
|
|
31
|
+
editorData,
|
|
32
|
+
setEditorData,
|
|
33
|
+
} = useRenderStore((s) => ({
|
|
34
|
+
appConfig: s.appConfig,
|
|
35
|
+
setAppConfig: s.setAppConfig,
|
|
36
|
+
previewMode: s.previewMode,
|
|
37
|
+
setPreviewMode: s.setPreviewMode,
|
|
38
|
+
editorData: s.editorData,
|
|
39
|
+
setEditorData: s.setEditorData,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const [isDebugOpen, setIsDebugOpen] = useState(false);
|
|
43
|
+
const [isLocalizationOpen, setIsLocalizationOpen] = useState(false);
|
|
44
|
+
const [isColorsOpen, setIsColorsOpen] = useState(false);
|
|
45
|
+
|
|
46
|
+
const languages = useMemo(() => ['en', 'tr', 'ar'], []);
|
|
47
|
+
const activeLanguage = appConfig.defaultLanguage ?? 'en';
|
|
48
|
+
|
|
49
|
+
const handleLocalicationChange = (data: Localication) => {
|
|
50
|
+
setAppConfig({ ...appConfig, localication: data });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const themeIsActive = appConfig.theme === 'dark';
|
|
54
|
+
const rtlIsActive = appConfig.isRtl ?? false;
|
|
55
|
+
const previewIsActive = previewMode;
|
|
56
|
+
const themeIcon: IconsType = themeIsActive ? 'moon-bold' : 'sun';
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<div className={['rb-bottom-bar', className].filter(Boolean).join(' ')}>
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
className={`rb-bottom-bar__button${themeIsActive ? ' is-active' : ''}`}
|
|
64
|
+
aria-label="Theme"
|
|
65
|
+
aria-pressed={themeIsActive}
|
|
66
|
+
onClick={() =>
|
|
67
|
+
setAppConfig({
|
|
68
|
+
...appConfig,
|
|
69
|
+
theme: appConfig.theme === 'dark' ? 'light' : 'dark',
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
<Icon iconType={themeIcon} size={20} color="currentColor" alt="" />
|
|
74
|
+
</button>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
className={`rb-bottom-bar__button rb-bottom-bar__button--rtl${rtlIsActive ? ' is-active' : ''}`}
|
|
78
|
+
aria-label="RTL"
|
|
79
|
+
aria-pressed={rtlIsActive}
|
|
80
|
+
onClick={() =>
|
|
81
|
+
setAppConfig({ ...appConfig, isRtl: !(appConfig.isRtl ?? false) })
|
|
82
|
+
}
|
|
83
|
+
>
|
|
84
|
+
<Icon iconType={rtlIcon} size={18} color="currentColor" alt="" />
|
|
85
|
+
<span className="rb-bottom-bar__rtl-text">RTL</span>
|
|
86
|
+
</button>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className={`rb-bottom-bar__button rb-bottom-bar__button--preview${previewIsActive ? ' is-active' : ''}`}
|
|
90
|
+
aria-label="Magic cursor tool"
|
|
91
|
+
aria-pressed={previewIsActive}
|
|
92
|
+
onClick={() => setPreviewMode(!previewMode)}
|
|
93
|
+
>
|
|
94
|
+
<Icon
|
|
95
|
+
iconType={magicCursorIcon}
|
|
96
|
+
size={20}
|
|
97
|
+
color="currentColor"
|
|
98
|
+
alt=""
|
|
99
|
+
/>
|
|
100
|
+
</button>
|
|
101
|
+
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
className={`rb-bottom-bar__button${isDebugOpen ? ' is-active' : ''}`}
|
|
105
|
+
aria-label="Debug"
|
|
106
|
+
aria-pressed={isDebugOpen}
|
|
107
|
+
onClick={() => setIsDebugOpen(true)}
|
|
108
|
+
>
|
|
109
|
+
<Icon iconType={debugIcon} size={20} color="currentColor" alt="" />
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
className={`rb-bottom-bar__button${isLocalizationOpen ? ' is-active' : ''}`}
|
|
114
|
+
aria-label="Localization"
|
|
115
|
+
aria-pressed={isLocalizationOpen}
|
|
116
|
+
onClick={() => setIsLocalizationOpen(true)}
|
|
117
|
+
>
|
|
118
|
+
<Icon
|
|
119
|
+
iconType={localizationIcon}
|
|
120
|
+
size={20}
|
|
121
|
+
color="currentColor"
|
|
122
|
+
alt=""
|
|
123
|
+
/>
|
|
124
|
+
</button>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
className={`rb-bottom-bar__button${isColorsOpen ? ' is-active' : ''}`}
|
|
128
|
+
aria-label="Color"
|
|
129
|
+
aria-pressed={isColorsOpen}
|
|
130
|
+
onClick={() => setIsColorsOpen(true)}
|
|
131
|
+
>
|
|
132
|
+
<Icon iconType={colorIcon} size={20} color="currentColor" alt="" />
|
|
133
|
+
</button>
|
|
134
|
+
|
|
135
|
+
<div className="rb-bottom-bar__spacer" />
|
|
136
|
+
|
|
137
|
+
<div className="rb-bottom-bar__langs" aria-label="Language">
|
|
138
|
+
{languages.map((language) => (
|
|
139
|
+
<button
|
|
140
|
+
key={language}
|
|
141
|
+
type="button"
|
|
142
|
+
className={`rb-bottom-bar__lang${activeLanguage === language ? ' is-active' : ''}`}
|
|
143
|
+
onClick={() =>
|
|
144
|
+
setAppConfig({ ...appConfig, defaultLanguage: language })
|
|
145
|
+
}
|
|
146
|
+
>
|
|
147
|
+
{language}
|
|
148
|
+
</button>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{isLocalizationOpen && (
|
|
154
|
+
<LocalicationModal
|
|
155
|
+
data={appConfig.localication ?? {}}
|
|
156
|
+
onChange={handleLocalicationChange}
|
|
157
|
+
onClose={() => setIsLocalizationOpen(false)}
|
|
158
|
+
/>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{isColorsOpen && (
|
|
162
|
+
<ScreenColorsModal
|
|
163
|
+
appConfig={appConfig}
|
|
164
|
+
onChange={setAppConfig}
|
|
165
|
+
onClose={() => setIsColorsOpen(false)}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{isDebugOpen && (
|
|
170
|
+
<Modal
|
|
171
|
+
onClose={() => setIsDebugOpen(false)}
|
|
172
|
+
ariaLabelledBy="debug-json-editor-title"
|
|
173
|
+
className="modal--large modal--scrollable"
|
|
174
|
+
contentClassName="localication-modal__content"
|
|
175
|
+
>
|
|
176
|
+
<div className="modal__header localication-modal__header">
|
|
177
|
+
<div className="localication-modal__header-main">
|
|
178
|
+
<h3 id="debug-json-editor-title" className="modal__title">
|
|
179
|
+
Debug JSON
|
|
180
|
+
</h3>
|
|
181
|
+
<p className="localication-modal__description">
|
|
182
|
+
Inspect and edit raw node JSON.
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
className="editor-button"
|
|
188
|
+
onClick={() => setIsDebugOpen(false)}
|
|
189
|
+
>
|
|
190
|
+
Close
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="localication-modal__body">
|
|
194
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
195
|
+
<Checkbox
|
|
196
|
+
label="Preview mode"
|
|
197
|
+
checked={previewMode}
|
|
198
|
+
onChange={setPreviewMode}
|
|
199
|
+
/>
|
|
200
|
+
<Checkbox
|
|
201
|
+
label="Dark Mode"
|
|
202
|
+
checked={appConfig.theme === 'dark'}
|
|
203
|
+
onChange={(checked) =>
|
|
204
|
+
setAppConfig({
|
|
205
|
+
...appConfig,
|
|
206
|
+
theme: checked ? 'dark' : 'light',
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
/>
|
|
210
|
+
<Checkbox
|
|
211
|
+
label="Is RTL"
|
|
212
|
+
checked={appConfig.isRtl ?? false}
|
|
213
|
+
onChange={(checked) =>
|
|
214
|
+
setAppConfig({ ...appConfig, isRtl: checked })
|
|
215
|
+
}
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
<div
|
|
219
|
+
className="localication-modal__editor"
|
|
220
|
+
style={{ marginTop: 12 }}
|
|
221
|
+
>
|
|
222
|
+
<JsonTextEditor
|
|
223
|
+
rootName="node"
|
|
224
|
+
value={editorData ?? {}}
|
|
225
|
+
onChange={(next) =>
|
|
226
|
+
setEditorData(analyseAndProccess(next as Node) as Node)
|
|
227
|
+
}
|
|
228
|
+
className="localication-modal__json-editor"
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</Modal>
|
|
233
|
+
)}
|
|
234
|
+
</>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
@@ -27,6 +27,9 @@ export function EditorHeader({
|
|
|
27
27
|
useLogRender('EditorHeader');
|
|
28
28
|
const [isDevicesModalOpen, setIsDevicesModalOpen] = useState(false);
|
|
29
29
|
const copiedNode = useRenderStore((s) => s.copiedNode);
|
|
30
|
+
const storeCurrent = useRenderStore((s) => s.current);
|
|
31
|
+
const storeEditorData = useRenderStore((s) => s.editorData);
|
|
32
|
+
const storeSetEditorData = useRenderStore((s) => s.setEditorData);
|
|
30
33
|
const {
|
|
31
34
|
device: selectedDevice,
|
|
32
35
|
setDevice,
|
|
@@ -36,6 +39,9 @@ export function EditorHeader({
|
|
|
36
39
|
setDevice: s.setDevice,
|
|
37
40
|
setCurrent: s.setCurrent,
|
|
38
41
|
}));
|
|
42
|
+
const effectiveCurrent = current ?? storeCurrent;
|
|
43
|
+
const effectiveEditorData = editorData ?? storeEditorData;
|
|
44
|
+
const effectiveSetEditorData = setEditorData ?? storeSetEditorData;
|
|
39
45
|
|
|
40
46
|
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
41
47
|
if (root === target) return next;
|
|
@@ -64,17 +70,18 @@ export function EditorHeader({
|
|
|
64
70
|
return root;
|
|
65
71
|
}
|
|
66
72
|
const handleCopy = () => {
|
|
67
|
-
if (
|
|
73
|
+
if (effectiveCurrent) copyNode(effectiveCurrent);
|
|
68
74
|
};
|
|
69
75
|
const handlePaste = () => {
|
|
70
|
-
if (!
|
|
76
|
+
if (!effectiveCurrent || !effectiveEditorData || !effectiveSetEditorData)
|
|
77
|
+
return;
|
|
71
78
|
if (!copiedNode) return;
|
|
72
79
|
const cloned = JSON.parse(JSON.stringify(copiedNode)) as Node;
|
|
73
|
-
const updated = replaceNode(
|
|
80
|
+
const updated = replaceNode(effectiveEditorData, effectiveCurrent, cloned);
|
|
74
81
|
useRenderStore.setState({
|
|
75
82
|
copiedNode: null,
|
|
76
83
|
});
|
|
77
|
-
|
|
84
|
+
effectiveSetEditorData(updated);
|
|
78
85
|
//TODO: current and editor must be sync!! and tested more
|
|
79
86
|
// Important: selection is stored by reference. After replacing `current` in the tree,
|
|
80
87
|
// we must point selection to the new (cloned) node reference to keep "current node"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import Lottie from 'lottie-react';
|
|
2
|
+
import loadingAnimation from '../assets/loading_animation.json';
|
|
3
|
+
|
|
4
|
+
export function LoadingComponent() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="rb-loading">
|
|
7
|
+
<Lottie animationData={loadingAnimation as any} loop autoplay />
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type MobilePanelToggleButtonProps = {
|
|
2
|
+
label: string;
|
|
3
|
+
isActive: boolean;
|
|
4
|
+
ariaLabel: string;
|
|
5
|
+
ariaControls: string;
|
|
6
|
+
onClick: () => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function MobilePanelToggleButton({
|
|
10
|
+
label,
|
|
11
|
+
isActive,
|
|
12
|
+
ariaLabel,
|
|
13
|
+
ariaControls,
|
|
14
|
+
onClick,
|
|
15
|
+
}: MobilePanelToggleButtonProps) {
|
|
16
|
+
return (
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
className={`mobile-panel-toggle__button${isActive ? ' mobile-panel-toggle__button--active' : ''}`}
|
|
20
|
+
aria-label={ariaLabel}
|
|
21
|
+
aria-expanded={isActive}
|
|
22
|
+
aria-controls={ariaControls}
|
|
23
|
+
onClick={onClick}
|
|
24
|
+
>
|
|
25
|
+
<span className="mobile-panel-toggle__icon" aria-hidden="true">
|
|
26
|
+
<svg viewBox="0 0 16 12" role="presentation" focusable="false">
|
|
27
|
+
<path
|
|
28
|
+
d="M1 1h14M1 6h14M1 11h14"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
strokeWidth="2"
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
fill="none"
|
|
33
|
+
/>
|
|
34
|
+
</svg>
|
|
35
|
+
</span>
|
|
36
|
+
<span className="mobile-panel-toggle__label">{label}</span>
|
|
37
|
+
</button>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures a boolean becomes `true` only after a minimum delay.
|
|
5
|
+
* Resets to `false` whenever `deps` change.
|
|
6
|
+
*
|
|
7
|
+
* Common use-case: keep a loading indicator visible for at least N ms.
|
|
8
|
+
*/
|
|
9
|
+
export function useMinimumDelay(delayMs: number, deps: unknown[] = []) {
|
|
10
|
+
const [done, setDone] = useState<boolean>(false);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
setDone(false);
|
|
14
|
+
const timer = setTimeout(() => setDone(true), delayMs);
|
|
15
|
+
return () => clearTimeout(timer);
|
|
16
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17
|
+
}, [delayMs, ...deps]);
|
|
18
|
+
|
|
19
|
+
return done;
|
|
20
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export type MobileEditorPanel = 'builder' | 'attributes';
|
|
4
|
+
|
|
5
|
+
export type UseMobileEditorPanelsOptions = {
|
|
6
|
+
breakpoint?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function useMobileEditorPanels(
|
|
10
|
+
options: UseMobileEditorPanelsOptions = {},
|
|
11
|
+
) {
|
|
12
|
+
const { breakpoint = 1000 } = options;
|
|
13
|
+
|
|
14
|
+
const [mobilePanel, setMobilePanel] = useState<MobileEditorPanel | null>(
|
|
15
|
+
null,
|
|
16
|
+
);
|
|
17
|
+
const [isMobile, setIsMobile] = useState<boolean>(() => {
|
|
18
|
+
if (typeof window === 'undefined') return false;
|
|
19
|
+
return window.innerWidth <= breakpoint;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
function handleResize() {
|
|
24
|
+
setIsMobile(window.innerWidth <= breakpoint);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
handleResize();
|
|
28
|
+
window.addEventListener('resize', handleResize);
|
|
29
|
+
return () => window.removeEventListener('resize', handleResize);
|
|
30
|
+
}, [breakpoint]);
|
|
31
|
+
|
|
32
|
+
// Reset active panel when switching between mobile/desktop layouts
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setMobilePanel(null);
|
|
35
|
+
}, [isMobile]);
|
|
36
|
+
|
|
37
|
+
const toggleMobilePanel = useCallback((panel: MobileEditorPanel) => {
|
|
38
|
+
setMobilePanel((prev) => (prev === panel ? null : panel));
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const closeMobilePanels = useCallback(() => {
|
|
42
|
+
setMobilePanel(null);
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const leftPanelIsOpen = !isMobile || mobilePanel === 'builder';
|
|
46
|
+
const attributesPanelIsOpen = !isMobile || mobilePanel === 'attributes';
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
isMobile,
|
|
50
|
+
mobilePanel,
|
|
51
|
+
toggleMobilePanel,
|
|
52
|
+
closeMobilePanels,
|
|
53
|
+
leftPanelIsOpen,
|
|
54
|
+
attributesPanelIsOpen,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import type { AppConfig } from '../types/PreviewConfig';
|
|
3
|
+
import type { ProjectColors } from '../types/Project';
|
|
4
|
+
import { logger } from '../utils/logger';
|
|
5
|
+
|
|
6
|
+
export type UseSyncProjectPageStoreArgs = {
|
|
7
|
+
appConfig: AppConfig;
|
|
8
|
+
name: string;
|
|
9
|
+
projectColors: ProjectColors | undefined;
|
|
10
|
+
setAppConfig: (appConfig: AppConfig) => void;
|
|
11
|
+
setProjectName: (name: string) => void;
|
|
12
|
+
setProjectColors: (colors: ProjectColors | undefined) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Syncs ProjectPage props into the render store.
|
|
17
|
+
* Kept as a small hook to keep ProjectPage UI-focused.
|
|
18
|
+
*/
|
|
19
|
+
export function useSyncProjectPageStore({
|
|
20
|
+
appConfig,
|
|
21
|
+
name,
|
|
22
|
+
projectColors,
|
|
23
|
+
setAppConfig,
|
|
24
|
+
setProjectName,
|
|
25
|
+
setProjectColors,
|
|
26
|
+
}: UseSyncProjectPageStoreArgs) {
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setAppConfig(appConfig);
|
|
29
|
+
logger.verbose('ProjectPage', 'appConfig applied', appConfig);
|
|
30
|
+
}, [appConfig, setAppConfig]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
setProjectName(name);
|
|
34
|
+
}, [name, setProjectName]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
setProjectColors(projectColors);
|
|
38
|
+
return () => setProjectColors(undefined);
|
|
39
|
+
}, [projectColors, setProjectColors]);
|
|
40
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AppConfig } from '../types/PreviewConfig';
|
|
3
|
+
import Modal from './Modal';
|
|
4
|
+
|
|
5
|
+
const screenStyleDefaults = {
|
|
6
|
+
light: { backgroundColor: '#FDFDFD', color: '#161827' },
|
|
7
|
+
dark: { backgroundColor: '#12131A', color: '#E9EBF9' },
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
type ScreenMode = keyof typeof screenStyleDefaults;
|
|
11
|
+
type ScreenColorKey = keyof (typeof screenStyleDefaults)['light'];
|
|
12
|
+
|
|
13
|
+
const colorFields = [
|
|
14
|
+
{
|
|
15
|
+
id: 'light-bg',
|
|
16
|
+
label: 'Light Background Color',
|
|
17
|
+
mode: 'light' as ScreenMode,
|
|
18
|
+
key: 'backgroundColor' as ScreenColorKey,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'light-color',
|
|
22
|
+
label: 'Light Color',
|
|
23
|
+
mode: 'light' as ScreenMode,
|
|
24
|
+
key: 'color' as ScreenColorKey,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'dark-bg',
|
|
28
|
+
label: 'Dark Background Color',
|
|
29
|
+
mode: 'dark' as ScreenMode,
|
|
30
|
+
key: 'backgroundColor' as ScreenColorKey,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'dark-color',
|
|
34
|
+
label: 'Dark Color',
|
|
35
|
+
mode: 'dark' as ScreenMode,
|
|
36
|
+
key: 'color' as ScreenColorKey,
|
|
37
|
+
},
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
type ScreenColorsModalProps = {
|
|
41
|
+
appConfig: AppConfig;
|
|
42
|
+
onChange: (next: AppConfig) => void;
|
|
43
|
+
onClose: () => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function ScreenColorsModal({
|
|
47
|
+
appConfig,
|
|
48
|
+
onChange,
|
|
49
|
+
onClose,
|
|
50
|
+
}: ScreenColorsModalProps) {
|
|
51
|
+
const getScreenColorValue = (mode: ScreenMode, key: ScreenColorKey) =>
|
|
52
|
+
appConfig.screenStyle?.[mode]?.[key] ?? screenStyleDefaults[mode][key];
|
|
53
|
+
|
|
54
|
+
const handleScreenStyleChange = (
|
|
55
|
+
mode: ScreenMode,
|
|
56
|
+
key: ScreenColorKey,
|
|
57
|
+
value: string,
|
|
58
|
+
) => {
|
|
59
|
+
onChange({
|
|
60
|
+
...appConfig,
|
|
61
|
+
screenStyle: {
|
|
62
|
+
...screenStyleDefaults,
|
|
63
|
+
...appConfig.screenStyle,
|
|
64
|
+
[mode]: {
|
|
65
|
+
...screenStyleDefaults[mode],
|
|
66
|
+
...appConfig.screenStyle?.[mode],
|
|
67
|
+
[key]: value,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Modal
|
|
75
|
+
onClose={onClose}
|
|
76
|
+
ariaLabelledBy="screen-colors-modal-title"
|
|
77
|
+
className="modal--large modal--scrollable"
|
|
78
|
+
>
|
|
79
|
+
<div className="modal__header">
|
|
80
|
+
<h3 id="screen-colors-modal-title" className="modal__title">
|
|
81
|
+
Screen Colors
|
|
82
|
+
</h3>
|
|
83
|
+
<button type="button" className="editor-button" onClick={onClose}>
|
|
84
|
+
Close
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
<div className="modal__body">
|
|
88
|
+
<div
|
|
89
|
+
style={{
|
|
90
|
+
display: 'grid',
|
|
91
|
+
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
|
|
92
|
+
gap: 12,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{colorFields.map(({ id, label, mode, key }) => (
|
|
96
|
+
<React.Fragment key={id}>
|
|
97
|
+
<div>{label}</div>
|
|
98
|
+
<input
|
|
99
|
+
id={id}
|
|
100
|
+
type="color"
|
|
101
|
+
className="input input--color"
|
|
102
|
+
value={getScreenColorValue(mode, key)}
|
|
103
|
+
onChange={(e) =>
|
|
104
|
+
handleScreenStyleChange(mode, key, e.target.value)
|
|
105
|
+
}
|
|
106
|
+
/>
|
|
107
|
+
</React.Fragment>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</Modal>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default ScreenColorsModal;
|
package/src/modals/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { DeviceSelectorModal } from './DeviceSelectorModal';
|
|
|
4
4
|
export { ColorModal } from './ColorModal';
|
|
5
5
|
export { IconPickerModal } from './IconPickerModal';
|
|
6
6
|
export { LocalicationModal } from './LocalicationModal';
|
|
7
|
+
export { ScreenColorsModal } from './ScreenColorsModal';
|
|
7
8
|
export { MockableFeatureModal } from './MockableFeatureModal';
|
|
8
9
|
export { ProductEditModal } from './ProductEditModal';
|
|
9
10
|
export { ProductPresetsModal } from './ProductPresetsModal';
|