@developer_tribe/react-builder 1.0.2 → 1.0.4

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 (147) hide show
  1. package/dist/AttributesEditor.d.ts +3 -1
  2. package/dist/RenderPage.d.ts +2 -1
  3. package/dist/android.svg +43 -0
  4. package/dist/apple.svg +16 -0
  5. package/dist/attributes-editor/Field.d.ts +4 -2
  6. package/dist/attributes-editor/SizeField.d.ts +9 -0
  7. package/dist/attributes-editor/SpecialCategorySection.d.ts +2 -1
  8. package/dist/build-components/BackgroundImage/BackgroundImage.d.ts +5 -0
  9. package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +45 -0
  10. package/dist/build-components/Button/ButtonProps.generated.d.ts +8 -0
  11. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +8 -0
  12. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +8 -0
  13. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +8 -0
  14. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +8 -0
  15. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +8 -0
  16. package/dist/build-components/Image/ImageProps.generated.d.ts +8 -0
  17. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +8 -0
  18. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +8 -1
  19. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +8 -0
  20. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +9 -3
  21. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +8 -0
  22. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +9 -1
  23. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +8 -0
  24. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +8 -1
  25. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +8 -0
  26. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +8 -0
  27. package/dist/build-components/Text/TextProps.generated.d.ts +8 -0
  28. package/dist/build-components/View/ViewProps.generated.d.ts +8 -0
  29. package/dist/build-components/index.d.ts +2 -1
  30. package/dist/build-components/patterns.generated.d.ts +1612 -46
  31. package/dist/components/AttributesEditorPanel.d.ts +3 -4
  32. package/dist/components/Builder.d.ts +2 -1
  33. package/dist/components/BuilderButton.d.ts +9 -0
  34. package/dist/components/JsonTextEditor.d.ts +9 -0
  35. package/dist/index.cjs.js +5 -5
  36. package/dist/index.cjs.js.map +1 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.esm.js +5 -5
  39. package/dist/index.esm.js.map +1 -1
  40. package/dist/modals/ColorModal.d.ts +3 -1
  41. package/dist/pages/ProjectPage.d.ts +3 -3
  42. package/dist/pages/tabs/BuilderPanel.d.ts +8 -0
  43. package/dist/pages/tabs/SideTool.d.ts +8 -0
  44. package/dist/store.d.ts +9 -1
  45. package/dist/styles.css +1 -1
  46. package/dist/types/Project.d.ts +11 -0
  47. package/dist/utils/analyseNode.d.ts +1 -0
  48. package/dist/utils/extractImageStyle.d.ts +2 -1
  49. package/dist/utils/extractTextStyle.d.ts +8 -1
  50. package/dist/utils/extractViewStyle.d.ts +7 -1
  51. package/dist/utils/parseColor.d.ts +7 -0
  52. package/dist/utils/selection.d.ts +7 -0
  53. package/dist/utils/useMergedStyle.d.ts +2 -0
  54. package/package.json +2 -5
  55. package/src/.DS_Store +0 -0
  56. package/src/AttributesEditor.tsx +83 -16
  57. package/src/RenderPage.tsx +86 -4
  58. package/src/attributes-editor/Field.tsx +60 -165
  59. package/src/attributes-editor/SizeField.tsx +184 -0
  60. package/src/attributes-editor/SpecialCategorySection.tsx +12 -4
  61. package/src/build-components/BackgroundImage/BackgroundImage.tsx +77 -0
  62. package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +61 -0
  63. package/src/build-components/BackgroundImage/pattern.json +45 -0
  64. package/src/build-components/Button/Button.tsx +29 -4
  65. package/src/build-components/Button/ButtonProps.generated.ts +8 -0
  66. package/src/build-components/Carousel/Carousel.tsx +25 -3
  67. package/src/build-components/Carousel/CarouselProps.generated.ts +8 -0
  68. package/src/build-components/CarouselButtons/CarouselButtons.tsx +19 -4
  69. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +8 -0
  70. package/src/build-components/CarouselDots/CarouselDots.tsx +13 -4
  71. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +8 -0
  72. package/src/build-components/CarouselItem/CarouselItem.tsx +20 -4
  73. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +8 -0
  74. package/src/build-components/CarouselProvider/CarouselProvider.tsx +14 -3
  75. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +8 -0
  76. package/src/build-components/Image/Image.tsx +27 -9
  77. package/src/build-components/Image/ImageProps.generated.ts +8 -0
  78. package/src/build-components/Image/pattern.json +1 -9
  79. package/src/build-components/Onboard/Onboard.tsx +2 -2
  80. package/src/build-components/Onboard/OnboardProps.generated.ts +8 -0
  81. package/src/build-components/OnboardButton/OnboardButton.tsx +11 -7
  82. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +8 -1
  83. package/src/build-components/OnboardButtons/OnboardButtons.tsx +17 -5
  84. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +8 -0
  85. package/src/build-components/OnboardDot/OnboardDot.tsx +68 -39
  86. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +9 -3
  87. package/src/build-components/OnboardDot/pattern.json +3 -19
  88. package/src/build-components/OnboardFooter/OnboardFooter.tsx +37 -14
  89. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +8 -0
  90. package/src/build-components/OnboardImage/OnboardImage.tsx +28 -6
  91. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +9 -1
  92. package/src/build-components/OnboardItem/OnboardItem.tsx +15 -14
  93. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +8 -0
  94. package/src/build-components/OnboardProvider/OnboardProvider.tsx +35 -20
  95. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +8 -1
  96. package/src/build-components/OnboardProvider/pattern.json +0 -8
  97. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +8 -0
  98. package/src/build-components/OnboardSubtitle/pattern.json +1 -1
  99. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +8 -0
  100. package/src/build-components/OnboardTitle/pattern.json +1 -1
  101. package/src/build-components/RenderNode.generated.tsx +3 -0
  102. package/src/build-components/Text/Text.tsx +28 -10
  103. package/src/build-components/Text/TextProps.generated.ts +8 -0
  104. package/src/build-components/View/View.tsx +25 -3
  105. package/src/build-components/View/ViewProps.generated.ts +8 -0
  106. package/src/build-components/View/pattern.json +67 -1
  107. package/src/build-components/index.ts +5 -0
  108. package/src/build-components/patterns.generated.ts +1620 -46
  109. package/src/components/AttributesEditorPanel.tsx +13 -6
  110. package/src/components/Builder.tsx +200 -56
  111. package/src/components/BuilderButton.tsx +127 -0
  112. package/src/components/DeviceNavigationBar.tsx +0 -1
  113. package/src/components/EditorHeader.tsx +11 -1
  114. package/src/components/JsonTextEditor.tsx +185 -0
  115. package/src/index.ts +2 -2
  116. package/src/mockOS/components/MockOSRouter.tsx +17 -3
  117. package/src/mockOS/context/MockOSContext.tsx +0 -5
  118. package/src/mockOS/managers/mockPermissionManager.ts +0 -4
  119. package/src/mockOS/managers/navigationManager.ts +1 -6
  120. package/src/modals/ColorModal.tsx +306 -71
  121. package/src/modals/LocalicationModal.tsx +4 -5
  122. package/src/modals/Modal.tsx +8 -1
  123. package/src/pages/ProjectPage.tsx +299 -55
  124. package/src/pages/tabs/{BuilderTab.tsx → BuilderPanel.tsx} +13 -9
  125. package/src/pages/tabs/SideTool.tsx +260 -0
  126. package/src/size-matters/index.ts +6 -0
  127. package/src/store.ts +18 -1
  128. package/src/styles/base/_global.scss +163 -7
  129. package/src/styles/components/_attributes-editor.scss +12 -0
  130. package/src/styles/components/_editor-shell.scss +25 -0
  131. package/src/styles/foundation/_sizes.scss +1 -1
  132. package/src/styles/layout/_builder.scss +66 -10
  133. package/src/styles/modals/_color-modal.scss +59 -1
  134. package/src/styles/utilities/_carousel.scss +9 -8
  135. package/src/types/Project.ts +14 -0
  136. package/src/utils/analyseNode.ts +98 -0
  137. package/src/utils/extractImageStyle.ts +3 -6
  138. package/src/utils/extractTextStyle.ts +19 -82
  139. package/src/utils/extractViewStyle.ts +41 -12
  140. package/src/utils/parseColor.ts +43 -0
  141. package/src/utils/selection.ts +24 -0
  142. package/src/utils/useMergedStyle.ts +16 -0
  143. package/dist/pages/tabs/BuilderTab.d.ts +0 -9
  144. package/dist/pages/tabs/DebugTab.d.ts +0 -7
  145. package/dist/pages/tabs/PreviewTab.d.ts +0 -3
  146. package/src/pages/tabs/DebugTab.tsx +0 -64
  147. package/src/pages/tabs/PreviewTab.tsx +0 -206
@@ -1,39 +1,75 @@
1
- import React, { useMemo, useRef, useState } from 'react';
1
+ import React, { useId, useMemo, useRef, useState } from 'react';
2
+ import type { ProjectColorTokenMap, ProjectColors } from '../types/Project';
2
3
  import Modal from './Modal';
3
4
 
4
5
  const SAVED_COLORS_KEY = 'attributes-editor-saved-colors';
5
6
 
6
- const POPULAR_COLORS: Array<{ label: string; value: string }> = [
7
- { label: 'White', value: '#FFFFFF' },
8
- { label: 'Black', value: '#000000' },
9
- { label: 'Text Gray', value: '#1F1F1F' },
10
- { label: 'Muted Text', value: '#4A4A4A' },
11
- { label: 'Background', value: '#F5F5F5' },
12
- { label: 'Primary Blue', value: '#0A84FF' },
13
- { label: 'Success Green', value: '#34C759' },
14
- { label: 'Accent Purple', value: '#AF52DE' },
15
- ];
16
-
17
7
  type ColorOption = {
8
+ id: string;
18
9
  label?: string;
19
10
  value: string;
11
+ tokenLabel?: string;
12
+ };
13
+
14
+ type ColorGroup = {
15
+ title: string;
16
+ options: ColorOption[];
17
+ emptyMessage?: string;
20
18
  };
21
19
 
22
20
  type ColorModalProps = {
23
21
  value?: string;
24
- projectColors?: string[];
22
+ projectColors?: ProjectColors;
25
23
  onSelect: (color: string) => void;
26
24
  onClose: () => void;
27
25
  onClear: () => void;
28
26
  };
29
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
+
30
62
  const readSavedColors = (): string[] => {
31
63
  if (typeof window === 'undefined') return [];
32
64
  try {
33
65
  const stored = window.localStorage.getItem(SAVED_COLORS_KEY);
34
66
  if (!stored) return [];
35
67
  const parsed = JSON.parse(stored);
36
- return Array.isArray(parsed) ? parsed : [];
68
+ if (!Array.isArray(parsed)) return [];
69
+ return parsed
70
+ .filter((value) => typeof value === 'string')
71
+ .map((value) => value.trim().toLowerCase())
72
+ .filter(Boolean);
37
73
  } catch {
38
74
  return [];
39
75
  }
@@ -48,9 +84,59 @@ const persistSavedColors = (colors: string[]) => {
48
84
  }
49
85
  };
50
86
 
87
+ const STATIC_PREFIX = 'STATIC_COLORS.';
88
+ const THEME_PREFIX = 'THEME_COLORS.';
89
+
90
+ export const resolveProjectColorValue = (
91
+ candidate?: string,
92
+ projectColors?: ProjectColors,
93
+ ): string | undefined => {
94
+ if (!candidate || typeof candidate !== 'string' || !projectColors) {
95
+ return undefined;
96
+ }
97
+ const trimmedCandidate = candidate.trim();
98
+ if (!trimmedCandidate) return undefined;
99
+
100
+ if (trimmedCandidate.startsWith(STATIC_PREFIX)) {
101
+ const token = trimmedCandidate.slice(STATIC_PREFIX.length);
102
+ const resolved = projectColors.STATIC_COLORS?.[token];
103
+ return typeof resolved === 'string' && resolved.trim()
104
+ ? resolved.trim()
105
+ : undefined;
106
+ }
107
+
108
+ if (trimmedCandidate.startsWith(THEME_PREFIX)) {
109
+ const remainder = trimmedCandidate.slice(THEME_PREFIX.length);
110
+ if (!remainder) return undefined;
111
+ const themeParts = remainder.split('.');
112
+ if (themeParts.length === 1) {
113
+ const token = themeParts[0];
114
+ const themes = projectColors.THEME_COLORS;
115
+ if (!themes) return undefined;
116
+ for (const themeTokens of Object.values(themes)) {
117
+ const resolved = themeTokens?.[token];
118
+ if (typeof resolved === 'string' && resolved.trim()) {
119
+ return resolved.trim();
120
+ }
121
+ }
122
+ return undefined;
123
+ }
124
+ const [themeName, ...tokenSegments] = themeParts;
125
+ const token = tokenSegments.join('.');
126
+ if (!themeName || !token) return undefined;
127
+ const themeTokens = projectColors.THEME_COLORS?.[themeName];
128
+ const resolved = themeTokens?.[token];
129
+ return typeof resolved === 'string' && resolved.trim()
130
+ ? resolved.trim()
131
+ : undefined;
132
+ }
133
+
134
+ return undefined;
135
+ };
136
+
51
137
  export function ColorModal({
52
138
  value,
53
- projectColors = [],
139
+ projectColors,
54
140
  onSelect,
55
141
  onClose,
56
142
  onClear,
@@ -58,39 +144,66 @@ export function ColorModal({
58
144
  const [savedColors, setSavedColors] = useState<string[]>(() =>
59
145
  readSavedColors(),
60
146
  );
147
+ const [useColorNames, setUseColorNames] = useState(true);
61
148
  const colorInputRef = useRef<HTMLInputElement | null>(null);
149
+ const colorNameToggleId = useId();
150
+ const colorPickerInputId = useId();
151
+
152
+ const selectedColorPreview = useMemo(
153
+ () => resolveProjectColorValue(value, projectColors) ?? value,
154
+ [value, projectColors],
155
+ );
62
156
 
63
- const uniqueProjectColors = useMemo(() => {
64
- const seen = new Set<string>();
65
- return projectColors
66
- .map((color) => color?.trim())
67
- .filter(
68
- (color): color is string =>
69
- Boolean(color) && !seen.has(color!.toLowerCase()),
70
- )
71
- .map((color) => {
72
- seen.add(color.toLowerCase());
73
- return color;
157
+ const projectColorGroups = useMemo<ColorGroup[]>(() => {
158
+ if (!projectColors) return [];
159
+ const groups: ColorGroup[] = [];
160
+
161
+ const staticOptions = mapTokensToOptions(projectColors.STATIC_COLORS, {
162
+ groupKey: 'static',
163
+ tokenLabelPrefix: 'STATIC_COLORS',
164
+ });
165
+ if (staticOptions.length) {
166
+ groups.push({
167
+ title: 'Static colors',
168
+ options: staticOptions,
169
+ emptyMessage: 'No static colors defined.',
74
170
  });
75
- }, [projectColors]);
171
+ }
76
172
 
77
- const projectColorOptions = useMemo<ColorOption[]>(
78
- () => uniqueProjectColors.map((color) => ({ value: color })),
79
- [uniqueProjectColors],
80
- );
173
+ const themeColors = projectColors.THEME_COLORS ?? {};
174
+ Object.entries(themeColors).forEach(([themeName, themeTokens]) => {
175
+ const options = mapTokensToOptions(themeTokens, {
176
+ groupKey: `theme-${themeName || 'default'}`,
177
+ tokenLabelPrefix: 'THEME_COLORS',
178
+ });
179
+ if (!options.length) return;
180
+ const normalizedThemeName = themeName?.trim()
181
+ ? formatTokenLabel(themeName)
182
+ : 'Theme';
183
+ groups.push({
184
+ title: `Theme: ${normalizedThemeName}`,
185
+ options,
186
+ emptyMessage: `No colors defined for ${normalizedThemeName}.`,
187
+ });
188
+ });
189
+
190
+ return groups;
191
+ }, [projectColors]);
81
192
 
82
193
  const savedColorOptions = useMemo<ColorOption[]>(
83
- () => savedColors.map((color) => ({ value: color })),
194
+ () =>
195
+ savedColors.map((color, index) => ({
196
+ id: `saved-${index}-${color}`,
197
+ label: color,
198
+ value: color,
199
+ })),
84
200
  [savedColors],
85
201
  );
86
202
 
87
- const handleAddColorClick = () => {
88
- colorInputRef.current?.click();
89
- };
90
-
91
- const handleColorPicked = (event: React.ChangeEvent<HTMLInputElement>) => {
92
- const picked = event.target.value;
203
+ const applyPickedColor = (raw: string | undefined) => {
204
+ const picked = raw?.trim().toLowerCase();
93
205
  if (!picked) return;
206
+
94
207
  setSavedColors((prev) => {
95
208
  if (prev.includes(picked)) return prev;
96
209
  const next = [...prev, picked];
@@ -99,11 +212,91 @@ export function ColorModal({
99
212
  });
100
213
  onSelect(picked);
101
214
  onClose();
102
- event.target.value = '';
103
215
  };
104
216
 
105
- const handleSelectColor = (hex: string) => {
106
- onSelect(hex);
217
+ const openPickerWithTempInput = () => {
218
+ if (typeof document === 'undefined') return;
219
+
220
+ const temp = document.createElement('input');
221
+ temp.type = 'color';
222
+ temp.value = selectedColorPreview?.toString() || '#000000';
223
+
224
+ // Keep it in the DOM and "not display:none" so Safari/iOS reliably opens it.
225
+ temp.style.position = 'fixed';
226
+ temp.style.left = '-1000px';
227
+ temp.style.top = '0';
228
+ temp.style.width = '40px';
229
+ temp.style.height = '40px';
230
+ temp.style.opacity = '0';
231
+
232
+ const cleanup = () => {
233
+ temp.removeEventListener('change', onChange);
234
+ temp.removeEventListener('input', onChange);
235
+ temp.removeEventListener('blur', cleanup);
236
+ temp.remove();
237
+ };
238
+
239
+ const onChange = () => {
240
+ applyPickedColor(temp.value);
241
+ cleanup();
242
+ };
243
+
244
+ temp.addEventListener('change', onChange);
245
+ temp.addEventListener('input', onChange);
246
+ temp.addEventListener('blur', cleanup);
247
+
248
+ document.body.appendChild(temp);
249
+ try {
250
+ temp.focus({ preventScroll: true });
251
+ } catch {
252
+ // no-op
253
+ }
254
+ // Next tick helps some browsers recognize it as a user-gesture flow.
255
+ requestAnimationFrame(() => temp.click());
256
+ };
257
+
258
+ const handleAddColorClick = () => {
259
+ const input = colorInputRef.current;
260
+
261
+ // Prefer showPicker when available (Chromium).
262
+ const maybeShowPicker = input
263
+ ? (
264
+ input as HTMLInputElement & {
265
+ showPicker?: () => void;
266
+ }
267
+ ).showPicker
268
+ : undefined;
269
+
270
+ if (input && typeof maybeShowPicker === 'function') {
271
+ try {
272
+ input.focus({ preventScroll: true });
273
+ } catch {
274
+ // no-op
275
+ }
276
+ maybeShowPicker.call(input);
277
+ return;
278
+ }
279
+
280
+ // Fallback: temporary input for Safari/iOS and browsers that block click on hidden inputs.
281
+ openPickerWithTempInput();
282
+ };
283
+
284
+ const handlePreviewKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
285
+ if (event.key !== 'Enter' && event.key !== ' ') return;
286
+ event.preventDefault();
287
+ handleAddColorClick();
288
+ };
289
+
290
+ const handleColorPicked = (event: React.ChangeEvent<HTMLInputElement>) => {
291
+ applyPickedColor(event.target.value);
292
+ // Keep the input value valid for type="color" (empty string can be invalid).
293
+ event.target.value = '#000000';
294
+ };
295
+
296
+ const handleSelectColor = (option: ColorOption) => {
297
+ const nextValue =
298
+ useColorNames && option.tokenLabel ? option.tokenLabel : option.value;
299
+ onSelect(nextValue);
107
300
  onClose();
108
301
  };
109
302
 
@@ -124,11 +317,16 @@ export function ColorModal({
124
317
 
125
318
  <div className="color-modal__selected">
126
319
  <div className="color-modal__selected-info">
127
- <span
128
- aria-hidden
320
+ <label
321
+ htmlFor={colorPickerInputId}
322
+ role="button"
323
+ aria-label="Open color picker"
324
+ tabIndex={0}
325
+ title="Pick a color"
129
326
  className="color-modal__selected-preview"
130
- style={{ background: value ?? 'transparent' }}
131
- />
327
+ style={{ background: selectedColorPreview ?? 'transparent' }}
328
+ onKeyDown={handlePreviewKeyDown}
329
+ ></label>
132
330
  <div>
133
331
  <p className="color-modal__selected-label">Selected color</p>
134
332
  <p className="color-modal__selected-value">{value ?? 'None'}</p>
@@ -145,44 +343,74 @@ export function ColorModal({
145
343
  ) : null}
146
344
  </div>
147
345
 
148
- <ColorSection
149
- title="Project colors"
150
- options={projectColorOptions}
151
- emptyMessage="No project colors detected yet."
152
- activeValue={value}
153
- onSelect={handleSelectColor}
154
- />
346
+ <div className="color-modal__toggle">
347
+ <label
348
+ htmlFor={colorNameToggleId}
349
+ className="color-modal__toggle-label"
350
+ >
351
+ <input
352
+ id={colorNameToggleId}
353
+ type="checkbox"
354
+ className="color-modal__toggle-input"
355
+ checked={useColorNames}
356
+ onChange={(event) => setUseColorNames(event.target.checked)}
357
+ />
358
+ Use color names
359
+ </label>
360
+ <p className="color-modal__toggle-description">
361
+ Output tokens like STATIC_COLORS.PRIMARY instead of raw values.
362
+ </p>
363
+ </div>
364
+
365
+ {projectColorGroups.length ? (
366
+ projectColorGroups.map((group) => (
367
+ <ColorSection
368
+ key={group.title}
369
+ title={group.title}
370
+ options={group.options}
371
+ emptyMessage={group.emptyMessage}
372
+ activeValue={value}
373
+ onSelect={handleSelectColor}
374
+ />
375
+ ))
376
+ ) : (
377
+ <ColorSection
378
+ title="Project colors"
379
+ options={[]}
380
+ emptyMessage="No project colors detected yet."
381
+ activeValue={value}
382
+ onSelect={handleSelectColor}
383
+ />
384
+ )}
155
385
 
156
386
  <ColorSection
157
387
  title="Saved colors"
158
388
  options={savedColorOptions}
159
389
  emptyMessage="Add colors you use often for quick access."
160
390
  action={
161
- <button
162
- type="button"
163
- className="color-modal__link-button"
164
- onClick={handleAddColorClick}
165
- >
391
+ <span className="color-modal__link-button color-modal__add-color">
166
392
  Add color
167
- </button>
393
+ <input
394
+ type="color"
395
+ className="color-modal__add-color-input"
396
+ onChange={handleColorPicked}
397
+ defaultValue="#000000"
398
+ aria-label="Add color"
399
+ />
400
+ </span>
168
401
  }
169
402
  activeValue={value}
170
403
  onSelect={handleSelectColor}
171
404
  />
172
405
 
173
- <ColorSection
174
- title="Popular colors"
175
- options={POPULAR_COLORS}
176
- emptyMessage="Popular palettes unavailable."
177
- activeValue={value}
178
- onSelect={handleSelectColor}
179
- />
180
-
181
406
  <input
182
407
  ref={colorInputRef}
408
+ id={colorPickerInputId}
183
409
  type="color"
184
410
  className="color-modal__input"
185
411
  onChange={handleColorPicked}
412
+ defaultValue="#000000"
413
+ tabIndex={-1}
186
414
  />
187
415
  </Modal>
188
416
  );
@@ -194,7 +422,7 @@ type ColorSectionProps = {
194
422
  emptyMessage?: string;
195
423
  action?: React.ReactNode;
196
424
  activeValue?: string;
197
- onSelect: (value: string) => void;
425
+ onSelect: (option: ColorOption) => void;
198
426
  };
199
427
 
200
428
  function ColorSection({
@@ -215,10 +443,13 @@ function ColorSection({
215
443
  <div className="color-section__swatches">
216
444
  {options.map((option) => (
217
445
  <ColorSwatch
218
- key={`${title}-${option.value}`}
446
+ key={option.id}
219
447
  option={option}
220
- isActive={option.value === activeValue}
221
- onSelect={() => onSelect(option.value)}
448
+ isActive={
449
+ (option.tokenLabel && option.tokenLabel === activeValue) ||
450
+ option.value === activeValue
451
+ }
452
+ onSelect={() => onSelect(option)}
222
453
  />
223
454
  ))}
224
455
  </div>
@@ -260,7 +491,11 @@ function ColorSwatch({ option, isActive, onSelect }: ColorSwatchProps) {
260
491
  <span className="color-modal__swatch-label">
261
492
  {option.label ?? option.value}
262
493
  </span>
263
- <span className="color-modal__swatch-value">{option.value}</span>
494
+ <span className="color-modal__swatch-value">
495
+ {option.tokenLabel
496
+ ? `${option.tokenLabel} - ${option.value}`
497
+ : option.value}
498
+ </span>
264
499
  </button>
265
500
  );
266
501
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { JsonEditor } from 'json-edit-react';
2
+ import { JsonTextEditor } from '../components/JsonTextEditor';
3
3
  import { Localication } from '../types/PreviewConfig';
4
4
  import Modal from './Modal';
5
5
 
@@ -38,12 +38,11 @@ export function LocalicationModal({
38
38
  </div>
39
39
  <div className="localication-modal__body">
40
40
  <div className="localication-modal__editor">
41
- <JsonEditor
41
+ <JsonTextEditor
42
42
  rootName="localication"
43
- data={normalizedData}
44
- setData={(next) => onChange(next as Localication)}
43
+ value={normalizedData}
44
+ onChange={(next) => onChange(next as Localication)}
45
45
  className="localication-modal__json-editor"
46
- maxWidth={'100%'}
47
46
  />
48
47
  </div>
49
48
  </div>
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect } from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
 
3
4
  type ModalProps = {
4
5
  onClose: () => void;
@@ -33,7 +34,7 @@ export function Modal({
33
34
  return () => window.removeEventListener('keydown', handleKeyDown);
34
35
  }, [closeOnEsc, onClose]);
35
36
 
36
- return (
37
+ const modalNode = (
37
38
  <div
38
39
  className={`modal${className ? ` ${className}` : ''}`}
39
40
  role="dialog"
@@ -52,6 +53,12 @@ export function Modal({
52
53
  </div>
53
54
  </div>
54
55
  );
56
+
57
+ if (typeof document === 'undefined') {
58
+ return modalNode;
59
+ }
60
+
61
+ return createPortal(modalNode, document.body);
55
62
  }
56
63
 
57
64
  export default Modal;