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 +14 -1
- package/bin/purgetss +12 -1
- package/dist/purgetss.ui.js +1 -1
- package/experimental/completions2.js +178 -1
- package/lib/templates/purgetss.config.js.cjs +27 -6
- package/package.json +1 -1
- package/src/cli/commands/brand.js +1 -1
- package/src/core/branding/brand-config.js +44 -13
- package/src/core/branding/cleanup-legacy.js +0 -7
- package/src/core/branding/ensure-brand-section.js +28 -7
- package/src/core/branding/gen-android-adaptive.js +5 -5
- package/src/core/branding/gen-android-default.js +52 -0
- package/src/core/branding/gen-android-legacy.js +2 -2
- package/src/core/branding/gen-splash.js +2 -2
- package/src/core/branding/index.js +92 -21
- package/src/core/branding/post-gen-notes.js +25 -11
- package/src/shared/config-manager.js +17 -9
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
|
|
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
|
|
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})')
|
package/dist/purgetss.ui.js
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
@@ -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: {
|
|
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,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
63
|
-
bgColorExplicit: Boolean(cliOptions.bgColor ??
|
|
64
|
-
darkBgColor
|
|
65
|
-
|
|
66
|
-
|
|
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 ??
|
|
71
|
-
splash: Boolean(cliOptions.splash ??
|
|
101
|
+
notification: Boolean(cliOptions.notification ?? android.notification ?? false),
|
|
102
|
+
splash: Boolean(cliOptions.splash ?? android.splash ?? false),
|
|
72
103
|
|
|
73
|
-
withDark: cliOptions.dark !== false && (
|
|
74
|
-
withTinted: cliOptions.tinted !== false && (
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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 (
|
|
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(
|
|
32
|
-
const {
|
|
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(
|
|
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 (
|
|
80
|
-
const innerMono = await sharp(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
198
|
-
const ios = await genIos(tight, bgColor,
|
|
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 =
|
|
242
|
-
logger.bullet(`Adaptive icons (foreground + background + monochrome${monoLabel}) × 5`)
|
|
243
|
-
const adaptiveFiles = await genAndroidAdaptive(
|
|
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(
|
|
247
|
-
const legacyFiles = await genAndroidLegacy(
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 (
|
|
331
|
-
throw new Error(`--padding must be between 0 and 40 (got: ${
|
|
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,
|
|
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(
|
|
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,
|
|
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:
|
|
70
|
-
logger.bullet(`
|
|
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')}
|
|
75
|
-
console.log(` ${flag('--
|
|
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')}
|
|
79
|
-
console.log(` ${flag('--
|
|
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
|
|
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
|
|
38
|
-
* self-documenting values like `
|
|
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.
|
|
132
|
-
configFile.brand.padding =
|
|
133
|
-
configFile.brand.
|
|
134
|
-
configFile.brand.
|
|
135
|
-
configFile.brand.
|
|
136
|
-
configFile.brand.
|
|
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
|