@developer_tribe/react-builder 1.0.1 → 1.0.3

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 (218) hide show
  1. package/dist/AttributesEditor.d.ts +3 -1
  2. package/dist/DeviceMockFrame.d.ts +2 -1
  3. package/dist/RenderPage.d.ts +5 -3
  4. package/dist/attributes-editor/Field.d.ts +17 -0
  5. package/dist/attributes-editor/FieldInfoTooltip.d.ts +7 -0
  6. package/dist/attributes-editor/LayoutPreviewPicker.d.ts +12 -0
  7. package/dist/attributes-editor/SpecialCategorySection.d.ts +20 -0
  8. package/dist/attributes-editor/types.d.ts +14 -0
  9. package/dist/background.jpg +0 -0
  10. package/dist/build-components/BackgroundImage/BackgroundImage.d.ts +5 -0
  11. package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +44 -0
  12. package/dist/build-components/Button/Button.d.ts +1 -1
  13. package/dist/build-components/Button/ButtonProps.generated.d.ts +33 -1
  14. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +34 -1
  15. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +32 -0
  16. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +32 -0
  17. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +34 -1
  18. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +34 -1
  19. package/dist/build-components/Image/ImageProps.generated.d.ts +32 -3
  20. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +34 -1
  21. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +32 -0
  22. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +32 -0
  23. package/dist/build-components/OnboardDot/OnboardDot.d.ts +1 -1
  24. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +29 -0
  25. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +11 -5
  26. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +32 -3
  27. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +31 -3
  28. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +32 -5
  29. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +11 -5
  30. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +11 -5
  31. package/dist/build-components/Text/TextProps.generated.d.ts +11 -5
  32. package/dist/build-components/View/ViewProps.generated.d.ts +10 -4
  33. package/dist/build-components/index.d.ts +2 -1
  34. package/dist/build-components/patterns.generated.d.ts +6288 -136
  35. package/dist/components/AttributesEditorPanel.d.ts +3 -4
  36. package/dist/components/Breadcrumb.d.ts +3 -1
  37. package/dist/components/Builder.d.ts +2 -1
  38. package/dist/components/BuilderButton.d.ts +9 -0
  39. package/dist/components/Checkbox.d.ts +17 -0
  40. package/dist/components/DeviceButton.d.ts +8 -0
  41. package/dist/components/DeviceNavigationBar.d.ts +10 -0
  42. package/dist/components/DeviceStatusBar.d.ts +9 -0
  43. package/dist/components/EditorHeader.d.ts +3 -8
  44. package/dist/index.cjs.js +5 -5
  45. package/dist/index.cjs.js.map +1 -1
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.esm.js +5 -5
  48. package/dist/index.esm.js.map +1 -1
  49. package/dist/mockOS/components/MockLaunchScreenComponent.d.ts +6 -0
  50. package/dist/mockOS/components/MockOSRouter.d.ts +8 -0
  51. package/dist/mockOS/components/PermissionModal.d.ts +9 -0
  52. package/dist/mockOS/context/MockOSContext.d.ts +36 -0
  53. package/dist/mockOS/hooks/useMockNavigation.d.ts +3 -0
  54. package/dist/mockOS/hooks/useMockPermission.d.ts +3 -0
  55. package/dist/mockOS/index.d.ts +9 -0
  56. package/dist/mockOS/managers/mockPermissionManager.d.ts +10 -0
  57. package/dist/mockOS/managers/navigationManager.d.ts +17 -0
  58. package/dist/modals/AddComponentModal.d.ts +8 -0
  59. package/dist/modals/ColorModal.d.ts +11 -0
  60. package/dist/modals/DeviceSelectorModal.d.ts +9 -0
  61. package/dist/modals/LocalicationModal.d.ts +8 -0
  62. package/dist/modals/Modal.d.ts +12 -0
  63. package/dist/modals/index.d.ts +5 -0
  64. package/dist/pages/ProjectPage.d.ts +3 -3
  65. package/dist/pages/tabs/BuilderPanel.d.ts +8 -0
  66. package/dist/pages/tabs/{DebugTab.d.ts → SideTool.d.ts} +2 -2
  67. package/dist/store.d.ts +7 -3
  68. package/dist/styles.css +1 -1
  69. package/dist/types/Project.d.ts +11 -0
  70. package/dist/utils/analyseNode.d.ts +1 -0
  71. package/dist/utils/extractTextStyle.d.ts +8 -1
  72. package/dist/utils/extractViewStyle.d.ts +8 -1
  73. package/dist/utils/parseColor.d.ts +7 -0
  74. package/dist/utils/patterns.d.ts +24 -0
  75. package/package.json +2 -1
  76. package/scripts/prebuild/utils/createGeneratedProps.js +11 -3
  77. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +45 -6
  78. package/scripts/prebuild/utils/validatePatternJson.js +13 -5
  79. package/src/AttributesEditor.tsx +493 -310
  80. package/src/DeviceMockFrame.tsx +21 -37
  81. package/src/RenderPage.tsx +86 -7
  82. package/src/assets/images/android.svg +42 -42
  83. package/src/assets/images/apple.svg +15 -15
  84. package/src/attributes-editor/Field.tsx +669 -0
  85. package/src/attributes-editor/FieldInfoTooltip.tsx +49 -0
  86. package/src/attributes-editor/LayoutPreviewPicker.tsx +199 -0
  87. package/src/attributes-editor/SpecialCategorySection.tsx +285 -0
  88. package/src/attributes-editor/types.ts +30 -0
  89. package/src/build-components/BackgroundImage/BackgroundImage.tsx +87 -0
  90. package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +60 -0
  91. package/src/build-components/BackgroundImage/pattern.json +45 -0
  92. package/src/build-components/Button/Button.tsx +37 -2
  93. package/src/build-components/Button/ButtonProps.generated.ts +44 -1
  94. package/src/build-components/Button/pattern.json +31 -2
  95. package/src/build-components/Carousel/Carousel.tsx +39 -2
  96. package/src/build-components/Carousel/CarouselProps.generated.ts +46 -1
  97. package/src/build-components/Carousel/pattern.json +10 -0
  98. package/src/build-components/CarouselButtons/CarouselButtons.tsx +21 -2
  99. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +43 -0
  100. package/src/build-components/CarouselButtons/pattern.json +22 -0
  101. package/src/build-components/CarouselDots/CarouselDots.tsx +49 -8
  102. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +43 -0
  103. package/src/build-components/CarouselDots/pattern.json +15 -0
  104. package/src/build-components/CarouselItem/CarouselItem.tsx +21 -2
  105. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +46 -1
  106. package/src/build-components/CarouselItem/pattern.json +7 -0
  107. package/src/build-components/CarouselProvider/CarouselProvider.tsx +21 -2
  108. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +46 -1
  109. package/src/build-components/CarouselProvider/pattern.json +7 -0
  110. package/src/build-components/Image/Image.tsx +33 -2
  111. package/src/build-components/Image/ImageProps.generated.ts +43 -3
  112. package/src/build-components/Image/pattern.json +46 -3
  113. package/src/build-components/Onboard/Onboard.tsx +6 -1
  114. package/src/build-components/Onboard/OnboardProps.generated.ts +46 -1
  115. package/src/build-components/Onboard/pattern.json +11 -0
  116. package/src/build-components/OnboardButton/OnboardButton.tsx +54 -6
  117. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +43 -0
  118. package/src/build-components/OnboardButton/pattern.json +71 -5
  119. package/src/build-components/OnboardButtons/OnboardButtons.tsx +33 -11
  120. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +43 -0
  121. package/src/build-components/OnboardButtons/pattern.json +70 -4
  122. package/src/build-components/OnboardDot/OnboardDot.tsx +113 -4
  123. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +29 -0
  124. package/src/build-components/OnboardDot/pattern.json +55 -2
  125. package/src/build-components/OnboardFooter/OnboardFooter.tsx +20 -4
  126. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +11 -5
  127. package/src/build-components/OnboardFooter/pattern.json +58 -2
  128. package/src/build-components/OnboardImage/OnboardImage.tsx +49 -5
  129. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +43 -3
  130. package/src/build-components/OnboardImage/pattern.json +21 -0
  131. package/src/build-components/OnboardItem/OnboardItem.tsx +17 -1
  132. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +42 -3
  133. package/src/build-components/OnboardItem/pattern.json +38 -2
  134. package/src/build-components/OnboardProvider/OnboardProvider.tsx +52 -18
  135. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +44 -5
  136. package/src/build-components/OnboardProvider/pattern.json +44 -5
  137. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +11 -5
  138. package/src/build-components/OnboardSubtitle/pattern.json +7 -1
  139. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +11 -5
  140. package/src/build-components/OnboardTitle/pattern.json +7 -1
  141. package/src/build-components/RenderNode.generated.tsx +3 -0
  142. package/src/build-components/Text/Text.tsx +34 -6
  143. package/src/build-components/Text/TextProps.generated.ts +11 -5
  144. package/src/build-components/Text/pattern.json +38 -2
  145. package/src/build-components/View/View.tsx +33 -6
  146. package/src/build-components/View/ViewProps.generated.ts +10 -4
  147. package/src/build-components/View/pattern.json +285 -19
  148. package/src/build-components/index.ts +5 -0
  149. package/src/build-components/patterns.generated.ts +6346 -143
  150. package/src/components/AttributesEditorPanel.tsx +17 -64
  151. package/src/components/Breadcrumb.tsx +37 -5
  152. package/src/components/Builder.tsx +311 -108
  153. package/src/components/BuilderButton.tsx +127 -0
  154. package/src/components/Checkbox.tsx +81 -0
  155. package/src/components/DeviceButton.tsx +39 -0
  156. package/src/components/DeviceNavigationBar.tsx +201 -0
  157. package/src/components/DeviceStatusBar.tsx +85 -0
  158. package/src/components/EditorHeader.tsx +26 -74
  159. package/src/index.ts +2 -2
  160. package/src/mockOS/components/MockLaunchScreenComponent.tsx +43 -0
  161. package/src/mockOS/components/MockOSRouter.tsx +123 -0
  162. package/src/mockOS/components/PermissionModal.tsx +270 -0
  163. package/src/mockOS/context/MockOSContext.tsx +179 -0
  164. package/src/mockOS/hooks/useMockNavigation.ts +11 -0
  165. package/src/mockOS/hooks/useMockPermission.ts +11 -0
  166. package/src/mockOS/index.ts +26 -0
  167. package/src/mockOS/managers/mockPermissionManager.ts +54 -0
  168. package/src/mockOS/managers/navigationManager.ts +91 -0
  169. package/src/modals/AddComponentModal.tsx +313 -0
  170. package/src/modals/ColorModal.tsx +425 -0
  171. package/src/modals/DeviceSelectorModal.tsx +57 -0
  172. package/src/modals/LocalicationModal.tsx +54 -0
  173. package/src/modals/Modal.tsx +57 -0
  174. package/src/modals/index.ts +5 -0
  175. package/src/pages/ProjectPage.tsx +307 -71
  176. package/src/pages/tabs/{BuilderTab.tsx → BuilderPanel.tsx} +13 -9
  177. package/src/pages/tabs/SideTool.tsx +259 -0
  178. package/src/size-matters/index.ts +27 -5
  179. package/src/store.ts +13 -5
  180. package/src/styles/base/_global.scss +404 -0
  181. package/src/styles/components/_attributes-editor.scss +273 -0
  182. package/src/styles/components/_editor-shell.scss +212 -0
  183. package/src/styles/components/_mockos-router.scss +140 -0
  184. package/src/styles/components/_ui-components.scss +183 -0
  185. package/src/styles/foundation/_colors.scss +8 -0
  186. package/src/styles/{_mixins.scss → foundation/_mixins.scss} +5 -4
  187. package/src/styles/{_reset.scss → foundation/_reset.scss} +5 -2
  188. package/src/styles/foundation/_sizes.scss +37 -0
  189. package/src/styles/foundation/_typography.scss +4 -0
  190. package/src/styles/foundation/_variables.scss +3 -0
  191. package/src/styles/index.scss +22 -136
  192. package/src/styles/layout/_builder.scss +124 -0
  193. package/src/styles/layout/_pages.scss +3 -0
  194. package/src/styles/modals/_add-component.scss +122 -0
  195. package/src/styles/modals/_color-modal.scss +159 -0
  196. package/src/styles/modals/_device-selector.scss +18 -0
  197. package/src/styles/modals/_localication-modal.scss +68 -0
  198. package/src/styles/modals/_modal-shell.scss +46 -0
  199. package/src/styles/utilities/_carousel.scss +125 -0
  200. package/src/types/Project.ts +14 -0
  201. package/src/types/images.d.ts +8 -0
  202. package/src/utils/analyseNode.ts +98 -0
  203. package/src/utils/extractTextStyle.ts +28 -10
  204. package/src/utils/extractViewStyle.ts +77 -9
  205. package/src/utils/parseColor.ts +43 -0
  206. package/src/utils/patterns.ts +33 -0
  207. package/dist/build-components/OnboardDot/OnboardExpandingDotProps.generated.d.ts +0 -10
  208. package/dist/pages/tabs/BuilderTab.d.ts +0 -9
  209. package/dist/pages/tabs/PreviewTab.d.ts +0 -3
  210. package/src/build-components/OnboardDot/OnboardExpandingDotProps.generated.ts +0 -20
  211. package/src/pages/tabs/DebugTab.tsx +0 -23
  212. package/src/pages/tabs/PreviewTab.tsx +0 -194
  213. package/src/styles/_variables.scss +0 -27
  214. package/src/styles/builder.scss +0 -60
  215. package/src/styles/components.scss +0 -88
  216. package/src/styles/editor.scss +0 -174
  217. package/src/styles/global.scss +0 -200
  218. package/src/styles/pages.scss +0 -2
@@ -0,0 +1,425 @@
1
+ import React, { useId, useMemo, useRef, useState } from 'react';
2
+ import type { ProjectColorTokenMap, ProjectColors } from '../types/Project';
3
+ import Modal from './Modal';
4
+
5
+ const SAVED_COLORS_KEY = 'attributes-editor-saved-colors';
6
+
7
+ type ColorOption = {
8
+ id: string;
9
+ label?: string;
10
+ value: string;
11
+ tokenLabel?: string;
12
+ };
13
+
14
+ type ColorGroup = {
15
+ title: string;
16
+ options: ColorOption[];
17
+ emptyMessage?: string;
18
+ };
19
+
20
+ type ColorModalProps = {
21
+ value?: string;
22
+ projectColors?: ProjectColors;
23
+ onSelect: (color: string) => void;
24
+ onClose: () => void;
25
+ onClear: () => void;
26
+ };
27
+
28
+ const formatTokenLabel = (token: string) =>
29
+ token
30
+ .split(/[_-]/g)
31
+ .filter(Boolean)
32
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
33
+ .join(' ');
34
+
35
+ type MapTokensOptions = {
36
+ groupKey?: string;
37
+ tokenLabelPrefix?: string;
38
+ };
39
+
40
+ const mapTokensToOptions = (
41
+ tokenMap?: ProjectColorTokenMap,
42
+ options?: MapTokensOptions,
43
+ ): ColorOption[] => {
44
+ if (!tokenMap) return [];
45
+ const prefix = options?.groupKey ?? 'group';
46
+ return Object.entries(tokenMap)
47
+ .filter(([, value]) => typeof value === 'string' && value.trim().length > 0)
48
+ .map(([token, value]) => {
49
+ const trimmedValue = value.trim();
50
+ const tokenLabel = options?.tokenLabelPrefix
51
+ ? `${options.tokenLabelPrefix}.${token}`
52
+ : undefined;
53
+ return {
54
+ id: `${prefix}-${token}`,
55
+ label: formatTokenLabel(token),
56
+ value: trimmedValue,
57
+ tokenLabel,
58
+ };
59
+ });
60
+ };
61
+
62
+ const readSavedColors = (): string[] => {
63
+ if (typeof window === 'undefined') return [];
64
+ try {
65
+ const stored = window.localStorage.getItem(SAVED_COLORS_KEY);
66
+ if (!stored) return [];
67
+ const parsed = JSON.parse(stored);
68
+ return Array.isArray(parsed) ? parsed : [];
69
+ } catch {
70
+ return [];
71
+ }
72
+ };
73
+
74
+ const persistSavedColors = (colors: string[]) => {
75
+ if (typeof window === 'undefined') return;
76
+ try {
77
+ window.localStorage.setItem(SAVED_COLORS_KEY, JSON.stringify(colors));
78
+ } catch {
79
+ // no-op
80
+ }
81
+ };
82
+
83
+ const STATIC_PREFIX = 'STATIC_COLORS.';
84
+ const THEME_PREFIX = 'THEME_COLORS.';
85
+
86
+ export const resolveProjectColorValue = (
87
+ candidate?: string,
88
+ projectColors?: ProjectColors,
89
+ ): string | undefined => {
90
+ if (!candidate || typeof candidate !== 'string' || !projectColors) {
91
+ return undefined;
92
+ }
93
+ const trimmedCandidate = candidate.trim();
94
+ if (!trimmedCandidate) return undefined;
95
+
96
+ if (trimmedCandidate.startsWith(STATIC_PREFIX)) {
97
+ const token = trimmedCandidate.slice(STATIC_PREFIX.length);
98
+ const resolved = projectColors.STATIC_COLORS?.[token];
99
+ return typeof resolved === 'string' && resolved.trim()
100
+ ? resolved.trim()
101
+ : undefined;
102
+ }
103
+
104
+ if (trimmedCandidate.startsWith(THEME_PREFIX)) {
105
+ const remainder = trimmedCandidate.slice(THEME_PREFIX.length);
106
+ if (!remainder) return undefined;
107
+ const themeParts = remainder.split('.');
108
+ if (themeParts.length === 1) {
109
+ const token = themeParts[0];
110
+ const themes = projectColors.THEME_COLORS;
111
+ if (!themes) return undefined;
112
+ for (const themeTokens of Object.values(themes)) {
113
+ const resolved = themeTokens?.[token];
114
+ if (typeof resolved === 'string' && resolved.trim()) {
115
+ return resolved.trim();
116
+ }
117
+ }
118
+ return undefined;
119
+ }
120
+ const [themeName, ...tokenSegments] = themeParts;
121
+ const token = tokenSegments.join('.');
122
+ if (!themeName || !token) return undefined;
123
+ const themeTokens = projectColors.THEME_COLORS?.[themeName];
124
+ const resolved = themeTokens?.[token];
125
+ return typeof resolved === 'string' && resolved.trim()
126
+ ? resolved.trim()
127
+ : undefined;
128
+ }
129
+
130
+ return undefined;
131
+ };
132
+
133
+ export function ColorModal({
134
+ value,
135
+ projectColors,
136
+ onSelect,
137
+ onClose,
138
+ onClear,
139
+ }: ColorModalProps) {
140
+ const [savedColors, setSavedColors] = useState<string[]>(() =>
141
+ readSavedColors(),
142
+ );
143
+ const [useColorNames, setUseColorNames] = useState(true);
144
+ const colorInputRef = useRef<HTMLInputElement | null>(null);
145
+ const colorNameToggleId = useId();
146
+
147
+ const selectedColorPreview = useMemo(
148
+ () => resolveProjectColorValue(value, projectColors) ?? value,
149
+ [value, projectColors],
150
+ );
151
+
152
+ const projectColorGroups = useMemo<ColorGroup[]>(() => {
153
+ if (!projectColors) return [];
154
+ const groups: ColorGroup[] = [];
155
+
156
+ const staticOptions = mapTokensToOptions(projectColors.STATIC_COLORS, {
157
+ groupKey: 'static',
158
+ tokenLabelPrefix: 'STATIC_COLORS',
159
+ });
160
+ if (staticOptions.length) {
161
+ groups.push({
162
+ title: 'Static colors',
163
+ options: staticOptions,
164
+ emptyMessage: 'No static colors defined.',
165
+ });
166
+ }
167
+
168
+ const themeColors = projectColors.THEME_COLORS ?? {};
169
+ Object.entries(themeColors).forEach(([themeName, themeTokens]) => {
170
+ const options = mapTokensToOptions(themeTokens, {
171
+ groupKey: `theme-${themeName || 'default'}`,
172
+ tokenLabelPrefix: 'THEME_COLORS',
173
+ });
174
+ if (!options.length) return;
175
+ const normalizedThemeName = themeName?.trim()
176
+ ? formatTokenLabel(themeName)
177
+ : 'Theme';
178
+ groups.push({
179
+ title: `Theme: ${normalizedThemeName}`,
180
+ options,
181
+ emptyMessage: `No colors defined for ${normalizedThemeName}.`,
182
+ });
183
+ });
184
+
185
+ return groups;
186
+ }, [projectColors]);
187
+
188
+ const savedColorOptions = useMemo<ColorOption[]>(
189
+ () =>
190
+ savedColors.map((color, index) => ({
191
+ id: `saved-${index}-${color}`,
192
+ label: color,
193
+ value: color,
194
+ })),
195
+ [savedColors],
196
+ );
197
+
198
+ const handleAddColorClick = () => {
199
+ colorInputRef.current?.click();
200
+ };
201
+
202
+ const handlePreviewKeyDown = (
203
+ event: React.KeyboardEvent<HTMLSpanElement>,
204
+ ) => {
205
+ if (event.key !== 'Enter' && event.key !== ' ') return;
206
+ event.preventDefault();
207
+ handleAddColorClick();
208
+ };
209
+
210
+ const handleColorPicked = (event: React.ChangeEvent<HTMLInputElement>) => {
211
+ const picked = event.target.value;
212
+ if (!picked) return;
213
+ setSavedColors((prev) => {
214
+ if (prev.includes(picked)) return prev;
215
+ const next = [...prev, picked];
216
+ persistSavedColors(next);
217
+ return next;
218
+ });
219
+ onSelect(picked);
220
+ onClose();
221
+ event.target.value = '';
222
+ };
223
+
224
+ const handleSelectColor = (option: ColorOption) => {
225
+ const nextValue =
226
+ useColorNames && option.tokenLabel ? option.tokenLabel : option.value;
227
+ onSelect(nextValue);
228
+ onClose();
229
+ };
230
+
231
+ return (
232
+ <Modal
233
+ onClose={onClose}
234
+ ariaLabelledBy="color-picker-title"
235
+ contentClassName="color-modal"
236
+ >
237
+ <div className="modal__header">
238
+ <h3 id="color-picker-title" className="modal__title">
239
+ Pick a color
240
+ </h3>
241
+ <button type="button" className="editor-button" onClick={onClose}>
242
+ Close
243
+ </button>
244
+ </div>
245
+
246
+ <div className="color-modal__selected">
247
+ <div className="color-modal__selected-info">
248
+ <span
249
+ role="button"
250
+ aria-label="Open color picker"
251
+ tabIndex={0}
252
+ title="Pick a color"
253
+ className="color-modal__selected-preview"
254
+ style={{ background: selectedColorPreview ?? 'transparent' }}
255
+ onClick={handleAddColorClick}
256
+ onKeyDown={handlePreviewKeyDown}
257
+ />
258
+ <div>
259
+ <p className="color-modal__selected-label">Selected color</p>
260
+ <p className="color-modal__selected-value">{value ?? 'None'}</p>
261
+ </div>
262
+ </div>
263
+ {value ? (
264
+ <button
265
+ type="button"
266
+ className="color-modal__link-button"
267
+ onClick={onClear}
268
+ >
269
+ Clear
270
+ </button>
271
+ ) : null}
272
+ </div>
273
+
274
+ <div className="color-modal__toggle">
275
+ <label
276
+ htmlFor={colorNameToggleId}
277
+ className="color-modal__toggle-label"
278
+ >
279
+ <input
280
+ id={colorNameToggleId}
281
+ type="checkbox"
282
+ className="color-modal__toggle-input"
283
+ checked={useColorNames}
284
+ onChange={(event) => setUseColorNames(event.target.checked)}
285
+ />
286
+ Use color names
287
+ </label>
288
+ <p className="color-modal__toggle-description">
289
+ Output tokens like STATIC_COLORS.PRIMARY instead of raw values.
290
+ </p>
291
+ </div>
292
+
293
+ {projectColorGroups.length ? (
294
+ projectColorGroups.map((group) => (
295
+ <ColorSection
296
+ key={group.title}
297
+ title={group.title}
298
+ options={group.options}
299
+ emptyMessage={group.emptyMessage}
300
+ activeValue={value}
301
+ onSelect={handleSelectColor}
302
+ />
303
+ ))
304
+ ) : (
305
+ <ColorSection
306
+ title="Project colors"
307
+ options={[]}
308
+ emptyMessage="No project colors detected yet."
309
+ activeValue={value}
310
+ onSelect={handleSelectColor}
311
+ />
312
+ )}
313
+
314
+ <ColorSection
315
+ title="Saved colors"
316
+ options={savedColorOptions}
317
+ emptyMessage="Add colors you use often for quick access."
318
+ action={
319
+ <button
320
+ type="button"
321
+ className="color-modal__link-button"
322
+ onClick={handleAddColorClick}
323
+ >
324
+ Add color
325
+ </button>
326
+ }
327
+ activeValue={value}
328
+ onSelect={handleSelectColor}
329
+ />
330
+
331
+ <input
332
+ ref={colorInputRef}
333
+ type="color"
334
+ className="color-modal__input"
335
+ onChange={handleColorPicked}
336
+ />
337
+ </Modal>
338
+ );
339
+ }
340
+
341
+ type ColorSectionProps = {
342
+ title: string;
343
+ options: ColorOption[];
344
+ emptyMessage?: string;
345
+ action?: React.ReactNode;
346
+ activeValue?: string;
347
+ onSelect: (option: ColorOption) => void;
348
+ };
349
+
350
+ function ColorSection({
351
+ title,
352
+ options,
353
+ emptyMessage,
354
+ action,
355
+ activeValue,
356
+ onSelect,
357
+ }: ColorSectionProps) {
358
+ return (
359
+ <section className="color-section">
360
+ <div className="color-section__header">
361
+ <p className="color-section__title">{title}</p>
362
+ {action}
363
+ </div>
364
+ {options.length ? (
365
+ <div className="color-section__swatches">
366
+ {options.map((option) => (
367
+ <ColorSwatch
368
+ key={option.id}
369
+ option={option}
370
+ isActive={
371
+ (option.tokenLabel && option.tokenLabel === activeValue) ||
372
+ option.value === activeValue
373
+ }
374
+ onSelect={() => onSelect(option)}
375
+ />
376
+ ))}
377
+ </div>
378
+ ) : (
379
+ <p className="color-section__empty">
380
+ {emptyMessage ?? 'No colors available.'}
381
+ </p>
382
+ )}
383
+ </section>
384
+ );
385
+ }
386
+
387
+ type ColorSwatchProps = {
388
+ option: ColorOption;
389
+ isActive?: boolean;
390
+ onSelect: () => void;
391
+ };
392
+
393
+ function ColorSwatch({ option, isActive, onSelect }: ColorSwatchProps) {
394
+ const isLight =
395
+ option.value?.trim().toLowerCase() === '#ffffff' ||
396
+ option.value?.trim().toLowerCase() === '#fff';
397
+
398
+ return (
399
+ <button
400
+ type="button"
401
+ onClick={onSelect}
402
+ className={`color-modal__swatch${
403
+ isActive ? ' color-modal__swatch--active' : ''
404
+ }`}
405
+ >
406
+ <span
407
+ aria-hidden
408
+ className={`color-modal__swatch-preview${
409
+ isLight ? ' color-modal__swatch-preview--light' : ''
410
+ }`}
411
+ style={{ background: option.value ?? 'transparent' }}
412
+ />
413
+ <span className="color-modal__swatch-label">
414
+ {option.label ?? option.value}
415
+ </span>
416
+ <span className="color-modal__swatch-value">
417
+ {option.tokenLabel
418
+ ? `${option.tokenLabel} - ${option.value}`
419
+ : option.value}
420
+ </span>
421
+ </button>
422
+ );
423
+ }
424
+
425
+ export default ColorModal;
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { Device } from '../types/Device';
3
+ import Modal from './Modal';
4
+ import { DeviceButton } from '../components/DeviceButton';
5
+
6
+ type DeviceSelectorModalProps = {
7
+ devices: Device[];
8
+ selectedDevice: Device | null;
9
+ onSelect: (device: Device) => void;
10
+ onClose: () => void;
11
+ };
12
+
13
+ export function DeviceSelectorModal({
14
+ devices,
15
+ selectedDevice,
16
+ onSelect,
17
+ onClose,
18
+ }: DeviceSelectorModalProps) {
19
+ const handleDeviceSelect = (device: Device) => {
20
+ onSelect(device);
21
+ onClose();
22
+ };
23
+
24
+ return (
25
+ <Modal
26
+ onClose={onClose}
27
+ ariaLabelledBy="device-selector-title"
28
+ contentClassName="device-selector-modal"
29
+ >
30
+ <div className="modal__header">
31
+ <h3 id="device-selector-title" className="modal__title">
32
+ Select a device
33
+ </h3>
34
+ <button
35
+ type="button"
36
+ className="editor-button"
37
+ aria-label="Close device selector"
38
+ onClick={onClose}
39
+ >
40
+ Close
41
+ </button>
42
+ </div>
43
+ <div className="device-selector-modal__grid" role="list">
44
+ {devices.map((device) => (
45
+ <DeviceButton
46
+ key={device.name}
47
+ device={device}
48
+ selectedDevice={selectedDevice}
49
+ onSelect={handleDeviceSelect}
50
+ />
51
+ ))}
52
+ </div>
53
+ </Modal>
54
+ );
55
+ }
56
+
57
+ export default DeviceSelectorModal;
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import { JsonEditor } from 'json-edit-react';
3
+ import { Localication } from '../types/PreviewConfig';
4
+ import Modal from './Modal';
5
+
6
+ type LocalicationModalProps = {
7
+ data: Localication;
8
+ onChange: (next: Localication) => void;
9
+ onClose: () => void;
10
+ };
11
+
12
+ export function LocalicationModal({
13
+ data,
14
+ onChange,
15
+ onClose,
16
+ }: LocalicationModalProps) {
17
+ const normalizedData = data ?? {};
18
+
19
+ return (
20
+ <Modal
21
+ onClose={onClose}
22
+ ariaLabelledBy="localication-modal-title"
23
+ className="modal--large modal--scrollable localication-modal"
24
+ contentClassName="localication-modal__content"
25
+ >
26
+ <div className="modal__header localication-modal__header">
27
+ <div className="localication-modal__header-main">
28
+ <h3 id="localication-modal-title" className="modal__title">
29
+ Localization data
30
+ </h3>
31
+ <p className="localication-modal__description">
32
+ Manage your translations directly from the JSON structure.
33
+ </p>
34
+ </div>
35
+ <button type="button" className="editor-button" onClick={onClose}>
36
+ Close
37
+ </button>
38
+ </div>
39
+ <div className="localication-modal__body">
40
+ <div className="localication-modal__editor">
41
+ <JsonEditor
42
+ rootName="localication"
43
+ data={normalizedData}
44
+ setData={(next) => onChange(next as Localication)}
45
+ className="localication-modal__json-editor"
46
+ maxWidth={'100%'}
47
+ />
48
+ </div>
49
+ </div>
50
+ </Modal>
51
+ );
52
+ }
53
+
54
+ export default LocalicationModal;
@@ -0,0 +1,57 @@
1
+ import React, { useEffect } from 'react';
2
+
3
+ type ModalProps = {
4
+ onClose: () => void;
5
+ ariaLabelledBy?: string;
6
+ children: React.ReactNode;
7
+ className?: string;
8
+ contentClassName?: string;
9
+ closeOnOverlayClick?: boolean;
10
+ closeOnEsc?: boolean;
11
+ };
12
+
13
+ export function Modal({
14
+ onClose,
15
+ ariaLabelledBy,
16
+ children,
17
+ className,
18
+ contentClassName,
19
+ closeOnOverlayClick = true,
20
+ closeOnEsc = true,
21
+ }: ModalProps) {
22
+ useEffect(() => {
23
+ if (!closeOnEsc) return undefined;
24
+ if (typeof window === 'undefined') return undefined;
25
+
26
+ const handleKeyDown = (event: KeyboardEvent) => {
27
+ if (event.key === 'Escape') {
28
+ onClose();
29
+ }
30
+ };
31
+
32
+ window.addEventListener('keydown', handleKeyDown);
33
+ return () => window.removeEventListener('keydown', handleKeyDown);
34
+ }, [closeOnEsc, onClose]);
35
+
36
+ return (
37
+ <div
38
+ className={`modal${className ? ` ${className}` : ''}`}
39
+ role="dialog"
40
+ aria-modal="true"
41
+ aria-labelledby={ariaLabelledBy}
42
+ >
43
+ <div
44
+ className="modal__overlay"
45
+ onClick={closeOnOverlayClick ? onClose : undefined}
46
+ />
47
+ <div
48
+ className={`modal__content${contentClassName ? ` ${contentClassName}` : ''}`}
49
+ role="document"
50
+ >
51
+ {children}
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export default Modal;
@@ -0,0 +1,5 @@
1
+ export { default as Modal } from './Modal';
2
+ export { AddComponentModal } from './AddComponentModal';
3
+ export { DeviceSelectorModal } from './DeviceSelectorModal';
4
+ export { ColorModal } from './ColorModal';
5
+ export { LocalicationModal } from './LocalicationModal';