rw-elements-tools 1.2.0

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 (185) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1333 -0
  3. package/bin/cli.js +213 -0
  4. package/build-properties.js +654 -0
  5. package/build-shared-hooks.js +253 -0
  6. package/config.js +148 -0
  7. package/controls/Animations/AnimationEffects.js +111 -0
  8. package/controls/Animations/AnimationSettings.js +437 -0
  9. package/controls/Animations/Reveals.js +168 -0
  10. package/controls/Animations/ScrollAnimation_Opacity.js +15 -0
  11. package/controls/Animations/ScrollAnimation_Rotate.js +17 -0
  12. package/controls/Animations/ScrollAnimation_Scale.js +18 -0
  13. package/controls/Animations/ScrollAnimation_Translate.js +31 -0
  14. package/controls/Background/Background.js +66 -0
  15. package/controls/Background/BackgroundButton.js +69 -0
  16. package/controls/Background/BackgroundColor.js +28 -0
  17. package/controls/Background/BackgroundContainer.js +73 -0
  18. package/controls/Background/BackgroundGradient.js +149 -0
  19. package/controls/Background/BackgroundImage.js +108 -0
  20. package/controls/Background/BackgroundOnlyColor.js +53 -0
  21. package/controls/Background/BackgroundTransparent.js +66 -0
  22. package/controls/Background/BackgroundVideo.js +9 -0
  23. package/controls/Background/Color.js +52 -0
  24. package/controls/Background/Gradient.js +263 -0
  25. package/controls/Background/GradientContainer.js +263 -0
  26. package/controls/Background/Image.js +269 -0
  27. package/controls/Background/Image_CMS.js +305 -0
  28. package/controls/Background/SVG.js +235 -0
  29. package/controls/Background/Video.js +29 -0
  30. package/controls/Borders/Border.js +114 -0
  31. package/controls/Borders/BorderColor.js +25 -0
  32. package/controls/Borders/BorderRadius.js +19 -0
  33. package/controls/Borders/BorderStyle.js +26 -0
  34. package/controls/Borders/BorderWidth.js +20 -0
  35. package/controls/Borders/Borders.js +69 -0
  36. package/controls/Borders/BordersContainer.js +90 -0
  37. package/controls/Borders/BordersInput.js +107 -0
  38. package/controls/Borders/Outline.js +100 -0
  39. package/controls/Borders/OutlineColor.js +25 -0
  40. package/controls/Borders/OutlineOffset.js +13 -0
  41. package/controls/Borders/OutlineStyle.js +26 -0
  42. package/controls/Borders/OutlineWidth.js +13 -0
  43. package/controls/Effects/BackdropBlur.js +11 -0
  44. package/controls/Effects/Blur.js +11 -0
  45. package/controls/Effects/BoxShadow.js +15 -0
  46. package/controls/Effects/Brightness.js +11 -0
  47. package/controls/Effects/DropShadow.js +14 -0
  48. package/controls/Effects/Effects.js +71 -0
  49. package/controls/Effects/Filters.js +114 -0
  50. package/controls/Effects/Opacity.js +14 -0
  51. package/controls/Effects/Saturate.js +11 -0
  52. package/controls/Layout/AspectRatio.js +53 -0
  53. package/controls/Layout/Container.js +24 -0
  54. package/controls/Layout/Hidden.js +9 -0
  55. package/controls/Layout/Inset.js +15 -0
  56. package/controls/Layout/Isolation.js +25 -0
  57. package/controls/Layout/Layout.js +38 -0
  58. package/controls/Layout/Overflow.js +33 -0
  59. package/controls/Layout/Position.js +37 -0
  60. package/controls/Layout/TopRightBottomLeft.js +90 -0
  61. package/controls/Layout/Visibility.js +25 -0
  62. package/controls/Layout/ZIndex.js +36 -0
  63. package/controls/Overlay/Color.js +52 -0
  64. package/controls/Overlay/Gradient.js +298 -0
  65. package/controls/Overlay/Image.js +226 -0
  66. package/controls/Overlay/Overlay.js +66 -0
  67. package/controls/Sizing/Height.js +18 -0
  68. package/controls/Sizing/MaxHeight.js +17 -0
  69. package/controls/Sizing/MaxWidth.js +17 -0
  70. package/controls/Sizing/MinHeight.js +18 -0
  71. package/controls/Sizing/MinWidth.js +18 -0
  72. package/controls/Sizing/Sizing.js +66 -0
  73. package/controls/Sizing/SizingContainer.js +122 -0
  74. package/controls/Sizing/SizingImage.js +75 -0
  75. package/controls/Sizing/SizingInput.js +71 -0
  76. package/controls/Sizing/SizingSVG.js +74 -0
  77. package/controls/Sizing/Width.js +18 -0
  78. package/controls/Spacing/Margin.js +17 -0
  79. package/controls/Spacing/Padding.js +17 -0
  80. package/controls/Spacing/Spacing.js +23 -0
  81. package/controls/Spacing/SpacingButton.js +42 -0
  82. package/controls/Spacing/SpacingContainer.js +32 -0
  83. package/controls/Spacing/SpacingInput.js +42 -0
  84. package/controls/Transforms/Rotate.js +13 -0
  85. package/controls/Transforms/Scale.js +13 -0
  86. package/controls/Transforms/Skew.js +25 -0
  87. package/controls/Transforms/TransformOrigin.js +12 -0
  88. package/controls/Transforms/Transforms.js +98 -0
  89. package/controls/Transforms/Translate.js +26 -0
  90. package/controls/Transitions/Delay.js +13 -0
  91. package/controls/Transitions/Duration.js +13 -0
  92. package/controls/Transitions/Property.js +42 -0
  93. package/controls/Transitions/TimingFunction.js +44 -0
  94. package/controls/Transitions/Transitions.js +20 -0
  95. package/controls/alignment/AlignContent.js +48 -0
  96. package/controls/alignment/AlignItems.js +64 -0
  97. package/controls/alignment/AlignSelf.js +34 -0
  98. package/controls/alignment/JustifyContent.js +44 -0
  99. package/controls/alignment/JustifyItems.js +32 -0
  100. package/controls/alignment/JustifySelf.js +34 -0
  101. package/controls/core/CSSClasses.js +11 -0
  102. package/controls/core/ControlType.js +25 -0
  103. package/controls/core/HTMLTag.js +80 -0
  104. package/controls/core/HoverGroup.js +38 -0
  105. package/controls/core/ID.js +12 -0
  106. package/controls/core/Image.js +95 -0
  107. package/controls/core/MenuItem.js +187 -0
  108. package/controls/core/ObjectFit.js +32 -0
  109. package/controls/core/ObjectPosition.js +65 -0
  110. package/controls/grid-flex/ActAsGridOrFlexItem.js +54 -0
  111. package/controls/grid-flex/ColEnd.js +28 -0
  112. package/controls/grid-flex/ColStart.js +27 -0
  113. package/controls/grid-flex/Columns.js +38 -0
  114. package/controls/grid-flex/FlexDirection.js +27 -0
  115. package/controls/grid-flex/FlexItem.js +106 -0
  116. package/controls/grid-flex/GridItem.js +41 -0
  117. package/controls/grid-flex/Order.js +45 -0
  118. package/controls/grid-flex/RowEnd.js +28 -0
  119. package/controls/grid-flex/RowStart.js +27 -0
  120. package/controls/grid-flex/Rows.js +38 -0
  121. package/controls/index.js +187 -0
  122. package/controls/interactive/ButtonFontAndTextStyles.js +208 -0
  123. package/controls/interactive/Filter.js +54 -0
  124. package/controls/interactive/InputFontAndTextStyles.js +156 -0
  125. package/controls/interactive/Link.js +13 -0
  126. package/controls/typography/HeadingColor.js +112 -0
  127. package/controls/typography/TextColor.js +51 -0
  128. package/controls/typography/TextDecoration.js +99 -0
  129. package/controls/typography/TextFontsAndTextStyles.js +243 -0
  130. package/controls/typography/TextSimple.js +55 -0
  131. package/controls/typography/TextStyles.js +55 -0
  132. package/controls/typography/Typography.js +13 -0
  133. package/index.js +19 -0
  134. package/package.json +55 -0
  135. package/properties/BackgroundType.js +18 -0
  136. package/properties/ButtonSize.js +19 -0
  137. package/properties/ContainerHeights.js +23 -0
  138. package/properties/ContainerWidths.js +27 -0
  139. package/properties/FontWeight.js +16 -0
  140. package/properties/GradientDirection.js +39 -0
  141. package/properties/LetterSpacing.js +13 -0
  142. package/properties/LineHeight.js +13 -0
  143. package/properties/RevealAnimations.js +12 -0
  144. package/properties/Slider.js +10 -0
  145. package/properties/TextAlign.js +23 -0
  146. package/properties/TransformOrigins.js +43 -0
  147. package/properties/TransitionNames.js +20 -0
  148. package/properties/index.js +13 -0
  149. package/shared-hooks/animations/globalAnimations.js +141 -0
  150. package/shared-hooks/animations/globalReveal.js +48 -0
  151. package/shared-hooks/background/globalBackground.js +306 -0
  152. package/shared-hooks/background/globalBgImageFetchPriority.js +34 -0
  153. package/shared-hooks/borders/globalBorders.js +85 -0
  154. package/shared-hooks/borders/globalOutline.js +39 -0
  155. package/shared-hooks/core/addPrefixToTailwindClasses.js +24 -0
  156. package/shared-hooks/core/advancedClasses.js +5 -0
  157. package/shared-hooks/core/classnames.js +92 -0
  158. package/shared-hooks/core/getHoverPrefix.js +21 -0
  159. package/shared-hooks/core/globalHTMLTag.js +17 -0
  160. package/shared-hooks/core/injectPrefixOnDarkModeColors.js +6 -0
  161. package/shared-hooks/effects/globalEffects.js +45 -0
  162. package/shared-hooks/effects/globalFilters.js +80 -0
  163. package/shared-hooks/effects/globalOverlay.js +166 -0
  164. package/shared-hooks/interactive/globalFilter.js +24 -0
  165. package/shared-hooks/interactive/globalLink.js +23 -0
  166. package/shared-hooks/layout/globalActAsGridOrFlexItem.js +66 -0
  167. package/shared-hooks/layout/globalLayout.js +50 -0
  168. package/shared-hooks/navigation/globalMenuItem.js +35 -0
  169. package/shared-hooks/navigation/globalNavItems.js +60 -0
  170. package/shared-hooks/navigation/globalNavTitle.js +23 -0
  171. package/shared-hooks/sizing/aspectRatioClasses.js +20 -0
  172. package/shared-hooks/sizing/globalSizing.js +19 -0
  173. package/shared-hooks/sizing/globalSizingContainer.js +40 -0
  174. package/shared-hooks/sizing/objectClasses.js +9 -0
  175. package/shared-hooks/spacing/globalSpacing.js +13 -0
  176. package/shared-hooks/spacing/globalSpacingMargin.js +11 -0
  177. package/shared-hooks/spacing/globalSpacingPadding.js +11 -0
  178. package/shared-hooks/transforms/globalTransforms.js +78 -0
  179. package/shared-hooks/transitions/getAlpineTransitionAttributesDesktop.js +111 -0
  180. package/shared-hooks/transitions/getAlpineTransitionAttributesMobile.js +110 -0
  181. package/shared-hooks/transitions/globalTransitions.js +48 -0
  182. package/shared-hooks/typography/globalButtonFontAndTextStyles.js +65 -0
  183. package/shared-hooks/typography/globalHeadingColor.js +69 -0
  184. package/shared-hooks/typography/globalInputFontAndTextStyles.js +40 -0
  185. package/shared-hooks/typography/globalTextFontsAndTextStyles.js +47 -0
@@ -0,0 +1,654 @@
1
+ /**
2
+ * Properties Build Script
3
+ *
4
+ * Processes properties.config.json files and generates properties.json files
5
+ * for RapidWeaver element components.
6
+ */
7
+
8
+ import { sync as globSync } from "glob";
9
+ import fs from "fs/promises";
10
+ import path from "path";
11
+ import { fileURLToPath } from "url";
12
+ import * as Controls from "./controls/index.js";
13
+ import * as Properties from "./properties/index.js";
14
+ import pkg from "lodash";
15
+ const { cloneDeep } = pkg;
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = path.dirname(__filename);
19
+
20
+ // =============================================================================
21
+ // CONSTANTS
22
+ // =============================================================================
23
+
24
+ /**
25
+ * List of theme-related property keys that can be overridden in config files.
26
+ * These are extracted from the property config and applied to the control.
27
+ */
28
+ const THEME_PROPERTY_KEYS = [
29
+ "themeColor",
30
+ "themeFont",
31
+ "themeBorderRadius",
32
+ "themeBorderWidth",
33
+ "themeSpacing",
34
+ "themeShadow",
35
+ "themeTextStyle",
36
+ ];
37
+
38
+ /**
39
+ * Controls automatically injected into the Advanced group.
40
+ */
41
+ const ADVANCED_GROUP_CONTROLS = [Controls.CSSClasses, Controls.ID];
42
+
43
+ // =============================================================================
44
+ // UTILITY FUNCTIONS
45
+ // =============================================================================
46
+
47
+ /**
48
+ * Capitalizes the first character of a string.
49
+ * @param {string} str - The string to capitalize.
50
+ * @returns {string} The string with first character capitalized.
51
+ */
52
+ function capitalize(str) {
53
+ if (!str) return str;
54
+ return str.charAt(0).toUpperCase() + str.slice(1);
55
+ }
56
+
57
+ /**
58
+ * Lowercases the first character of a string.
59
+ * @param {string} str - The string to lowercase.
60
+ * @returns {string} The string with first character lowercased.
61
+ */
62
+ function lowercaseFirst(str) {
63
+ if (!str) return str;
64
+ return str.charAt(0).toLowerCase() + str.slice(1);
65
+ }
66
+
67
+ /**
68
+ * Normalizes a control to always be an array.
69
+ * @param {Object|Array} control - A control object or array of controls.
70
+ * @returns {Array} Array of control objects.
71
+ */
72
+ function normalizeToArray(control) {
73
+ return Array.isArray(control) ? control : [control];
74
+ }
75
+
76
+ /**
77
+ * Filters an object to only include entries with non-null values.
78
+ * @param {Object} obj - The object to filter.
79
+ * @returns {Object} Object with only non-null values.
80
+ */
81
+ function filterNullValues(obj) {
82
+ return Object.fromEntries(
83
+ Object.entries(obj).filter(([_, value]) => value != null)
84
+ );
85
+ }
86
+
87
+ // =============================================================================
88
+ // ID TRANSFORMATION
89
+ // =============================================================================
90
+
91
+ /**
92
+ * Transforms an ID template by replacing {{value}} with the control's ID.
93
+ * Handles camelCase conventions:
94
+ * - If {{value}} is at the start, keeps original casing
95
+ * - If {{value}} is in the middle, capitalizes the control ID
96
+ * - Parts after {{value}} are capitalized
97
+ *
98
+ * @example
99
+ * // "prefix{{value}}Suffix" with controlId "margin" => "prefixMarginSuffix"
100
+ * // "{{value}}Suffix" with controlId "margin" => "marginSuffix"
101
+ *
102
+ * @param {string} template - The ID template containing {{value}}.
103
+ * @param {string} controlId - The control's original ID.
104
+ * @returns {string} The transformed ID.
105
+ */
106
+ function transformIdTemplate(template, controlId) {
107
+ if (!template?.includes("{{value}}") || !controlId) {
108
+ return controlId;
109
+ }
110
+
111
+ const placeholder = "{{value}}";
112
+ const valueIndex = template.indexOf(placeholder);
113
+ const beforeValue = template.slice(0, valueIndex);
114
+ const afterValue = template.slice(valueIndex + placeholder.length);
115
+
116
+ // Capitalize controlId only if placeholder is not at the start
117
+ const transformedId = valueIndex === 0 ? controlId : capitalize(controlId);
118
+
119
+ // Build the new ID
120
+ let newId = beforeValue + transformedId + afterValue;
121
+
122
+ // Ensure proper camelCase: lowercase first char if there's a prefix
123
+ if (beforeValue) {
124
+ newId = lowercaseFirst(newId);
125
+ }
126
+
127
+ // Capitalize the part after {{value}} if it exists
128
+ if (afterValue) {
129
+ const insertPosition = beforeValue.length + transformedId.length;
130
+ newId =
131
+ newId.slice(0, insertPosition) +
132
+ capitalize(newId.slice(insertPosition));
133
+ }
134
+
135
+ return newId;
136
+ }
137
+
138
+ /**
139
+ * Creates a transformer function for applying ID patterns to nested controls.
140
+ * @param {string} pattern - The ID pattern (may contain {{value}}).
141
+ * @returns {Function} A function that transforms ID strings.
142
+ */
143
+ function createIdTransformer(pattern) {
144
+ return (originalId) => {
145
+ if (pattern?.includes("{{value}}")) {
146
+ return pattern.replace("{{value}}", originalId);
147
+ }
148
+ return pattern || originalId;
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Recursively transforms all 'id' keys in a control structure.
154
+ * @param {*} control - The control object or array to transform.
155
+ * @param {Function} transformer - The ID transformer function.
156
+ * @returns {*} The control with transformed IDs.
157
+ */
158
+ function transformIdsRecursively(control, transformer) {
159
+ if (Array.isArray(control)) {
160
+ return control.map((item) => transformIdsRecursively(item, transformer));
161
+ }
162
+
163
+ if (typeof control === "object" && control !== null) {
164
+ const result = {};
165
+ for (const [key, value] of Object.entries(control)) {
166
+ if (key === "id" && typeof value === "string") {
167
+ result[key] = transformer(value);
168
+ } else {
169
+ result[key] = transformIdsRecursively(value, transformer);
170
+ }
171
+ }
172
+ return result;
173
+ }
174
+
175
+ return control;
176
+ }
177
+
178
+ // =============================================================================
179
+ // OVERRIDE & DEFAULT APPLICATION
180
+ // =============================================================================
181
+
182
+ /**
183
+ * Merges override properties into a control.
184
+ * For existing keys: strings replace, objects are shallow merged.
185
+ * New keys are added directly.
186
+ *
187
+ * @param {Object} control - The control to merge into.
188
+ * @param {Object} overrides - The override properties.
189
+ * @returns {Object} The control with overrides applied.
190
+ */
191
+ function mergeOverrides(control, overrides) {
192
+ const result = { ...control };
193
+ const remainingOverrides = { ...overrides };
194
+
195
+ for (const key of Object.keys(overrides)) {
196
+ if (key in result) {
197
+ if (typeof overrides[key] === "string") {
198
+ result[key] = overrides[key];
199
+ } else {
200
+ result[key] = { ...result[key], ...overrides[key] };
201
+ }
202
+ delete remainingOverrides[key];
203
+ }
204
+ }
205
+
206
+ // Add any remaining overrides as new properties
207
+ return { ...result, ...remainingOverrides };
208
+ }
209
+
210
+ /**
211
+ * Applies theme defaults to a control.
212
+ * Theme defaults override the corresponding theme property on the control.
213
+ *
214
+ * @param {Object} control - The control to apply theme defaults to.
215
+ * @param {Object} themeDefaults - Object containing theme property overrides.
216
+ * @returns {Object} The control with theme defaults applied.
217
+ */
218
+ function applyThemeDefaults(control, themeDefaults) {
219
+ if (!themeDefaults || Object.keys(themeDefaults).length === 0) {
220
+ return control;
221
+ }
222
+
223
+ const result = { ...control };
224
+ for (const [key, value] of Object.entries(themeDefaults)) {
225
+ if (key in result) {
226
+ result[key] = value;
227
+ }
228
+ }
229
+ return result;
230
+ }
231
+
232
+ /**
233
+ * Applies a default value to a control.
234
+ *
235
+ * For primitive defaults:
236
+ * - Sets control.default if it exists
237
+ * - Transforms item IDs if control has an items array
238
+ *
239
+ * For object defaults:
240
+ * - Merges into theme properties (themeColor, etc.)
241
+ * - Merges into control.default if it exists
242
+ *
243
+ * @param {Object} control - The control to apply defaults to.
244
+ * @param {*} defaultVal - The default value (primitive or object).
245
+ * @returns {Object} The control with defaults applied.
246
+ */
247
+ function applyDefaultValue(control, defaultVal) {
248
+ if (defaultVal === undefined) {
249
+ return control;
250
+ }
251
+
252
+ const result = { ...control };
253
+
254
+ if (typeof defaultVal !== "object") {
255
+ // Primitive default: set directly and transform item IDs
256
+ if (result.default !== undefined) {
257
+ result.default = defaultVal;
258
+ }
259
+
260
+ // Transform item IDs using the default value as a pattern
261
+ if (result.items && Array.isArray(result.items)) {
262
+ const transformer = createIdTransformer(defaultVal);
263
+ result.items = result.items.map((item) => {
264
+ if (item.id && typeof item.id === "string") {
265
+ return { ...item, id: transformer(item.id) };
266
+ }
267
+ return item;
268
+ });
269
+ }
270
+ } else {
271
+ // Object default: apply to theme properties and control.default
272
+ for (const key of Object.keys(result)) {
273
+ if (key.startsWith("theme") && typeof result[key] === "object") {
274
+ result[key] = { ...result[key], default: defaultVal };
275
+ }
276
+ }
277
+
278
+ if (result.default !== undefined) {
279
+ result.default = { ...result.default, ...defaultVal };
280
+ }
281
+ }
282
+
283
+ return result;
284
+ }
285
+
286
+ // =============================================================================
287
+ // GLOBAL CONTROL PROCESSING
288
+ // =============================================================================
289
+
290
+ /**
291
+ * Recursively processes nested globalControl references within a control.
292
+ * @param {*} control - The control or array to process.
293
+ * @returns {*} The control with all globalControls expanded.
294
+ */
295
+ function processNestedGlobalControls(control) {
296
+ if (Array.isArray(control)) {
297
+ return control.map(processNestedGlobalControls);
298
+ }
299
+
300
+ if (typeof control === "object" && control !== null && control.globalControl) {
301
+ return processProperty(control);
302
+ }
303
+
304
+ return control;
305
+ }
306
+
307
+ /**
308
+ * Processes 'use' key references in a property, replacing them with
309
+ * the referenced Property definition from src/properties/.
310
+ *
311
+ * @param {Object|Array} property - The property object or array to process.
312
+ * @returns {Object|Array} The property with 'use' keys resolved.
313
+ */
314
+ function resolveUseReferences(property) {
315
+ // Handle arrays by recursively processing each element
316
+ if (Array.isArray(property)) {
317
+ return property.map(resolveUseReferences);
318
+ }
319
+
320
+ const result = { ...property };
321
+
322
+ for (const [key, value] of Object.entries(result)) {
323
+ if (value && typeof value === "object" && value.use) {
324
+ const referencedProperty = Properties[value.use];
325
+
326
+ if (!referencedProperty) {
327
+ console.warn(`Property '${value.use}' not found in Properties.`);
328
+ continue;
329
+ }
330
+
331
+ // Merge: referenced property as base, then override with local values
332
+ const { use: _, ...localOverrides } = value;
333
+ result[key] = { ...referencedProperty, ...localOverrides };
334
+ }
335
+ }
336
+
337
+ return result;
338
+ }
339
+
340
+ /**
341
+ * Processes a single control with all transformations:
342
+ * 1. Deep clone to avoid mutations
343
+ * 2. Apply overrides
344
+ * 3. Apply default values
345
+ * 4. Apply theme defaults
346
+ * 5. Transform ID using template
347
+ * 6. Process nested globalControls
348
+ * 7. Apply ID transformation to nested IDs
349
+ * 8. Resolve 'use' references
350
+ *
351
+ * @param {Object} control - The base control definition.
352
+ * @param {string} idTemplate - ID template with {{value}} placeholder.
353
+ * @param {Object} overrides - Property overrides to apply.
354
+ * @param {*} defaultVal - Default value to apply.
355
+ * @param {Object} themeDefaults - Theme property overrides.
356
+ * @returns {Object|Array} The fully processed control(s).
357
+ */
358
+ function processControl(control, idTemplate, overrides, defaultVal, themeDefaults) {
359
+ // 1. Deep clone to avoid mutating the original
360
+ let result = cloneDeep(control);
361
+
362
+ // 2. Apply property overrides
363
+ result = mergeOverrides(result, overrides);
364
+
365
+ // 3. Apply default value
366
+ result = applyDefaultValue(result, defaultVal);
367
+
368
+ // 4. Apply theme defaults
369
+ result = applyThemeDefaults(result, themeDefaults);
370
+
371
+ // 5. Transform the control's ID using the template
372
+ if (idTemplate?.includes("{{value}}") && result.id) {
373
+ result.id = transformIdTemplate(idTemplate, result.id);
374
+ }
375
+
376
+ // 6. Process any nested globalControls
377
+ result = processNestedGlobalControls(result);
378
+
379
+ // 7. Apply ID transformation to all nested IDs
380
+ const idTransformer = createIdTransformer(result.id || "");
381
+ result = transformIdsRecursively(result, idTransformer);
382
+
383
+ // 8. Resolve 'use' references
384
+ return resolveUseReferences(result);
385
+ }
386
+
387
+ /**
388
+ * Extracts theme defaults from a property configuration.
389
+ * @param {Object} property - The property config object.
390
+ * @returns {Object} Object containing only defined theme properties.
391
+ */
392
+ function extractThemeDefaults(property) {
393
+ const themeProps = {};
394
+ for (const key of THEME_PROPERTY_KEYS) {
395
+ if (property[key] != null) {
396
+ themeProps[key] = property[key];
397
+ }
398
+ }
399
+ return themeProps;
400
+ }
401
+
402
+ /**
403
+ * Processes a property from the config file.
404
+ * If the property has a globalControl, expands it with overrides.
405
+ * Otherwise, just resolves 'use' references.
406
+ *
407
+ * @param {Object} property - The property configuration object.
408
+ * @returns {Array} Array of processed property objects.
409
+ */
410
+ function processProperty(property) {
411
+ // No globalControl: just resolve 'use' references
412
+ if (!property.globalControl) {
413
+ return [resolveUseReferences(property)];
414
+ }
415
+
416
+ const globalControlName = property.globalControl;
417
+ const globalControl = Controls[globalControlName];
418
+
419
+ if (!globalControl) {
420
+ console.warn(`Global control '${globalControlName}' not found.`);
421
+ return [];
422
+ }
423
+
424
+ // Extract special properties from the config
425
+ const {
426
+ globalControl: _,
427
+ default: defaultVal,
428
+ id: idTemplate,
429
+ ...rest
430
+ } = property;
431
+
432
+ // Separate theme defaults from other overrides
433
+ const themeDefaults = extractThemeDefaults(rest);
434
+ const overrides = {};
435
+
436
+ for (const [key, value] of Object.entries(rest)) {
437
+ if (!THEME_PROPERTY_KEYS.includes(key)) {
438
+ overrides[key] = value;
439
+ }
440
+ }
441
+
442
+ // Process each control in the globalControl (may be array)
443
+ const controls = normalizeToArray(globalControl);
444
+ const processed = controls.map((ctrl) =>
445
+ processControl(ctrl, idTemplate, overrides, defaultVal, themeDefaults)
446
+ );
447
+
448
+ // Flatten in case processControl returns arrays (from nested globalControls)
449
+ return processed.flat();
450
+ }
451
+
452
+ // =============================================================================
453
+ // ADVANCED GROUP HANDLING
454
+ // =============================================================================
455
+
456
+ /**
457
+ * Ensures the config has a properly structured Advanced group at the end.
458
+ * Injects standard controls (CSSClasses, ID) at the beginning of the group.
459
+ *
460
+ * @param {Object} config - The configuration object with groups array.
461
+ * @returns {Object} The config with Advanced group properly set up.
462
+ */
463
+ function setupAdvancedGroup(config) {
464
+ const advancedControls = ADVANCED_GROUP_CONTROLS.flatMap(normalizeToArray);
465
+
466
+ const existingIndex = config.groups.findIndex(
467
+ (group) => group.title === "Advanced"
468
+ );
469
+
470
+ if (existingIndex !== -1) {
471
+ // Move existing Advanced group to end with injected controls
472
+ const [advancedGroup] = config.groups.splice(existingIndex, 1);
473
+ config.groups.push({
474
+ ...advancedGroup,
475
+ icon: "gearshape",
476
+ properties: [...advancedControls, ...advancedGroup.properties],
477
+ });
478
+ } else {
479
+ // Create new Advanced group
480
+ config.groups.push({
481
+ title: "Advanced",
482
+ icon: "gearshape",
483
+ properties: advancedControls,
484
+ });
485
+ }
486
+
487
+ return config;
488
+ }
489
+
490
+ // =============================================================================
491
+ // FILE PROCESSING
492
+ // =============================================================================
493
+
494
+ /**
495
+ * Reads and processes a properties.config.json file, generating properties.json.
496
+ *
497
+ * @param {string} configPath - Path to the properties.config.json file.
498
+ */
499
+ async function processConfigFile(configPath) {
500
+ try {
501
+ const fileDir = path.dirname(configPath);
502
+ const configContent = await fs.readFile(configPath, "utf8");
503
+ let config = JSON.parse(configContent);
504
+
505
+ // Set up the Advanced group with standard controls
506
+ config = setupAdvancedGroup(config);
507
+
508
+ // Process all properties in each group
509
+ config.groups = config.groups.map((group) => ({
510
+ ...group,
511
+ properties: group.properties.flatMap(processProperty),
512
+ }));
513
+
514
+ // Write the processed config
515
+ const outputPath = path.join(fileDir, "properties.json");
516
+ await fs.writeFile(outputPath, JSON.stringify(config, null, 2));
517
+ } catch (error) {
518
+ console.error(`Error processing file ${configPath}:`, error);
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Checks if a properties.config.json exists in a directory and processes it.
524
+ *
525
+ * @param {string} dirPath - The directory path to check.
526
+ */
527
+ async function processDirectoryIfConfigExists(dirPath) {
528
+ try {
529
+ const configPath = path.join(dirPath, "properties.config.json");
530
+
531
+ try {
532
+ await fs.access(configPath);
533
+ await processConfigFile(configPath);
534
+ } catch {
535
+ // Config file doesn't exist, skip silently
536
+ }
537
+ } catch (error) {
538
+ console.error(`Error processing directory ${dirPath}:`, error);
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Finds all directories matching a glob pattern and processes their configs.
544
+ *
545
+ * @param {string} packsDir - The packs directory to search.
546
+ */
547
+ async function processAllConfigs(packsDir) {
548
+ try {
549
+ const pattern = path.join(packsDir, "**/*.elementsdevpack/components/com.**/**");
550
+ const directories = globSync(pattern, { absolute: true });
551
+
552
+ console.log(`[properties] Found ${directories.length} component directories`);
553
+
554
+ let processed = 0;
555
+ for (const dir of directories) {
556
+ const configPath = path.join(dir, "properties.config.json");
557
+ try {
558
+ await fs.access(configPath);
559
+ await processConfigFile(configPath);
560
+ processed++;
561
+ } catch {
562
+ // Config file doesn't exist, skip silently
563
+ }
564
+ }
565
+
566
+ console.log(`[properties] Processed ${processed} properties.config.json files`);
567
+ } catch (error) {
568
+ console.error("[properties] Error reading directories:", error);
569
+ throw error;
570
+ }
571
+ }
572
+
573
+ // =============================================================================
574
+ // EXPORTS
575
+ // =============================================================================
576
+
577
+ /**
578
+ * Main entry point for building properties.
579
+ *
580
+ * @param {Object} config - Configuration object from resolveConfig()
581
+ * @param {string} config.packsDir - Absolute path to the packs directory
582
+ */
583
+ export async function buildProperties(config) {
584
+ console.log(`[properties] Building properties...`);
585
+ await processAllConfigs(config.packsDir);
586
+ console.log(`[properties] Build complete`);
587
+ }
588
+
589
+ /**
590
+ * Starts watch mode for continuous building of properties
591
+ * @param {Object} config - Configuration object
592
+ * @param {string} config.packsDir - Absolute path to the packs directory
593
+ */
594
+ export async function startWatch(config) {
595
+ const packsDir = config.packsDir;
596
+
597
+ console.log('[properties] Watch mode enabled. Listening for changes...');
598
+
599
+ let building = false;
600
+ const rebuild = async () => {
601
+ if (building) return;
602
+ building = true;
603
+ try {
604
+ await buildProperties(config);
605
+ } catch (err) {
606
+ console.error('[properties] Build error:', err.message || err);
607
+ } finally {
608
+ building = false;
609
+ }
610
+ };
611
+
612
+ // Watch the packs directory for properties.config.json changes
613
+ try {
614
+ const { default: fsModule } = await import('fs');
615
+ const watcher = fsModule.watch(packsDir, { recursive: true }, (eventType, filename) => {
616
+ if (!filename) return;
617
+ const lower = filename.toLowerCase();
618
+ if (lower.endsWith('properties.config.json')) {
619
+ console.log(`[properties] Change detected: ${filename} (${eventType})`);
620
+ rebuild();
621
+ }
622
+ });
623
+
624
+ watcher.on('error', (err) => {
625
+ console.error('[properties] Watcher error:', err);
626
+ });
627
+ } catch (err) {
628
+ console.warn(`[properties] Could not watch ${packsDir}: ${err.message}`);
629
+ }
630
+
631
+ await rebuild();
632
+ }
633
+
634
+ // Allow direct execution for backwards compatibility
635
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
636
+ // Direct execution: use default path
637
+ const WATCH = process.argv.includes('--watch') || process.argv.includes('-w');
638
+ const defaultPacksDir = path.resolve(__dirname, "..", "packs");
639
+ const config = { packsDir: defaultPacksDir };
640
+
641
+ (async () => {
642
+ try {
643
+ if (WATCH) {
644
+ await startWatch(config);
645
+ } else {
646
+ await buildProperties(config);
647
+ }
648
+ } catch (err) {
649
+ console.error("[properties] Build failed:", err);
650
+ process.exit(1);
651
+ }
652
+ })();
653
+ }
654
+