purgetss 7.7.1 → 7.11.1

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 (42) hide show
  1. package/README.md +28 -0
  2. package/bin/purgetss +23 -0
  3. package/dist/purgetss.ui.js +1 -1
  4. package/lib/templates/create/index.xml +1 -1
  5. package/lib/templates/purgetss.config.js.cjs +3 -1
  6. package/package.json +2 -2
  7. package/src/cli/commands/build.js +9 -4
  8. package/src/cli/commands/images.js +49 -2
  9. package/src/cli/commands/purge.js +31 -4
  10. package/src/cli/commands/shades.js +2 -2
  11. package/src/cli/utils/cli-helpers.js +15 -5
  12. package/src/cli/utils/unsupported-class-reporter.js +209 -0
  13. package/src/core/analyzers/class-extractor.js +54 -0
  14. package/src/core/analyzers/controller-svg-refs.js +154 -0
  15. package/src/core/branding/brand-config.js +7 -0
  16. package/src/core/branding/ensure-brand-section.js +4 -3
  17. package/src/core/branding/gen-feature-graphic.js +57 -0
  18. package/src/core/branding/index.js +28 -4
  19. package/src/core/branding/post-gen-notes.js +2 -2
  20. package/{experimental/completions2.js → src/core/builders/auto-utilities-builder.js} +74 -40
  21. package/src/core/builders/tailwind-builder.js +2 -2
  22. package/src/core/builders/tailwind-helpers.js +0 -444
  23. package/src/core/images/ensure-images-section.js +6 -4
  24. package/src/core/images/gen-scales.js +96 -13
  25. package/src/core/images/index.js +121 -9
  26. package/src/core/purger/icon-purger.js +7 -3
  27. package/src/core/purger/tailwind-purger.js +43 -5
  28. package/src/core/svg/cache.js +96 -0
  29. package/src/core/svg/derive-dimensions.js +120 -0
  30. package/src/core/svg/index.js +215 -0
  31. package/src/core/svg/resolve-classes.js +46 -0
  32. package/src/core/svg/sync-images.js +278 -0
  33. package/src/core/svg/tss-reader.js +134 -0
  34. package/src/dev/builders/tailwind-builder.js +3 -11
  35. package/src/shared/config-manager.js +72 -3
  36. package/src/shared/error-reporter.js +117 -0
  37. package/src/shared/helpers/colors.js +57 -13
  38. package/src/shared/helpers/core.js +0 -19
  39. package/src/shared/helpers/utils.js +146 -36
  40. package/src/shared/logger.js +12 -0
  41. package/src/shared/semantic-helpers.js +143 -0
  42. package/src/shared/validation/config-validator.js +167 -0
@@ -1,39 +1,10 @@
1
1
  /**
2
2
  * PurgeTSS v7.1.0 - Core Builder: Tailwind Helpers
3
- * Helper functions for building Tailwind CSS
4
- *
5
- * COPIED from src/index.js during refactorization - NO CHANGES to logic.
6
3
  *
7
4
  * @since 7.1.0
8
5
  * @author César Estrada
9
6
  */
10
7
 
11
- import _ from 'lodash'
12
- import defaultColors from 'tailwindcss/colors.js'
13
-
14
- // Import functions from their new modular locations
15
- import { getConfigFile } from '../../shared/config-manager.js'
16
- import { removeDeprecatedColors, fixPercentages } from '../../shared/helpers.js'
17
-
18
- // Get config once for this module
19
- const configFile = getConfigFile()
20
-
21
- /**
22
- * Remove fit, max, min values from width, height and spacing objects
23
- * @param {Object} theObject - Object with width, height, spacing properties
24
- */
25
- export function removeFitMaxMin(theObject) {
26
- delete theObject.width.fit
27
- delete theObject.width.max
28
- delete theObject.width.min
29
- delete theObject.height.fit
30
- delete theObject.height.max
31
- delete theObject.height.min
32
- delete theObject.spacing.fit
33
- delete theObject.spacing.max
34
- delete theObject.spacing.min
35
- }
36
-
37
8
  /**
38
9
  * Combine keys from theme and extend, with fallback to base values
39
10
  * @param {Object} values - Theme values object
@@ -44,418 +15,3 @@ export function removeFitMaxMin(theObject) {
44
15
  export function combineKeys(values, base, key) {
45
16
  return (values[key]) ? { ...values[key], ...values.extend[key] } : { ...base, ...values.extend[key] }
46
17
  }
47
-
48
- /**
49
- * Get base values for Tailwind building
50
- * This function prepares all the base theme values needed for building
51
- * @param {Object} defaultTheme - Default Tailwind theme
52
- * @returns {Object} Base values object
53
- */
54
- export function getBaseValues(defaultTheme) {
55
- const defaultThemeWidth = defaultTheme.width({ theme: () => (defaultTheme.spacing) })
56
- const defaultThemeHeight = defaultTheme.height({ theme: () => (defaultTheme.spacing) })
57
-
58
- removeDeprecatedColors(defaultColors)
59
-
60
- // !Prepare values
61
- const tiResets = { full: '100%' }
62
- const allWidthsCombined = (configFile.theme.spacing) ? { ...configFile.theme.spacing, ...tiResets } : { ...defaultThemeWidth }
63
- const allHeightsCombined = (configFile.theme.spacing) ? { ...configFile.theme.spacing, ...tiResets } : { ...defaultThemeHeight }
64
- const allSpacingCombined = (configFile.theme.spacing) ? { ...configFile.theme.spacing, ...tiResets } : { ...defaultThemeWidth, ...defaultThemeHeight }
65
-
66
- const themeOrDefaultValues = {
67
- width: configFile.theme.width ?? allWidthsCombined,
68
- height: configFile.theme.height ?? allHeightsCombined,
69
- spacing: configFile.theme.spacing ?? allSpacingCombined,
70
- fontSize: configFile.theme.spacing ?? defaultTheme.fontSize,
71
- colors: configFile.theme.colors ?? { transparent: 'transparent', ...defaultColors }
72
- }
73
-
74
- // ! Remove unnecessary values
75
- removeFitMaxMin(themeOrDefaultValues)
76
-
77
- // ! Merge with extend values
78
- const base = {
79
- width: { ...themeOrDefaultValues.spacing, ...configFile.theme.extend.spacing, ...themeOrDefaultValues.width, ...configFile.theme.extend.width },
80
- height: { ...themeOrDefaultValues.spacing, ...configFile.theme.extend.spacing, ...themeOrDefaultValues.height, ...configFile.theme.extend.height },
81
- colors: { ...themeOrDefaultValues.colors, ...configFile.theme.extend.colors },
82
- spacing: { ...themeOrDefaultValues.spacing, ...configFile.theme.extend.spacing },
83
- fontSize: { ...themeOrDefaultValues.fontSize, ...configFile.theme.extend.spacing, ...configFile.theme.extend.fontSize },
84
- columns: { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12 },
85
- delay: { 0: '0ms', 25: '25ms', 50: '50ms', 250: '250ms', 350: '350ms', 400: '400ms', 450: '450ms', 600: '600ms', 800: '800ms', 900: '900ms', 2000: '2000ms', 3000: '3000ms', 4000: '4000ms', 5000: '5000ms' }
86
- }
87
-
88
- fixPercentages(base.width)
89
- fixPercentages(base.height)
90
- fixPercentages(base.spacing)
91
-
92
- return base
93
- }
94
-
95
- /**
96
- * Combine all values for Tailwind building - MASSIVE function with all Titanium properties
97
- * This function builds the complete values object used in legacy Tailwind building
98
- * @param {Object} base - Base values from getBaseValues()
99
- * @param {Object} defaultTheme - Default Tailwind theme
100
- * @returns {Object} Complete values object for building
101
- */
102
- export function combineAllValues(base, defaultTheme) {
103
- const allValues = {}
104
-
105
- // ! Custom Window, View and ImageView
106
- // Merge extend values into theme (same as colors, spacing, etc.)
107
- _.each(['Window', 'View', 'ImageView'], comp => {
108
- if (configFile.theme.extend[comp]) {
109
- configFile.theme[comp] = _.merge({}, configFile.theme[comp], configFile.theme.extend[comp])
110
- delete configFile.theme.extend[comp]
111
- }
112
- // Normalize shorthand: { apply: '...' } → { default: { apply: '...' } }
113
- if (configFile.theme[comp] && configFile.theme[comp].apply && !configFile.theme[comp].default) {
114
- configFile.theme[comp] = { default: configFile.theme[comp] }
115
- }
116
- })
117
-
118
- // Merge user config WITH defaults, then write back to configFile.theme
119
- // so that downstream functions pick up the full merged object
120
- configFile.theme.Window = _.merge({ default: { backgroundColor: '#FFFFFF' } }, configFile.theme.Window)
121
- configFile.theme.ImageView = _.merge({ ios: { hires: true } }, configFile.theme.ImageView)
122
- configFile.theme.View = _.merge({ default: { width: 'Ti.UI.SIZE', height: 'Ti.UI.SIZE' } }, configFile.theme.View)
123
-
124
- allValues.Window = configFile.theme.Window
125
- allValues.ImageView = configFile.theme.ImageView
126
- allValues.View = configFile.theme.View
127
-
128
- // ! Width, height and margin properties
129
- // INFO: sizingProperties: For glossary generator only... Do not move from this position.
130
- allValues.sizingProperties = {}
131
-
132
- allValues.height = base.height
133
- allValues.width = base.width
134
- allValues.margin = combineKeys(configFile.theme, base.spacing, 'margin')
135
- allValues.marginAlternate = combineKeys(configFile.theme, base.spacing, 'margin')
136
-
137
- // ! Properties with constant values
138
- // INFO: constantProperties: For glossary generator only... Do not move from this position.
139
- allValues.constantProperties = {}
140
-
141
- // allValues.audioStreamType = {};
142
- // allValues.category = {};
143
- allValues.accessibilityHidden = {}
144
- allValues.accessoryType = {}
145
- allValues.activeIconIsMask = {}
146
- allValues.activityEnterTransition = {}
147
- allValues.activityExitTransition = {}
148
- allValues.activityIndicatorStyle = {}
149
- allValues.activityReenterTransition = {}
150
- allValues.activityReturnTransition = {}
151
- allValues.activitySharedElementEnterTransition = {}
152
- allValues.activitySharedElementExitTransition = {}
153
- allValues.activitySharedElementReenterTransition = {}
154
- allValues.activitySharedElementReturnTransition = {}
155
- allValues.alertDialogStyle = {}
156
- allValues.allowsBackForwardNavigationGestures = {}
157
- allValues.allowsLinkPreview = {}
158
- allValues.allowsMultipleSelectionDuringEditing = {}
159
- allValues.allowsMultipleSelectionInteraction = {}
160
- allValues.allowsSelection = {}
161
- allValues.allowsSelectionDuringEditing = {}
162
- allValues.allowUserCustomization = {}
163
- allValues.anchorPoint = {}
164
- allValues.autoAdjustScrollViewInsets = {}
165
- allValues.autocapitalization = {}
166
- allValues.autocorrect = {}
167
- allValues.autofillType = {}
168
- allValues.autoLink = {}
169
- allValues.autoreverse = {}
170
- allValues.autorotate = {}
171
- allValues.backgroundBlendMode = {}
172
- allValues.backgroundLinearGradient = {}
173
- allValues.backgroundRadialGradient = {}
174
- allValues.backgroundRepeat = {}
175
- allValues.borderStyle = {}
176
- allValues.bubbleParent = {}
177
- allValues.buttonStyle = {}
178
- allValues.cacheMode = {}
179
- allValues.cachePolicy = {}
180
- allValues.calendarViewShown = {}
181
- allValues.canCancelEvents = {}
182
- allValues.cancelable = {}
183
- allValues.canceledOnTouchOutside = {}
184
- allValues.canDelete = {}
185
- allValues.canEdit = {}
186
- allValues.canInsert = {}
187
- allValues.canMove = {}
188
- allValues.canScroll = {}
189
- allValues.caseInsensitiveSearch = {}
190
- allValues.checkable = {}
191
- allValues.clearButtonMode = {}
192
- allValues.clearOnEdit = {}
193
- allValues.clipMode = {}
194
- allValues.constraint = {}
195
- allValues.contentHeightAndWidth = {}
196
- allValues.curve = {}
197
- allValues.datePickerStyle = {}
198
- allValues.defaultItemTemplate = {}
199
- allValues.dimBackgroundForSearch = {}
200
- allValues.disableBounce = {}
201
- allValues.disableContextMenu = {}
202
- allValues.displayCaps = {}
203
- allValues.displayHomeAsUp = {}
204
- allValues.draggingType = {}
205
- allValues.drawerIndicatorEnabled = {}
206
- allValues.drawerLockMode = {}
207
- allValues.dropShadow = {}
208
- allValues.duration = {}
209
- allValues.editable = {}
210
- allValues.editing = {}
211
- allValues.ellipsize = {}
212
- allValues.enableCopy = {}
213
- allValues.enabled = {}
214
- allValues.enableJavascriptInterface = {}
215
- allValues.enableReturnKey = {}
216
- allValues.enableZoomControls = {}
217
- allValues.exitOnClose = {}
218
- allValues.extendBackground = {}
219
- allValues.extendEdges = {}
220
- allValues.extendSafeArea = {}
221
- allValues.fastScroll = {}
222
- allValues.filterAnchored = {}
223
- allValues.filterAttribute = {}
224
- allValues.filterCaseInsensitive = {}
225
- allValues.filterTouchesWhenObscured = {}
226
- allValues.flags = {}
227
- allValues.flagSecure = {}
228
- allValues.flip = {}
229
- allValues.focusable = {}
230
- allValues.fontStyle = {}
231
- allValues.footerDividersEnabled = {}
232
- allValues.format24 = {}
233
- allValues.fullscreen = {}
234
- allValues.gravity = {}
235
- allValues.gridColumnsRowsStartEnd = {}
236
- allValues.gridFlow = {}
237
- allValues.gridSystem = {}
238
- allValues.hasCheck = {}
239
- allValues.hasChild = {}
240
- allValues.hasDetail = {}
241
- allValues.headerDividersEnabled = {}
242
- allValues.hiddenBehavior = {}
243
- allValues.hideLoadIndicator = {}
244
- allValues.hidesBackButton = {}
245
- allValues.hidesBarsOnSwipe = {}
246
- allValues.hidesBarsOnTap = {}
247
- allValues.hidesBarsWhenKeyboardAppears = {}
248
- allValues.hideSearchOnSelection = {}
249
- allValues.hideShadow = {}
250
- allValues.hidesSearchBarWhenScrolling = {}
251
- allValues.hintType = {}
252
- allValues.hires = {}
253
- allValues.homeButtonEnabled = {}
254
- allValues.homeIndicatorAutoHidden = {}
255
- allValues.horizontalWrap = {}
256
- allValues.html = {}
257
- allValues.icon = {}
258
- allValues.iconified = {}
259
- allValues.iconifiedByDefault = {}
260
- allValues.iconIsMask = {}
261
- allValues.ignoreSslError = {}
262
- allValues.imageTouchFeedback = {}
263
- allValues.includeFontPadding = {}
264
- allValues.includeOpaqueBars = {}
265
- allValues.keepScreenOn = {}
266
- allValues.keepSectionsInSearch = {}
267
- allValues.keyboardAppearance = {}
268
- allValues.keyboardDismissMode = {}
269
- allValues.keyboardDisplayRequiresUserAction = {}
270
- allValues.keyboardType = {}
271
- allValues.largeTitleDisplayMode = {}
272
- allValues.largeTitleEnabled = {}
273
- allValues.layout = {}
274
- allValues.lazyLoadingEnabled = {}
275
- allValues.leftButtonMode = {}
276
- allValues.leftDrawerLockMode = {}
277
- allValues.lightTouchEnabled = {}
278
- allValues.listViewStyle = {}
279
- allValues.loginKeyboardType = {}
280
- allValues.loginReturnKeyType = {}
281
- allValues.mixedContentMode = {}
282
- allValues.modal = {}
283
- allValues.moveable = {}
284
- allValues.moving = {}
285
- allValues.nativeSpinner = {}
286
- allValues.navBarHidden = {}
287
- allValues.navigationMode = {}
288
- allValues.orientationModes = {}
289
- allValues.overlayEnabled = {}
290
- allValues.overrideCurrentAnimation = {}
291
- allValues.overScrollMode = {}
292
- allValues.showPagingControl = {}
293
- allValues.pagingControlOnTop = {}
294
- allValues.passwordKeyboardType = {}
295
- allValues.passwordMask = {}
296
- allValues.pickerType = {}
297
- allValues.placement = {}
298
- allValues.pluginState = {}
299
- allValues.preventCornerOverlap = {}
300
- allValues.preventDefaultImage = {}
301
- allValues.previewActionStyle = {}
302
- allValues.progressBarStyle = {}
303
- allValues.progressIndicatorType = {}
304
- allValues.pruneSectionsOnEdit = {}
305
- allValues.requestedOrientation = {}
306
- allValues.resultsSeparatorStyle = {}
307
- allValues.returnKeyType = {}
308
- allValues.reverse = {}
309
- allValues.rightButtonMode = {}
310
- allValues.rightDrawerLockMode = {}
311
- allValues.scalesPageToFit = {}
312
- allValues.scrollable = {}
313
- allValues.scrollIndicators = {}
314
- allValues.scrollIndicatorStyle = {}
315
- allValues.scrollingEnabled = {}
316
- allValues.scrollsToTop = {}
317
- allValues.scrollType = {}
318
- allValues.searchAsChild = {}
319
- allValues.searchBarStyle = {}
320
- allValues.searchHidden = {}
321
- allValues.selectionGranularity = {}
322
- allValues.selectionOpens = {}
323
- allValues.selectionStyle = {}
324
- allValues.separatorStyle = {}
325
- allValues.shiftMode = {}
326
- allValues.showAsAction = {}
327
- allValues.showBookmark = {}
328
- allValues.showCancel = {}
329
- allValues.showHorizontalScrollIndicator = {}
330
- allValues.showSearchBarInNavBar = {}
331
- allValues.showSelectionCheck = {}
332
- allValues.showUndoRedoActions = {}
333
- allValues.showVerticalScrollIndicator = {}
334
- allValues.smoothScrollOnTabClick = {}
335
- allValues.statusBarStyle = {}
336
- allValues.submitEnabled = {}
337
- allValues.suppressReturn = {}
338
- allValues.sustainedPerformanceMode = {}
339
- allValues.swipeToClose = {}
340
- allValues.switchStyle = {}
341
- allValues.systemButton = {}
342
- allValues.tabBarHidden = {}
343
- allValues.tabbedBarStyle = {}
344
- allValues.tabGroupStyle = {}
345
- allValues.tableViewStyle = {}
346
- allValues.tabsTranslucent = {}
347
- allValues.textAlign = {}
348
- allValues.theme = {}
349
- allValues.titleAttributesShadow = {}
350
- allValues.toolbarEnabled = {}
351
- allValues.touchEnabled = {}
352
- allValues.touchFeedback = {}
353
- allValues.translucent = {}
354
- allValues.useCompatPadding = {}
355
- allValues.useSpinner = {}
356
- allValues.verticalAlign = {}
357
- allValues.verticalBounce = {}
358
- allValues.viewShadow = {}
359
- allValues.visible = {}
360
- allValues.willHandleTouches = {}
361
- allValues.willScrollOnStatusTap = {}
362
- allValues.windowPixelFormat = {}
363
- allValues.windowSoftInputMode = {}
364
- allValues.wobble = {}
365
-
366
- // ! Configurable properties
367
- // INFO: configurableProperties: For glossary generator only... Do not move from this position.
368
- allValues.configurableProperties = {}
369
-
370
- allValues.activeTab = combineKeys(configFile.theme, base.spacing, 'activeTab')
371
- allValues.backgroundLeftCap = combineKeys(configFile.theme, base.spacing, 'backgroundLeftCap')
372
- allValues.backgroundPaddingBottom = combineKeys(configFile.theme, base.spacing, 'backgroundPaddingBottom')
373
- allValues.backgroundPaddingLeft = combineKeys(configFile.theme, base.spacing, 'backgroundPaddingLeft')
374
- allValues.backgroundPaddingRight = combineKeys(configFile.theme, base.spacing, 'backgroundPaddingRight')
375
- allValues.backgroundPaddingTop = combineKeys(configFile.theme, base.spacing, 'backgroundPaddingTop')
376
- allValues.backgroundTopCap = combineKeys(configFile.theme, base.spacing, 'backgroundTopCap')
377
- allValues.borderRadius = combineKeys(configFile.theme, base.spacing, 'borderRadius')
378
- allValues.borderWidth = combineKeys(configFile.theme, base.spacing, 'borderWidth')
379
- allValues.bottomNavigation = combineKeys(configFile.theme, base.spacing, 'bottomNavigation')
380
- allValues.cacheSize = combineKeys(configFile.theme, base.spacing, 'cacheSize')
381
- allValues.columnCount = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12 }, 'columnCount')
382
- allValues.contentHeight = combineKeys(configFile.theme, base.height, 'contentHeight')
383
- allValues.contentWidth = combineKeys(configFile.theme, base.width, 'contentWidth')
384
- allValues.countDownDuration = combineKeys(configFile.theme, base.spacing, 'countDownDuration')
385
- allValues.elevation = combineKeys(configFile.theme, base.spacing, 'elevation')
386
- allValues.fontFamily = combineKeys(configFile.theme, {}, 'fontFamily')
387
- allValues.fontSize = combineKeys(configFile.theme, base.fontSize, 'fontSize')
388
- allValues.fontWeight = combineKeys(configFile.theme, defaultTheme.fontWeight, 'fontWeight')
389
- allValues.gap = combineKeys(configFile.theme, base.spacing, 'gap')
390
- allValues.indentionLevel = combineKeys(configFile.theme, base.spacing, 'indentionLevel')
391
- allValues.keyboardToolbarHeight = combineKeys(configFile.theme, base.spacing, 'keyboardToolbarHeight')
392
- allValues.leftButtonPadding = combineKeys(configFile.theme, base.spacing, 'leftButtonPadding')
393
- allValues.leftWidth = combineKeys(configFile.theme, base.width, 'leftWidth')
394
- allValues.lines = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10 }, 'lines')
395
- allValues.maxElevation = combineKeys(configFile.theme, base.spacing, 'maxElevation')
396
- allValues.maxLines = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10 }, 'maxLines')
397
- allValues.maxRowHeight = combineKeys(configFile.theme, base.height, 'maxRowHeight')
398
- allValues.maxZoomScale = combineKeys(configFile.theme, { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5 }, 'maxZoomScale')
399
- allValues.minimumFontSize = combineKeys(configFile.theme, base.fontSize, 'minimumFontSize')
400
- allValues.minRowHeight = combineKeys(configFile.theme, base.height, 'minRowHeight')
401
- allValues.minZoomScale = combineKeys(configFile.theme, { 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5 }, 'minZoomScale')
402
- allValues.offsets = combineKeys(configFile.theme, base.spacing, 'offsets')
403
- allValues.opacity = combineKeys(configFile.theme, defaultTheme.opacity, 'opacity')
404
- allValues.padding = combineKeys(configFile.theme, base.spacing, 'padding')
405
- allValues.pagingControlAlpha = combineKeys(configFile.theme, defaultTheme.opacity, 'pagingControlAlpha')
406
- allValues.pagingControlHeight = combineKeys(configFile.theme, base.height, 'pagingControlHeight')
407
- allValues.pagingControlTimeout = combineKeys(configFile.theme, base.delay, 'pagingControlTimeout')
408
- allValues.repeat = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, infinite: -1 }, 'repeat')
409
- allValues.repeatCount = combineKeys(configFile.theme, { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, infinite: -1 }, 'repeatCount')
410
- allValues.rightButtonPadding = combineKeys(configFile.theme, base.spacing, 'rightButtonPadding')
411
- allValues.rightWidth = combineKeys(configFile.theme, base.width, 'rightWidth')
412
- allValues.rotate = combineKeys(configFile.theme, defaultTheme.rotate, 'rotate')
413
-
414
- // ! Custom Color properties
415
- // INFO: colorProperties: For glossary generator only... Do not move from this position.
416
- allValues.colorProperties = {}
417
-
418
- allValues.activeTintColor = combineKeys(configFile.theme, base.colors, 'activeTintColor')
419
- allValues.activeTitleColor = combineKeys(configFile.theme, base.colors, 'activeTitleColor')
420
- allValues.backgroundColor = combineKeys(configFile.theme, base.colors, 'backgroundColor')
421
- allValues.backgroundDisabledColor = combineKeys(configFile.theme, base.colors, 'backgroundDisabledColor')
422
- allValues.backgroundFocusedColor = combineKeys(configFile.theme, base.colors, 'backgroundFocusedColor')
423
- allValues.backgroundGradient = combineKeys(configFile.theme, base.colors, 'backgroundGradient')
424
- allValues.backgroundSelectedColor = combineKeys(configFile.theme, base.colors, 'backgroundSelectedColor')
425
- allValues.backgroundSelectedGradient = combineKeys(configFile.theme, base.colors, 'backgroundSelectedGradient')
426
- allValues.badgeColor = combineKeys(configFile.theme, base.colors, 'badgeColor')
427
- allValues.barColor = combineKeys(configFile.theme, base.colors, 'barColor')
428
- allValues.borderColor = combineKeys(configFile.theme, base.colors, 'borderColor')
429
- allValues.currentPageIndicatorColor = combineKeys(configFile.theme, base.colors, 'currentPageIndicatorColor')
430
- allValues.dateTimeColor = combineKeys(configFile.theme, base.colors, 'dateTimeColor')
431
- allValues.disabledColor = combineKeys(configFile.theme, base.colors, 'disabledColor')
432
- allValues.highlightedColor = combineKeys(configFile.theme, base.colors, 'highlightedColor')
433
- allValues.hintTextColor = combineKeys(configFile.theme, base.colors, 'hintTextColor')
434
- allValues.imageTouchFeedbackColor = combineKeys(configFile.theme, base.colors, 'imageTouchFeedbackColor')
435
- allValues.pageIndicatorColor = combineKeys(configFile.theme, base.colors, 'pageIndicatorColor')
436
- allValues.pagingControlColor = combineKeys(configFile.theme, base.colors, 'pagingControlColor')
437
- allValues.pullBackgroundColor = combineKeys(configFile.theme, base.colors, 'pullBackgroundColor')
438
- allValues.resultsBackgroundColor = combineKeys(configFile.theme, base.colors, 'resultsBackgroundColor')
439
- allValues.resultsSeparatorColor = combineKeys(configFile.theme, base.colors, 'resultsSeparatorColor')
440
- allValues.selectedBackgroundColor = combineKeys(configFile.theme, base.colors, 'selectedBackgroundColor')
441
- allValues.selectedBackgroundGradient = combineKeys(configFile.theme, base.colors, 'selectedBackgroundGradient')
442
- allValues.selectedColor = combineKeys(configFile.theme, base.colors, 'selectedColor')
443
- allValues.separatorColor = combineKeys(configFile.theme, base.colors, 'separatorColor')
444
- allValues.shadowColor = combineKeys(configFile.theme, base.colors, 'shadowColor')
445
- allValues.tabsBackgroundColor = combineKeys(configFile.theme, base.colors, 'tabsBackgroundColor')
446
- allValues.tabsBackgroundDisabledColor = combineKeys(configFile.theme, base.colors, 'tabsBackgroundDisabledColor')
447
- allValues.tabsBackgroundFocusedColor = combineKeys(configFile.theme, base.colors, 'tabsBackgroundFocusedColor')
448
- allValues.tabsBackgroundSelectedColor = combineKeys(configFile.theme, base.colors, 'tabsBackgroundSelectedColor')
449
- allValues.textColor = combineKeys(configFile.theme, base.colors, 'textColor')
450
- allValues.tintColor = combineKeys(configFile.theme, base.colors, 'tintColor')
451
- allValues.titleColor = combineKeys(configFile.theme, base.colors, 'titleColor')
452
- allValues.titleDisabledColor = combineKeys(configFile.theme, base.colors, 'titleDisabledColor')
453
- allValues.titleFocusedColor = combineKeys(configFile.theme, base.colors, 'titleFocusedColor')
454
- allValues.titleHighlightedColor = combineKeys(configFile.theme, base.colors, 'titleHighlightedColor')
455
- allValues.titleSelectedColor = combineKeys(configFile.theme, base.colors, 'titleSelectedColor')
456
- allValues.touchFeedbackColor = combineKeys(configFile.theme, base.colors, 'touchFeedbackColor')
457
- allValues.viewShadowColor = combineKeys(configFile.theme, base.colors, 'viewShadowColor')
458
- allValues.navTintColor = combineKeys(configFile.theme, base.colors, 'navTintColor')
459
-
460
- return allValues
461
- }
@@ -22,7 +22,9 @@ import { logger } from '../branding/branding-logger.js'
22
22
  const IMAGES_BLOCK = ` images: {
23
23
  quality: 85, // JPEG/WebP/AVIF quality (0-100)
24
24
  format: null, // null = keep original; 'webp' | 'jpeg' | 'png' to convert every image
25
- confirmOverwrites: true // prompt before overwriting files (set false to skip)
25
+ autoSync: true, // false = SVG pipeline computes dims but doesn't write to images.files
26
+ confirmOverwrites: true, // prompt before overwriting files (set false to skip)
27
+ files: [] // per-file overrides: [{ filename: 'images/<sub>/<name>.<ext>', width, height? }]
26
28
  },
27
29
  `
28
30
 
@@ -47,11 +49,11 @@ export function ensureImagesSection() {
47
49
  fs.writeFileSync(projectsConfigJS, patched, 'utf8')
48
50
  console.log()
49
51
  logger.success(`Added ${chalk.cyan('images:')} section to ${chalk.cyan('./purgetss/config.cjs')} with default values.`)
50
- console.log(` Edit that block to customize defaults (quality, format).`)
51
- console.log(` CLI flags always win over config values.`)
52
+ console.log(' Edit that block to customize defaults (quality, format).')
53
+ console.log(' CLI flags always win over config values.')
52
54
  console.log()
53
55
  } catch (err) {
54
56
  logger.warning(`Could not auto-add images: section to config.cjs (${err.message}).`)
55
- logger.warning(`The command will still run using built-in defaults.`)
57
+ logger.warning('The command will still run using built-in defaults.')
56
58
  }
57
59
  }
@@ -73,6 +73,49 @@ export const IPHONE_SCALES = Object.freeze([
73
73
  { suffix: '@3x', factor: 3 / 4 }
74
74
  ])
75
75
 
76
+ // Resolve target dimensions for a single scale.
77
+ // `factor * 4` recovers the integer multiplier (1, 1.5, 2, 3, 4 for Android;
78
+ // 1, 2, 3 for iPhone) only because every entry in *_SCALES is normalized to
79
+ // n/4 with the largest scale (xxxhdpi/@4x) at 4/4. If a future density is
80
+ // added beyond xxxhdpi, this conversion factor needs to be revisited.
81
+ //
82
+ // `baseHeight` pins the height explicitly (e.g. SVG pipeline resolved both
83
+ // w-* and h-* to numbers); when omitted, height follows the source aspect.
84
+ function computeScaleTarget(srcMeta, factor, baseWidth, baseHeight) {
85
+ // No pin in either direction → fall back to the source as the 4× master.
86
+ if (baseWidth == null && baseHeight == null) {
87
+ return {
88
+ targetWidth: Math.max(1, Math.round(srcMeta.width * factor)),
89
+ targetHeight: Math.max(1, Math.round(srcMeta.height * factor))
90
+ }
91
+ }
92
+ const multiplier = factor * 4
93
+ const widthOverHeight = srcMeta.width > 0 && srcMeta.height > 0
94
+ ? srcMeta.width / srcMeta.height
95
+ : 1
96
+ const heightOverWidth = srcMeta.width > 0 && srcMeta.height > 0
97
+ ? srcMeta.height / srcMeta.width
98
+ : 1
99
+
100
+ if (baseWidth != null) {
101
+ const targetWidth = Math.max(1, Math.round(baseWidth * multiplier))
102
+ const targetHeight = baseHeight != null
103
+ ? Math.max(1, Math.round(baseHeight * multiplier))
104
+ : Math.max(1, Math.round(baseWidth * multiplier * heightOverWidth))
105
+ return { targetWidth, targetHeight }
106
+ }
107
+
108
+ // Height pinned, width derived from inverse aspect.
109
+ const targetHeight = Math.max(1, Math.round(baseHeight * multiplier))
110
+ const targetWidth = Math.max(1, Math.round(baseHeight * multiplier * widthOverHeight))
111
+ return { targetWidth, targetHeight }
112
+ }
113
+
114
+ // Hard ceiling for any individual PNG output. Mirrors the constant exported
115
+ // from the SVG pipeline; centralizing it here would create a cycle, so we keep
116
+ // a local copy and rely on derive-dimensions to enforce the dp-side budget.
117
+ const MAX_OUTPUT_PIXELS = 4096
118
+
76
119
  /**
77
120
  * Scale a source image into all Android density variants.
78
121
  *
@@ -85,19 +128,19 @@ export const IPHONE_SCALES = Object.freeze([
85
128
  * @returns {Promise<string[]>} Paths written
86
129
  */
87
130
  export async function genAndroidScales(sourceFile, relPath, androidBaseDir, opts = {}) {
88
- const { format = null, quality = 85 } = opts
131
+ const { format = null, quality = 85, baseWidth = null, baseHeight = null, opacity = null, padding = null } = opts
89
132
  const src = await readSource(sourceFile)
90
133
  const written = []
91
134
 
92
135
  for (const { name, factor } of ANDROID_SCALES) {
93
- const targetWidth = Math.max(1, Math.round(src.meta.width * factor))
94
- const targetHeight = Math.max(1, Math.round(src.meta.height * factor))
136
+ const { targetWidth, targetHeight } = computeScaleTarget(src.meta, factor, baseWidth, baseHeight)
137
+ assertWithinCap(targetWidth, targetHeight, sourceFile, name)
95
138
 
96
139
  const outDir = path.join(androidBaseDir, name, path.dirname(relPath))
97
140
  fs.mkdirSync(outDir, { recursive: true })
98
141
 
99
142
  const outPath = path.join(outDir, renameWithFormat(path.basename(relPath), format, src.isSvg))
100
- await writeScaled(src, outPath, targetWidth, targetHeight, format, quality)
143
+ await writeScaled(src, outPath, targetWidth, targetHeight, format, quality, opacity, padding)
101
144
  written.push(outPath)
102
145
  }
103
146
  return written
@@ -113,7 +156,7 @@ export async function genAndroidScales(sourceFile, relPath, androidBaseDir, opts
113
156
  * @returns {Promise<string[]>} Paths written
114
157
  */
115
158
  export async function genIphoneScales(sourceFile, relPath, iphoneBaseDir, opts = {}) {
116
- const { format = null, quality = 85 } = opts
159
+ const { format = null, quality = 85, baseWidth = null, baseHeight = null, opacity = null, padding = null } = opts
117
160
  const src = await readSource(sourceFile)
118
161
  const written = []
119
162
 
@@ -122,8 +165,8 @@ export async function genIphoneScales(sourceFile, relPath, iphoneBaseDir, opts =
122
165
  fs.mkdirSync(outDir, { recursive: true })
123
166
 
124
167
  for (const { suffix, factor } of IPHONE_SCALES) {
125
- const targetWidth = Math.max(1, Math.round(src.meta.width * factor))
126
- const targetHeight = Math.max(1, Math.round(src.meta.height * factor))
168
+ const { targetWidth, targetHeight } = computeScaleTarget(src.meta, factor, baseWidth, baseHeight)
169
+ assertWithinCap(targetWidth, targetHeight, sourceFile, suffix || '@1x')
127
170
 
128
171
  // SVG sources can't be written as SVG by Sharp — fall back to PNG if the
129
172
  // user didn't specify an explicit output format.
@@ -131,12 +174,21 @@ export async function genIphoneScales(sourceFile, relPath, iphoneBaseDir, opts =
131
174
  const outName = `${parsed.name}${suffix}${ext}`
132
175
  const outPath = path.join(outDir, outName)
133
176
 
134
- await writeScaled(src, outPath, targetWidth, targetHeight, format, quality)
177
+ await writeScaled(src, outPath, targetWidth, targetHeight, format, quality, opacity, padding)
135
178
  written.push(outPath)
136
179
  }
137
180
  return written
138
181
  }
139
182
 
183
+ function assertWithinCap(width, height, sourceFile, label) {
184
+ if (width > MAX_OUTPUT_PIXELS || height > MAX_OUTPUT_PIXELS) {
185
+ throw new Error(
186
+ `${path.basename(sourceFile)} at ${label} would render ${width}×${height}px, ` +
187
+ `which exceeds the ${MAX_OUTPUT_PIXELS}px cap. Reduce the resolved width or override it manually in config.cjs > images.files.`
188
+ )
189
+ }
190
+ }
191
+
140
192
  function renameWithFormat(filename, format, isSvg = false) {
141
193
  if (format) {
142
194
  const parsed = path.parse(filename)
@@ -150,15 +202,46 @@ function renameWithFormat(filename, format, isSvg = false) {
150
202
  return filename
151
203
  }
152
204
 
153
- async function writeScaled(src, outPath, width, height, format, quality) {
154
- const targetMax = Math.max(width, height)
205
+ async function writeScaled(src, outPath, width, height, format, quality, opacity, paddingPct) {
206
+ // Padding shrinks the rendered image inside the same canvas so each density
207
+ // gets symmetric transparent borders. Computed from the canvas dimensions so
208
+ // the visual ratio (e.g. 15%) is identical across every density variant.
209
+ const padX = paddingPct ? Math.floor(width * paddingPct / 100) : 0
210
+ const padY = paddingPct ? Math.floor(height * paddingPct / 100) : 0
211
+ const innerW = Math.max(1, width - 2 * padX)
212
+ const innerH = Math.max(1, height - 2 * padY)
213
+ const targetMax = Math.max(innerW, innerH)
214
+
155
215
  let pipeline = buildScalePipeline(src, targetMax).resize({
156
- width,
157
- height,
216
+ width: innerW,
217
+ height: innerH,
158
218
  fit: 'contain',
159
219
  background: { r: 0, g: 0, b: 0, alpha: 0 }
160
220
  })
161
221
 
222
+ if (padX > 0 || padY > 0) {
223
+ pipeline = pipeline.extend({
224
+ top: padY,
225
+ bottom: padY,
226
+ left: padX,
227
+ right: padX,
228
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
229
+ })
230
+ }
231
+
232
+ // Apply opacity by multiplying the dest alpha against a uniform-alpha tile.
233
+ // `dest-in` keeps RGB and multiplies dest alpha by source alpha (opacity/100).
234
+ if (opacity != null && opacity < 100) {
235
+ pipeline = pipeline
236
+ .ensureAlpha()
237
+ .composite([{
238
+ input: Buffer.from([255, 255, 255, Math.round(255 * opacity / 100)]),
239
+ raw: { width: 1, height: 1, channels: 4 },
240
+ tile: true,
241
+ blend: 'dest-in'
242
+ }])
243
+ }
244
+
162
245
  // For SVG sources without an explicit format, coerce output to PNG
163
246
  // (Sharp cannot write SVG).
164
247
  const fallbackExt = src.isSvg ? 'png' : path.extname(src.path).slice(1).toLowerCase()
@@ -170,7 +253,7 @@ async function writeScaled(src, outPath, width, height, format, quality) {
170
253
 
171
254
  function applyFormat(pipeline, format, quality) {
172
255
  switch (format) {
173
- case 'png': return pipeline.png({ quality, compressionLevel: 9 })
256
+ case 'png': return pipeline.png({ compressionLevel: 9 })
174
257
  case 'webp': return pipeline.webp({ quality })
175
258
  case 'avif': return pipeline.avif({ quality })
176
259
  case 'tiff': return pipeline.tiff({ quality, compression: 'lzw' })