purgetss 7.6.2 → 7.7.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.
package/README.md CHANGED
@@ -10,13 +10,14 @@
10
10
 
11
11
  </div>
12
12
 
13
- **PurgeTSS** is a toolkit for building mobile apps with the [Titanium framework](https://titaniumsdk.com). It provides utility classes, icon font support, an Animation module, a grid system, and color generation commands (`shades` for tonal palettes, `semantic` for Light/Dark mode semantic colors).
13
+ **PurgeTSS** is a toolkit for building mobile apps with the [Titanium framework](https://titaniumsdk.com). It gives you utility classes, icon font support, an Animation module, a grid system, and color generation commands (`shades` for tonal palettes, `semantic` for Light/Dark mode semantic colors).
14
14
 
15
15
  ---
16
16
 
17
17
  - 23,300+ utility classes for styling Titanium views
18
18
  - Parses XML files to generate a clean `app.tss` with only the classes your project uses
19
19
  - Customizable defaults via `config.cjs`, with JIT classes for arbitrary values
20
+ - `brand` command for Titanium icons and branding assets, including Android launcher variants, optional Android 12+ splash artwork, and legacy `default.png` fallback generation
20
21
  - Icon font support: Font Awesome, Material Icons, Material Symbols, Framework7-Icons
21
22
  - `build-fonts` command generates `fonts.tss` with class definitions and fontFamily selectors
22
23
  - `shades` command generates color shades from any hex color
@@ -374,6 +375,18 @@ Button: {
374
375
  - **Alloy Framework** (for most commands)
375
376
  - **Node.js 20+** (required for the CLI tool)
376
377
 
378
+ ## Recent changes
379
+
380
+ ### v7.7.0
381
+
382
+ - `brand` now uses grouped config sections: `brand.logos`, `brand.padding`, `brand.android`, `brand.ios`, and `brand.colors`.
383
+ - `brand` supports separate Android artwork through `brand.logos.androidLauncher` / `--icon-logo` and `brand.logos.androidSplash` / `--splash-logo`.
384
+ - `brand` regenerates `app/assets/android/default.png` in Alloy projects, or `Resources/android/default.png` in Classic projects, so older Android splash paths still have a fallback.
385
+ - `cleanup-legacy` no longer removes `default.png`.
386
+ - Branding help and docs now spell out the difference between Android launcher icons, Android 12+ `splash_icon.png`, and legacy Android splash assets.
387
+
388
+ See the full release notes in [CHANGELOG.md](./CHANGELOG.md).
389
+
377
390
  ## Table of Content
378
391
 
379
392
  - [Installation](https://purgetss.com/docs/installation)
package/bin/purgetss CHANGED
@@ -262,11 +262,14 @@ program
262
262
  Writes directly into the current project (Alloy or Classic — auto-detected).
263
263
  Logos auto-discovered from ${chalk.cyan('purgetss/brand/')}:
264
264
  ${chalk.yellow('logo.{svg,png}')} required — main logo
265
+ ${chalk.yellow('logo-icon.{svg,png}')} optional — square Android launcher mark
265
266
  ${chalk.yellow('logo-mono.{svg,png}')} optional — monochrome layer + notifications
266
267
  ${chalk.yellow('logo-dark.{svg,png}')} optional — iOS 18+ dark variant
267
268
  ${chalk.yellow('logo-tinted.{svg,png}')} optional — iOS 18+ tinted variant
269
+ ${chalk.yellow('logo-splash.{svg,png}')} optional — Android 12+ splash icon override
268
270
 
269
271
  Defaults come from the ${chalk.cyan('brand:')} section in ${chalk.cyan('purgetss/config.cjs')}.
272
+ The recommended workflow is: put files in ${chalk.cyan('purgetss/brand/')}, then only use config/CLI for overrides.
270
273
  CLI flags always win over config values.
271
274
 
272
275
  Generates:
@@ -276,6 +279,8 @@ Generates:
276
279
  ${chalk.yellow('mipmap-*/ic_launcher_{foreground,background,monochrome}.png')} Android adaptive × 5
277
280
  ${chalk.yellow('mipmap-*/ic_launcher.png')} Android legacy × 5
278
281
  ${chalk.yellow('mipmap-anydpi-v26/ic_launcher.xml')} Adaptive icon binder
282
+ ${chalk.yellow('drawable-*/splash_icon.png')} Android 12+ splash icon (with ${chalk.cyan('--splash')})
283
+ ${chalk.yellow('app/assets/android/default.png')} Android <12 legacy splash fallback
279
284
 
280
285
  Android dark/light mode is handled by the ${chalk.yellow('monochrome')} adaptive layer
281
286
  (Android 13+ tints it from the wallpaper + theme). No separate dark file exists.
@@ -284,16 +289,22 @@ Examples:
284
289
  ${chalk.cyan('purgetss brand')} # uses purgetss/brand/logo.svg + config
285
290
  ${chalk.cyan('purgetss brand')} logo.svg # explicit logo path
286
291
  ${chalk.cyan('purgetss brand')} --bg-color "#0B1326" # override config bg
292
+ ${chalk.cyan('purgetss brand')} --android-adaptive-padding 22 # more breathing room for adaptive icons
293
+ ${chalk.cyan('purgetss brand')} --icon-logo app-icon.svg # separate square Android icon
287
294
  ${chalk.cyan('purgetss brand')} --dark-bg-color "#1C1C1E" --no-tinted # customize dark, skip tinted
288
295
  ${chalk.cyan('purgetss brand')} --dry-run # preview without writing
289
296
  ${chalk.cyan('purgetss brand')} --cleanup-legacy --dry-run # preview legacy cleanup
290
297
  `)
291
298
  .option('--bg-color <hex>', 'Background color for Android adaptive + iOS flatten (default: #FFFFFF)')
292
- .option('--padding <n>', 'Android safe-zone % (range 12-20, default: 15)', (v) => parseInt(v, 10))
299
+ .option('--padding <n>', 'Shortcut: sets both Android paddings to the same value', (v) => parseInt(v, 10))
300
+ .option('--android-adaptive-padding <n>', 'Adaptive icon safe-zone % (default: 19)', (v) => parseInt(v, 10))
301
+ .option('--android-legacy-padding <n>', 'Legacy ic_launcher.png padding % (default: 10)', (v) => parseInt(v, 10))
293
302
  .option('--ios-padding <n>', 'iOS aesthetic % (typical 2-6, default: 4)', (v) => parseInt(v, 10))
294
303
  .option('--notification', 'Also generate ic_stat_notify.png × 5 densities')
295
304
  .option('--splash', 'Also generate Android 12+ splash_icon.png × 5 densities')
305
+ .option('--icon-logo <path>', 'Override the Android launcher icon logo (otherwise purgetss/brand/logo-icon.{svg,png})')
296
306
  .option('--monochrome-logo <path>', 'Override the monochrome logo (otherwise purgetss/brand/logo-mono.{svg,png})')
307
+ .option('--splash-logo <path>', 'Override the Android 12+ splash icon logo (otherwise purgetss/brand/logo-splash.{svg,png})')
297
308
  .option('--dark-bg-color <hex>', 'Opt into opaque dark bg for DefaultIcon-Dark.png (default: transparent per Apple HIG)')
298
309
  .option('--dark-logo <path>', 'Override the dark logo (otherwise purgetss/brand/logo-dark.{svg,png})')
299
310
  .option('--tinted-logo <path>', 'Override the tinted logo (otherwise purgetss/brand/logo-tinted.{svg,png})')
@@ -1,4 +1,4 @@
1
- // PurgeTSS v7.6.0
1
+ // PurgeTSS v7.7.0
2
2
  // Created by César Estrada
3
3
  // https://purgetss.com
4
4
 
@@ -30,6 +30,182 @@ const logger = {
30
30
  file: (...args) => console.log(purgeLabel, chalk.yellow(args.join(' ')), 'file created!')
31
31
  }
32
32
 
33
+ // Keys whose numeric values are interpreted with `ti.ui.defaultunit` from tiapp.xml.
34
+ // The glossary .md files for these keys receive an inline "// Unit: ..." note
35
+ // so Context7 (and humans) always retrieve the unit clarification alongside the values.
36
+ // Keep this list in sync with `docs/best-practices/4-values-and-units.md` in purgetss-docs.
37
+ const UNIT_DEPENDENT_KEYS = new Set([
38
+ // configurableProperties (Ti.UI.* native dimensional properties)
39
+ 'backgroundLeftCap', 'backgroundPaddingBottom', 'backgroundPaddingLeft',
40
+ 'backgroundPaddingRight', 'backgroundPaddingTop', 'backgroundTopCap',
41
+ 'borderRadius', 'borderWidth', 'bottom', 'contentHeight', 'contentWidth',
42
+ 'elevation', 'height', 'imageHeight', 'imagePadding', 'keyboardToolbarHeight',
43
+ 'left', 'leftButtonPadding', 'leftTrackLeftCap', 'leftTrackTopCap', 'leftWidth',
44
+ 'letterSpacing', 'lineSpacing', 'maxElevation', 'maximumLineHeight',
45
+ 'maxRowHeight', 'minimumLineHeight', 'minRowHeight', 'padding', 'paddingBottom',
46
+ 'paddingLeft', 'paddingRight', 'paddingTop', 'pageHeight', 'pageWidth',
47
+ 'pagingControlHeight', 'paragraphSpacingAfter', 'paragraphSpacingBefore',
48
+ 'right', 'rightButtonPadding', 'rightTrackLeftCap', 'rightTrackTopCap',
49
+ 'rightWidth', 'rowHeight', 'sectionHeaderTopPadding', 'separatorHeight',
50
+ 'shadowRadius', 'statusBarHeight', 'targetImageHeight', 'targetImageWidth',
51
+ 'titlePadding', 'top', 'uprightHeight', 'uprightWidth', 'width',
52
+ 'xOffset', 'yOffset',
53
+ // compoundClasses that bundle dimensional properties
54
+ 'borderRadius-alternative', 'borderRadius-full', 'content-height-and-width',
55
+ 'dropShadow', 'fontSize', 'margin', 'margin-alternative', 'minimumFontSize',
56
+ 'padding-alternative', 'size', 'titleAttributesShadow-alternative',
57
+ 'viewShadowOffset', 'widthHeight'
58
+ ])
59
+
60
+ const UNIT_NOTE_LINES = [
61
+ '// Unit: numeric values are unitless. Titanium interprets them using ti.ui.defaultunit in tiapp.xml (Alloy template default: dp, not pixels).',
62
+ '// Docs: /docs/best-practices/values-and-units'
63
+ ]
64
+
65
+ function injectUnitNote(key, classes) {
66
+ if (!UNIT_DEPENDENT_KEYS.has(key)) return classes
67
+ const lines = classes.split('\n')
68
+ let lastCommentIdx = -1
69
+ for (let i = 0; i < lines.length; i++) {
70
+ const trimmed = lines[i].trim()
71
+ if (trimmed.startsWith('//')) {
72
+ lastCommentIdx = i
73
+ } else if (trimmed !== '' && lastCommentIdx !== -1) {
74
+ break
75
+ }
76
+ }
77
+ if (lastCommentIdx === -1) return classes
78
+ lines.splice(lastCommentIdx + 1, 0, ...UNIT_NOTE_LINES)
79
+ return lines.join('\n')
80
+ }
81
+
82
+ // Scaffolding metadata for the glossary subfolders. Every field here drives
83
+ // what the generator writes for each category — update this array and rerun
84
+ // the build to refresh both the subfolder `index.md` and its entry in the
85
+ // root `index.md`.
86
+ //
87
+ // Fields:
88
+ // id folder name (matches purgetss-docs/glossary/<id>/)
89
+ // label Sidebar label (used in `_category_.json` and subfolder H1)
90
+ // intro Paragraph shown at the top of the subfolder's own index.md
91
+ // rootDescription Longer paragraph shown under the category in the root index.md
92
+ // rootLinkText (optional) Override for the link text in the root index.md
93
+ // — use this when the link text on the landing page differs
94
+ // from the sidebar label. Falls back to `label` when absent.
95
+ // unitsCallout (optional) If present, adds a `:::info Units` admonition
96
+ // to the subfolder's index.md pointing at the canonical
97
+ // values-and-units page.
98
+ const GLOSSARY_SUBFOLDERS = [
99
+ {
100
+ id: 'booleanProperties',
101
+ label: 'Boolean Properties',
102
+ intro: 'Use these properties when a setting is strictly true/false. Entries cover accessibility flags, UI state toggles, media permissions, and similar binary behaviors. Browse the list in the sidebar for the complete catalog.',
103
+ rootDescription: 'Properties that can only have two values: true or false. These properties are fundamental for controlling the behavior and appearance of elements in your application. Includes over 200 properties organized into categories such as accessibility, interface controls, media, and security.'
104
+ },
105
+ {
106
+ id: 'colorProperties',
107
+ label: 'Color Properties',
108
+ intro: 'Color properties control text, backgrounds, borders, icons, and state colors. Values accept hex, RGB, named colors, and platform-supported formats. Use the sidebar list to jump to a specific property.',
109
+ rootDescription: 'Properties that allow you to customize your application\'s colors. Accepts various color formats (hex, RGB, predefined names) and covers everything from basic element colors to complex visual effects, including backgrounds, borders, text, icons, and element states.'
110
+ },
111
+ {
112
+ id: 'compoundClasses',
113
+ label: 'Compound Properties',
114
+ rootLinkText: 'Compound Classes',
115
+ intro: 'Compound classes bundle multiple style attributes into one utility for faster styling. They cover layout, typography, spacing, and effects with alternative syntaxes where supported. See the sidebar for each entry.',
116
+ rootDescription: 'Advanced classes that combine multiple style attributes into a single property. These classes provide sophisticated styling capabilities for layout, positioning, typography, and visual effects, with multiple variations and alternative syntaxes.',
117
+ unitsCallout: 'Many compound classes on this page produce dimensional values (margins, paddings, sizes, radii, shadow offsets, font sizes, translations). Those numbers are **unitless** — Titanium resolves them using `ti.ui.defaultunit` in `tiapp.xml`. The Alloy template default is `dp` (density-independent pixels), **not raw pixels**. See [Values and Units](/docs/best-practices/values-and-units) for the full rules and exceptions.'
118
+ },
119
+ {
120
+ id: 'configurableProperties',
121
+ label: 'Configurable Properties',
122
+ intro: 'These properties accept numeric or measurable values such as sizes, spacing, opacity, durations, and weights. Use them to fine-tune UI details with precise values. The sidebar lists all available properties.',
123
+ rootDescription: 'Numeric or measurable properties that allow you to fine-tune the appearance and behavior of UI elements. Includes dimensions, spacing, visual effects, typography, and animations, all accepting specific values like numbers, dimensions, or durations.',
124
+ unitsCallout: 'Dimensional values on this page (sizes, spacing, radii, font sizes, shadow radii, elevation, offsets, line heights, letter spacing) are **unitless**. Titanium resolves them at runtime using `ti.ui.defaultunit` in `tiapp.xml`. The Alloy template default is `dp` (density-independent pixels), **not raw pixels**. See [Values and Units](/docs/best-practices/values-and-units) for the full rules and exceptions.'
125
+ },
126
+ {
127
+ id: 'constantProperties',
128
+ label: 'Constant Properties',
129
+ intro: 'Constant properties accept predefined values or enumerations (e.g., modes, types, categories). Use them to enforce valid states and platform-specific options. Browse the sidebar for the full list.',
130
+ rootDescription: 'Predefined values or enumerations that determine specific behaviors, styles, or states of UI elements. These properties only accept predefined values to ensure consistency and functionality, covering interface states, user input, media, authorizations, and notifications.'
131
+ }
132
+ ]
133
+
134
+ // Header + global units callout for the root `glossary/index.md`. The list of
135
+ // categories below `## Categories` is derived from GLOSSARY_SUBFOLDERS by
136
+ // `buildRootIndex()` — add a subfolder to the array and it will appear here.
137
+ const GLOSSARY_ROOT_HEADER = `---
138
+ sidebar_position: 1
139
+ ---
140
+
141
+ # Glossary of Terms
142
+
143
+ Welcome to the PurgeTSS glossary. Here you'll find a complete list of terms, properties, and concepts used throughout our documentation, organized into categories for easy reference.
144
+
145
+ :::info Units — read this first
146
+ Numeric values shown in the property pages below are **unitless**. Titanium interprets them using \`ti.ui.defaultunit\` in \`tiapp.xml\` — the Alloy template ships with \`dp\` (density-independent pixels), **not raw pixels**. See [Values and Units](/docs/best-practices/values-and-units) for the full explanation.
147
+ :::
148
+
149
+ ## Categories
150
+ `
151
+
152
+ function buildRootIndex() {
153
+ let content = GLOSSARY_ROOT_HEADER
154
+ for (const folder of GLOSSARY_SUBFOLDERS) {
155
+ const linkText = folder.rootLinkText ?? folder.label
156
+ content += `\n### [${linkText}](./${folder.id}/index.md)\n`
157
+ content += `${folder.rootDescription}\n`
158
+ }
159
+ return content
160
+ }
161
+
162
+ function buildSubfolderIndex(folder) {
163
+ let content = '---\n'
164
+ content += `title: ${folder.label}\n`
165
+ content += `slug: /glossary/${folder.id}\n`
166
+ content += '---\n\n'
167
+ content += folder.intro + '\n'
168
+ if (folder.unitsCallout) {
169
+ content += '\n:::info Units — important\n'
170
+ content += folder.unitsCallout + '\n'
171
+ content += ':::\n'
172
+ }
173
+ return content
174
+ }
175
+
176
+ function glossaryBaseFolder() {
177
+ if (!fs.existsSync(projectsConfigJS)) return path.resolve(__dirname, '../dist/glossary/')
178
+ if (saveGlossary) return cwd + '/purgetss/glossary/'
179
+ return ''
180
+ }
181
+
182
+ function scaffoldGlossary() {
183
+ const baseFolder = glossaryBaseFolder()
184
+ if (!baseFolder) return
185
+
186
+ // Wipe the output tree so stale property files from prior runs don't linger.
187
+ if (fs.existsSync(baseFolder)) fs.rmSync(baseFolder, { recursive: true })
188
+ makeSureFolderExists(baseFolder)
189
+
190
+ // Root-level files
191
+ saveFile(path.join(baseFolder, '_category_.json'), JSON.stringify({
192
+ label: 'Glossary of Terms',
193
+ position: 1
194
+ }, null, 2) + '\n')
195
+ saveFile(path.join(baseFolder, 'index.md'), buildRootIndex())
196
+
197
+ // Subfolders with their _category_.json and index.md
198
+ for (const folder of GLOSSARY_SUBFOLDERS) {
199
+ const sub = path.join(baseFolder, folder.id)
200
+ makeSureFolderExists(sub)
201
+ saveFile(path.join(sub, '_category_.json'), JSON.stringify({
202
+ label: folder.label,
203
+ link: { type: 'doc', id: `${folder.id}/index` }
204
+ }, null, 2) + '\n')
205
+ saveFile(path.join(sub, 'index.md'), buildSubfolderIndex(folder))
206
+ }
207
+ }
208
+
33
209
  let configFile = getConfigFile()
34
210
  configFile.purge = configFile.purge ?? { mode: 'all' }
35
211
  configFile.theme = configFile.theme ?? {}
@@ -59,6 +235,7 @@ function autoBuildUtilitiesTSS(options = {}) {
59
235
  }
60
236
 
61
237
  saveGlossary = options.glossary ?? false
238
+ scaffoldGlossary()
62
239
  let tailwindStyles = fs.readFileSync(path.resolve(__dirname, '../lib/templates/tailwind/custom-template.tss'), 'utf8')
63
240
  tailwindStyles += (fs.existsSync(projectsConfigJS)) ? `// config.js file updated on: ${getFileUpdatedDate(projectsConfigJS)}\n` : '// default config.js file\n'
64
241
 
@@ -161,7 +338,7 @@ function generateGlossary(_key, _theClasses, _keyName = null) {
161
338
  }
162
339
 
163
340
  makeSureFolderExists(destinationFolder)
164
- saveFile(`${destinationFolder}/${_key}.md`, '```css' + _theClasses + '```\n')
341
+ saveFile(`${destinationFolder}/${_key}.md`, '```css' + injectUnitNote(_key, _theClasses) + '```\n')
165
342
  }
166
343
 
167
344
  return _theClasses
@@ -13,12 +13,33 @@ module.exports = {
13
13
  }
14
14
  },
15
15
  brand: {
16
- splash: false, // also generate splash_icon.png × 5
17
- padding: '15%', // Android safe-zone. Range: 12% tight (mature logos) 20% conservative. Spec floor 19.44%.
18
- iosPadding: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
19
- darkBgColor: null, // opaque dark bg for DefaultIcon-Dark.png (null = transparent per Apple HIG)
20
- bgColor: '#FFFFFF', // Android adaptive bg + iOS/marketplace flatten
21
- notification: false, // also generate ic_stat_notify.png × 5
16
+ logos: {
17
+ // Optional overrides. If omitted, PurgeTSS auto-discovers files from purgetss/brand/:
18
+ // primary: './docs/logo.svg',
19
+ // androidLauncher: './docs/app-icon.svg',
20
+ // androidSplash: './docs/splash.svg',
21
+ // monochrome: './docs/logo-mono.svg',
22
+ // iosDark: './docs/logo-dark.svg',
23
+ // iosTinted: './docs/logo-tinted.svg'
24
+ },
25
+ padding: {
26
+ ios: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
27
+ androidLegacy: '10%', // legacy ic_launcher.png padding
28
+ androidAdaptive: '19%' // adaptive foreground padding near the Android safe-zone
29
+ },
30
+ android: {
31
+ splash: false, // also generate splash_icon.png × 5
32
+ notification: false // also generate ic_stat_notify.png × 5
33
+ },
34
+ colors: {
35
+ background: '#FFFFFF' // Android adaptive bg + iOS/marketplace flatten
36
+ },
37
+ // Optional iOS overrides:
38
+ // ios: {
39
+ // dark: false,
40
+ // tinted: false,
41
+ // darkBackground: '#111111'
42
+ // },
22
43
  confirmOverwrites: true // prompt before overwriting files (set false to skip)
23
44
  },
24
45
  images: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "purgetss",
4
- "version": "7.6.2",
4
+ "version": "7.7.0",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "purgetss": "bin/purgetss"
@@ -64,6 +64,6 @@ function printMissingLogoHelp(projectRoot) {
64
64
  console.log()
65
65
  console.log(' Alternatives:')
66
66
  console.log(` ${chalk.gray('•')} Pass the logo explicitly: ${chalk.cyan('purgetss brand path/to/logo.svg')}`)
67
- console.log(` ${chalk.gray('•')} Point to it from config.cjs: ${chalk.gray('brand: { logo: \'./docs/logo.svg\' }')}`)
67
+ console.log(` ${chalk.gray('•')} Point to it from config.cjs: ${chalk.gray('brand: { logos: { primary: \'./docs/logo.svg\' } }')}`)
68
68
  console.log()
69
69
  }
@@ -9,9 +9,11 @@
9
9
  * Also auto-discovers logo images inside `./purgetss/brand/` following the
10
10
  * project convention:
11
11
  * logo.{svg,png} → required (main logo)
12
+ * logo-icon.{svg,png} → optional (square launcher/splash mark for Android)
12
13
  * logo-mono.{svg,png} → optional (monochrome layer + notifications)
13
14
  * logo-dark.{svg,png} → optional (iOS 18+ dark variant)
14
15
  * logo-tinted.{svg,png} → optional (iOS 18+ tinted variant)
16
+ * logo-splash.{svg,png} → optional (Android 12+ splash icon override)
15
17
  *
16
18
  * CLI --monochrome-logo / --dark-logo / --tinted-logo always override
17
19
  * discovery, and the positional argument overrides the main logo.
@@ -52,26 +54,55 @@ function findLogoFile(baseDir, baseName) {
52
54
  export function resolveBrandConfig(cliOptions, cliLogo, projectRoot) {
53
55
  const brandConfig = loadBrandSection()
54
56
  const brandDir = path.join(projectRoot, BRAND_DIR)
57
+ const logos = brandConfig.logos || {}
58
+ const padding = brandConfig.padding || {}
59
+ const android = brandConfig.android || {}
60
+ const ios = brandConfig.ios || {}
61
+ const colors = brandConfig.colors || {}
62
+
63
+ const androidAdaptivePadding = cliOptions.androidAdaptivePadding
64
+ ?? cliOptions.padding
65
+ ?? padding.androidAdaptive
66
+ ?? 19
67
+
68
+ const androidLegacyPadding = cliOptions.androidLegacyPadding
69
+ ?? padding.androidLegacy
70
+ ?? 10
71
+
72
+ const iosPadding = cliOptions.iosPadding
73
+ ?? padding.ios
74
+ ?? 4
75
+
76
+ const bgColor = cliOptions.bgColor
77
+ ?? colors.background
78
+ ?? '#FFFFFF'
79
+
80
+ const darkBgColor = cliOptions.darkBgColor
81
+ ?? ios.darkBackground
82
+ ?? null
55
83
 
56
84
  const resolved = {
57
- logo: pickLogo(cliLogo, brandConfig.logo, brandDir, 'logo', projectRoot),
58
- monochromeLogo: pickLogo(cliOptions.monochromeLogo, brandConfig.monochromeLogo, brandDir, 'logo-mono', projectRoot),
59
- darkLogo: pickLogo(cliOptions.darkLogo, brandConfig.darkLogo, brandDir, 'logo-dark', projectRoot),
60
- tintedLogo: pickLogo(cliOptions.tintedLogo, brandConfig.tintedLogo, brandDir, 'logo-tinted', projectRoot),
85
+ logo: pickLogo(cliLogo, logos.primary, brandDir, 'logo', projectRoot),
86
+ iconLogo: pickLogo(cliOptions.iconLogo, logos.androidLauncher, brandDir, 'logo-icon', projectRoot),
87
+ monochromeLogo: pickLogo(cliOptions.monochromeLogo, logos.monochrome, brandDir, 'logo-mono', projectRoot),
88
+ darkLogo: pickLogo(cliOptions.darkLogo, logos.iosDark, brandDir, 'logo-dark', projectRoot),
89
+ tintedLogo: pickLogo(cliOptions.tintedLogo, logos.iosTinted, brandDir, 'logo-tinted', projectRoot),
90
+ splashLogo: pickLogo(cliOptions.splashLogo, logos.androidSplash, brandDir, 'logo-splash', projectRoot),
61
91
 
62
- bgColor: cliOptions.bgColor ?? brandConfig.bgColor ?? '#FFFFFF',
63
- bgColorExplicit: Boolean(cliOptions.bgColor ?? brandConfig.bgColor),
64
- darkBgColor: cliOptions.darkBgColor ?? brandConfig.darkBgColor ?? null,
65
- padding: cliOptions.padding ?? brandConfig.padding ?? 15,
66
- iosPadding: cliOptions.iosPadding ?? brandConfig.iosPadding ?? 4,
92
+ bgColor,
93
+ bgColorExplicit: Boolean(cliOptions.bgColor ?? colors.background),
94
+ darkBgColor,
95
+ androidAdaptivePadding,
96
+ androidLegacyPadding,
97
+ iosPadding,
67
98
 
68
99
  // Kitchen-sink defaults: adaptive + marketplace are always generated; only
69
100
  // notification and splash are opt-in. Config can pre-enable them.
70
- notification: Boolean(cliOptions.notification ?? brandConfig.notification ?? false),
71
- splash: Boolean(cliOptions.splash ?? brandConfig.splash ?? false),
101
+ notification: Boolean(cliOptions.notification ?? android.notification ?? false),
102
+ splash: Boolean(cliOptions.splash ?? android.splash ?? false),
72
103
 
73
- withDark: cliOptions.dark !== false && (brandConfig.dark ?? true),
74
- withTinted: cliOptions.tinted !== false && (brandConfig.tinted ?? true),
104
+ withDark: cliOptions.dark !== false && (ios.dark ?? true),
105
+ withTinted: cliOptions.tinted !== false && (ios.tinted ?? true),
75
106
 
76
107
  cleanupLegacy: Boolean(cliOptions.cleanupLegacy),
77
108
  aggressive: Boolean(cliOptions.aggressive),
@@ -57,13 +57,6 @@ export async function cleanupLegacy({ projectRoot, projectType, aggressive = fal
57
57
  }
58
58
 
59
59
  if (adaptive && layout.androidAssets) {
60
- const defaultPng = path.join(layout.androidAssets, 'default.png')
61
- if (fs.existsSync(defaultPng)) {
62
- conditional.push({
63
- path: defaultPng,
64
- reason: 'Legacy Android splash PNG (adaptive launcher handles splash now)'
65
- })
66
- }
67
60
  const appicon = path.join(layout.androidAssets, 'appicon.png')
68
61
  if (fs.existsSync(appicon)) {
69
62
  conditional.push({
@@ -23,12 +23,33 @@ import { projectsConfigJS, projectsPurge_TSS_Brand_Folder } from '../../shared/c
23
23
  import { logger } from './branding-logger.js'
24
24
 
25
25
  const BRAND_BLOCK = ` brand: {
26
- splash: false, // also generate splash_icon.png × 5
27
- padding: '15%', // Android safe-zone. Range: 12% tight (mature logos) 20% conservative. Spec floor 19.44%.
28
- iosPadding: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
29
- darkBgColor: null, // opaque dark bg for DefaultIcon-Dark.png (null = transparent per Apple HIG)
30
- bgColor: '#FFFFFF', // Android adaptive bg + iOS/marketplace flatten
31
- notification: false, // also generate ic_stat_notify.png × 5
26
+ logos: {
27
+ // Optional overrides. If omitted, PurgeTSS auto-discovers files from purgetss/brand/:
28
+ // primary: './docs/logo.svg',
29
+ // androidLauncher: './docs/app-icon.svg',
30
+ // androidSplash: './docs/splash.svg',
31
+ // monochrome: './docs/logo-mono.svg',
32
+ // iosDark: './docs/logo-dark.svg',
33
+ // iosTinted: './docs/logo-tinted.svg'
34
+ },
35
+ padding: {
36
+ ios: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
37
+ androidLegacy: '10%', // legacy ic_launcher.png padding
38
+ androidAdaptive: '19%' // adaptive foreground padding near the Android safe-zone
39
+ },
40
+ android: {
41
+ splash: false, // also generate splash_icon.png × 5
42
+ notification: false // also generate ic_stat_notify.png × 5
43
+ },
44
+ colors: {
45
+ background: '#FFFFFF' // Android adaptive bg + iOS/marketplace flatten
46
+ },
47
+ // Optional iOS overrides:
48
+ // ios: {
49
+ // dark: false,
50
+ // tinted: false,
51
+ // darkBackground: '#111111'
52
+ // },
32
53
  confirmOverwrites: true // prompt before overwriting files (set false to skip)
33
54
  },
34
55
  `
@@ -70,7 +91,7 @@ export function ensureBrandSection() {
70
91
  fs.writeFileSync(projectsConfigJS, patched, 'utf8')
71
92
  console.log()
72
93
  logger.success(`Added ${chalk.cyan('brand:')} section to ${chalk.cyan('./purgetss/config.cjs')} with default values.`)
73
- console.log(' Edit that block to customize brand defaults (bgColor, padding, etc.).')
94
+ console.log(' Edit that block to customize brand defaults (logos, padding, colors, etc.).')
74
95
  console.log(' CLI flags always win over config values.')
75
96
  console.log()
76
97
  } catch (err) {
@@ -28,8 +28,8 @@ export const DENSITIES = [
28
28
  { name: 'xxxhdpi', size: 432 }
29
29
  ]
30
30
 
31
- export async function genAndroidAdaptive(tightMaster, bgColor, paddingPct, resRoot, opts = {}) {
32
- const { monoTight = null } = opts
31
+ export async function genAndroidAdaptive(masterPng, bgColor, paddingPct, resRoot, opts = {}) {
32
+ const { monoMaster = null } = opts
33
33
  const generated = []
34
34
 
35
35
  for (const { name, size } of DENSITIES) {
@@ -42,7 +42,7 @@ export async function genAndroidAdaptive(tightMaster, bgColor, paddingPct, resRo
42
42
  const monochromePath = path.join(dir, 'ic_launcher_monochrome.png')
43
43
 
44
44
  // Foreground: logo sized to inner, centered on transparent canvas
45
- const innerLogo = await sharp(tightMaster)
45
+ const innerLogo = await sharp(masterPng)
46
46
  .resize({
47
47
  width: inner,
48
48
  height: inner,
@@ -76,8 +76,8 @@ export async function genAndroidAdaptive(tightMaster, bgColor, paddingPct, resRo
76
76
  .toFile(backgroundPath)
77
77
 
78
78
  // Monochrome: white silhouette with preserved alpha.
79
- if (monoTight) {
80
- const innerMono = await sharp(monoTight)
79
+ if (monoMaster) {
80
+ const innerMono = await sharp(monoMaster)
81
81
  .resize({
82
82
  width: inner,
83
83
  height: inner,
@@ -0,0 +1,52 @@
1
+ /**
2
+ * PurgeTSS - gen-android-default
3
+ *
4
+ * Generates the legacy Android splash fallback used by Titanium projects via
5
+ * app/assets/android/default.png. This path is still relevant on Android <12
6
+ * when the app does not provide a custom windowBackground theme.
7
+ *
8
+ * Output path:
9
+ * Alloy -> app/assets/android/default.png
10
+ * Classic -> Resources/android/default.png
11
+ *
12
+ * @fileoverview Legacy Android default.png splash generator
13
+ * @author César Estrada
14
+ */
15
+
16
+ import fs from 'fs'
17
+ import path from 'path'
18
+ import sharp from 'sharp'
19
+
20
+ const DEFAULT_WIDTH = 1440
21
+ const DEFAULT_HEIGHT = 2560
22
+
23
+ export async function genAndroidDefault(masterPng, bgColor, outDir) {
24
+ fs.mkdirSync(outDir, { recursive: true })
25
+
26
+ const outPath = path.join(outDir, 'default.png')
27
+ const innerWidth = Math.floor(DEFAULT_WIDTH * 0.72)
28
+ const innerHeight = Math.floor(DEFAULT_HEIGHT * 0.26)
29
+
30
+ const innerLogo = await sharp(masterPng)
31
+ .resize({
32
+ width: innerWidth,
33
+ height: innerHeight,
34
+ fit: 'inside',
35
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
36
+ })
37
+ .toBuffer()
38
+
39
+ await sharp({
40
+ create: {
41
+ width: DEFAULT_WIDTH,
42
+ height: DEFAULT_HEIGHT,
43
+ channels: 4,
44
+ background: bgColor
45
+ }
46
+ })
47
+ .composite([{ input: innerLogo, gravity: 'center' }])
48
+ .png({ compressionLevel: 9 })
49
+ .toFile(outPath)
50
+
51
+ return outPath
52
+ }
@@ -22,7 +22,7 @@ export const LEGACY_DENSITIES = [
22
22
  { name: 'xxxhdpi', size: 192 }
23
23
  ]
24
24
 
25
- export async function genAndroidLegacy(tightMaster, bgColor, paddingPct, resRoot) {
25
+ export async function genAndroidLegacy(masterPng, bgColor, paddingPct, resRoot) {
26
26
  const generated = []
27
27
  // Legacy icons have no adaptive mask — they render as drawn. Use ~60% of
28
28
  // the adaptive padding so the logo fills more of the canvas.
@@ -35,7 +35,7 @@ export async function genAndroidLegacy(tightMaster, bgColor, paddingPct, resRoot
35
35
 
36
36
  const outPath = path.join(dir, 'ic_launcher.png')
37
37
 
38
- const innerLogo = await sharp(tightMaster)
38
+ const innerLogo = await sharp(masterPng)
39
39
  .resize({
40
40
  width: inner,
41
41
  height: inner,
@@ -26,7 +26,7 @@ export const SPLASH_DENSITIES = [
26
26
  { name: 'xxxhdpi', size: 1152 }
27
27
  ]
28
28
 
29
- export async function genSplash(tightMaster, resRoot) {
29
+ export async function genSplash(masterPng, resRoot) {
30
30
  const generated = []
31
31
 
32
32
  for (const { name, size } of SPLASH_DENSITIES) {
@@ -36,7 +36,7 @@ export async function genSplash(tightMaster, resRoot) {
36
36
 
37
37
  const outPath = path.join(dir, 'splash_icon.png')
38
38
 
39
- const innerLogo = await sharp(tightMaster)
39
+ const innerLogo = await sharp(masterPng)
40
40
  .resize({
41
41
  width: inner,
42
42
  height: inner,
@@ -35,6 +35,7 @@
35
35
  import fs from 'fs'
36
36
  import os from 'os'
37
37
  import path from 'path'
38
+ import sharp from 'sharp'
38
39
  import { logger } from './branding-logger.js'
39
40
  import { logger as mainLogger } from '../../shared/logger.js'
40
41
  import { confirmWithAlways } from '../../shared/prompt.js'
@@ -45,6 +46,7 @@ import { genIosDark } from './gen-ios-dark.js'
45
46
  import { genIosTinted } from './gen-ios-tinted.js'
46
47
  import { genAndroidAdaptive } from './gen-android-adaptive.js'
47
48
  import { genAndroidLegacy } from './gen-android-legacy.js'
49
+ import { genAndroidDefault } from './gen-android-default.js'
48
50
  import { genMarketplace } from './gen-marketplace.js'
49
51
  import { genNotification } from './gen-notification.js'
50
52
  import { genSplash } from './gen-splash.js'
@@ -56,6 +58,8 @@ import { printPostGenNotes } from './post-gen-notes.js'
56
58
  export async function runBranding(opts) {
57
59
  const {
58
60
  logo,
61
+ iconLogo = null,
62
+ splashLogo = null,
59
63
  monochromeLogo = null,
60
64
  darkLogo = null,
61
65
  darkBgColor = null,
@@ -64,7 +68,8 @@ export async function runBranding(opts) {
64
68
  tintedLogo = null,
65
69
  bgColor = '#FFFFFF',
66
70
  bgColorExplicit = false,
67
- padding = 15,
71
+ androidAdaptivePadding = 19,
72
+ androidLegacyPadding = 10,
68
73
  iosPadding = 4,
69
74
  notification = false,
70
75
  splash = false,
@@ -79,7 +84,7 @@ export async function runBranding(opts) {
79
84
  confirmOverwrites = true
80
85
  } = opts
81
86
 
82
- validateOptions({ logo, bgColor, darkBgColor, padding, iosPadding, cleanupLegacy: runCleanup })
87
+ validateOptions({ logo, bgColor, darkBgColor, androidAdaptivePadding, androidLegacyPadding, iosPadding, cleanupLegacy: runCleanup })
83
88
 
84
89
  const projectType = detectProjectType(projectRoot)
85
90
  const isInPlace = inPlace && !output
@@ -92,7 +97,7 @@ export async function runBranding(opts) {
92
97
  if (logo) {
93
98
  logger.property('Logo: ', logo)
94
99
  logger.property('Background: ', bgColor)
95
- logger.property('Padding: ', `Android ${padding}% / iOS ${iosPadding}% per side`)
100
+ logger.property('Padding: ', `Android adaptive ${androidAdaptivePadding}% / Android legacy ${androidLegacyPadding}% / iOS ${iosPadding}% per side`)
96
101
  console.log()
97
102
  logger.property(isInPlace ? 'Writing IN PLACE to: ' : 'Staging: ', isInPlace ? projectRoot : stagingRoot)
98
103
  }
@@ -157,6 +162,8 @@ export async function runBranding(opts) {
157
162
  lines.push(`${androidResStaging}/mipmap-{mdpi,hdpi,xhdpi,xxhdpi,xxxhdpi}/ic_launcher_{foreground,background,monochrome}.png`)
158
163
  lines.push(`${androidResStaging}/mipmap-{...}/ic_launcher.png (legacy)`)
159
164
  lines.push(`${androidResStaging}/mipmap-anydpi-v26/ic_launcher.xml`)
165
+ const androidAssetsStaging = getStagingAndroidAssetsRoot(stagingRoot, projectType)
166
+ if (androidAssetsStaging) lines.push(`${androidAssetsStaging}/default.png (Android <12 legacy splash fallback)`)
160
167
  if (notification) lines.push(`${androidResStaging}/drawable-*/ic_stat_notify.png × 5`)
161
168
  if (splash) lines.push(`${androidResStaging}/drawable-*/splash_icon.png × 5`)
162
169
  mainLogger.block('[dry-run] Would generate:', ...lines)
@@ -179,9 +186,32 @@ export async function runBranding(opts) {
179
186
  logger.section('Logos')
180
187
  logger.bullet('Dual logos (square + tight)')
181
188
  const logoBase = path.join(tempDir, '_logo')
182
- const { tight } = await prepareMaster(logo, logoBase)
189
+ const { square, tight } = await prepareMaster(logo, logoBase)
183
190
 
184
- let monoTight = null
191
+ let iconMaster = square
192
+ if (iconLogo) {
193
+ if (!fs.existsSync(iconLogo)) {
194
+ throw new Error(`Android icon logo not found: ${iconLogo}`)
195
+ }
196
+ logger.bullet(`Android icon logo: ${iconLogo}`)
197
+ const iconBase = path.join(tempDir, '_logo_icon')
198
+ const iconResult = await prepareMaster(iconLogo, iconBase)
199
+ iconMaster = iconResult.square
200
+ }
201
+ await warnIfLogoAspectIsUnsafeForLauncher(tight)
202
+
203
+ let splashMaster = iconMaster
204
+ if (splashLogo) {
205
+ if (!fs.existsSync(splashLogo)) {
206
+ throw new Error(`Android splash logo not found: ${splashLogo}`)
207
+ }
208
+ logger.bullet(`Android splash logo: ${splashLogo}`)
209
+ const splashBase = path.join(tempDir, '_logo_splash')
210
+ const splashResult = await prepareMaster(splashLogo, splashBase)
211
+ splashMaster = splashResult.square
212
+ }
213
+
214
+ let monoMaster = null
185
215
  if (monochromeLogo) {
186
216
  if (!fs.existsSync(monochromeLogo)) {
187
217
  throw new Error(`Monochrome logo not found: ${monochromeLogo}`)
@@ -189,13 +219,13 @@ export async function runBranding(opts) {
189
219
  logger.bullet(`Monochrome logo: ${monochromeLogo}`)
190
220
  const monoBase = path.join(tempDir, '_logo_mono')
191
221
  const monoResult = await prepareMaster(monochromeLogo, monoBase)
192
- monoTight = monoResult.tight
222
+ monoMaster = monoResult.square
193
223
  }
194
224
 
195
225
  // ---- Section: iOS & marketplace ----------------------------------------
196
226
  logger.section('iOS & marketplace')
197
- logger.bullet(`DefaultIcon.png (Android-safe padding ${padding}%) + DefaultIcon-ios.png (iOS padding ${iosPadding}%)`)
198
- const ios = await genIos(tight, bgColor, padding, iosPadding, stagingRoot)
227
+ logger.bullet(`DefaultIcon.png (Android-safe padding ${androidAdaptivePadding}%) + DefaultIcon-ios.png (iOS padding ${iosPadding}%)`)
228
+ const ios = await genIos(tight, bgColor, androidAdaptivePadding, iosPadding, stagingRoot)
199
229
  generated.push(ios.defaultIcon, ios.defaultIconIos)
200
230
 
201
231
  if (withDark) {
@@ -238,29 +268,36 @@ export async function runBranding(opts) {
238
268
  // ---- Section: Android --------------------------------------------------
239
269
  logger.section('Android')
240
270
 
241
- const monoLabel = monoTight ? ', monochrome from --monochrome-logo' : ''
242
- logger.bullet(`Adaptive icons (foreground + background + monochrome${monoLabel}) × 5`)
243
- const adaptiveFiles = await genAndroidAdaptive(tight, bgColor, padding, androidResStaging, { monoTight })
271
+ const monoLabel = monoMaster ? ', monochrome from --monochrome-logo' : ''
272
+ logger.bullet(`Adaptive icons (foreground + background + monochrome${monoLabel}, padding ${androidAdaptivePadding}%) × 5`)
273
+ const adaptiveFiles = await genAndroidAdaptive(iconMaster, bgColor, androidAdaptivePadding, androidResStaging, { monoMaster })
244
274
  generated.push(...adaptiveFiles)
245
275
 
246
- logger.bullet('Legacy ic_launcher.png × 5')
247
- const legacyFiles = await genAndroidLegacy(tight, bgColor, padding, androidResStaging)
276
+ logger.bullet(`Legacy ic_launcher.png × 5 (padding ${androidLegacyPadding}%)`)
277
+ const legacyFiles = await genAndroidLegacy(iconMaster, bgColor, androidLegacyPadding, androidResStaging)
248
278
  generated.push(...legacyFiles)
249
279
 
250
280
  const xmlPath = genIcLauncherXml(androidResStaging)
251
281
  generated.push(xmlPath)
252
282
  logger.bullet(`Adaptive icon XML: ${xmlPath}`)
253
283
 
284
+ const androidDefaultDir = getStagingAndroidAssetsRoot(stagingRoot, projectType)
285
+ if (androidDefaultDir) {
286
+ logger.bullet('Legacy Android default.png splash fallback')
287
+ const defaultSplashPath = await genAndroidDefault(splashMaster, bgColor, androidDefaultDir)
288
+ generated.push(defaultSplashPath)
289
+ }
290
+
254
291
  if (notification) {
255
- const monoLabelNotif = monoTight ? ' from --monochrome-logo' : ' whitened from logo'
292
+ const monoLabelNotif = monoMaster ? ' from --monochrome-logo' : ' whitened from logo'
256
293
  logger.bullet(`Notification icons (white+alpha, edge-to-edge${monoLabelNotif}) × 5`)
257
- const notifFiles = await genNotification(monoTight || tight, androidResStaging)
294
+ const notifFiles = await genNotification(monoMaster || iconMaster, androidResStaging)
258
295
  generated.push(...notifFiles)
259
296
  }
260
297
 
261
298
  if (splash) {
262
299
  logger.bullet('Splash icons × 5')
263
- const splashFiles = await genSplash(tight, androidResStaging)
300
+ const splashFiles = await genSplash(splashMaster, androidResStaging)
264
301
  generated.push(...splashFiles)
265
302
  }
266
303
 
@@ -277,12 +314,16 @@ export async function runBranding(opts) {
277
314
  const tmpFiles = [
278
315
  path.join(tempDir, '_logo_square.png'),
279
316
  path.join(tempDir, '_logo_tight.png'),
317
+ path.join(tempDir, '_logo_icon_square.png'),
318
+ path.join(tempDir, '_logo_icon_tight.png'),
280
319
  path.join(tempDir, '_logo_mono_square.png'),
281
320
  path.join(tempDir, '_logo_mono_tight.png'),
282
321
  path.join(tempDir, '_logo_dark_square.png'),
283
322
  path.join(tempDir, '_logo_dark_tight.png'),
284
323
  path.join(tempDir, '_logo_tinted_square.png'),
285
- path.join(tempDir, '_logo_tinted_tight.png')
324
+ path.join(tempDir, '_logo_tinted_tight.png'),
325
+ path.join(tempDir, '_logo_splash_square.png'),
326
+ path.join(tempDir, '_logo_splash_tight.png')
286
327
  ]
287
328
  for (const tmp of tmpFiles) {
288
329
  if (fs.existsSync(tmp)) fs.unlinkSync(tmp)
@@ -300,7 +341,8 @@ export async function runBranding(opts) {
300
341
  projectRoot,
301
342
  stagingRoot,
302
343
  bgColor,
303
- padding,
344
+ androidAdaptivePadding,
345
+ androidLegacyPadding,
304
346
  iosPadding,
305
347
  withSplash: splash,
306
348
  withNotification: notification,
@@ -311,13 +353,39 @@ export async function runBranding(opts) {
311
353
  return { stagingRoot, generated }
312
354
  }
313
355
 
356
+ async function warnIfLogoAspectIsUnsafeForLauncher(tightLogoPath) {
357
+ const meta = await sharp(tightLogoPath).metadata()
358
+ const width = meta.width || 0
359
+ const height = meta.height || 0
360
+
361
+ if (!width || !height) return
362
+
363
+ const aspect = width / height
364
+ const isWideWordmark = aspect > 1.25
365
+ const isTallWordmark = aspect < 0.8
366
+
367
+ if (!isWideWordmark && !isTallWordmark) return
368
+
369
+ logger.warning('The source logo is not close to square.')
370
+ logger.warning(`Aspect ratio detected: ${width}×${height} (${aspect.toFixed(2)}:1).`)
371
+ logger.warning('Launcher icons and Android 12+ system splash screens work best with a square mark.')
372
+ logger.warning('A wide/tall wordmark can look cramped or cropped once centered inside icon masks.')
373
+ logger.warning('Recommendation: use a dedicated square app-icon source for `purgetss brand`.')
374
+ }
375
+
314
376
  function getStagingAndroidResRoot(stagingRoot, projectType) {
315
377
  if (projectType === 'alloy') return path.join(stagingRoot, 'app', 'platform', 'android', 'res')
316
378
  if (projectType === 'classic') return path.join(stagingRoot, 'platform', 'android', 'res')
317
379
  return path.join(stagingRoot, 'standalone', 'platform', 'android', 'res')
318
380
  }
319
381
 
320
- function validateOptions({ logo, bgColor, darkBgColor, padding, iosPadding, cleanupLegacy }) {
382
+ function getStagingAndroidAssetsRoot(stagingRoot, projectType) {
383
+ if (projectType === 'alloy') return path.join(stagingRoot, 'app', 'assets', 'android')
384
+ if (projectType === 'classic') return path.join(stagingRoot, 'Resources', 'android')
385
+ return null
386
+ }
387
+
388
+ function validateOptions({ logo, bgColor, darkBgColor, androidAdaptivePadding, androidLegacyPadding, iosPadding, cleanupLegacy }) {
321
389
  if (!logo && !cleanupLegacy) {
322
390
  throw new Error('Logo image path is required (unless using --cleanup-legacy alone).')
323
391
  }
@@ -327,8 +395,11 @@ function validateOptions({ logo, bgColor, darkBgColor, padding, iosPadding, clea
327
395
  if (darkBgColor && !/^#[0-9A-Fa-f]{6}$/.test(darkBgColor)) {
328
396
  throw new Error(`--dark-bg-color must be a 6-digit hex like #1C1C1E (got: ${darkBgColor}).`)
329
397
  }
330
- if (padding < 0 || padding > 40) {
331
- throw new Error(`--padding must be between 0 and 40 (got: ${padding}).`)
398
+ if (androidAdaptivePadding < 0 || androidAdaptivePadding > 40) {
399
+ throw new Error(`--android-adaptive-padding must be between 0 and 40 (got: ${androidAdaptivePadding}).`)
400
+ }
401
+ if (androidLegacyPadding < 0 || androidLegacyPadding > 40) {
402
+ throw new Error(`--android-legacy-padding must be between 0 and 40 (got: ${androidLegacyPadding}).`)
332
403
  }
333
404
  if (iosPadding < 0 || iosPadding > 40) {
334
405
  throw new Error(`--ios-padding must be between 0 and 40 (got: ${iosPadding}).`)
@@ -23,11 +23,11 @@ export function printPostGenNotes(opts) {
23
23
  }
24
24
 
25
25
  function printCompactSummary(opts) {
26
- const { projectType, projectRoot, stagingRoot, bgColor, padding, iosPadding, inPlace } = opts
26
+ const { projectType, projectRoot, stagingRoot, bgColor, androidAdaptivePadding, androidLegacyPadding, iosPadding, inPlace } = opts
27
27
 
28
28
  logger.section('Summary')
29
29
  logger.bullet(`Background: ${chalk.cyan(bgColor)}`)
30
- logger.bullet(`Padding: Android ${chalk.cyan(padding + '%')} / iOS ${chalk.cyan(iosPadding + '%')}`)
30
+ logger.bullet(`Padding: Android adaptive ${chalk.cyan(androidAdaptivePadding + '%')} / Android legacy ${chalk.cyan(androidLegacyPadding + '%')} / iOS ${chalk.cyan(iosPadding + '%')}`)
31
31
  logger.bullet(`${inPlace ? 'Written in place to' : 'Staged at'}: ${chalk.cyan(inPlace ? projectRoot : stagingRoot)}`)
32
32
 
33
33
  logger.section('Next steps')
@@ -56,7 +56,7 @@ function printCompactSummary(opts) {
56
56
  function printFullNotes(opts) {
57
57
  const {
58
58
  projectType, projectRoot, stagingRoot,
59
- bgColor, padding, iosPadding, withSplash, withNotification, inPlace
59
+ bgColor, androidAdaptivePadding, androidLegacyPadding, iosPadding, withSplash, withNotification, inPlace
60
60
  } = opts
61
61
 
62
62
  const code = (s) => chalk.gray(s)
@@ -66,17 +66,20 @@ function printFullNotes(opts) {
66
66
  logger.section('Notes on what was generated')
67
67
  logger.bullet(`Brand color ${chalk.cyan(bgColor)} was baked into Android adaptive background layer`)
68
68
  console.log(' and iOS/marketplace flattened masters (Apple rejects alpha).')
69
- logger.bullet(`Android padding: ${chalk.cyan(padding + '%')} (logo fills ${100 - 2 * padding}% of each mipmap canvas)`)
70
- logger.bullet(`iOS padding: ${chalk.cyan(iosPadding + '%')} (logo fills ${100 - 2 * iosPadding}% of DefaultIcon-ios and marketplace art)`)
69
+ logger.bullet(`Android adaptive padding: ${chalk.cyan(androidAdaptivePadding + '%')} (logo fills ${100 - 2 * androidAdaptivePadding}% of each adaptive foreground canvas)`)
70
+ logger.bullet(`Android legacy padding: ${chalk.cyan(androidLegacyPadding + '%')} (logo fills ${100 - 2 * androidLegacyPadding}% of each legacy launcher canvas)`)
71
+ logger.bullet(`iOS padding: ${chalk.cyan(iosPadding + '%')} (logo fills ${100 - 2 * iosPadding}% of DefaultIcon-ios and marketplace art)`)
71
72
 
72
73
  console.log()
73
74
  console.log(' If the logo looks cramped: re-run with higher padding')
74
- console.log(` ${flag('--padding 25-30')} (Android)`)
75
- console.log(` ${flag('--ios-padding 10-14')} (iOS)`)
75
+ console.log(` ${flag('--android-adaptive-padding 25-30')} (adaptive icon)`)
76
+ console.log(` ${flag('--android-legacy-padding 14-18')} (legacy icon)`)
77
+ console.log(` ${flag('--ios-padding 10-14')} (iOS)`)
76
78
  console.log()
77
79
  console.log(' If the logo looks too small: re-run with lower padding')
78
- console.log(` ${flag('--padding 19')} (Android spec floor)`)
79
- console.log(` ${flag('--ios-padding 2-3')} (matches first-party apps like Mail, Safari)`)
80
+ console.log(` ${flag('--android-adaptive-padding 19')} (adaptive spec floor)`)
81
+ console.log(` ${flag('--android-legacy-padding 8-12')} (legacy icon)`)
82
+ console.log(` ${flag('--ios-padding 2-3')} (matches first-party apps like Mail, Safari)`)
80
83
 
81
84
  logger.section('Configuration reminders')
82
85
  console.log(' The tool does NOT auto-edit tiapp.xml. Snippets below are optional —')
@@ -105,17 +108,28 @@ function printFullNotes(opts) {
105
108
  console.log()
106
109
  console.log(` ${num('3.')} ${chalk.cyan('Android 12+ splash screen')} — ${chalk.yellow('OPTIONAL, advanced')}`)
107
110
  console.log()
111
+ console.log(' Generated files: @drawable/splash_icon.png across densities.')
108
112
  console.log(' Titanium SDK 13.x shows a system splash automatically using your')
109
- console.log(' launcher icon. For most apps THE DEFAULT IS ENOUGH — do nothing.')
113
+ console.log(' launcher icon unless you wire a custom splash theme.')
114
+ console.log(' If you want the Android 12+ splash to use splash_icon instead of')
115
+ console.log(' ic_launcher, add a custom theme and point')
116
+ console.log(code(' <item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>'))
110
117
  }
111
118
 
119
+ console.log()
120
+ console.log(` ${num(withSplash ? '4.' : '3.')} ${chalk.cyan('Android <12 legacy splash')}`)
121
+ console.log(' PurgeTSS brand now regenerates app/assets/android/default.png as')
122
+ console.log(' a legacy fallback splash for Titanium projects.')
123
+ console.log(' If your app uses a custom windowBackground / background.9.png theme,')
124
+ console.log(' that custom theme still takes precedence.')
125
+
112
126
  if (withNotification) {
113
127
  const colorsDir = projectType === 'classic'
114
128
  ? 'platform/android/res/values'
115
129
  : 'app/platform/android/res/values'
116
130
 
117
131
  console.log()
118
- console.log(` ${num('4.')} ${chalk.cyan('FCM notification icon + tint')}`)
132
+ console.log(` ${num(withSplash ? '5.' : '4.')} ${chalk.cyan('FCM notification icon + tint')}`)
119
133
  console.log(' Only needed if you use firebase.cloudmessaging for push.')
120
134
  console.log()
121
135
  console.log(` Create ${flag(colorsDir + '/colors.xml')} (or merge):`)
@@ -34,11 +34,11 @@ const require = createRequire(import.meta.url)
34
34
  * '20' → 20
35
35
  * '20%' → 20
36
36
  *
37
- * Used for `brand.padding` and `brand.iosPadding` so users can write
38
- * self-documenting values like `padding: '25%'` in their config.
37
+ * Used for the `brand.padding.*` values so users can write
38
+ * self-documenting values like `androidAdaptive: '19%'` in their config.
39
39
  *
40
40
  * @param {number|string} value
41
- * @param {string} fieldName - Config path for error messages (e.g. 'brand.padding')
41
+ * @param {string} fieldName - Config path for error messages (e.g. 'brand.padding.androidAdaptive')
42
42
  * @returns {number} Integer 0-40
43
43
  */
44
44
  function parsePadding(value, fieldName) {
@@ -128,12 +128,20 @@ export function getConfigFile() {
128
128
  configFile.purge.options.plugins = configFile.purge.options.plugins ?? []
129
129
 
130
130
  configFile.brand = configFile.brand ?? {}
131
- configFile.brand.bgColor = configFile.brand.bgColor ?? '#FFFFFF'
132
- configFile.brand.padding = parsePadding(configFile.brand.padding ?? 15, 'brand.padding')
133
- configFile.brand.iosPadding = parsePadding(configFile.brand.iosPadding ?? 4, 'brand.iosPadding')
134
- configFile.brand.darkBgColor = configFile.brand.darkBgColor ?? null
135
- configFile.brand.notification = configFile.brand.notification ?? false
136
- configFile.brand.splash = configFile.brand.splash ?? false
131
+ configFile.brand.logos = configFile.brand.logos ?? {}
132
+ configFile.brand.padding = configFile.brand.padding ?? {}
133
+ configFile.brand.padding.ios = parsePadding(configFile.brand.padding.ios ?? 4, 'brand.padding.ios')
134
+ configFile.brand.padding.androidLegacy = parsePadding(configFile.brand.padding.androidLegacy ?? 10, 'brand.padding.androidLegacy')
135
+ configFile.brand.padding.androidAdaptive = parsePadding(configFile.brand.padding.androidAdaptive ?? 19, 'brand.padding.androidAdaptive')
136
+ configFile.brand.android = configFile.brand.android ?? {}
137
+ configFile.brand.android.notification = configFile.brand.android.notification ?? false
138
+ configFile.brand.android.splash = configFile.brand.android.splash ?? false
139
+ configFile.brand.ios = configFile.brand.ios ?? {}
140
+ configFile.brand.ios.dark = configFile.brand.ios.dark ?? true
141
+ configFile.brand.ios.tinted = configFile.brand.ios.tinted ?? true
142
+ configFile.brand.ios.darkBackground = configFile.brand.ios.darkBackground ?? null
143
+ configFile.brand.colors = configFile.brand.colors ?? {}
144
+ configFile.brand.colors.background = configFile.brand.colors.background ?? '#FFFFFF'
137
145
 
138
146
  configFile.images = configFile.images ?? {}
139
147
  configFile.images.quality = configFile.images.quality ?? 85