@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.
Files changed (66) hide show
  1. package/dist/build-components/index.d.ts +1 -2
  2. package/dist/build-components/patterns.generated.d.ts +56 -439
  3. package/dist/components/AttributesEditorPanel.d.ts +2 -2
  4. package/dist/components/BottomBar.d.ts +8 -0
  5. package/dist/components/Checkbox.d.ts +1 -1
  6. package/dist/components/LoadingComponent.d.ts +1 -0
  7. package/dist/components/MobilePanelToggleButton.d.ts +8 -0
  8. package/dist/hooks/useMinimumDelay.d.ts +7 -0
  9. package/dist/hooks/useMobileEditorPanels.d.ts +12 -0
  10. package/dist/hooks/useSyncProjectPageStore.d.ts +15 -0
  11. package/dist/index.cjs.js +3 -3
  12. package/dist/index.cjs.js.map +1 -1
  13. package/dist/index.esm.js +3 -3
  14. package/dist/index.esm.js.map +1 -1
  15. package/dist/index.native.cjs.js +1 -1
  16. package/dist/index.native.cjs.js.map +1 -1
  17. package/dist/index.native.esm.js +4 -4
  18. package/dist/index.native.esm.js.map +1 -1
  19. package/dist/modals/ScreenColorsModal.d.ts +8 -0
  20. package/dist/modals/index.d.ts +1 -0
  21. package/dist/pages/tabs/BuilderPanel.d.ts +2 -2
  22. package/dist/store.d.ts +6 -0
  23. package/dist/styles.css +1 -1
  24. package/dist/utils/nodeTree.d.ts +5 -0
  25. package/package.json +1 -1
  26. package/src/RenderPage.tsx +4 -1
  27. package/src/assets/samples/carousel-sample.json +99 -81
  28. package/src/assets/samples/simple-1.json +8 -2
  29. package/src/assets/samples/simple-2.json +36 -9
  30. package/src/assets/samples/vpn-onboard-1.json +27 -23
  31. package/src/assets/samples/vpn-onboard-2.json +279 -275
  32. package/src/assets/samples/vpn-onboard-3.json +247 -246
  33. package/src/assets/samples/vpn-onboard-4.json +247 -246
  34. package/src/assets/samples/vpn-onboard-5.json +375 -369
  35. package/src/assets/samples/vpn-onboard-6.json +252 -248
  36. package/src/build-components/RenderNode.generated.tsx +0 -7
  37. package/src/build-components/View/pattern.json +2 -2
  38. package/src/build-components/index.ts +0 -5
  39. package/src/build-components/patterns.generated.ts +56 -455
  40. package/src/components/AttributesEditorPanel.tsx +12 -8
  41. package/src/components/BottomBar.tsx +236 -0
  42. package/src/components/EditorHeader.tsx +11 -4
  43. package/src/components/LoadingComponent.tsx +10 -0
  44. package/src/components/MobilePanelToggleButton.tsx +39 -0
  45. package/src/hooks/useMinimumDelay.ts +20 -0
  46. package/src/hooks/useMobileEditorPanels.ts +56 -0
  47. package/src/hooks/useSyncProjectPageStore.ts +40 -0
  48. package/src/modals/ScreenColorsModal.tsx +115 -0
  49. package/src/modals/index.ts +1 -0
  50. package/src/pages/ProjectPage.tsx +53 -243
  51. package/src/pages/tabs/BuilderPanel.tsx +14 -8
  52. package/src/store.ts +10 -6
  53. package/src/styles/base/_global.scss +12 -4
  54. package/src/styles/components/_attributes-editor.scss +9 -1
  55. package/src/styles/components/_bottom-bar.scss +113 -0
  56. package/src/styles/components/_editor-shell.scss +0 -19
  57. package/src/styles/index.scss +1 -0
  58. package/src/utils/analyseNodeByPatterns.ts +15 -0
  59. package/src/utils/nodeTree.ts +99 -0
  60. package/dist/build-components/PaywallSubscriButton/PaywallSubscriButton.d.ts +0 -5
  61. package/dist/build-components/PaywallSubscriButton/PaywallSubscriButtonProps.generated.d.ts +0 -50
  62. package/dist/pages/tabs/SideTool.d.ts +0 -8
  63. package/src/build-components/PaywallSubscriButton/PaywallSubscriButton.tsx +0 -10
  64. package/src/build-components/PaywallSubscriButton/PaywallSubscriButtonProps.generated.ts +0 -77
  65. package/src/build-components/PaywallSubscriButton/pattern.json +0 -27
  66. 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: any;
9
- onChange: (data: Node) => void;
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((s) => ({
20
- current: s.current,
21
- setCurrent: s.setCurrent,
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 (current) copyNode(current);
73
+ if (effectiveCurrent) copyNode(effectiveCurrent);
68
74
  };
69
75
  const handlePaste = () => {
70
- if (!current || !editorData || !setEditorData) return;
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(editorData, current, cloned);
80
+ const updated = replaceNode(effectiveEditorData, effectiveCurrent, cloned);
74
81
  useRenderStore.setState({
75
82
  copiedNode: null,
76
83
  });
77
- setEditorData(updated);
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;
@@ -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';