@developer_tribe/react-builder 1.0.1 → 1.0.2
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/DeviceMockFrame.d.ts +2 -1
- package/dist/RenderPage.d.ts +4 -3
- package/dist/attributes-editor/Field.d.ts +16 -0
- package/dist/attributes-editor/FieldInfoTooltip.d.ts +7 -0
- package/dist/attributes-editor/LayoutPreviewPicker.d.ts +12 -0
- package/dist/attributes-editor/SpecialCategorySection.d.ts +19 -0
- package/dist/attributes-editor/types.d.ts +14 -0
- package/dist/background.jpg +0 -0
- package/dist/build-components/Button/Button.d.ts +1 -1
- package/dist/build-components/Button/ButtonProps.generated.d.ts +26 -1
- package/dist/build-components/Carousel/CarouselProps.generated.d.ts +27 -1
- package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +25 -0
- package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +25 -0
- package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +27 -1
- package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +27 -1
- package/dist/build-components/Image/ImageProps.generated.d.ts +25 -3
- package/dist/build-components/Onboard/OnboardProps.generated.d.ts +27 -1
- package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +25 -0
- package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +25 -0
- package/dist/build-components/OnboardDot/OnboardDot.d.ts +1 -1
- package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +22 -0
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +4 -5
- package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +25 -3
- package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +24 -3
- package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +25 -4
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +4 -5
- package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +4 -5
- package/dist/build-components/Text/TextProps.generated.d.ts +4 -5
- package/dist/build-components/View/ViewProps.generated.d.ts +3 -4
- package/dist/build-components/patterns.generated.d.ts +4855 -132
- package/dist/components/Breadcrumb.d.ts +3 -1
- package/dist/components/Checkbox.d.ts +17 -0
- package/dist/components/DeviceButton.d.ts +8 -0
- package/dist/components/DeviceNavigationBar.d.ts +10 -0
- package/dist/components/DeviceStatusBar.d.ts +9 -0
- package/dist/components/EditorHeader.d.ts +3 -8
- package/dist/index.cjs.js +5 -5
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +5 -5
- package/dist/index.esm.js.map +1 -1
- package/dist/mockOS/components/MockLaunchScreenComponent.d.ts +6 -0
- package/dist/mockOS/components/MockOSRouter.d.ts +8 -0
- package/dist/mockOS/components/PermissionModal.d.ts +9 -0
- package/dist/mockOS/context/MockOSContext.d.ts +36 -0
- package/dist/mockOS/hooks/useMockNavigation.d.ts +3 -0
- package/dist/mockOS/hooks/useMockPermission.d.ts +3 -0
- package/dist/mockOS/index.d.ts +9 -0
- package/dist/mockOS/managers/mockPermissionManager.d.ts +10 -0
- package/dist/mockOS/managers/navigationManager.d.ts +17 -0
- package/dist/modals/AddComponentModal.d.ts +8 -0
- package/dist/modals/ColorModal.d.ts +9 -0
- package/dist/modals/DeviceSelectorModal.d.ts +9 -0
- package/dist/modals/LocalicationModal.d.ts +8 -0
- package/dist/modals/Modal.d.ts +12 -0
- package/dist/modals/index.d.ts +5 -0
- package/dist/pages/ProjectPage.d.ts +1 -1
- package/dist/store.d.ts +0 -2
- package/dist/styles.css +1 -1
- package/dist/utils/patterns.d.ts +24 -0
- package/package.json +2 -1
- package/scripts/prebuild/utils/createGeneratedProps.js +11 -3
- package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +45 -6
- package/scripts/prebuild/utils/validatePatternJson.js +13 -5
- package/src/AttributesEditor.tsx +433 -312
- package/src/DeviceMockFrame.tsx +21 -37
- package/src/RenderPage.tsx +5 -4
- package/src/assets/images/android.svg +42 -42
- package/src/assets/images/apple.svg +15 -15
- package/src/attributes-editor/Field.tsx +662 -0
- package/src/attributes-editor/FieldInfoTooltip.tsx +49 -0
- package/src/attributes-editor/LayoutPreviewPicker.tsx +199 -0
- package/src/attributes-editor/SpecialCategorySection.tsx +284 -0
- package/src/attributes-editor/types.ts +30 -0
- package/src/build-components/Button/Button.tsx +10 -2
- package/src/build-components/Button/ButtonProps.generated.ts +37 -1
- package/src/build-components/Button/pattern.json +31 -2
- package/src/build-components/Carousel/Carousel.tsx +15 -2
- package/src/build-components/Carousel/CarouselProps.generated.ts +39 -1
- package/src/build-components/Carousel/pattern.json +10 -0
- package/src/build-components/CarouselButtons/CarouselButtons.tsx +6 -2
- package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +36 -0
- package/src/build-components/CarouselButtons/pattern.json +22 -0
- package/src/build-components/CarouselDots/CarouselDots.tsx +40 -8
- package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +36 -0
- package/src/build-components/CarouselDots/pattern.json +15 -0
- package/src/build-components/CarouselItem/CarouselItem.tsx +5 -2
- package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +39 -1
- package/src/build-components/CarouselItem/pattern.json +7 -0
- package/src/build-components/CarouselProvider/CarouselProvider.tsx +10 -2
- package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +39 -1
- package/src/build-components/CarouselProvider/pattern.json +7 -0
- package/src/build-components/Image/Image.tsx +8 -2
- package/src/build-components/Image/ImageProps.generated.ts +36 -3
- package/src/build-components/Image/pattern.json +46 -3
- package/src/build-components/Onboard/Onboard.tsx +6 -1
- package/src/build-components/Onboard/OnboardProps.generated.ts +39 -1
- package/src/build-components/Onboard/pattern.json +11 -0
- package/src/build-components/OnboardButton/OnboardButton.tsx +46 -5
- package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +36 -0
- package/src/build-components/OnboardButton/pattern.json +71 -5
- package/src/build-components/OnboardButtons/OnboardButtons.tsx +20 -10
- package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +36 -0
- package/src/build-components/OnboardButtons/pattern.json +70 -4
- package/src/build-components/OnboardDot/OnboardDot.tsx +104 -4
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +22 -0
- package/src/build-components/OnboardDot/pattern.json +54 -1
- package/src/build-components/OnboardFooter/OnboardFooter.tsx +9 -3
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +4 -5
- package/src/build-components/OnboardFooter/pattern.json +58 -2
- package/src/build-components/OnboardImage/OnboardImage.tsx +27 -5
- package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +36 -3
- package/src/build-components/OnboardImage/pattern.json +21 -0
- package/src/build-components/OnboardItem/OnboardItem.tsx +6 -1
- package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +35 -3
- package/src/build-components/OnboardItem/pattern.json +38 -2
- package/src/build-components/OnboardProvider/OnboardProvider.tsx +20 -8
- package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +37 -4
- package/src/build-components/OnboardProvider/pattern.json +51 -4
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +4 -5
- package/src/build-components/OnboardSubtitle/pattern.json +6 -0
- package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +4 -5
- package/src/build-components/OnboardTitle/pattern.json +6 -0
- package/src/build-components/Text/Text.tsx +7 -3
- package/src/build-components/Text/TextProps.generated.ts +4 -5
- package/src/build-components/Text/pattern.json +38 -2
- package/src/build-components/View/View.tsx +9 -6
- package/src/build-components/View/ViewProps.generated.ts +3 -4
- package/src/build-components/View/pattern.json +227 -19
- package/src/build-components/patterns.generated.ts +4905 -139
- package/src/components/AttributesEditorPanel.tsx +7 -61
- package/src/components/Breadcrumb.tsx +37 -5
- package/src/components/Builder.tsx +180 -77
- package/src/components/Checkbox.tsx +81 -0
- package/src/components/DeviceButton.tsx +39 -0
- package/src/components/DeviceNavigationBar.tsx +201 -0
- package/src/components/DeviceStatusBar.tsx +85 -0
- package/src/components/EditorHeader.tsx +26 -74
- package/src/mockOS/components/MockLaunchScreenComponent.tsx +43 -0
- package/src/mockOS/components/MockOSRouter.tsx +115 -0
- package/src/mockOS/components/PermissionModal.tsx +270 -0
- package/src/mockOS/context/MockOSContext.tsx +179 -0
- package/src/mockOS/hooks/useMockNavigation.ts +11 -0
- package/src/mockOS/hooks/useMockPermission.ts +11 -0
- package/src/mockOS/index.ts +26 -0
- package/src/mockOS/managers/mockPermissionManager.ts +54 -0
- package/src/mockOS/managers/navigationManager.ts +91 -0
- package/src/modals/AddComponentModal.tsx +313 -0
- package/src/modals/ColorModal.tsx +268 -0
- package/src/modals/DeviceSelectorModal.tsx +57 -0
- package/src/modals/LocalicationModal.tsx +54 -0
- package/src/modals/Modal.tsx +57 -0
- package/src/modals/index.ts +5 -0
- package/src/pages/ProjectPage.tsx +19 -21
- package/src/pages/tabs/DebugTab.tsx +50 -9
- package/src/pages/tabs/PreviewTab.tsx +52 -40
- package/src/size-matters/index.ts +21 -5
- package/src/store.ts +0 -4
- package/src/styles/{global.scss → base/_global.scss} +92 -39
- package/src/styles/components/_attributes-editor.scss +261 -0
- package/src/styles/{editor.scss → components/_editor-shell.scss} +72 -57
- package/src/styles/components/_mockos-router.scss +140 -0
- package/src/styles/components/_ui-components.scss +183 -0
- package/src/styles/foundation/_colors.scss +8 -0
- package/src/styles/{_mixins.scss → foundation/_mixins.scss} +5 -4
- package/src/styles/{_reset.scss → foundation/_reset.scss} +5 -2
- package/src/styles/foundation/_sizes.scss +37 -0
- package/src/styles/foundation/_typography.scss +4 -0
- package/src/styles/foundation/_variables.scss +3 -0
- package/src/styles/index.scss +22 -136
- package/src/styles/layout/_builder.scss +68 -0
- package/src/styles/layout/_pages.scss +3 -0
- package/src/styles/modals/_add-component.scss +122 -0
- package/src/styles/modals/_color-modal.scss +130 -0
- package/src/styles/modals/_device-selector.scss +18 -0
- package/src/styles/modals/_localication-modal.scss +68 -0
- package/src/styles/modals/_modal-shell.scss +46 -0
- package/src/styles/utilities/_carousel.scss +125 -0
- package/src/types/images.d.ts +8 -0
- package/src/utils/extractTextStyle.ts +4 -2
- package/src/utils/extractViewStyle.ts +51 -7
- package/src/utils/patterns.ts +33 -0
- package/dist/build-components/OnboardDot/OnboardExpandingDotProps.generated.d.ts +0 -10
- package/src/build-components/OnboardDot/OnboardExpandingDotProps.generated.ts +0 -20
- package/src/styles/_variables.scss +0 -27
- package/src/styles/builder.scss +0 -60
- package/src/styles/components.scss +0 -88
- package/src/styles/pages.scss +0 -2
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import Modal from './Modal';
|
|
3
|
+
import { getPatternByType } from '../utils/patterns';
|
|
4
|
+
|
|
5
|
+
type AddComponentModalProps = {
|
|
6
|
+
allowedChildTypes: string[];
|
|
7
|
+
parentType?: string | null;
|
|
8
|
+
onSelect: (type: string) => void;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type ComponentOption = {
|
|
13
|
+
type: string;
|
|
14
|
+
label: string;
|
|
15
|
+
desiredParents: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type ComponentGroup = {
|
|
19
|
+
token: string;
|
|
20
|
+
displayLabel: string;
|
|
21
|
+
options: ComponentOption[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const FALLBACK_PARENT = 'all';
|
|
25
|
+
const EXCLUDE_PREFIX = '!=';
|
|
26
|
+
|
|
27
|
+
function normalizeDesiredParents(list?: string[]): string[] {
|
|
28
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
29
|
+
return [FALLBACK_PARENT];
|
|
30
|
+
}
|
|
31
|
+
return list;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function matchesDesiredChildToken(token: string, childType: string): boolean {
|
|
35
|
+
if (token.startsWith('=')) return token.slice(1) === childType;
|
|
36
|
+
if (token.startsWith('>')) return token.slice(1) === childType;
|
|
37
|
+
return token === childType;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function filterOptionsByDesiredChildren(
|
|
41
|
+
options: ComponentOption[],
|
|
42
|
+
desiredChildren?: string[],
|
|
43
|
+
): ComponentOption[] {
|
|
44
|
+
if (!Array.isArray(desiredChildren) || desiredChildren.length === 0) {
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tokens = desiredChildren
|
|
49
|
+
.map((token) => token.trim())
|
|
50
|
+
.filter((token) => token.length > 0);
|
|
51
|
+
|
|
52
|
+
if (tokens.length === 0) {
|
|
53
|
+
return options;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return options.filter(({ type }) => {
|
|
57
|
+
let hasPositiveRule = false;
|
|
58
|
+
let matchesPositiveRule = false;
|
|
59
|
+
|
|
60
|
+
for (const token of tokens) {
|
|
61
|
+
if (token.startsWith(EXCLUDE_PREFIX)) {
|
|
62
|
+
if (type === token.slice(EXCLUDE_PREFIX.length)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (token === FALLBACK_PARENT) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
hasPositiveRule = true;
|
|
73
|
+
if (matchesDesiredChildToken(token, type)) {
|
|
74
|
+
matchesPositiveRule = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!hasPositiveRule) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return matchesPositiveRule;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatTokenLabel(token: string): string {
|
|
87
|
+
if (token === 'all') return 'Anywhere';
|
|
88
|
+
if (token === 'root') return 'Root only';
|
|
89
|
+
if (token.startsWith('!=')) return `Not under ${token.slice(2)}`;
|
|
90
|
+
if (token.startsWith('=')) return `Direct child of ${token.slice(1)}`;
|
|
91
|
+
if (token.startsWith('>')) return `Inside ${token.slice(1)}`;
|
|
92
|
+
return token;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tokenMatchesParent(
|
|
96
|
+
token: string,
|
|
97
|
+
parentType?: string | null,
|
|
98
|
+
): boolean {
|
|
99
|
+
if (token === 'all') return true;
|
|
100
|
+
if (!parentType || parentType.length === 0) {
|
|
101
|
+
return token === 'root';
|
|
102
|
+
}
|
|
103
|
+
if (token.startsWith('!=')) return false;
|
|
104
|
+
if (token.startsWith('=')) return token.slice(1) === parentType;
|
|
105
|
+
if (token.startsWith('>')) return token.slice(1) === parentType;
|
|
106
|
+
return token === parentType;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildGroups(
|
|
110
|
+
options: ComponentOption[],
|
|
111
|
+
tokenFilter?: (token: string, option: ComponentOption) => boolean,
|
|
112
|
+
): ComponentGroup[] {
|
|
113
|
+
const map = new Map<string, ComponentOption[]>();
|
|
114
|
+
|
|
115
|
+
options.forEach((option) => {
|
|
116
|
+
option.desiredParents.forEach((token) => {
|
|
117
|
+
if (tokenFilter && !tokenFilter(token, option)) return;
|
|
118
|
+
const existing = map.get(token);
|
|
119
|
+
if (existing) {
|
|
120
|
+
if (!existing.some((item) => item.type === option.type)) {
|
|
121
|
+
existing.push(option);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
map.set(token, [option]);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return Array.from(map.entries())
|
|
130
|
+
.map(([token, bucket]) => ({
|
|
131
|
+
token,
|
|
132
|
+
displayLabel: formatTokenLabel(token),
|
|
133
|
+
options: bucket.sort((a, b) => a.label.localeCompare(b.label)),
|
|
134
|
+
}))
|
|
135
|
+
.sort((a, b) => a.displayLabel.localeCompare(b.displayLabel));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function AddComponentModal({
|
|
139
|
+
allowedChildTypes,
|
|
140
|
+
parentType,
|
|
141
|
+
onSelect,
|
|
142
|
+
onClose,
|
|
143
|
+
}: AddComponentModalProps) {
|
|
144
|
+
const [showAllComponents, setShowAllComponents] = useState(false);
|
|
145
|
+
|
|
146
|
+
const componentOptions = useMemo<ComponentOption[]>(() => {
|
|
147
|
+
const uniqueTypes = Array.from(new Set(allowedChildTypes));
|
|
148
|
+
return uniqueTypes
|
|
149
|
+
.map((type) => {
|
|
150
|
+
const pattern = getPatternByType(type);
|
|
151
|
+
const label = pattern?.meta?.label?.trim() || type;
|
|
152
|
+
const desiredParents = normalizeDesiredParents(
|
|
153
|
+
pattern?.meta?.desiredParent,
|
|
154
|
+
);
|
|
155
|
+
return {
|
|
156
|
+
type,
|
|
157
|
+
label,
|
|
158
|
+
desiredParents,
|
|
159
|
+
};
|
|
160
|
+
})
|
|
161
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
162
|
+
}, [allowedChildTypes]);
|
|
163
|
+
|
|
164
|
+
const parentDesiredChildren = useMemo<string[] | undefined>(() => {
|
|
165
|
+
if (!parentType) return undefined;
|
|
166
|
+
const desiredChildren = getPatternByType(parentType)?.meta?.desiredChildren;
|
|
167
|
+
if (!Array.isArray(desiredChildren) || desiredChildren.length === 0) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
return desiredChildren
|
|
171
|
+
.map((token) => token.trim())
|
|
172
|
+
.filter((token) => token.length > 0);
|
|
173
|
+
}, [parentType]);
|
|
174
|
+
|
|
175
|
+
const filteredComponentOptions = useMemo<ComponentOption[]>(
|
|
176
|
+
() =>
|
|
177
|
+
filterOptionsByDesiredChildren(componentOptions, parentDesiredChildren),
|
|
178
|
+
[componentOptions, parentDesiredChildren],
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const canToggleAllComponents = !parentDesiredChildren?.length;
|
|
182
|
+
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!canToggleAllComponents) {
|
|
185
|
+
setShowAllComponents(false);
|
|
186
|
+
}
|
|
187
|
+
}, [canToggleAllComponents]);
|
|
188
|
+
|
|
189
|
+
const recommendedGroups = useMemo<ComponentGroup[]>(
|
|
190
|
+
() =>
|
|
191
|
+
buildGroups(filteredComponentOptions, (token) =>
|
|
192
|
+
tokenMatchesParent(token, parentType),
|
|
193
|
+
),
|
|
194
|
+
[filteredComponentOptions, parentType],
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const allGroups = useMemo<ComponentGroup[]>(
|
|
198
|
+
() => buildGroups(filteredComponentOptions),
|
|
199
|
+
[filteredComponentOptions],
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const handleSelect = (type: string) => {
|
|
203
|
+
onSelect(type);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const renderOption = (option: ComponentOption) => (
|
|
207
|
+
<button
|
|
208
|
+
key={option.type}
|
|
209
|
+
type="button"
|
|
210
|
+
className="add-component-modal__card"
|
|
211
|
+
onClick={() => handleSelect(option.type)}
|
|
212
|
+
role="listitem"
|
|
213
|
+
>
|
|
214
|
+
<div className="add-component-modal__thumbnail" aria-hidden="true">
|
|
215
|
+
<span>Preview</span>
|
|
216
|
+
</div>
|
|
217
|
+
<span className="add-component-modal__title">{option.label}</span>
|
|
218
|
+
</button>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const renderGroups = (groups: ComponentGroup[]) =>
|
|
222
|
+
groups.map((group) => (
|
|
223
|
+
<div key={group.token} className="add-component-modal__group">
|
|
224
|
+
<h5 className="add-component-modal__group-title">
|
|
225
|
+
{group.displayLabel}
|
|
226
|
+
</h5>
|
|
227
|
+
<div className="add-component-modal__grid" role="list">
|
|
228
|
+
{group.options.map(renderOption)}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
));
|
|
232
|
+
|
|
233
|
+
const hasRecommended = recommendedGroups.length > 0;
|
|
234
|
+
const hasComponents = filteredComponentOptions.length > 0;
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<Modal
|
|
238
|
+
onClose={onClose}
|
|
239
|
+
ariaLabelledBy="add-component-title"
|
|
240
|
+
contentClassName="add-component-modal"
|
|
241
|
+
>
|
|
242
|
+
<div className="modal__header">
|
|
243
|
+
<h3 id="add-component-title" className="modal__title">
|
|
244
|
+
Add component
|
|
245
|
+
</h3>
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
className="editor-button"
|
|
249
|
+
aria-label="Close add component modal"
|
|
250
|
+
onClick={onClose}
|
|
251
|
+
>
|
|
252
|
+
Close
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div className="add-component-modal__body">
|
|
257
|
+
<section className="add-component-modal__section">
|
|
258
|
+
<h4 className="add-component-modal__section-title">
|
|
259
|
+
Recommended Components
|
|
260
|
+
</h4>
|
|
261
|
+
{hasRecommended ? (
|
|
262
|
+
renderGroups(recommendedGroups)
|
|
263
|
+
) : (
|
|
264
|
+
<p className="add-component-modal__empty">
|
|
265
|
+
{hasComponents
|
|
266
|
+
? 'No direct recommendations for this parent.'
|
|
267
|
+
: 'No components available for this node.'}
|
|
268
|
+
</p>
|
|
269
|
+
)}
|
|
270
|
+
</section>
|
|
271
|
+
|
|
272
|
+
{canToggleAllComponents && (
|
|
273
|
+
<label className="add-component-modal__toggle">
|
|
274
|
+
<input
|
|
275
|
+
type="checkbox"
|
|
276
|
+
checked={showAllComponents}
|
|
277
|
+
onChange={(event) => setShowAllComponents(event.target.checked)}
|
|
278
|
+
/>
|
|
279
|
+
<span>Show rest (All Components)</span>
|
|
280
|
+
</label>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{canToggleAllComponents && showAllComponents && (
|
|
284
|
+
<section
|
|
285
|
+
className="add-component-modal__section"
|
|
286
|
+
aria-live="polite"
|
|
287
|
+
id="add-component-modal-all"
|
|
288
|
+
>
|
|
289
|
+
<h4 className="add-component-modal__section-title">
|
|
290
|
+
All Components
|
|
291
|
+
</h4>
|
|
292
|
+
{allGroups.length ? (
|
|
293
|
+
renderGroups(allGroups)
|
|
294
|
+
) : (
|
|
295
|
+
<p className="add-component-modal__empty">
|
|
296
|
+
No components available.
|
|
297
|
+
</p>
|
|
298
|
+
)}
|
|
299
|
+
</section>
|
|
300
|
+
)}
|
|
301
|
+
{!showAllComponents && !hasRecommended && (
|
|
302
|
+
<p className="add-component-modal__empty">
|
|
303
|
+
{canToggleAllComponents
|
|
304
|
+
? 'Nothing to suggest yet—toggle “Show rest” to browse all components.'
|
|
305
|
+
: 'Nothing to suggest yet for this parent.'}
|
|
306
|
+
</p>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
</Modal>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export default AddComponentModal;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
2
|
+
import Modal from './Modal';
|
|
3
|
+
|
|
4
|
+
const SAVED_COLORS_KEY = 'attributes-editor-saved-colors';
|
|
5
|
+
|
|
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
|
+
type ColorOption = {
|
|
18
|
+
label?: string;
|
|
19
|
+
value: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type ColorModalProps = {
|
|
23
|
+
value?: string;
|
|
24
|
+
projectColors?: string[];
|
|
25
|
+
onSelect: (color: string) => void;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
onClear: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const readSavedColors = (): string[] => {
|
|
31
|
+
if (typeof window === 'undefined') return [];
|
|
32
|
+
try {
|
|
33
|
+
const stored = window.localStorage.getItem(SAVED_COLORS_KEY);
|
|
34
|
+
if (!stored) return [];
|
|
35
|
+
const parsed = JSON.parse(stored);
|
|
36
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const persistSavedColors = (colors: string[]) => {
|
|
43
|
+
if (typeof window === 'undefined') return;
|
|
44
|
+
try {
|
|
45
|
+
window.localStorage.setItem(SAVED_COLORS_KEY, JSON.stringify(colors));
|
|
46
|
+
} catch {
|
|
47
|
+
// no-op
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function ColorModal({
|
|
52
|
+
value,
|
|
53
|
+
projectColors = [],
|
|
54
|
+
onSelect,
|
|
55
|
+
onClose,
|
|
56
|
+
onClear,
|
|
57
|
+
}: ColorModalProps) {
|
|
58
|
+
const [savedColors, setSavedColors] = useState<string[]>(() =>
|
|
59
|
+
readSavedColors(),
|
|
60
|
+
);
|
|
61
|
+
const colorInputRef = useRef<HTMLInputElement | null>(null);
|
|
62
|
+
|
|
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;
|
|
74
|
+
});
|
|
75
|
+
}, [projectColors]);
|
|
76
|
+
|
|
77
|
+
const projectColorOptions = useMemo<ColorOption[]>(
|
|
78
|
+
() => uniqueProjectColors.map((color) => ({ value: color })),
|
|
79
|
+
[uniqueProjectColors],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const savedColorOptions = useMemo<ColorOption[]>(
|
|
83
|
+
() => savedColors.map((color) => ({ value: color })),
|
|
84
|
+
[savedColors],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const handleAddColorClick = () => {
|
|
88
|
+
colorInputRef.current?.click();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const handleColorPicked = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
92
|
+
const picked = event.target.value;
|
|
93
|
+
if (!picked) return;
|
|
94
|
+
setSavedColors((prev) => {
|
|
95
|
+
if (prev.includes(picked)) return prev;
|
|
96
|
+
const next = [...prev, picked];
|
|
97
|
+
persistSavedColors(next);
|
|
98
|
+
return next;
|
|
99
|
+
});
|
|
100
|
+
onSelect(picked);
|
|
101
|
+
onClose();
|
|
102
|
+
event.target.value = '';
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleSelectColor = (hex: string) => {
|
|
106
|
+
onSelect(hex);
|
|
107
|
+
onClose();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<Modal
|
|
112
|
+
onClose={onClose}
|
|
113
|
+
ariaLabelledBy="color-picker-title"
|
|
114
|
+
contentClassName="color-modal"
|
|
115
|
+
>
|
|
116
|
+
<div className="modal__header">
|
|
117
|
+
<h3 id="color-picker-title" className="modal__title">
|
|
118
|
+
Pick a color
|
|
119
|
+
</h3>
|
|
120
|
+
<button type="button" className="editor-button" onClick={onClose}>
|
|
121
|
+
Close
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="color-modal__selected">
|
|
126
|
+
<div className="color-modal__selected-info">
|
|
127
|
+
<span
|
|
128
|
+
aria-hidden
|
|
129
|
+
className="color-modal__selected-preview"
|
|
130
|
+
style={{ background: value ?? 'transparent' }}
|
|
131
|
+
/>
|
|
132
|
+
<div>
|
|
133
|
+
<p className="color-modal__selected-label">Selected color</p>
|
|
134
|
+
<p className="color-modal__selected-value">{value ?? 'None'}</p>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
{value ? (
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
className="color-modal__link-button"
|
|
141
|
+
onClick={onClear}
|
|
142
|
+
>
|
|
143
|
+
Clear
|
|
144
|
+
</button>
|
|
145
|
+
) : null}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<ColorSection
|
|
149
|
+
title="Project colors"
|
|
150
|
+
options={projectColorOptions}
|
|
151
|
+
emptyMessage="No project colors detected yet."
|
|
152
|
+
activeValue={value}
|
|
153
|
+
onSelect={handleSelectColor}
|
|
154
|
+
/>
|
|
155
|
+
|
|
156
|
+
<ColorSection
|
|
157
|
+
title="Saved colors"
|
|
158
|
+
options={savedColorOptions}
|
|
159
|
+
emptyMessage="Add colors you use often for quick access."
|
|
160
|
+
action={
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
className="color-modal__link-button"
|
|
164
|
+
onClick={handleAddColorClick}
|
|
165
|
+
>
|
|
166
|
+
Add color
|
|
167
|
+
</button>
|
|
168
|
+
}
|
|
169
|
+
activeValue={value}
|
|
170
|
+
onSelect={handleSelectColor}
|
|
171
|
+
/>
|
|
172
|
+
|
|
173
|
+
<ColorSection
|
|
174
|
+
title="Popular colors"
|
|
175
|
+
options={POPULAR_COLORS}
|
|
176
|
+
emptyMessage="Popular palettes unavailable."
|
|
177
|
+
activeValue={value}
|
|
178
|
+
onSelect={handleSelectColor}
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<input
|
|
182
|
+
ref={colorInputRef}
|
|
183
|
+
type="color"
|
|
184
|
+
className="color-modal__input"
|
|
185
|
+
onChange={handleColorPicked}
|
|
186
|
+
/>
|
|
187
|
+
</Modal>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type ColorSectionProps = {
|
|
192
|
+
title: string;
|
|
193
|
+
options: ColorOption[];
|
|
194
|
+
emptyMessage?: string;
|
|
195
|
+
action?: React.ReactNode;
|
|
196
|
+
activeValue?: string;
|
|
197
|
+
onSelect: (value: string) => void;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
function ColorSection({
|
|
201
|
+
title,
|
|
202
|
+
options,
|
|
203
|
+
emptyMessage,
|
|
204
|
+
action,
|
|
205
|
+
activeValue,
|
|
206
|
+
onSelect,
|
|
207
|
+
}: ColorSectionProps) {
|
|
208
|
+
return (
|
|
209
|
+
<section className="color-section">
|
|
210
|
+
<div className="color-section__header">
|
|
211
|
+
<p className="color-section__title">{title}</p>
|
|
212
|
+
{action}
|
|
213
|
+
</div>
|
|
214
|
+
{options.length ? (
|
|
215
|
+
<div className="color-section__swatches">
|
|
216
|
+
{options.map((option) => (
|
|
217
|
+
<ColorSwatch
|
|
218
|
+
key={`${title}-${option.value}`}
|
|
219
|
+
option={option}
|
|
220
|
+
isActive={option.value === activeValue}
|
|
221
|
+
onSelect={() => onSelect(option.value)}
|
|
222
|
+
/>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
) : (
|
|
226
|
+
<p className="color-section__empty">
|
|
227
|
+
{emptyMessage ?? 'No colors available.'}
|
|
228
|
+
</p>
|
|
229
|
+
)}
|
|
230
|
+
</section>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
type ColorSwatchProps = {
|
|
235
|
+
option: ColorOption;
|
|
236
|
+
isActive?: boolean;
|
|
237
|
+
onSelect: () => void;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function ColorSwatch({ option, isActive, onSelect }: ColorSwatchProps) {
|
|
241
|
+
const isLight =
|
|
242
|
+
option.value?.trim().toLowerCase() === '#ffffff' ||
|
|
243
|
+
option.value?.trim().toLowerCase() === '#fff';
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
onClick={onSelect}
|
|
249
|
+
className={`color-modal__swatch${
|
|
250
|
+
isActive ? ' color-modal__swatch--active' : ''
|
|
251
|
+
}`}
|
|
252
|
+
>
|
|
253
|
+
<span
|
|
254
|
+
aria-hidden
|
|
255
|
+
className={`color-modal__swatch-preview${
|
|
256
|
+
isLight ? ' color-modal__swatch-preview--light' : ''
|
|
257
|
+
}`}
|
|
258
|
+
style={{ background: option.value ?? 'transparent' }}
|
|
259
|
+
/>
|
|
260
|
+
<span className="color-modal__swatch-label">
|
|
261
|
+
{option.label ?? option.value}
|
|
262
|
+
</span>
|
|
263
|
+
<span className="color-modal__swatch-value">{option.value}</span>
|
|
264
|
+
</button>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
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;
|