@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.
- package/dist/AttributesEditor.d.ts +3 -1
- package/dist/RenderPage.d.ts +2 -1
- package/dist/android.svg +43 -0
- package/dist/apple.svg +16 -0
- package/dist/attributes-editor/Field.d.ts +4 -2
- package/dist/attributes-editor/SizeField.d.ts +9 -0
- package/dist/attributes-editor/SpecialCategorySection.d.ts +2 -1
- package/dist/build-components/BackgroundImage/BackgroundImage.d.ts +5 -0
- package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +45 -0
- package/dist/build-components/Button/ButtonProps.generated.d.ts +8 -0
- package/dist/build-components/Carousel/CarouselProps.generated.d.ts +8 -0
- package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +8 -0
- package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +8 -0
- package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +8 -0
- package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +8 -0
- package/dist/build-components/Image/ImageProps.generated.d.ts +8 -0
- package/dist/build-components/Onboard/OnboardProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +8 -1
- package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +9 -3
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +9 -1
- package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +8 -1
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +8 -0
- package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +8 -0
- package/dist/build-components/Text/TextProps.generated.d.ts +8 -0
- package/dist/build-components/View/ViewProps.generated.d.ts +8 -0
- package/dist/build-components/index.d.ts +2 -1
- package/dist/build-components/patterns.generated.d.ts +1612 -46
- package/dist/components/AttributesEditorPanel.d.ts +3 -4
- package/dist/components/Builder.d.ts +2 -1
- package/dist/components/BuilderButton.d.ts +9 -0
- package/dist/components/JsonTextEditor.d.ts +9 -0
- package/dist/index.cjs.js +5 -5
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +5 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/modals/ColorModal.d.ts +3 -1
- package/dist/pages/ProjectPage.d.ts +3 -3
- package/dist/pages/tabs/BuilderPanel.d.ts +8 -0
- package/dist/pages/tabs/SideTool.d.ts +8 -0
- package/dist/store.d.ts +9 -1
- package/dist/styles.css +1 -1
- package/dist/types/Project.d.ts +11 -0
- package/dist/utils/analyseNode.d.ts +1 -0
- package/dist/utils/extractImageStyle.d.ts +2 -1
- package/dist/utils/extractTextStyle.d.ts +8 -1
- package/dist/utils/extractViewStyle.d.ts +7 -1
- package/dist/utils/parseColor.d.ts +7 -0
- package/dist/utils/selection.d.ts +7 -0
- package/dist/utils/useMergedStyle.d.ts +2 -0
- package/package.json +2 -5
- package/src/.DS_Store +0 -0
- package/src/AttributesEditor.tsx +83 -16
- package/src/RenderPage.tsx +86 -4
- package/src/attributes-editor/Field.tsx +60 -165
- package/src/attributes-editor/SizeField.tsx +184 -0
- package/src/attributes-editor/SpecialCategorySection.tsx +12 -4
- package/src/build-components/BackgroundImage/BackgroundImage.tsx +77 -0
- package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +61 -0
- package/src/build-components/BackgroundImage/pattern.json +45 -0
- package/src/build-components/Button/Button.tsx +29 -4
- package/src/build-components/Button/ButtonProps.generated.ts +8 -0
- package/src/build-components/Carousel/Carousel.tsx +25 -3
- package/src/build-components/Carousel/CarouselProps.generated.ts +8 -0
- package/src/build-components/CarouselButtons/CarouselButtons.tsx +19 -4
- package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +8 -0
- package/src/build-components/CarouselDots/CarouselDots.tsx +13 -4
- package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +8 -0
- package/src/build-components/CarouselItem/CarouselItem.tsx +20 -4
- package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +8 -0
- package/src/build-components/CarouselProvider/CarouselProvider.tsx +14 -3
- package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +8 -0
- package/src/build-components/Image/Image.tsx +27 -9
- package/src/build-components/Image/ImageProps.generated.ts +8 -0
- package/src/build-components/Image/pattern.json +1 -9
- package/src/build-components/Onboard/Onboard.tsx +2 -2
- package/src/build-components/Onboard/OnboardProps.generated.ts +8 -0
- package/src/build-components/OnboardButton/OnboardButton.tsx +11 -7
- package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +8 -1
- package/src/build-components/OnboardButtons/OnboardButtons.tsx +17 -5
- package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +8 -0
- package/src/build-components/OnboardDot/OnboardDot.tsx +68 -39
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +9 -3
- package/src/build-components/OnboardDot/pattern.json +3 -19
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +37 -14
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +8 -0
- package/src/build-components/OnboardImage/OnboardImage.tsx +28 -6
- package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +9 -1
- package/src/build-components/OnboardItem/OnboardItem.tsx +15 -14
- package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +8 -0
- package/src/build-components/OnboardProvider/OnboardProvider.tsx +35 -20
- package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +8 -1
- package/src/build-components/OnboardProvider/pattern.json +0 -8
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +8 -0
- package/src/build-components/OnboardSubtitle/pattern.json +1 -1
- package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +8 -0
- package/src/build-components/OnboardTitle/pattern.json +1 -1
- package/src/build-components/RenderNode.generated.tsx +3 -0
- package/src/build-components/Text/Text.tsx +28 -10
- package/src/build-components/Text/TextProps.generated.ts +8 -0
- package/src/build-components/View/View.tsx +25 -3
- package/src/build-components/View/ViewProps.generated.ts +8 -0
- package/src/build-components/View/pattern.json +67 -1
- package/src/build-components/index.ts +5 -0
- package/src/build-components/patterns.generated.ts +1620 -46
- package/src/components/AttributesEditorPanel.tsx +13 -6
- package/src/components/Builder.tsx +200 -56
- package/src/components/BuilderButton.tsx +127 -0
- package/src/components/DeviceNavigationBar.tsx +0 -1
- package/src/components/EditorHeader.tsx +11 -1
- package/src/components/JsonTextEditor.tsx +185 -0
- package/src/index.ts +2 -2
- package/src/mockOS/components/MockOSRouter.tsx +17 -3
- package/src/mockOS/context/MockOSContext.tsx +0 -5
- package/src/mockOS/managers/mockPermissionManager.ts +0 -4
- package/src/mockOS/managers/navigationManager.ts +1 -6
- package/src/modals/ColorModal.tsx +306 -71
- package/src/modals/LocalicationModal.tsx +4 -5
- package/src/modals/Modal.tsx +8 -1
- package/src/pages/ProjectPage.tsx +299 -55
- package/src/pages/tabs/{BuilderTab.tsx → BuilderPanel.tsx} +13 -9
- package/src/pages/tabs/SideTool.tsx +260 -0
- package/src/size-matters/index.ts +6 -0
- package/src/store.ts +18 -1
- package/src/styles/base/_global.scss +163 -7
- package/src/styles/components/_attributes-editor.scss +12 -0
- package/src/styles/components/_editor-shell.scss +25 -0
- package/src/styles/foundation/_sizes.scss +1 -1
- package/src/styles/layout/_builder.scss +66 -10
- package/src/styles/modals/_color-modal.scss +59 -1
- package/src/styles/utilities/_carousel.scss +9 -8
- package/src/types/Project.ts +14 -0
- package/src/utils/analyseNode.ts +98 -0
- package/src/utils/extractImageStyle.ts +3 -6
- package/src/utils/extractTextStyle.ts +19 -82
- package/src/utils/extractViewStyle.ts +41 -12
- package/src/utils/parseColor.ts +43 -0
- package/src/utils/selection.ts +24 -0
- package/src/utils/useMergedStyle.ts +16 -0
- package/dist/pages/tabs/BuilderTab.d.ts +0 -9
- package/dist/pages/tabs/DebugTab.d.ts +0 -7
- package/dist/pages/tabs/PreviewTab.d.ts +0 -3
- package/src/pages/tabs/DebugTab.tsx +0 -64
- 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?:
|
|
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
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
171
|
+
}
|
|
76
172
|
|
|
77
|
-
|
|
78
|
-
()
|
|
79
|
-
|
|
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
|
-
() =>
|
|
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
|
|
88
|
-
|
|
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
|
|
106
|
-
|
|
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
|
-
<
|
|
128
|
-
|
|
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:
|
|
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
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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: (
|
|
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={
|
|
446
|
+
key={option.id}
|
|
219
447
|
option={option}
|
|
220
|
-
isActive={
|
|
221
|
-
|
|
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">
|
|
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 {
|
|
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
|
-
<
|
|
41
|
+
<JsonTextEditor
|
|
42
42
|
rootName="localication"
|
|
43
|
-
|
|
44
|
-
|
|
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>
|
package/src/modals/Modal.tsx
CHANGED
|
@@ -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
|
-
|
|
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;
|