@developer_tribe/react-builder 0.1.32 → 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.
Files changed (218) hide show
  1. package/dist/DeviceMockFrame.d.ts +2 -17
  2. package/dist/RenderPage.d.ts +4 -11
  3. package/dist/attributes-editor/Field.d.ts +16 -0
  4. package/dist/attributes-editor/FieldInfoTooltip.d.ts +7 -0
  5. package/dist/attributes-editor/LayoutPreviewPicker.d.ts +12 -0
  6. package/dist/attributes-editor/SpecialCategorySection.d.ts +19 -0
  7. package/dist/attributes-editor/types.d.ts +14 -0
  8. package/dist/background.jpg +0 -0
  9. package/dist/build-components/Button/Button.d.ts +1 -1
  10. package/dist/build-components/Button/ButtonProps.generated.d.ts +26 -1
  11. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +27 -1
  12. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +25 -0
  13. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +25 -0
  14. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +27 -1
  15. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +27 -1
  16. package/dist/build-components/Image/ImageProps.generated.d.ts +25 -3
  17. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +27 -1
  18. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +25 -0
  19. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +25 -0
  20. package/dist/build-components/OnboardDot/OnboardDot.d.ts +1 -1
  21. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +22 -0
  22. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +4 -5
  23. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +25 -3
  24. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +24 -3
  25. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +25 -4
  26. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +4 -5
  27. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +4 -5
  28. package/dist/build-components/Text/TextProps.generated.d.ts +4 -5
  29. package/dist/build-components/View/ViewProps.generated.d.ts +3 -4
  30. package/dist/build-components/index.d.ts +1 -0
  31. package/dist/build-components/patterns.generated.d.ts +4855 -132
  32. package/dist/components/AttributesEditorPanel.d.ts +9 -0
  33. package/dist/components/Breadcrumb.d.ts +15 -0
  34. package/dist/components/Builder.d.ts +9 -0
  35. package/dist/components/Checkbox.d.ts +17 -0
  36. package/dist/components/DeviceButton.d.ts +8 -0
  37. package/dist/components/DeviceNavigationBar.d.ts +10 -0
  38. package/dist/components/DeviceStatusBar.d.ts +9 -0
  39. package/dist/components/EditorHeader.d.ts +10 -0
  40. package/dist/index.cjs.js +6 -5
  41. package/dist/index.cjs.js.map +1 -0
  42. package/dist/index.d.ts +8 -4
  43. package/dist/index.esm.js +6 -5
  44. package/dist/index.esm.js.map +1 -0
  45. package/dist/mockOS/components/MockLaunchScreenComponent.d.ts +6 -0
  46. package/dist/mockOS/components/MockOSRouter.d.ts +8 -0
  47. package/dist/mockOS/components/PermissionModal.d.ts +9 -0
  48. package/dist/mockOS/context/MockOSContext.d.ts +36 -0
  49. package/dist/mockOS/hooks/useMockNavigation.d.ts +3 -0
  50. package/dist/mockOS/hooks/useMockPermission.d.ts +3 -0
  51. package/dist/mockOS/index.d.ts +9 -0
  52. package/dist/mockOS/managers/mockPermissionManager.d.ts +10 -0
  53. package/dist/mockOS/managers/navigationManager.d.ts +17 -0
  54. package/dist/modals/AddComponentModal.d.ts +8 -0
  55. package/dist/modals/ColorModal.d.ts +9 -0
  56. package/dist/modals/DeviceSelectorModal.d.ts +9 -0
  57. package/dist/modals/LocalicationModal.d.ts +8 -0
  58. package/dist/modals/Modal.d.ts +12 -0
  59. package/dist/modals/index.d.ts +5 -0
  60. package/dist/pages/ProjectPage.d.ts +11 -0
  61. package/dist/pages/tabs/BuilderTab.d.ts +9 -0
  62. package/dist/pages/tabs/DebugTab.d.ts +7 -0
  63. package/dist/pages/tabs/PreviewTab.d.ts +3 -0
  64. package/dist/store.d.ts +15 -18
  65. package/dist/styles.css +1 -1
  66. package/dist/types/PreviewConfig.d.ts +6 -3
  67. package/dist/types/Project.d.ts +12 -2
  68. package/dist/utils/copyNode.d.ts +2 -0
  69. package/dist/utils/logger.d.ts +11 -0
  70. package/dist/utils/patterns.d.ts +24 -0
  71. package/dist/utils/useLogRender.d.ts +1 -0
  72. package/package.json +17 -9
  73. package/scripts/prebuild/utils/createBuildComponentsIndex.js +15 -1
  74. package/scripts/prebuild/utils/createGeneratedProps.js +11 -3
  75. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +45 -6
  76. package/scripts/prebuild/utils/validatePatternJson.js +13 -5
  77. package/src/AttributesEditor.tsx +434 -311
  78. package/src/DeviceMockFrame.tsx +42 -67
  79. package/src/RenderPage.tsx +8 -44
  80. package/src/assets/images/android.svg +43 -0
  81. package/src/assets/images/apple.svg +16 -0
  82. package/src/assets/images/background.jpg +0 -0
  83. package/src/assets/samples/carousel-sample.json +2 -3
  84. package/src/assets/samples/getSamples.ts +49 -12
  85. package/src/assets/samples/simple-1.json +1 -2
  86. package/src/assets/samples/simple-2.json +1 -2
  87. package/src/assets/samples/vpn-onboard-1.json +1 -2
  88. package/src/assets/samples/vpn-onboard-2.json +1 -2
  89. package/src/assets/samples/vpn-onboard-3.json +1 -2
  90. package/src/assets/samples/vpn-onboard-4.json +1 -2
  91. package/src/assets/samples/vpn-onboard-5.json +1 -2
  92. package/src/assets/samples/vpn-onboard-6.json +1 -2
  93. package/src/attributes-editor/Field.tsx +662 -0
  94. package/src/attributes-editor/FieldInfoTooltip.tsx +49 -0
  95. package/src/attributes-editor/LayoutPreviewPicker.tsx +199 -0
  96. package/src/attributes-editor/SpecialCategorySection.tsx +284 -0
  97. package/src/attributes-editor/types.ts +30 -0
  98. package/src/build-components/Button/Button.tsx +12 -2
  99. package/src/build-components/Button/ButtonProps.generated.ts +37 -1
  100. package/src/build-components/Button/pattern.json +31 -2
  101. package/src/build-components/Carousel/Carousel.tsx +17 -2
  102. package/src/build-components/Carousel/CarouselProps.generated.ts +39 -1
  103. package/src/build-components/Carousel/pattern.json +10 -0
  104. package/src/build-components/CarouselButtons/CarouselButtons.tsx +8 -2
  105. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +36 -0
  106. package/src/build-components/CarouselButtons/pattern.json +22 -0
  107. package/src/build-components/CarouselDots/CarouselDots.tsx +42 -8
  108. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +36 -0
  109. package/src/build-components/CarouselDots/pattern.json +15 -0
  110. package/src/build-components/CarouselItem/CarouselItem.tsx +7 -2
  111. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +39 -1
  112. package/src/build-components/CarouselItem/pattern.json +7 -0
  113. package/src/build-components/CarouselProvider/CarouselProvider.tsx +10 -2
  114. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +39 -1
  115. package/src/build-components/CarouselProvider/pattern.json +7 -0
  116. package/src/build-components/Image/Image.tsx +10 -2
  117. package/src/build-components/Image/ImageProps.generated.ts +36 -3
  118. package/src/build-components/Image/pattern.json +46 -3
  119. package/src/build-components/Onboard/Onboard.tsx +8 -1
  120. package/src/build-components/Onboard/OnboardProps.generated.ts +39 -1
  121. package/src/build-components/Onboard/pattern.json +11 -0
  122. package/src/build-components/OnboardButton/OnboardButton.tsx +53 -9
  123. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +36 -0
  124. package/src/build-components/OnboardButton/pattern.json +71 -5
  125. package/src/build-components/OnboardButtons/OnboardButtons.tsx +27 -17
  126. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +36 -0
  127. package/src/build-components/OnboardButtons/pattern.json +70 -4
  128. package/src/build-components/OnboardDot/OnboardDot.tsx +106 -4
  129. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +22 -0
  130. package/src/build-components/OnboardDot/pattern.json +54 -1
  131. package/src/build-components/OnboardFooter/OnboardFooter.tsx +14 -6
  132. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +4 -5
  133. package/src/build-components/OnboardFooter/pattern.json +58 -2
  134. package/src/build-components/OnboardImage/OnboardImage.tsx +29 -5
  135. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +36 -3
  136. package/src/build-components/OnboardImage/pattern.json +21 -0
  137. package/src/build-components/OnboardItem/OnboardItem.tsx +8 -1
  138. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +35 -3
  139. package/src/build-components/OnboardItem/pattern.json +38 -2
  140. package/src/build-components/OnboardProvider/OnboardProvider.tsx +22 -8
  141. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +37 -4
  142. package/src/build-components/OnboardProvider/pattern.json +51 -4
  143. package/src/build-components/OnboardSubtitle/OnboardSubtitle.tsx +2 -0
  144. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +4 -5
  145. package/src/build-components/OnboardSubtitle/pattern.json +6 -0
  146. package/src/build-components/OnboardTitle/OnboardTitle.tsx +2 -0
  147. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +4 -5
  148. package/src/build-components/OnboardTitle/pattern.json +6 -0
  149. package/src/build-components/Text/Text.tsx +12 -6
  150. package/src/build-components/Text/TextProps.generated.ts +4 -5
  151. package/src/build-components/Text/pattern.json +38 -2
  152. package/src/build-components/View/View.tsx +11 -6
  153. package/src/build-components/View/ViewProps.generated.ts +3 -4
  154. package/src/build-components/View/pattern.json +227 -19
  155. package/src/build-components/index.ts +22 -0
  156. package/src/build-components/patterns.generated.ts +4905 -139
  157. package/src/components/AttributesEditorPanel.tsx +58 -0
  158. package/src/components/Breadcrumb.tsx +80 -0
  159. package/src/components/Builder.tsx +375 -0
  160. package/src/components/Checkbox.tsx +81 -0
  161. package/src/components/DeviceButton.tsx +39 -0
  162. package/src/components/DeviceNavigationBar.tsx +201 -0
  163. package/src/components/DeviceStatusBar.tsx +85 -0
  164. package/src/components/EditorHeader.tsx +138 -0
  165. package/src/index.ts +8 -4
  166. package/src/mockOS/components/MockLaunchScreenComponent.tsx +43 -0
  167. package/src/mockOS/components/MockOSRouter.tsx +115 -0
  168. package/src/mockOS/components/PermissionModal.tsx +270 -0
  169. package/src/mockOS/context/MockOSContext.tsx +179 -0
  170. package/src/mockOS/hooks/useMockNavigation.ts +11 -0
  171. package/src/mockOS/hooks/useMockPermission.ts +11 -0
  172. package/src/mockOS/index.ts +26 -0
  173. package/src/mockOS/managers/mockPermissionManager.ts +54 -0
  174. package/src/mockOS/managers/navigationManager.ts +91 -0
  175. package/src/modals/AddComponentModal.tsx +313 -0
  176. package/src/modals/ColorModal.tsx +268 -0
  177. package/src/modals/DeviceSelectorModal.tsx +57 -0
  178. package/src/modals/LocalicationModal.tsx +54 -0
  179. package/src/modals/Modal.tsx +57 -0
  180. package/src/modals/index.ts +5 -0
  181. package/src/pages/ProjectPage.tsx +150 -0
  182. package/src/pages/tabs/BuilderTab.tsx +33 -0
  183. package/src/pages/tabs/DebugTab.tsx +64 -0
  184. package/src/pages/tabs/PreviewTab.tsx +206 -0
  185. package/src/size-matters/index.ts +25 -5
  186. package/src/store.ts +56 -38
  187. package/src/styles/base/_global.scss +253 -0
  188. package/src/styles/components/_attributes-editor.scss +261 -0
  189. package/src/styles/components/_editor-shell.scss +189 -0
  190. package/src/styles/components/_mockos-router.scss +140 -0
  191. package/src/styles/components/_ui-components.scss +183 -0
  192. package/src/styles/foundation/_colors.scss +8 -0
  193. package/src/styles/foundation/_mixins.scss +22 -0
  194. package/src/styles/{_reset.scss → foundation/_reset.scss} +5 -2
  195. package/src/styles/foundation/_sizes.scss +37 -0
  196. package/src/styles/foundation/_typography.scss +4 -0
  197. package/src/styles/foundation/_variables.scss +3 -0
  198. package/src/styles/index.scss +22 -129
  199. package/src/styles/layout/_builder.scss +68 -0
  200. package/src/styles/layout/_pages.scss +3 -0
  201. package/src/styles/modals/_add-component.scss +122 -0
  202. package/src/styles/modals/_color-modal.scss +130 -0
  203. package/src/styles/modals/_device-selector.scss +18 -0
  204. package/src/styles/modals/_localication-modal.scss +68 -0
  205. package/src/styles/modals/_modal-shell.scss +46 -0
  206. package/src/styles/utilities/_carousel.scss +125 -0
  207. package/src/types/PreviewConfig.ts +14 -5
  208. package/src/types/Project.ts +15 -2
  209. package/src/types/images.d.ts +8 -0
  210. package/src/utils/copyNode.ts +7 -0
  211. package/src/utils/extractTextStyle.ts +8 -4
  212. package/src/utils/extractViewStyle.ts +51 -7
  213. package/src/utils/getDevices.ts +1 -0
  214. package/src/utils/logger.ts +76 -0
  215. package/src/utils/patterns.ts +33 -0
  216. package/src/utils/useLogRender.ts +13 -0
  217. package/dist/build-components/OnboardDot/OnboardExpandingDotProps.generated.d.ts +0 -10
  218. package/src/build-components/OnboardDot/OnboardExpandingDotProps.generated.ts +0 -20
@@ -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;