@developer_tribe/react-builder 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/dist/AttributesEditor.d.ts +3 -1
  2. package/dist/DeviceMockFrame.d.ts +2 -1
  3. package/dist/RenderPage.d.ts +5 -3
  4. package/dist/attributes-editor/Field.d.ts +17 -0
  5. package/dist/attributes-editor/FieldInfoTooltip.d.ts +7 -0
  6. package/dist/attributes-editor/LayoutPreviewPicker.d.ts +12 -0
  7. package/dist/attributes-editor/SpecialCategorySection.d.ts +20 -0
  8. package/dist/attributes-editor/types.d.ts +14 -0
  9. package/dist/background.jpg +0 -0
  10. package/dist/build-components/BackgroundImage/BackgroundImage.d.ts +5 -0
  11. package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +44 -0
  12. package/dist/build-components/Button/Button.d.ts +1 -1
  13. package/dist/build-components/Button/ButtonProps.generated.d.ts +33 -1
  14. package/dist/build-components/Carousel/CarouselProps.generated.d.ts +34 -1
  15. package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +32 -0
  16. package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +32 -0
  17. package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +34 -1
  18. package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +34 -1
  19. package/dist/build-components/Image/ImageProps.generated.d.ts +32 -3
  20. package/dist/build-components/Onboard/OnboardProps.generated.d.ts +34 -1
  21. package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +32 -0
  22. package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +32 -0
  23. package/dist/build-components/OnboardDot/OnboardDot.d.ts +1 -1
  24. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +29 -0
  25. package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +11 -5
  26. package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +32 -3
  27. package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +31 -3
  28. package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +32 -5
  29. package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +11 -5
  30. package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +11 -5
  31. package/dist/build-components/Text/TextProps.generated.d.ts +11 -5
  32. package/dist/build-components/View/ViewProps.generated.d.ts +10 -4
  33. package/dist/build-components/index.d.ts +2 -1
  34. package/dist/build-components/patterns.generated.d.ts +6288 -136
  35. package/dist/components/AttributesEditorPanel.d.ts +3 -4
  36. package/dist/components/Breadcrumb.d.ts +3 -1
  37. package/dist/components/Builder.d.ts +2 -1
  38. package/dist/components/BuilderButton.d.ts +9 -0
  39. package/dist/components/Checkbox.d.ts +17 -0
  40. package/dist/components/DeviceButton.d.ts +8 -0
  41. package/dist/components/DeviceNavigationBar.d.ts +10 -0
  42. package/dist/components/DeviceStatusBar.d.ts +9 -0
  43. package/dist/components/EditorHeader.d.ts +3 -8
  44. package/dist/index.cjs.js +5 -5
  45. package/dist/index.cjs.js.map +1 -1
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.esm.js +5 -5
  48. package/dist/index.esm.js.map +1 -1
  49. package/dist/mockOS/components/MockLaunchScreenComponent.d.ts +6 -0
  50. package/dist/mockOS/components/MockOSRouter.d.ts +8 -0
  51. package/dist/mockOS/components/PermissionModal.d.ts +9 -0
  52. package/dist/mockOS/context/MockOSContext.d.ts +36 -0
  53. package/dist/mockOS/hooks/useMockNavigation.d.ts +3 -0
  54. package/dist/mockOS/hooks/useMockPermission.d.ts +3 -0
  55. package/dist/mockOS/index.d.ts +9 -0
  56. package/dist/mockOS/managers/mockPermissionManager.d.ts +10 -0
  57. package/dist/mockOS/managers/navigationManager.d.ts +17 -0
  58. package/dist/modals/AddComponentModal.d.ts +8 -0
  59. package/dist/modals/ColorModal.d.ts +11 -0
  60. package/dist/modals/DeviceSelectorModal.d.ts +9 -0
  61. package/dist/modals/LocalicationModal.d.ts +8 -0
  62. package/dist/modals/Modal.d.ts +12 -0
  63. package/dist/modals/index.d.ts +5 -0
  64. package/dist/pages/ProjectPage.d.ts +3 -3
  65. package/dist/pages/tabs/BuilderPanel.d.ts +8 -0
  66. package/dist/pages/tabs/{DebugTab.d.ts → SideTool.d.ts} +2 -2
  67. package/dist/store.d.ts +7 -3
  68. package/dist/styles.css +1 -1
  69. package/dist/types/Project.d.ts +11 -0
  70. package/dist/utils/analyseNode.d.ts +1 -0
  71. package/dist/utils/extractTextStyle.d.ts +8 -1
  72. package/dist/utils/extractViewStyle.d.ts +8 -1
  73. package/dist/utils/parseColor.d.ts +7 -0
  74. package/dist/utils/patterns.d.ts +24 -0
  75. package/package.json +2 -1
  76. package/scripts/prebuild/utils/createGeneratedProps.js +11 -3
  77. package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +45 -6
  78. package/scripts/prebuild/utils/validatePatternJson.js +13 -5
  79. package/src/AttributesEditor.tsx +493 -310
  80. package/src/DeviceMockFrame.tsx +21 -37
  81. package/src/RenderPage.tsx +86 -7
  82. package/src/assets/images/android.svg +42 -42
  83. package/src/assets/images/apple.svg +15 -15
  84. package/src/attributes-editor/Field.tsx +669 -0
  85. package/src/attributes-editor/FieldInfoTooltip.tsx +49 -0
  86. package/src/attributes-editor/LayoutPreviewPicker.tsx +199 -0
  87. package/src/attributes-editor/SpecialCategorySection.tsx +285 -0
  88. package/src/attributes-editor/types.ts +30 -0
  89. package/src/build-components/BackgroundImage/BackgroundImage.tsx +87 -0
  90. package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +60 -0
  91. package/src/build-components/BackgroundImage/pattern.json +45 -0
  92. package/src/build-components/Button/Button.tsx +37 -2
  93. package/src/build-components/Button/ButtonProps.generated.ts +44 -1
  94. package/src/build-components/Button/pattern.json +31 -2
  95. package/src/build-components/Carousel/Carousel.tsx +39 -2
  96. package/src/build-components/Carousel/CarouselProps.generated.ts +46 -1
  97. package/src/build-components/Carousel/pattern.json +10 -0
  98. package/src/build-components/CarouselButtons/CarouselButtons.tsx +21 -2
  99. package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +43 -0
  100. package/src/build-components/CarouselButtons/pattern.json +22 -0
  101. package/src/build-components/CarouselDots/CarouselDots.tsx +49 -8
  102. package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +43 -0
  103. package/src/build-components/CarouselDots/pattern.json +15 -0
  104. package/src/build-components/CarouselItem/CarouselItem.tsx +21 -2
  105. package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +46 -1
  106. package/src/build-components/CarouselItem/pattern.json +7 -0
  107. package/src/build-components/CarouselProvider/CarouselProvider.tsx +21 -2
  108. package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +46 -1
  109. package/src/build-components/CarouselProvider/pattern.json +7 -0
  110. package/src/build-components/Image/Image.tsx +33 -2
  111. package/src/build-components/Image/ImageProps.generated.ts +43 -3
  112. package/src/build-components/Image/pattern.json +46 -3
  113. package/src/build-components/Onboard/Onboard.tsx +6 -1
  114. package/src/build-components/Onboard/OnboardProps.generated.ts +46 -1
  115. package/src/build-components/Onboard/pattern.json +11 -0
  116. package/src/build-components/OnboardButton/OnboardButton.tsx +54 -6
  117. package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +43 -0
  118. package/src/build-components/OnboardButton/pattern.json +71 -5
  119. package/src/build-components/OnboardButtons/OnboardButtons.tsx +33 -11
  120. package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +43 -0
  121. package/src/build-components/OnboardButtons/pattern.json +70 -4
  122. package/src/build-components/OnboardDot/OnboardDot.tsx +113 -4
  123. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +29 -0
  124. package/src/build-components/OnboardDot/pattern.json +55 -2
  125. package/src/build-components/OnboardFooter/OnboardFooter.tsx +20 -4
  126. package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +11 -5
  127. package/src/build-components/OnboardFooter/pattern.json +58 -2
  128. package/src/build-components/OnboardImage/OnboardImage.tsx +49 -5
  129. package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +43 -3
  130. package/src/build-components/OnboardImage/pattern.json +21 -0
  131. package/src/build-components/OnboardItem/OnboardItem.tsx +17 -1
  132. package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +42 -3
  133. package/src/build-components/OnboardItem/pattern.json +38 -2
  134. package/src/build-components/OnboardProvider/OnboardProvider.tsx +52 -18
  135. package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +44 -5
  136. package/src/build-components/OnboardProvider/pattern.json +44 -5
  137. package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +11 -5
  138. package/src/build-components/OnboardSubtitle/pattern.json +7 -1
  139. package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +11 -5
  140. package/src/build-components/OnboardTitle/pattern.json +7 -1
  141. package/src/build-components/RenderNode.generated.tsx +3 -0
  142. package/src/build-components/Text/Text.tsx +34 -6
  143. package/src/build-components/Text/TextProps.generated.ts +11 -5
  144. package/src/build-components/Text/pattern.json +38 -2
  145. package/src/build-components/View/View.tsx +33 -6
  146. package/src/build-components/View/ViewProps.generated.ts +10 -4
  147. package/src/build-components/View/pattern.json +285 -19
  148. package/src/build-components/index.ts +5 -0
  149. package/src/build-components/patterns.generated.ts +6346 -143
  150. package/src/components/AttributesEditorPanel.tsx +17 -64
  151. package/src/components/Breadcrumb.tsx +37 -5
  152. package/src/components/Builder.tsx +311 -108
  153. package/src/components/BuilderButton.tsx +127 -0
  154. package/src/components/Checkbox.tsx +81 -0
  155. package/src/components/DeviceButton.tsx +39 -0
  156. package/src/components/DeviceNavigationBar.tsx +201 -0
  157. package/src/components/DeviceStatusBar.tsx +85 -0
  158. package/src/components/EditorHeader.tsx +26 -74
  159. package/src/index.ts +2 -2
  160. package/src/mockOS/components/MockLaunchScreenComponent.tsx +43 -0
  161. package/src/mockOS/components/MockOSRouter.tsx +123 -0
  162. package/src/mockOS/components/PermissionModal.tsx +270 -0
  163. package/src/mockOS/context/MockOSContext.tsx +179 -0
  164. package/src/mockOS/hooks/useMockNavigation.ts +11 -0
  165. package/src/mockOS/hooks/useMockPermission.ts +11 -0
  166. package/src/mockOS/index.ts +26 -0
  167. package/src/mockOS/managers/mockPermissionManager.ts +54 -0
  168. package/src/mockOS/managers/navigationManager.ts +91 -0
  169. package/src/modals/AddComponentModal.tsx +313 -0
  170. package/src/modals/ColorModal.tsx +425 -0
  171. package/src/modals/DeviceSelectorModal.tsx +57 -0
  172. package/src/modals/LocalicationModal.tsx +54 -0
  173. package/src/modals/Modal.tsx +57 -0
  174. package/src/modals/index.ts +5 -0
  175. package/src/pages/ProjectPage.tsx +307 -71
  176. package/src/pages/tabs/{BuilderTab.tsx → BuilderPanel.tsx} +13 -9
  177. package/src/pages/tabs/SideTool.tsx +259 -0
  178. package/src/size-matters/index.ts +27 -5
  179. package/src/store.ts +13 -5
  180. package/src/styles/base/_global.scss +404 -0
  181. package/src/styles/components/_attributes-editor.scss +273 -0
  182. package/src/styles/components/_editor-shell.scss +212 -0
  183. package/src/styles/components/_mockos-router.scss +140 -0
  184. package/src/styles/components/_ui-components.scss +183 -0
  185. package/src/styles/foundation/_colors.scss +8 -0
  186. package/src/styles/{_mixins.scss → foundation/_mixins.scss} +5 -4
  187. package/src/styles/{_reset.scss → foundation/_reset.scss} +5 -2
  188. package/src/styles/foundation/_sizes.scss +37 -0
  189. package/src/styles/foundation/_typography.scss +4 -0
  190. package/src/styles/foundation/_variables.scss +3 -0
  191. package/src/styles/index.scss +22 -136
  192. package/src/styles/layout/_builder.scss +124 -0
  193. package/src/styles/layout/_pages.scss +3 -0
  194. package/src/styles/modals/_add-component.scss +122 -0
  195. package/src/styles/modals/_color-modal.scss +159 -0
  196. package/src/styles/modals/_device-selector.scss +18 -0
  197. package/src/styles/modals/_localication-modal.scss +68 -0
  198. package/src/styles/modals/_modal-shell.scss +46 -0
  199. package/src/styles/utilities/_carousel.scss +125 -0
  200. package/src/types/Project.ts +14 -0
  201. package/src/types/images.d.ts +8 -0
  202. package/src/utils/analyseNode.ts +98 -0
  203. package/src/utils/extractTextStyle.ts +28 -10
  204. package/src/utils/extractViewStyle.ts +77 -9
  205. package/src/utils/parseColor.ts +43 -0
  206. package/src/utils/patterns.ts +33 -0
  207. package/dist/build-components/OnboardDot/OnboardExpandingDotProps.generated.d.ts +0 -10
  208. package/dist/pages/tabs/BuilderTab.d.ts +0 -9
  209. package/dist/pages/tabs/PreviewTab.d.ts +0 -3
  210. package/src/build-components/OnboardDot/OnboardExpandingDotProps.generated.ts +0 -20
  211. package/src/pages/tabs/DebugTab.tsx +0 -23
  212. package/src/pages/tabs/PreviewTab.tsx +0 -194
  213. package/src/styles/_variables.scss +0 -27
  214. package/src/styles/builder.scss +0 -60
  215. package/src/styles/components.scss +0 -88
  216. package/src/styles/editor.scss +0 -174
  217. package/src/styles/global.scss +0 -200
  218. package/src/styles/pages.scss +0 -2
@@ -0,0 +1,199 @@
1
+ import React from 'react';
2
+ import type { ViewPropsGenerated } from '../build-components/View/ViewProps.generated';
3
+ import { LayoutContext, LayoutFieldName } from './types';
4
+
5
+ type LayoutPreviewPickerProps = {
6
+ mode: LayoutFieldName;
7
+ options: string[];
8
+ value?: string;
9
+ onChange: (val?: string) => void;
10
+ layoutContext?: LayoutContext;
11
+ viewAttributes?: Partial<ViewPropsGenerated['attributes']>;
12
+ };
13
+
14
+ export function LayoutPreviewPicker({
15
+ mode,
16
+ options,
17
+ value,
18
+ onChange,
19
+ layoutContext,
20
+ viewAttributes,
21
+ }: LayoutPreviewPickerProps) {
22
+ if (!options.length) {
23
+ return null;
24
+ }
25
+
26
+ const selectedValue =
27
+ typeof value === 'string'
28
+ ? value
29
+ : (layoutContext?.[mode as keyof LayoutContext] as string | undefined);
30
+
31
+ return (
32
+ <div
33
+ style={{
34
+ display: 'flex',
35
+ flexWrap: 'wrap',
36
+ gap: 6,
37
+ paddingBottom: 2,
38
+ }}
39
+ >
40
+ {options.map((option) => {
41
+ const isActive = selectedValue === option;
42
+ return (
43
+ <button
44
+ key={option}
45
+ type="button"
46
+ onClick={() => onChange(option)}
47
+ style={{
48
+ borderRadius: 8,
49
+ padding: 5,
50
+ width: 110,
51
+ flex: '0 0 auto',
52
+ border: isActive ? '2px solid #222' : '1px solid #d5d5d5',
53
+ background: '#fff',
54
+ textAlign: 'left',
55
+ display: 'flex',
56
+ flexDirection: 'column',
57
+ gap: 3,
58
+ cursor: 'pointer',
59
+ boxShadow: isActive ? '0 2px 4px rgba(0,0,0,0.08)' : 'none',
60
+ }}
61
+ >
62
+ <LayoutPreview
63
+ mode={mode}
64
+ option={option}
65
+ isActive={isActive}
66
+ layoutContext={layoutContext}
67
+ viewAttributes={viewAttributes}
68
+ />
69
+ <span style={{ fontWeight: 600, fontSize: 11 }}>
70
+ {formatLayoutLabel(option)}
71
+ </span>
72
+ </button>
73
+ );
74
+ })}
75
+ </div>
76
+ );
77
+ }
78
+
79
+ type LayoutPreviewProps = {
80
+ mode: LayoutFieldName;
81
+ option: string;
82
+ isActive: boolean;
83
+ layoutContext?: LayoutContext;
84
+ viewAttributes?: Partial<ViewPropsGenerated['attributes']>;
85
+ };
86
+
87
+ function LayoutPreview({
88
+ mode,
89
+ option,
90
+ isActive,
91
+ layoutContext,
92
+ viewAttributes,
93
+ }: LayoutPreviewProps) {
94
+ const resolved = resolvePreviewContext(mode, option, layoutContext);
95
+ const isColumn = resolved.flexDirection?.startsWith('column');
96
+ const resolvedBackground =
97
+ viewAttributes?.backgroundColor ?? (isActive ? '#fffdf5' : '#fafafa');
98
+ const resolvedGap = parseNumeric(viewAttributes?.gap) ?? 3;
99
+ const resolvedBorderRadius = parseNumeric(viewAttributes?.borderRadius) ?? 4;
100
+
101
+ const stretchOverrides =
102
+ resolved.alignItems === 'stretch'
103
+ ? isColumn
104
+ ? { width: '100%' }
105
+ : { height: '100%' }
106
+ : undefined;
107
+
108
+ return (
109
+ <div
110
+ style={{
111
+ borderRadius: resolvedBorderRadius,
112
+ border: '1px dashed rgba(0,0,0,0.2)',
113
+ padding: 5,
114
+ background: resolvedBackground,
115
+ }}
116
+ >
117
+ <div
118
+ style={{
119
+ display: 'flex',
120
+ flexDirection: resolved.flexDirection ?? 'row',
121
+ justifyContent: resolved.justifyContent ?? 'flex-start',
122
+ alignItems: resolved.alignItems ?? 'stretch',
123
+ gap: resolvedGap,
124
+ width: '100%',
125
+ minHeight: isColumn ? 52 : 40,
126
+ transition: 'all 0.2s ease',
127
+ }}
128
+ >
129
+ {Array.from({ length: 3 }).map((_, idx) => (
130
+ <span
131
+ key={idx}
132
+ style={{
133
+ width: isColumn ? 22 : 10,
134
+ height: isColumn ? 10 : 10,
135
+ borderRadius: 2.5,
136
+ background: '#f7a500',
137
+ border: '1px solid rgba(0,0,0,0.1)',
138
+ flex: mode === 'justifyContent' ? '0 0 auto' : undefined,
139
+ ...(stretchOverrides ?? {}),
140
+ }}
141
+ />
142
+ ))}
143
+ </div>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ function resolvePreviewContext(
149
+ mode: LayoutFieldName,
150
+ option: string,
151
+ layoutContext?: LayoutContext,
152
+ ): LayoutContext {
153
+ const fallback: LayoutContext = {
154
+ flexDirection: layoutContext?.flexDirection ?? 'row',
155
+ alignItems: layoutContext?.alignItems ?? 'center',
156
+ justifyContent: layoutContext?.justifyContent ?? 'flex-start',
157
+ };
158
+
159
+ switch (mode) {
160
+ case 'flexDirection':
161
+ return {
162
+ flexDirection: option as LayoutContext['flexDirection'],
163
+ alignItems: fallback.alignItems,
164
+ justifyContent: fallback.justifyContent,
165
+ };
166
+ case 'alignItems':
167
+ return {
168
+ flexDirection: fallback.flexDirection,
169
+ alignItems: option as LayoutContext['alignItems'],
170
+ justifyContent: fallback.justifyContent,
171
+ };
172
+ case 'justifyContent':
173
+ return {
174
+ flexDirection: fallback.flexDirection,
175
+ alignItems: fallback.alignItems,
176
+ justifyContent: option as LayoutContext['justifyContent'],
177
+ };
178
+ default:
179
+ return fallback;
180
+ }
181
+ }
182
+
183
+ function parseNumeric(value?: string | number | null) {
184
+ if (value === null || value === undefined) {
185
+ return undefined;
186
+ }
187
+ if (typeof value === 'number') {
188
+ return value;
189
+ }
190
+ const parsed = Number.parseFloat(value);
191
+ return Number.isNaN(parsed) ? undefined : parsed;
192
+ }
193
+
194
+ function formatLayoutLabel(option: string) {
195
+ return option
196
+ .replace(/[-_]/g, ' ')
197
+ .replace(/\s+/g, ' ')
198
+ .replace(/^\w|\s\w/g, (char) => char.toUpperCase());
199
+ }
@@ -0,0 +1,285 @@
1
+ import React, { useState } from 'react';
2
+ import { NodeDefaultAttribute } from '../types/Node';
3
+ import type { ProjectColors } from '../types/Project';
4
+ import { Field } from './Field';
5
+ import { FieldInfoTooltip } from './FieldInfoTooltip';
6
+ import {
7
+ AttributeMetaMap,
8
+ LayoutContext,
9
+ SchemaEntry,
10
+ isBooleanFieldType,
11
+ } from './types';
12
+
13
+ type SpecialCategorySectionProps = {
14
+ category: string;
15
+ entries: SchemaEntry[];
16
+ attributeMeta?: AttributeMetaMap;
17
+ attributes: NodeDefaultAttribute;
18
+ onAttributeChange: (name: string, value: unknown) => void;
19
+ componentType?: string;
20
+ projectColors?: ProjectColors;
21
+ layoutContext?: LayoutContext;
22
+ viewAttributes?: NodeDefaultAttribute;
23
+ meta?: {
24
+ label?: string;
25
+ description?: string;
26
+ };
27
+ };
28
+
29
+ type BoxFieldSlot = 'top' | 'bottom' | 'left' | 'right';
30
+
31
+ const FIELD_SLOT_MATCHERS: Record<BoxFieldSlot, RegExp> = {
32
+ top: /(^|[-_])(top)$/i,
33
+ bottom: /(^|[-_])(bottom)$/i,
34
+ left: /(^|[-_])(left)$/i,
35
+ right: /(^|[-_])(right)$/i,
36
+ };
37
+
38
+ export function SpecialCategorySection({
39
+ category,
40
+ entries,
41
+ attributeMeta,
42
+ attributes,
43
+ onAttributeChange,
44
+ componentType,
45
+ projectColors,
46
+ layoutContext,
47
+ viewAttributes,
48
+ meta,
49
+ }: SpecialCategorySectionProps) {
50
+ const [showAdvanced, setShowAdvanced] = useState(false);
51
+ const normalizedTitle =
52
+ meta?.label && meta.label.trim().length > 0
53
+ ? meta.label
54
+ : category && category.length > 0
55
+ ? category.charAt(0).toUpperCase() + category.slice(1)
56
+ : 'Special';
57
+ const normalizedDescription = meta?.description;
58
+ const requiresAdvancedToggle =
59
+ category === 'padding' || category === 'margin' || category === 'offset';
60
+
61
+ const shouldUseBoxLayout = requiresAdvancedToggle;
62
+
63
+ const detectFieldSlot = (fieldName: string): BoxFieldSlot | undefined => {
64
+ if (!shouldUseBoxLayout) {
65
+ return undefined;
66
+ }
67
+ const normalized = fieldName
68
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
69
+ .replace(/\s+/g, '-')
70
+ .toLowerCase();
71
+ return (Object.keys(FIELD_SLOT_MATCHERS) as BoxFieldSlot[]).find((slot) =>
72
+ FIELD_SLOT_MATCHERS[slot].test(normalized),
73
+ );
74
+ };
75
+
76
+ const renderFields = () => {
77
+ if (!entries.length) {
78
+ return (
79
+ <p className="special-category-section__placeholder">
80
+ -- not defined --
81
+ </p>
82
+ );
83
+ }
84
+ const fieldsClassNames = [
85
+ 'special-category-section__fields',
86
+ shouldUseBoxLayout ? 'special-category-section__fields--box' : '',
87
+ ]
88
+ .filter(Boolean)
89
+ .join(' ');
90
+ return (
91
+ <div className={fieldsClassNames}>
92
+ {entries.map(({ name, type }) => {
93
+ const label = attributeMeta?.[name]?.label ?? name;
94
+ const description = attributeMeta?.[name]?.description;
95
+ const preferredScale = attributeMeta?.[name]?.preferedScale;
96
+ const currentValue = (attributes as Record<string, unknown>)[name];
97
+ const isBoolean = isBooleanFieldType(type);
98
+ const fieldSlot = detectFieldSlot(name);
99
+ const wrapperClassNames = [
100
+ 'attributes-editor__field-wrapper',
101
+ 'special-category-section__field',
102
+ isBoolean ? 'attributes-editor__field-wrapper--boolean' : '',
103
+ ]
104
+ .filter(Boolean)
105
+ .join(' ');
106
+ return (
107
+ <FieldInfoTooltip key={name} description={description}>
108
+ <div className={wrapperClassNames} data-field-slot={fieldSlot}>
109
+ {!isBoolean ? (
110
+ <p className="attributes-editor__field-label">{label}</p>
111
+ ) : null}
112
+ <Field
113
+ name={name}
114
+ type={type}
115
+ value={currentValue}
116
+ onChange={(val) => onAttributeChange(name, val)}
117
+ componentType={componentType}
118
+ projectColors={projectColors}
119
+ layoutContext={layoutContext}
120
+ viewAttributes={viewAttributes}
121
+ label={isBoolean ? label : undefined}
122
+ preferredScale={preferredScale}
123
+ />
124
+ </div>
125
+ </FieldInfoTooltip>
126
+ );
127
+ })}
128
+ </div>
129
+ );
130
+ };
131
+
132
+ const renderContent = () => {
133
+ if (!entries.length) {
134
+ return (
135
+ <p className="special-category-section__placeholder">
136
+ -- not defined --
137
+ </p>
138
+ );
139
+ }
140
+
141
+ if (!requiresAdvancedToggle) {
142
+ return renderFields();
143
+ }
144
+
145
+ // Separate directional fields (paddingTop, etc.) from shorthand fields (padding, paddingHorizontal, etc.)
146
+ const boxEntries = entries
147
+ .filter((entry) => detectFieldSlot(entry.name))
148
+ .sort((a, b) => {
149
+ const slotOrder: BoxFieldSlot[] = ['top', 'left', 'right', 'bottom'];
150
+ const slotA = detectFieldSlot(a.name);
151
+ const slotB = detectFieldSlot(b.name);
152
+ const indexA = slotA ? slotOrder.indexOf(slotA) : 999;
153
+ const indexB = slotB ? slotOrder.indexOf(slotB) : 999;
154
+ return indexA - indexB;
155
+ });
156
+ const baseEntries = entries.filter((entry) => !detectFieldSlot(entry.name));
157
+
158
+ if (boxEntries.length === 0) {
159
+ return (
160
+ <p className="special-category-section__placeholder">
161
+ -- not defined --
162
+ </p>
163
+ );
164
+ }
165
+
166
+ // Always show directional fields in box layout
167
+ const boxFieldsJSX = (
168
+ <div className="special-category-section__fields special-category-section__fields--box">
169
+ {boxEntries.map(({ name, type }) => {
170
+ const label = attributeMeta?.[name]?.label ?? name;
171
+ const description = attributeMeta?.[name]?.description;
172
+ const preferredScale = attributeMeta?.[name]?.preferedScale;
173
+ const currentValue = (attributes as Record<string, unknown>)[name];
174
+ const isBoolean = isBooleanFieldType(type);
175
+ const fieldSlot = detectFieldSlot(name);
176
+ const wrapperClassNames = [
177
+ 'attributes-editor__field-wrapper',
178
+ 'special-category-section__field',
179
+ isBoolean ? 'attributes-editor__field-wrapper--boolean' : '',
180
+ ]
181
+ .filter(Boolean)
182
+ .join(' ');
183
+ return (
184
+ <FieldInfoTooltip key={name} description={description}>
185
+ <div className={wrapperClassNames} data-field-slot={fieldSlot}>
186
+ {!isBoolean ? (
187
+ <p className="attributes-editor__field-label">{label}</p>
188
+ ) : null}
189
+ <Field
190
+ name={name}
191
+ type={type}
192
+ value={currentValue}
193
+ onChange={(val) => onAttributeChange(name, val)}
194
+ componentType={componentType}
195
+ projectColors={projectColors}
196
+ layoutContext={layoutContext}
197
+ viewAttributes={viewAttributes}
198
+ label={isBoolean ? label : undefined}
199
+ preferredScale={preferredScale}
200
+ />
201
+ </div>
202
+ </FieldInfoTooltip>
203
+ );
204
+ })}
205
+ </div>
206
+ );
207
+
208
+ // Show shorthand fields only when advanced is toggled
209
+ const baseFieldsJSX =
210
+ showAdvanced && baseEntries.length > 0 ? (
211
+ <div className="special-category-section__fields">
212
+ {baseEntries.map(({ name, type }) => {
213
+ const label = attributeMeta?.[name]?.label ?? name;
214
+ const description = attributeMeta?.[name]?.description;
215
+ const preferredScale = attributeMeta?.[name]?.preferedScale;
216
+ const currentValue = (attributes as Record<string, unknown>)[name];
217
+ const isBoolean = isBooleanFieldType(type);
218
+ const wrapperClassNames = [
219
+ 'attributes-editor__field-wrapper',
220
+ 'special-category-section__field',
221
+ isBoolean ? 'attributes-editor__field-wrapper--boolean' : '',
222
+ ]
223
+ .filter(Boolean)
224
+ .join(' ');
225
+ return (
226
+ <FieldInfoTooltip key={name} description={description}>
227
+ <div className={wrapperClassNames}>
228
+ {!isBoolean ? (
229
+ <p className="attributes-editor__field-label">{label}</p>
230
+ ) : null}
231
+ <Field
232
+ name={name}
233
+ type={type}
234
+ value={currentValue}
235
+ onChange={(val) => onAttributeChange(name, val)}
236
+ componentType={componentType}
237
+ projectColors={projectColors}
238
+ layoutContext={layoutContext}
239
+ viewAttributes={viewAttributes}
240
+ label={isBoolean ? label : undefined}
241
+ preferredScale={preferredScale}
242
+ />
243
+ </div>
244
+ </FieldInfoTooltip>
245
+ );
246
+ })}
247
+ </div>
248
+ ) : null;
249
+
250
+ return (
251
+ <>
252
+ {boxFieldsJSX}
253
+ {baseFieldsJSX}
254
+ </>
255
+ );
256
+ };
257
+
258
+ // Check if we have shorthand fields to show the toggle
259
+ const baseEntries = entries.filter((entry) => !detectFieldSlot(entry.name));
260
+ const shouldShowToggle = requiresAdvancedToggle && baseEntries.length > 0;
261
+
262
+ return (
263
+ <section className="special-category-section">
264
+ <div className="special-category-section__header">
265
+ <p className="special-category-section__title">{normalizedTitle}</p>
266
+ {shouldShowToggle ? (
267
+ <button
268
+ type="button"
269
+ onClick={() => setShowAdvanced((prev) => !prev)}
270
+ className="special-category-section__toggle"
271
+ data-active={showAdvanced}
272
+ >
273
+ {showAdvanced ? 'Hide advanced' : 'Show advanced'}
274
+ </button>
275
+ ) : null}
276
+ </div>
277
+ {normalizedDescription ? (
278
+ <p className="special-category-section__description">
279
+ {normalizedDescription}
280
+ </p>
281
+ ) : null}
282
+ {renderContent()}
283
+ </section>
284
+ );
285
+ }
@@ -0,0 +1,30 @@
1
+ import type {
2
+ AlignItemsOptionType,
3
+ FlexDirectionOptionType,
4
+ JustifyContentOptionType,
5
+ } from '../build-components/View/ViewProps.generated';
6
+ import { getAttributeMeta } from '../utils/patterns';
7
+
8
+ export type SchemaEntry = {
9
+ name: string;
10
+ type: string | string[];
11
+ };
12
+
13
+ export type AttributeMetaMap = ReturnType<typeof getAttributeMeta>;
14
+
15
+ export type LayoutFieldName = 'flexDirection' | 'alignItems' | 'justifyContent';
16
+
17
+ export type LayoutContext = {
18
+ flexDirection?: FlexDirectionOptionType;
19
+ alignItems?: AlignItemsOptionType;
20
+ justifyContent?: JustifyContentOptionType;
21
+ };
22
+
23
+ const BOOLEAN_ALIASES = new Set(['bool', 'boolean']);
24
+
25
+ export function isBooleanFieldType(type: string | string[]): boolean {
26
+ if (Array.isArray(type)) return false;
27
+ if (typeof type !== 'string') return false;
28
+ const normalized = type.trim().toLowerCase();
29
+ return BOOLEAN_ALIASES.has(normalized);
30
+ }
@@ -0,0 +1,87 @@
1
+ import React, { useId, useMemo } from 'react';
2
+ import type { BackgroundImageComponentProps } from './BackgroundImageProps.generated';
3
+ import useNode from '../useNode';
4
+ import { useRenderStore } from '../../store';
5
+ import { extractViewStyle } from '../../utils/extractViewStyle';
6
+ import { useLogRender } from '../../utils/useLogRender';
7
+
8
+ function BackgroundImage({ node }: BackgroundImageComponentProps) {
9
+ useLogRender('BackgroundImage');
10
+ node = useNode(node);
11
+ const generatedId = useId();
12
+ const attributeName =
13
+ (node as any)?.sourceType ?? node.type ?? 'background-image';
14
+ const attributeKey = node.key ?? generatedId;
15
+
16
+ const { previewMode, current, appConfig, projectColors } = useRenderStore(
17
+ (s) => ({
18
+ previewMode: s.previewMode,
19
+ current: s.current,
20
+ appConfig: s.appConfig,
21
+ projectColors: s.projectColors,
22
+ }),
23
+ );
24
+
25
+ const baseStyle = useMemo(
26
+ () => extractViewStyle(node, { appConfig, projectColors }),
27
+ [node, appConfig, projectColors],
28
+ );
29
+ const backgroundStyle = useMemo(() => {
30
+ const attrs = node.attributes;
31
+ const style: React.CSSProperties = {
32
+ backgroundRepeat: 'no-repeat',
33
+ backgroundPosition: 'center',
34
+ };
35
+
36
+ if (attrs?.src) {
37
+ style.backgroundImage = `url(${attrs.src})`;
38
+ }
39
+
40
+ switch (attrs?.resizeMode) {
41
+ case 'cover':
42
+ style.backgroundSize = 'cover';
43
+ break;
44
+ case 'contain':
45
+ style.backgroundSize = 'contain';
46
+ break;
47
+ case 'stretch':
48
+ style.backgroundSize = '100% 100%';
49
+ break;
50
+ case 'center':
51
+ style.backgroundSize = 'auto';
52
+ break;
53
+ default:
54
+ style.backgroundSize = 'cover';
55
+ }
56
+
57
+ return style;
58
+ }, [node]);
59
+ const isSelected =
60
+ previewMode &&
61
+ !!current &&
62
+ (current as any)?.key &&
63
+ (node as any)?.key &&
64
+ (current as any).key === (node as any).key;
65
+
66
+ const mergedStyle = useMemo(
67
+ () => ({
68
+ ...baseStyle,
69
+ ...backgroundStyle,
70
+ }),
71
+ [baseStyle, backgroundStyle],
72
+ );
73
+
74
+ const style = isSelected
75
+ ? { ...mergedStyle, outline: '2px solid #2684FF' }
76
+ : mergedStyle;
77
+
78
+ return (
79
+ <div
80
+ attribute-name={attributeName}
81
+ attribute-key={attributeKey}
82
+ style={style}
83
+ />
84
+ );
85
+ }
86
+
87
+ export default React.memo(BackgroundImage);
@@ -0,0 +1,60 @@
1
+ /* AUTO-GENERATED FILE - DO NOT EDIT */
2
+
3
+ import type { NodeData } from '../../types/Node';
4
+
5
+ export type FlexDirectionOptionType = 'row' | 'column';
6
+ export type AlignItemsOptionType =
7
+ | 'flex-start'
8
+ | 'center'
9
+ | 'flex-end'
10
+ | 'stretch'
11
+ | 'baseline';
12
+ export type JustifyContentOptionType =
13
+ | 'flex-start'
14
+ | 'center'
15
+ | 'flex-end'
16
+ | 'space-between'
17
+ | 'space-around'
18
+ | 'space-evenly';
19
+ export type PositionOptionType = 'relative' | 'absolute';
20
+ export type ResizeModeOptionType = 'cover' | 'contain' | 'stretch' | 'center';
21
+
22
+ export interface BackgroundImagePropsGenerated {
23
+ child: string;
24
+ attributes: {
25
+ scrollable?: boolean;
26
+ flexDirection?: FlexDirectionOptionType;
27
+ alignItems?: AlignItemsOptionType;
28
+ justifyContent?: JustifyContentOptionType;
29
+ gap?: string;
30
+ padding?: string;
31
+ paddingHorizontal?: string;
32
+ paddingVertical?: string;
33
+ paddingTop?: string;
34
+ paddingBottom?: string;
35
+ paddingLeft?: string;
36
+ paddingRight?: string;
37
+ margin?: string;
38
+ marginVertical?: string;
39
+ marginTop?: string;
40
+ marginBottom?: string;
41
+ marginLeft?: string;
42
+ marginRight?: string;
43
+ backgroundColor?: string;
44
+ borderRadius?: string;
45
+ width?: string;
46
+ height?: string;
47
+ position?: PositionOptionType;
48
+ top?: string;
49
+ bottom?: string;
50
+ left?: string;
51
+ right?: string;
52
+ zIndex?: number;
53
+ src?: string;
54
+ resizeMode?: ResizeModeOptionType;
55
+ };
56
+ }
57
+
58
+ export interface BackgroundImageComponentProps {
59
+ node: NodeData<BackgroundImagePropsGenerated['attributes']>;
60
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "allowUnknownAttributes": false,
4
+ "pattern": {
5
+ "type": "background-image",
6
+ "children": "never",
7
+ "extends": "View",
8
+ "attributes": {
9
+ "src": "string",
10
+ "resizeMode": ["cover", "contain", "stretch", "center"]
11
+ },
12
+ "defaults": {
13
+ "resizeMode": "cover",
14
+ "width": "100%",
15
+ "height": "100%",
16
+ "position": "fixed",
17
+ "top": 0,
18
+ "left": 0,
19
+ "right": 0,
20
+ "bottom": 0,
21
+ "zIndex": 0
22
+ }
23
+ },
24
+ "meta": {
25
+ "desiredParent": ["all", "background"],
26
+ "label": "Background Image",
27
+ "description": "Background image.",
28
+ "attributes": {
29
+ "src": {
30
+ "label": "Src",
31
+ "description": "Image source URL.",
32
+ "category": "other",
33
+ "specialCategory": null,
34
+ "sort": 1
35
+ },
36
+ "resizeMode": {
37
+ "label": "Resize Mode",
38
+ "description": "How the image fits its container.",
39
+ "category": "style",
40
+ "specialCategory": null,
41
+ "sort": 4
42
+ }
43
+ }
44
+ }
45
+ }