@developer_tribe/react-builder 0.1.32 → 1.0.1

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 (86) hide show
  1. package/dist/DeviceMockFrame.d.ts +1 -17
  2. package/dist/RenderPage.d.ts +1 -9
  3. package/dist/build-components/index.d.ts +1 -0
  4. package/dist/components/AttributesEditorPanel.d.ts +9 -0
  5. package/dist/components/Breadcrumb.d.ts +13 -0
  6. package/dist/components/Builder.d.ts +9 -0
  7. package/dist/components/EditorHeader.d.ts +15 -0
  8. package/dist/index.cjs.js +6 -5
  9. package/dist/index.cjs.js.map +1 -0
  10. package/dist/index.d.ts +8 -4
  11. package/dist/index.esm.js +6 -5
  12. package/dist/index.esm.js.map +1 -0
  13. package/dist/pages/ProjectPage.d.ts +11 -0
  14. package/dist/pages/tabs/BuilderTab.d.ts +9 -0
  15. package/dist/pages/tabs/DebugTab.d.ts +7 -0
  16. package/dist/pages/tabs/PreviewTab.d.ts +3 -0
  17. package/dist/store.d.ts +17 -18
  18. package/dist/styles.css +1 -1
  19. package/dist/types/PreviewConfig.d.ts +6 -3
  20. package/dist/types/Project.d.ts +12 -2
  21. package/dist/utils/copyNode.d.ts +2 -0
  22. package/dist/utils/logger.d.ts +11 -0
  23. package/dist/utils/useLogRender.d.ts +1 -0
  24. package/package.json +16 -9
  25. package/scripts/prebuild/utils/createBuildComponentsIndex.js +15 -1
  26. package/src/AttributesEditor.tsx +2 -0
  27. package/src/DeviceMockFrame.tsx +22 -31
  28. package/src/RenderPage.tsx +5 -42
  29. package/src/assets/images/android.svg +43 -0
  30. package/src/assets/images/apple.svg +16 -0
  31. package/src/assets/images/background.jpg +0 -0
  32. package/src/assets/samples/carousel-sample.json +2 -3
  33. package/src/assets/samples/getSamples.ts +49 -12
  34. package/src/assets/samples/simple-1.json +1 -2
  35. package/src/assets/samples/simple-2.json +1 -2
  36. package/src/assets/samples/vpn-onboard-1.json +1 -2
  37. package/src/assets/samples/vpn-onboard-2.json +1 -2
  38. package/src/assets/samples/vpn-onboard-3.json +1 -2
  39. package/src/assets/samples/vpn-onboard-4.json +1 -2
  40. package/src/assets/samples/vpn-onboard-5.json +1 -2
  41. package/src/assets/samples/vpn-onboard-6.json +1 -2
  42. package/src/build-components/Button/Button.tsx +2 -0
  43. package/src/build-components/Carousel/Carousel.tsx +2 -0
  44. package/src/build-components/CarouselButtons/CarouselButtons.tsx +2 -0
  45. package/src/build-components/CarouselDots/CarouselDots.tsx +2 -0
  46. package/src/build-components/CarouselItem/CarouselItem.tsx +2 -0
  47. package/src/build-components/Image/Image.tsx +2 -0
  48. package/src/build-components/Onboard/Onboard.tsx +2 -0
  49. package/src/build-components/OnboardButton/OnboardButton.tsx +7 -4
  50. package/src/build-components/OnboardButtons/OnboardButtons.tsx +7 -7
  51. package/src/build-components/OnboardDot/OnboardDot.tsx +2 -0
  52. package/src/build-components/OnboardFooter/OnboardFooter.tsx +5 -3
  53. package/src/build-components/OnboardImage/OnboardImage.tsx +2 -0
  54. package/src/build-components/OnboardItem/OnboardItem.tsx +2 -0
  55. package/src/build-components/OnboardProvider/OnboardProvider.tsx +2 -0
  56. package/src/build-components/OnboardSubtitle/OnboardSubtitle.tsx +2 -0
  57. package/src/build-components/OnboardTitle/OnboardTitle.tsx +2 -0
  58. package/src/build-components/Text/Text.tsx +5 -3
  59. package/src/build-components/View/View.tsx +2 -0
  60. package/src/build-components/index.ts +22 -0
  61. package/src/components/AttributesEditorPanel.tsx +112 -0
  62. package/src/components/Breadcrumb.tsx +48 -0
  63. package/src/components/Builder.tsx +272 -0
  64. package/src/components/EditorHeader.tsx +186 -0
  65. package/src/index.ts +8 -4
  66. package/src/pages/ProjectPage.tsx +152 -0
  67. package/src/pages/tabs/BuilderTab.tsx +33 -0
  68. package/src/pages/tabs/DebugTab.tsx +23 -0
  69. package/src/pages/tabs/PreviewTab.tsx +194 -0
  70. package/src/size-matters/index.ts +5 -1
  71. package/src/store.ts +60 -38
  72. package/src/styles/_mixins.scss +21 -0
  73. package/src/styles/_variables.scss +27 -0
  74. package/src/styles/builder.scss +60 -0
  75. package/src/styles/components.scss +88 -0
  76. package/src/styles/editor.scss +174 -0
  77. package/src/styles/global.scss +200 -0
  78. package/src/styles/index.scss +7 -0
  79. package/src/styles/pages.scss +2 -0
  80. package/src/types/PreviewConfig.ts +14 -5
  81. package/src/types/Project.ts +15 -2
  82. package/src/utils/copyNode.ts +7 -0
  83. package/src/utils/extractTextStyle.ts +4 -2
  84. package/src/utils/getDevices.ts +1 -0
  85. package/src/utils/logger.ts +76 -0
  86. package/src/utils/useLogRender.ts +13 -0
@@ -0,0 +1,194 @@
1
+ import { JsonEditor } from 'json-edit-react';
2
+ import { Localication, TargetedScreenSize } from '../..';
3
+ import { useLogRender } from '../../utils/useLogRender';
4
+ import { useRenderStore } from '../../store';
5
+
6
+ type PreviewTabProps = {};
7
+
8
+ export function PreviewTab({}: PreviewTabProps) {
9
+ useLogRender('PreviewTab');
10
+ const { appConfig, setAppConfig } = useRenderStore((s) => ({
11
+ appConfig: s.appConfig,
12
+ setAppConfig: s.setAppConfig,
13
+ }));
14
+ return (
15
+ <div
16
+ role="tabpanel"
17
+ className="editor-panel editor-panel--active"
18
+ aria-hidden={false}
19
+ >
20
+ <div style={{ padding: 12 }}>
21
+ <div
22
+ style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}
23
+ >
24
+ <div>Default Language</div>
25
+ <select
26
+ value={appConfig.defaultLanguage ?? 'en'}
27
+ onChange={(e) => {
28
+ setAppConfig({
29
+ ...appConfig,
30
+ defaultLanguage: e.target.value,
31
+ });
32
+ }}
33
+ >
34
+ {Object.keys(appConfig.localication ?? {}).map((language) => (
35
+ <option key={language} value={language}>
36
+ {language}
37
+ </option>
38
+ ))}
39
+ </select>
40
+ </div>
41
+ <div
42
+ style={{
43
+ display: 'grid',
44
+ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
45
+ gap: 12,
46
+ marginTop: 8,
47
+ }}
48
+ >
49
+ <div>Light Background Color</div>
50
+ <input
51
+ type="color"
52
+ value={appConfig.screenStyle?.light.backgroundColor ?? '#FDFDFD'}
53
+ onChange={(e) => {
54
+ const next = {
55
+ ...appConfig.screenStyle,
56
+ light: {
57
+ ...(appConfig.screenStyle?.light ?? {
58
+ color: '#161827',
59
+ }),
60
+ backgroundColor: e.target.value,
61
+ },
62
+ };
63
+
64
+ setAppConfig({
65
+ ...appConfig,
66
+ screenStyle: next,
67
+ });
68
+ }}
69
+ className="input input--color"
70
+ />
71
+ <div>Light Color</div>
72
+ <input
73
+ type="color"
74
+ value={appConfig.screenStyle?.light.color ?? '#161827'}
75
+ onChange={(e) => {
76
+ const next = {
77
+ ...appConfig.screenStyle,
78
+ light: {
79
+ ...(appConfig.screenStyle?.light ?? {
80
+ backgroundColor: '#FDFDFD',
81
+ }),
82
+ color: e.target.value,
83
+ },
84
+ };
85
+
86
+ setAppConfig({
87
+ ...appConfig,
88
+ screenStyle: next,
89
+ });
90
+ }}
91
+ className="input input--color"
92
+ />
93
+ <div>Dark Background Color</div>
94
+ <input
95
+ type="color"
96
+ value={appConfig.screenStyle?.dark.backgroundColor ?? '#12131A'}
97
+ onChange={(e) => {
98
+ const next = {
99
+ ...appConfig.screenStyle,
100
+ dark: {
101
+ ...(appConfig.screenStyle?.dark ?? {
102
+ color: '#E9EBF9',
103
+ }),
104
+ backgroundColor: e.target.value,
105
+ },
106
+ };
107
+
108
+ setAppConfig({
109
+ ...appConfig,
110
+ screenStyle: next,
111
+ });
112
+ }}
113
+ className="input input--color"
114
+ />
115
+ <div>Dark Color</div>
116
+ <input
117
+ type="color"
118
+ value={appConfig.screenStyle?.dark.color ?? '#E9EBF9'}
119
+ onChange={(e) => {
120
+ const next = {
121
+ ...appConfig.screenStyle,
122
+ dark: {
123
+ ...(appConfig.screenStyle?.dark ?? {
124
+ backgroundColor: '#12131A',
125
+ }),
126
+ color: e.target.value,
127
+ },
128
+ };
129
+ setAppConfig({
130
+ ...appConfig,
131
+ screenStyle: next,
132
+ });
133
+ }}
134
+ className="input input--color"
135
+ />
136
+ </div>
137
+ <div
138
+ style={{
139
+ display: 'flex',
140
+ alignItems: 'center',
141
+ gap: 8,
142
+ marginTop: 8,
143
+ }}
144
+ >
145
+ <div>Dark Mode</div>
146
+ <input
147
+ type="checkbox"
148
+ checked={appConfig.theme === 'dark'}
149
+ onChange={(e) => {
150
+ const nextTheme = e.target.checked ? 'dark' : 'light';
151
+
152
+ setAppConfig({
153
+ ...appConfig,
154
+ theme: nextTheme,
155
+ });
156
+ }}
157
+ />
158
+ </div>
159
+ <div
160
+ style={{
161
+ display: 'flex',
162
+ alignItems: 'center',
163
+ gap: 8,
164
+ marginTop: 8,
165
+ }}
166
+ >
167
+ <div>Is RTL</div>
168
+ <input
169
+ type="checkbox"
170
+ checked={appConfig.isRtl ?? false}
171
+ onChange={(e) => {
172
+ setAppConfig({
173
+ ...appConfig,
174
+ isRtl: e.target.checked,
175
+ });
176
+ }}
177
+ />
178
+ </div>
179
+ <div style={{ marginTop: 8 }}>
180
+ <JsonEditor
181
+ rootName="localication"
182
+ data={appConfig.localication ?? {}}
183
+ setData={(data) => {
184
+ setAppConfig({
185
+ ...appConfig,
186
+ localication: data as Localication,
187
+ });
188
+ }}
189
+ />
190
+ </div>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
@@ -8,7 +8,11 @@ function getBaseDimensions() {
8
8
  ? [device.width, device.height]
9
9
  : [device.height, device.width];
10
10
 
11
- return { baseSize: currentState.baseSize, shortDimension, longDimension };
11
+ return {
12
+ baseSize: currentState.appConfig.baseSize,
13
+ shortDimension,
14
+ longDimension,
15
+ };
12
16
  }
13
17
  export function scale(size: number) {
14
18
  const { baseSize, shortDimension } = getBaseDimensions();
package/src/store.ts CHANGED
@@ -1,56 +1,78 @@
1
1
  import { createWithEqualityFn } from 'zustand/traditional';
2
2
  import { shallow } from 'zustand/shallow';
3
3
  import type { Device } from './types/Device';
4
- import type { Localication } from './types/PreviewConfig';
4
+ import {
5
+ defaultAppConfig,
6
+ type AppConfig,
7
+ type Localication,
8
+ } from './types/PreviewConfig';
5
9
  import { getDefaultDevice } from './utils/getDevices';
6
10
  import { ScreenStyle } from './RenderPage';
11
+ import { createJSONStorage } from 'zustand/middleware';
12
+ import { Node } from './types/Node';
13
+ import type { LogEntry, LogLevel } from './types/Project';
7
14
 
8
15
  type RenderStore = {
16
+ copiedNode: Node | null;
17
+ setCopiedNode: (node: Node | null) => void;
9
18
  device: Device;
10
- localication: Localication | null;
11
- defaultLanguage?: string;
12
- baseSize: {
13
- width: number;
14
- height: number;
15
- };
16
- theme: 'dark' | 'light';
17
- screenStyle: ScreenStyle;
18
- setBaseSize: (baseSize: { width: number; height: number }) => void;
19
19
  setDevice: (device: Device) => void;
20
- setLocalication: (localication: Localication | null) => void;
21
- setDefaultLanguage: (defaultLanguage?: string) => void;
22
- setTheme: (theme: 'dark' | 'light') => void;
23
- setScreenStyle: (screenStyle: ScreenStyle) => void;
20
+ appConfig: AppConfig;
21
+ setAppConfig: (appConfig: AppConfig) => void;
22
+ renderCount: number;
23
+ forceRender: () => void;
24
+ // Logging
25
+ logs: LogEntry[];
26
+ logLevel: LogLevel;
27
+ setLogLevel: (level: LogLevel) => void;
28
+ addLog: (
29
+ entry: Omit<LogEntry, 'id' | 'timestamp'> & {
30
+ id?: string;
31
+ timestamp?: number;
32
+ },
33
+ ) => void;
34
+ clearLogs: () => void;
24
35
  };
25
36
 
26
37
  export const useRenderStore = createWithEqualityFn<RenderStore>()(
27
38
  (set) => ({
39
+ copiedNode: null,
40
+ setCopiedNode: (node) => set({ copiedNode: node }),
41
+ renderCount: 0,
42
+ forceRender: () => set((state) => ({ renderCount: state.renderCount + 1 })),
28
43
  device: getDefaultDevice(),
29
- localication: null,
30
- defaultLanguage: undefined,
31
- baseSize: {
32
- width: 390,
33
- height: 844,
34
- },
35
- theme: 'light',
36
- screenStyle: {
37
- light: {
38
- backgroundColor: '#FDFDFD',
39
- color: '#161827',
40
- seperatorColor: '#E5E7EB',
41
- },
42
- dark: {
43
- backgroundColor: '#12131A',
44
- color: '#E9EBF9',
45
- seperatorColor: '#E5E7EB',
46
- },
47
- },
48
- setBaseSize: (baseSize) => set({ baseSize }),
49
44
  setDevice: (device) => set({ device }),
50
- setLocalication: (localication) => set({ localication }),
51
- setDefaultLanguage: (defaultLanguage) => set({ defaultLanguage }),
52
- setTheme: (theme) => set({ theme }),
53
- setScreenStyle: (screenStyle) => set({ screenStyle }),
45
+ appConfig: defaultAppConfig,
46
+ setAppConfig: (appConfig) => set({ appConfig }),
47
+ // Logging defaults
48
+ logs: [],
49
+ logLevel: 'INFO',
50
+ setLogLevel: (level) => set({ logLevel: level }),
51
+ addLog: (entry) =>
52
+ set((state) => {
53
+ const now = Date.now();
54
+ const id =
55
+ entry.id ?? `${now}-${Math.random().toString(36).slice(2, 8)}`;
56
+ const timestamp = entry.timestamp ?? now;
57
+ const newEntry: LogEntry = {
58
+ id,
59
+ timestamp,
60
+ level: entry.level,
61
+ source: entry.source,
62
+ message: entry.message,
63
+ payload: entry.payload,
64
+ };
65
+ return { logs: [...state.logs, newEntry] };
66
+ }),
67
+ clearLogs: () => set({ logs: [] }),
68
+ persist: {
69
+ name: 'render-store',
70
+ partialize: (state: RenderStore) => ({
71
+ copiedNode: state.copiedNode ?? null,
72
+ logLevel: state.logLevel,
73
+ }),
74
+ storage: createJSONStorage(() => localStorage),
75
+ },
54
76
  }),
55
77
  shallow,
56
78
  );
@@ -0,0 +1,21 @@
1
+ @use './variables' as *;
2
+ @mixin center-content {
3
+ display: flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ }
7
+
8
+ @mixin card {
9
+ background: #fff;
10
+ border: 1px solid $color-border;
11
+ border-radius: $radius-xs;
12
+ }
13
+
14
+ @mixin thin-scrollbar {
15
+ scrollbar-width: thin;
16
+ scrollbar-color: rgba(0, 0, 0, 0.25) transparent;
17
+ &::-webkit-scrollbar {
18
+ width: 8px;
19
+ height: 4px;
20
+ }
21
+ }
@@ -0,0 +1,27 @@
1
+ // Color palette
2
+ $color-background: #f3f4f6;
3
+ $color-border: #e5e7eb;
4
+ $color-text: #111827;
5
+ $color-muted: #9ca3af;
6
+ $color-error: #ef4444;
7
+ $color-button: cornflowerblue;
8
+
9
+ // Spacing
10
+ $space-1: 4px;
11
+ $space-2: 8px;
12
+ $space-3: 12px;
13
+ $space-4: 16px;
14
+ $space-5: 20px;
15
+ $space-6: 24px;
16
+
17
+ // Radius & shadow
18
+ $radius-xs: 4px;
19
+ $radius-sm: 8px;
20
+ $radius-md: 12px;
21
+ $radius-lg: 24px;
22
+ $shadow-card: 0 10px 30px rgba(0, 0, 0, 0.12);
23
+
24
+ // Fonts
25
+ $font-sans:
26
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial,
27
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif;
@@ -0,0 +1,60 @@
1
+ @use './variables' as *;
2
+ @use './mixins' as *;
3
+
4
+ .builder {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: $space-3;
8
+ }
9
+
10
+ .builder__breadcrumbs {
11
+ display: flex;
12
+ flex-direction: row;
13
+ gap: $space-2;
14
+ }
15
+
16
+ .builder__breadcrumb {
17
+ color: $color-muted;
18
+ font-size: 12px;
19
+ }
20
+
21
+ .builder__current {
22
+ font-weight: 600;
23
+ }
24
+
25
+ .builder__list {
26
+ display: flex;
27
+ flex-wrap: wrap;
28
+ gap: $space-2;
29
+ }
30
+
31
+ .builder__button {
32
+ @include card;
33
+ padding: $space-3 $space-3;
34
+ font-size: 14px;
35
+ background: $color-button;
36
+ color: #fff;
37
+ cursor: pointer;
38
+ }
39
+
40
+ .builder__node {
41
+ @include card;
42
+ padding: $space-3;
43
+ }
44
+
45
+ .builder__node-type {
46
+ margin: 0 0 $space-2 0;
47
+ font-weight: 600;
48
+ }
49
+
50
+ .builder__children {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: $space-2;
54
+ }
55
+
56
+ .builder__text,
57
+ .builder__placeholder {
58
+ color: $color-muted;
59
+ font-size: 12px;
60
+ }
@@ -0,0 +1,88 @@
1
+ @use './variables' as *;
2
+ @use './mixins' as *;
3
+
4
+ .editor-controls {
5
+ display: grid;
6
+ grid-template-columns: auto 1fr;
7
+ gap: $space-3;
8
+ padding: $space-4;
9
+ }
10
+
11
+ .editor-section {
12
+ background: #fff;
13
+ border: 1px solid $color-border;
14
+ border-radius: $radius-md;
15
+ padding: $space-4;
16
+ }
17
+
18
+ .form-row {
19
+ display: grid;
20
+ grid-template-columns: 160px 1fr;
21
+ align-items: center;
22
+ gap: $space-3;
23
+ margin-bottom: $space-3;
24
+ }
25
+
26
+ .form-actions {
27
+ display: flex;
28
+ gap: $space-3;
29
+ margin-top: $space-4;
30
+ }
31
+
32
+ .btn {
33
+ padding: 8px 12px;
34
+ border-radius: 8px;
35
+ border: 1px solid $color-border;
36
+ background: #fff;
37
+ cursor: pointer;
38
+ transition:
39
+ background 0.2s ease,
40
+ box-shadow 0.2s ease;
41
+
42
+ &:hover {
43
+ background: #f9fafb;
44
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
45
+ }
46
+ }
47
+
48
+ /* Inputs */
49
+ .input {
50
+ display: inline-flex;
51
+ align-items: center;
52
+ height: 32px;
53
+ padding: 0 8px;
54
+ border: 1px solid $color-border;
55
+ border-radius: $radius-md;
56
+ background: #fff;
57
+ color: $color-text;
58
+ font: inherit;
59
+ outline: none;
60
+ transition:
61
+ border-color 0.15s ease,
62
+ box-shadow 0.15s ease;
63
+
64
+ &:focus {
65
+ border-color: darken($color-border, 8%);
66
+ box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.04);
67
+ }
68
+ }
69
+
70
+ .input--color {
71
+ padding: 0;
72
+ width: 40px;
73
+ height: 28px;
74
+ border-radius: 6px;
75
+ cursor: pointer;
76
+
77
+ &::-webkit-color-swatch-wrapper {
78
+ padding: 0;
79
+ }
80
+ &::-webkit-color-swatch {
81
+ border: none;
82
+ border-radius: 4px;
83
+ }
84
+ &::-moz-color-swatch {
85
+ border: none;
86
+ border-radius: 4px;
87
+ }
88
+ }
@@ -0,0 +1,174 @@
1
+ @use './variables' as *;
2
+ @use './mixins' as *;
3
+
4
+ .editor-tabs {
5
+ display: flex;
6
+ gap: 8px;
7
+ border-bottom: 1px solid #e5e7eb;
8
+ }
9
+
10
+ .editor-tab {
11
+ padding: 8px 12px;
12
+ cursor: pointer;
13
+ border: 1px solid transparent;
14
+ border-top-left-radius: 4px;
15
+ border-top-right-radius: 4px;
16
+ color: #111827;
17
+ padding: 8px 12px;
18
+ }
19
+
20
+ .editor-tab--active {
21
+ background: #fff;
22
+ border-color: #e5e7eb #e5e7eb #fff #e5e7eb;
23
+ }
24
+
25
+ .editor-panels {
26
+ padding: 12px;
27
+ &.editor-panels-debug {
28
+ padding: 0;
29
+ }
30
+ }
31
+ .jer-editor-container {
32
+ padding-left: 0 !important;
33
+ padding-right: 0 !important;
34
+ }
35
+ .editor-panel {
36
+ display: none;
37
+ }
38
+
39
+ .editor-panel--active {
40
+ display: block;
41
+ }
42
+
43
+ /* Editor utility header */
44
+ .editor-header {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 12px;
48
+ padding: 0 $space-4;
49
+ height: 60px;
50
+ background: #f9fafb;
51
+ border-bottom: 1px solid #e5e7eb;
52
+ }
53
+
54
+ .editor-header__title {
55
+ color: #111827;
56
+ font-weight: 600;
57
+ }
58
+
59
+ .editor-header__devices {
60
+ @include thin-scrollbar;
61
+ display: flex;
62
+ flex-direction: row;
63
+ align-items: stretch;
64
+ gap: 8px;
65
+ overflow: auto;
66
+ height: 60px;
67
+ padding: $space-1;
68
+ }
69
+
70
+ .editor-device-button {
71
+ position: relative;
72
+ min-width: 160px;
73
+ height: 100%;
74
+ border: 1px solid #e5e7eb;
75
+ border-radius: 6px;
76
+ background: #ffffff;
77
+ color: #111827;
78
+ cursor: pointer;
79
+ font-size: 12px;
80
+ &.editor-device-button--selected {
81
+ border-color: #000;
82
+ }
83
+ }
84
+
85
+ .editor-device-button img {
86
+ position: absolute;
87
+ bottom: 4px;
88
+ right: 4px;
89
+ width: 16px;
90
+ height: 16px;
91
+ }
92
+
93
+ .editor-header__actions {
94
+ margin-left: auto; /* push actions to the far right */
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ }
99
+
100
+ .editor-button {
101
+ display: inline-flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ height: 36px;
105
+ min-width: 120px; /* visible even without content */
106
+ padding: 0 12px;
107
+ border: 1px solid #e5e7eb;
108
+ border-radius: 6px;
109
+ background: #ffffff;
110
+ color: #111827;
111
+ cursor: pointer;
112
+ }
113
+
114
+ .editor-button:disabled {
115
+ opacity: 0.6;
116
+ cursor: default;
117
+ }
118
+
119
+ /* Specific hooks for save buttons (kept empty for now) */
120
+ .editor-save-button,
121
+ .editor-save-previewconfig-button {
122
+ color: #000;
123
+ font-weight: 600;
124
+ font-size: 12px;
125
+ }
126
+
127
+ /* Modal */
128
+ .editor-modal {
129
+ position: fixed;
130
+ inset: 0;
131
+ z-index: 1000;
132
+ }
133
+
134
+ .editor-modal__overlay {
135
+ position: absolute;
136
+ inset: 0;
137
+ background: rgba(0, 0, 0, 0.4);
138
+ }
139
+
140
+ .editor-modal__content {
141
+ position: absolute;
142
+ top: 50%;
143
+ left: 50%;
144
+ transform: translate(-50%, -50%);
145
+ background: #fff;
146
+ border-radius: 8px;
147
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
148
+ width: min(960px, calc(100vw - 40px));
149
+ max-height: calc(100vh - 120px);
150
+ display: flex;
151
+ flex-direction: column;
152
+ }
153
+
154
+ .editor-modal__header {
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: space-between;
158
+ padding: 12px 16px;
159
+ border-bottom: 1px solid #e5e7eb;
160
+ }
161
+
162
+ .editor-device-grid {
163
+ padding: 16px;
164
+ display: grid;
165
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
166
+ gap: 12px;
167
+ overflow: auto;
168
+ }
169
+
170
+ /* Reuse device button style inside modal grid */
171
+ .editor-device-grid .editor-device-button {
172
+ height: 80px;
173
+ min-width: unset;
174
+ }