purgetss 7.7.0 → 7.9.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
@@ -17,7 +17,7 @@
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
+ - `brand` command for Titanium icons and branding assets, focused on the modern Titanium icon pipeline: Android adaptive icons, iOS icon variants, optional Android 12+ splash artwork, and a minimal `default.png` compatibility fallback
21
21
  - Icon font support: Font Awesome, Material Icons, Material Symbols, Framework7-Icons
22
22
  - `build-fonts` command generates `fonts.tss` with class definitions and fontFamily selectors
23
23
  - `shades` command generates color shades from any hex color
@@ -377,6 +377,14 @@ Button: {
377
377
 
378
378
  ## Recent changes
379
379
 
380
+ ### v7.8.0
381
+
382
+ - **`--width <n>` flag for `images`.** Pin Android `mdpi` / iPhone `@1x` to a specific width in pixels (e.g. `purgetss images logo.svg --width 256`); larger scales derive at ×1.5, ×2, ×3, ×4 with height staying proportional. Recommended when the source is an SVG export from Affinity / Illustrator with a disproportionate viewBox — the legacy 4× master convention produces unpredictable sizes there, and the new flag pins the result exactly. When you pass an SVG without `--width`, `images` now prints a one-time hint suggesting the flag, then falls back to the legacy behavior.
383
+ - **Class syntax pre-validation.** `purgetss` now halts with a structured `Class Syntax Error` block (file + line + suggested fix) when it detects known authoring mistakes in your class names: inverted negative sign (`top-(-10)` → `-top-(10)`), Tailwind-style brackets (`top-[10px]` → `top-(10px)`), empty parentheses (`wh-()`), whitespace inside parentheses (`wh-( 200 )`), and redundant `px` unit (`top-(10px)` → `top-(10)`).
384
+ - All offenders are reported in a single run, so you can fix them in one pass.
385
+ - Generic unknown classes (typos, vendor utilities not enabled, custom classes not yet declared) are NOT flagged by the validator — they continue to flow silently into the `// Unused or unsupported classes` block in `app.tss`, exactly as in previous versions.
386
+ - **Arbitrary-value parser no longer crashes on negative values inside parentheses.** Classes like `top-(-10)`, `mt-(-5)`, or `origin-(-10,-20)` used to trigger a `Cannot read properties of null (reading 'pop')` exception. The parser was rewritten to extract the `(...)` portion first, so a `-` inside the value never breaks the split — and the new pre-validator catches the inverted-sign form before it gets that far.
387
+
380
388
  ### v7.7.0
381
389
 
382
390
  - `brand` now uses grouped config sections: `brand.logos`, `brand.padding`, `brand.android`, `brand.ios`, and `brand.colors`.
@@ -384,6 +392,8 @@ Button: {
384
392
  - `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
393
  - `cleanup-legacy` no longer removes `default.png`.
386
394
  - Branding help and docs now spell out the difference between Android launcher icons, Android 12+ `splash_icon.png`, and legacy Android splash assets.
395
+ - `--notes` is explicit that `tiapp.xml` is not auto-edited and that apps with an existing custom Android theme should merge splash snippets into that theme instead of replacing it.
396
+ - `brand` does not try to manage older Android splash theme assets such as `background.png` or `background.9.png`; if a project still depends on them, that remains manual by design.
387
397
 
388
398
  See the full release notes in [CHANGELOG.md](./CHANGELOG.md).
389
399
 
package/bin/purgetss CHANGED
@@ -273,18 +273,25 @@ The recommended workflow is: put files in ${chalk.cyan('purgetss/brand/')}, then
273
273
  CLI flags always win over config values.
274
274
 
275
275
  Generates:
276
- ${chalk.yellow('DefaultIcon.png')} / ${chalk.yellow('DefaultIcon-ios.png')} Root icons (alpha + flattened)
277
- ${chalk.yellow('DefaultIcon-Dark.png')} / ${chalk.yellow('DefaultIcon-Tinted.png')} iOS 18+ variants (Apple HIG specs)
278
- ${chalk.yellow('iTunesConnect.png')} / ${chalk.yellow('MarketplaceArtwork.png')} App Store + Play Store artwork
276
+ ${chalk.yellow('DefaultIcon.png')}/${chalk.yellow('DefaultIcon-ios.png')} Root icons (alpha + flattened)
277
+ ${chalk.yellow('DefaultIcon-Dark.png')}/${chalk.yellow('DefaultIcon-Tinted.png')} iOS 18+ variants (Apple HIG specs)
278
+ ${chalk.yellow('iTunesConnect.png')}/${chalk.yellow('MarketplaceArtwork.png')} App Store + Play Store artwork
279
279
  ${chalk.yellow('mipmap-*/ic_launcher_{foreground,background,monochrome}.png')} Android adaptive × 5
280
- ${chalk.yellow('mipmap-*/ic_launcher.png')} Android legacy × 5
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
280
+ ${chalk.yellow('mipmap-*/ic_launcher.png')} Android legacy × 5
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 compatibility fallback
284
284
 
285
285
  Android dark/light mode is handled by the ${chalk.yellow('monochrome')} adaptive layer
286
286
  (Android 13+ tints it from the wallpaper + theme). No separate dark file exists.
287
287
 
288
+ Scope:
289
+ ${chalk.green('brand')} targets the modern Titanium icon pipeline: iOS app icons,
290
+ Android adaptive icons, and optional Android 12+ splash artwork.
291
+ Older Android splash themes such as ${chalk.yellow('background.png')} / ${chalk.yellow('background.9.png')}
292
+ are outside the normal scope of this command and should be managed manually
293
+ if a project still depends on them.
294
+
288
295
  Examples:
289
296
  ${chalk.cyan('purgetss brand')} # uses purgetss/brand/logo.svg + config
290
297
  ${chalk.cyan('purgetss brand')} logo.svg # explicit logo path
@@ -346,6 +353,13 @@ Writes directly to the project (auto-detects Alloy vs Classic):
346
353
  Defaults come from the ${chalk.cyan('images:')} section in ${chalk.cyan('purgetss/config.cjs')}.
347
354
  CLI flags always win over config values.
348
355
 
356
+ Sizing:
357
+ By default, sources are treated as 4× masters (xxxhdpi/@4x); all other
358
+ scales derive at 1/4, 1.5/4, 2/4, 3/4 of the source's natural pixels.
359
+ Pass ${chalk.cyan('--width <n>')} to pin Android mdpi (= iPhone @1x) to a specific width;
360
+ larger scales derive as ×1.5, ×2, ×3, ×4 (height stays proportional).
361
+ Recommended for SVGs from vector editors with oversized viewBoxes.
362
+
349
363
  Examples:
350
364
  ${chalk.cyan('purgetss images')} # uses purgetss/images/ + config
351
365
  ${chalk.cyan('purgetss images')} ./docs/screenshots # scope to one folder
@@ -354,12 +368,15 @@ Examples:
354
368
  ${chalk.cyan('purgetss images')} --ios # iPhone scales only
355
369
  ${chalk.cyan('purgetss images')} --format webp # convert all outputs to WebP
356
370
  ${chalk.cyan('purgetss images')} --format png --quality 95
371
+ ${chalk.cyan('purgetss images')} logo.svg --width 256 # pin @1x/mdpi to 256 px wide
372
+ ${chalk.cyan('purgetss images')} banner.svg --width 512 --format webp
357
373
  ${chalk.cyan('purgetss images')} --dry-run
358
374
  `)
359
375
  .option('--android', 'Generate only Android density variants (skip iPhone)')
360
376
  .option('--ios', 'Generate only iPhone scale variants (skip Android)')
361
377
  .option('--format <ext>', 'Convert all outputs to this format: webp|jpeg|png|avif|gif|tiff')
362
378
  .option('--quality <n>', 'JPEG/WebP/AVIF quality 0-100 (default: 85)', (v) => parseInt(v, 10))
379
+ .option('--width <n>', 'Target width in px for @1x/mdpi (other scales derive: ×1.5, ×2, ×3, ×4)', (v) => parseInt(v, 10))
363
380
  .option('--project <path>', 'Project root (default: cwd)')
364
381
  .option('--dry-run', 'Preview without writing any files')
365
382
  .option('-y, --yes', 'Skip the overwrite confirmation prompt')
@@ -1,4 +1,4 @@
1
- // PurgeTSS v7.7.0
1
+ // PurgeTSS v7.8.0
2
2
  // Created by César Estrada
3
3
  // https://purgetss.com
4
4
 
@@ -13,15 +13,7 @@ module.exports = {
13
13
  }
14
14
  },
15
15
  brand: {
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
- },
16
+ logos: {}, // empty = auto-discovers from purgetss/brand/
25
17
  padding: {
26
18
  ios: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
27
19
  androidLegacy: '10%', // legacy ic_launcher.png padding
@@ -31,15 +23,14 @@ module.exports = {
31
23
  splash: false, // also generate splash_icon.png × 5
32
24
  notification: false // also generate ic_stat_notify.png × 5
33
25
  },
26
+ ios: {
27
+ dark: true, // generate iOS 18+ Dark appearance icon
28
+ tinted: true, // generate iOS 18+ Tinted appearance icon
29
+ darkBackground: null // null = transparent per Apple HIG
30
+ },
34
31
  colors: {
35
32
  background: '#FFFFFF' // Android adaptive bg + iOS/marketplace flatten
36
33
  },
37
- // Optional iOS overrides:
38
- // ios: {
39
- // dark: false,
40
- // tinted: false,
41
- // darkBackground: '#111111'
42
- // },
43
34
  confirmOverwrites: true // prompt before overwriting files (set false to skip)
44
35
  },
45
36
  images: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "purgetss",
4
- "version": "7.7.0",
4
+ "version": "7.9.0",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "purgetss": "bin/purgetss"
@@ -17,7 +17,7 @@
17
17
  "dist/*.tss",
18
18
  "dist/configs/",
19
19
  "assets/fonts/",
20
- "experimental/completions2.js"
20
+ "src/core/builders/auto-utilities-builder.js"
21
21
  ],
22
22
  "publishConfig": {
23
23
  "access": "public"
@@ -14,6 +14,7 @@ import { alloyProject } from '../../shared/utils.js'
14
14
  import { ensureConfig } from '../../shared/config-manager.js'
15
15
  import { buildTailwindBasedOnConfigOptions } from '../../core/builders/tailwind-builder.js'
16
16
  import { createDefinitionsFile } from './init.js'
17
+ import { flushSemanticColors } from '../../shared/semantic-helpers.js'
17
18
 
18
19
  // Import FontAwesome functions from their new modular location
19
20
  import { buildFontAwesome, buildFontAwesomeJS } from '../../dev/builders/fontawesome-builder.js'
@@ -28,10 +29,14 @@ import { buildFontAwesome, buildFontAwesomeJS } from '../../dev/builders/fontawe
28
29
  export function build(options) {
29
30
  if (alloyProject()) {
30
31
  ensureConfig()
31
- buildTailwindBasedOnConfigOptions(options)
32
- buildFontAwesome()
33
- buildFontAwesomeJS()
34
- createDefinitionsFile()
32
+ try {
33
+ buildTailwindBasedOnConfigOptions(options)
34
+ buildFontAwesome()
35
+ buildFontAwesomeJS()
36
+ createDefinitionsFile()
37
+ } finally {
38
+ flushSemanticColors()
39
+ }
35
40
  return true
36
41
  }
37
42
  return false
@@ -37,6 +37,13 @@ export async function images(cliSource, options = {}) {
37
37
  process.exit(1)
38
38
  }
39
39
 
40
+ if (options.width !== undefined) {
41
+ if (!Number.isFinite(options.width) || !Number.isInteger(options.width) || options.width < 1 || options.width > 8192) {
42
+ logger.error(`Invalid --width '${options.width}'. Must be an integer between 1 and 8192.`)
43
+ process.exit(1)
44
+ }
45
+ }
46
+
40
47
  const format = options.format ?? cfg.format ?? null
41
48
  if (format && !VALID_FORMATS.has(format.toLowerCase())) {
42
49
  logger.error(`Invalid --format '${format}'. Valid: ${[...VALID_FORMATS].join(', ')}`)
@@ -57,6 +64,7 @@ export async function images(cliSource, options = {}) {
57
64
  iphoneOnly: Boolean(options.ios),
58
65
  format: format ? format.toLowerCase() : null,
59
66
  quality: options.quality ?? cfg.quality ?? 85,
67
+ baseWidth: options.width ?? null,
60
68
  dryRun: Boolean(options.dryRun),
61
69
  yes: Boolean(options.yes),
62
70
  confirmOverwrites: cfg.confirmOverwrites !== false
@@ -33,6 +33,7 @@ import { getConfigOptions, getConfigFile, ensureConfig } from '../../shared/conf
33
33
  // Import purger functions from core modules
34
34
  import { processControllers } from '../../core/analyzers/class-extractor.js'
35
35
  import { purgeTailwind } from '../../core/purger/tailwind-purger.js'
36
+ import { flushSemanticColors } from '../../shared/semantic-helpers.js'
36
37
  import {
37
38
  purgeFontAwesome,
38
39
  purgeMaterialIcons,
@@ -40,6 +41,7 @@ import {
40
41
  purgeFramework7
41
42
  } from '../../core/purger/icon-purger.js'
42
43
  import { purgeFonts } from '../../core/purger/fonts-purger.js'
44
+ import { validateClassSyntax } from '../utils/unsupported-class-reporter.js'
43
45
 
44
46
  // Global variables (EXACT copies from original src/index.js)
45
47
  let configOptions = {}
@@ -693,10 +695,21 @@ export function purgeClasses(options) {
693
695
 
694
696
  try {
695
697
  uniqueClasses = getUniqueClasses()
698
+
699
+ // Pre-validate class syntax. Halts on inverted negatives, Tailwind
700
+ // brackets, empty parens, etc. — but NOT on generic unknown classes
701
+ // (those fall through silently to "// Unused or unsupported classes").
702
+ validateClassSyntax({
703
+ classes: uniqueClasses,
704
+ viewPaths: getViewPaths(),
705
+ controllerPaths: getControllerPaths()
706
+ })
696
707
  } catch (error) {
697
- // Handle pre-validation errors (XML syntax errors detected before parsing)
698
- if (error.isPreValidationError) {
699
- // Error already printed by preValidateXML, exit cleanly
708
+ // Handle pre-validation errors (XML syntax errors detected before
709
+ // parsing) and class-syntax errors (authoring mistakes detected
710
+ // before purging). In both cases the error block was already
711
+ // printed; just exit cleanly.
712
+ if (error.isPreValidationError || error.isClassSyntaxError) {
700
713
  // eslint-disable-next-line n/no-process-exit
701
714
  process.exit(1)
702
715
  }
@@ -728,6 +741,7 @@ export function purgeClasses(options) {
728
741
 
729
742
  finish()
730
743
  } finally {
744
+ flushSemanticColors()
731
745
  logger.endSection()
732
746
  }
733
747
 
@@ -329,8 +329,8 @@ export function buildSemanticPalette(family, kebabName, alpha) {
329
329
  const mirror = sorted[sorted.length - 1 - i]
330
330
  const key = `${camelName}${shade.number}`
331
331
  semanticEntries[key] = {
332
- light: wrapValue(mirror.hexcode, alpha),
333
- dark: wrapValue(shade.hexcode, alpha)
332
+ light: wrapValue(shade.hexcode, alpha),
333
+ dark: wrapValue(mirror.hexcode, alpha)
334
334
  }
335
335
  configMapping[shade.number] = key
336
336
  })
@@ -0,0 +1,209 @@
1
+ /**
2
+ * PurgeTSS - Class Syntax Validator
3
+ *
4
+ * Pre-validation helper that runs BEFORE the purge starts. Scans every unique
5
+ * class name pulled from XML views (and JS controllers) for well-known
6
+ * authoring mistakes:
7
+ *
8
+ * - Inverted negative sign: top-(-10) → -top-(10)
9
+ * - Tailwind-style brackets: top-[10px] → top-(10px)
10
+ * - Empty parentheses: wh-() → wh-(<value>)
11
+ * - Whitespace in parens: wh-( 200 ) → wh-(200)
12
+ * - Redundant px units: top-(10px) → top-(10)
13
+ *
14
+ * On any match the validator collects every offender, prints one block per
15
+ * offender (file + line + fix, mirroring throwPreValidationError) and throws
16
+ * an `isClassSyntaxError` so the CLI exits cleanly without running the purge.
17
+ *
18
+ * Generic unknown classes (typos, vendor utilities not enabled, custom
19
+ * classes not yet defined) are intentionally left alone — they are still
20
+ * captured silently in the "// Unused or unsupported classes" section of
21
+ * app.tss, but never trigger warnings or halts. That's the only way to keep
22
+ * the workflow usable on projects with dozens of in-progress class names.
23
+ *
24
+ * @author César Estrada
25
+ */
26
+
27
+ import fs from 'fs'
28
+ import chalk from 'chalk'
29
+ import { logger } from '../../shared/logger.js'
30
+
31
+ const cwd = process.cwd()
32
+
33
+ /**
34
+ * Pattern detectors. Each detector receives the className and returns
35
+ * { issue, suggestion } when it recognises the problem, or null otherwise.
36
+ * Order matters: more specific detectors first.
37
+ */
38
+ const detectors = [
39
+ // top-(-10) → -top-(10)
40
+ function detectInvertedNegative(className) {
41
+ const m = className.match(/^([a-zA-Z][\w-]*?)-\(-([^()]+)\)$/)
42
+ if (!m) return null
43
+ return {
44
+ issue: 'Negative sign is inside the parentheses',
45
+ suggestion: `Use ${chalk.green(`"-${m[1]}-(${m[2]})"`)} — PurgeTSS expects the "-" prefix BEFORE the rule, not inside the value`
46
+ }
47
+ },
48
+
49
+ // top-[10px] → top-(10)
50
+ function detectTailwindBrackets(className) {
51
+ if (!className.includes('[') && !className.includes(']')) return null
52
+ const fixed = className.replace(/\[/g, '(').replace(/\]/g, ')')
53
+ return {
54
+ issue: 'Tailwind-style brackets "[ ]" are not supported',
55
+ suggestion: `Use parentheses instead: ${chalk.green(`"${fixed}"`)}`
56
+ }
57
+ },
58
+
59
+ // wh-() → "Add a value"
60
+ function detectEmptyParens(className) {
61
+ const m = className.match(/^([\w-]+)-\(\s*\)$/)
62
+ if (!m) return null
63
+ return {
64
+ issue: 'Empty value inside parentheses',
65
+ suggestion: `Add a value, e.g. ${chalk.green(`"${m[1]}-(10)"`)}`
66
+ }
67
+ },
68
+
69
+ // wh-( 200 ) → wh-(200)
70
+ function detectSpacesInParens(className) {
71
+ const m = className.match(/^([\w-]+)-\(\s+([^()]*?)\s*\)$|^([\w-]+)-\(([^()]*?)\s+\)$/)
72
+ if (!m) return null
73
+ const rule = m[1] || m[3]
74
+ const value = (m[2] || m[4] || '').trim()
75
+ if (!value) return null
76
+ return {
77
+ issue: 'Whitespace inside parentheses',
78
+ suggestion: `Remove the spaces: ${chalk.green(`"${rule}-(${value})"`)}`
79
+ }
80
+ },
81
+
82
+ // top-(10px) — units inside parens that PurgeTSS would strip anyway.
83
+ // Only flag when value is purely numeric+unit; never blanket-flag because
84
+ // some properties (durations, percentages) accept units.
85
+ function detectRedundantUnits(className) {
86
+ const m = className.match(/^([\w-]+)-\((-?\d+(?:\.\d+)?)px\)$/)
87
+ if (!m) return null
88
+ return {
89
+ issue: 'Explicit "px" unit is redundant',
90
+ suggestion: `PurgeTSS treats unit-less values as pixels: ${chalk.green(`"${m[1]}-(${m[2]})"`)}`
91
+ }
92
+ }
93
+ ]
94
+
95
+ function detectIssue(className) {
96
+ for (const detector of detectors) {
97
+ const result = detector(className)
98
+ if (result) return result
99
+ }
100
+ return null
101
+ }
102
+
103
+ /**
104
+ * Find every (file, line) where `className` appears as a whole token inside a
105
+ * class="..." attribute. Uses an attribute-then-split strategy (rather than
106
+ * matching the class name directly) so arbitrary-value class names like
107
+ * "top-(-10)" — which contain regex-special characters — do not need escaping
108
+ * and cannot false-match a partial substring.
109
+ */
110
+ function findLocations(className, paths) {
111
+ const locations = []
112
+ const attrRe = /class\s*=\s*["']([^"']*)["']/g
113
+
114
+ for (const filePath of paths) {
115
+ let content
116
+ try {
117
+ content = fs.readFileSync(filePath, 'utf8')
118
+ } catch {
119
+ continue
120
+ }
121
+ const lines = content.split(/\r?\n/)
122
+ for (let i = 0; i < lines.length; i++) {
123
+ attrRe.lastIndex = 0
124
+ let match
125
+ while ((match = attrRe.exec(lines[i])) !== null) {
126
+ const tokens = match[1].split(/\s+/).filter(Boolean)
127
+ if (tokens.includes(className)) {
128
+ locations.push({
129
+ filePath,
130
+ lineNumber: i + 1,
131
+ lineContent: lines[i].trim()
132
+ })
133
+ break
134
+ }
135
+ }
136
+ }
137
+ }
138
+ return locations
139
+ }
140
+
141
+ function relativePath(filePath) {
142
+ return filePath.startsWith(cwd + '/') ? filePath.slice(cwd.length + 1) : filePath
143
+ }
144
+
145
+ /**
146
+ * Pre-validate every unique class against the syntax detectors. Collects all
147
+ * offenders (so the dev sees the full list in a single run instead of
148
+ * one-fix-per-attempt), prints a block per offender, then throws.
149
+ *
150
+ * Generic unknown classes are NOT flagged — they fall through silently to
151
+ * the "// Unused or unsupported classes" comment block in app.tss.
152
+ *
153
+ * @param {Object} args
154
+ * @param {string[]} args.classes Unique classes pulled from views/controllers.
155
+ * @param {string[]} args.viewPaths Absolute paths to XML view files.
156
+ * @param {string[]} [args.controllerPaths] Absolute paths to JS controller files.
157
+ * @throws {Error} with `isClassSyntaxError === true` if any class matches a detector.
158
+ */
159
+ export function validateClassSyntax({ classes, viewPaths, controllerPaths = [] }) {
160
+ if (!classes || classes.length === 0) return
161
+
162
+ const allPaths = [...viewPaths, ...controllerPaths]
163
+ const offenders = []
164
+
165
+ for (const className of classes) {
166
+ const issue = detectIssue(className)
167
+ if (!issue) continue
168
+ offenders.push({
169
+ className,
170
+ issue,
171
+ locations: findLocations(className, allPaths)
172
+ })
173
+ }
174
+
175
+ if (offenders.length === 0) return
176
+
177
+ for (const offender of offenders) {
178
+ const lines = []
179
+ lines.push(`Class: ${chalk.yellow(`"${offender.className}"`)}`)
180
+
181
+ if (offender.locations.length > 0) {
182
+ const first = offender.locations[0]
183
+ lines.push(`File: ${chalk.yellow(`"${relativePath(first.filePath)}"`)}`)
184
+ lines.push(`Line: ${chalk.yellow(first.lineNumber)}`)
185
+ lines.push(`Content: ${chalk.gray(first.lineContent)}`)
186
+ if (offender.locations.length > 1) {
187
+ const more = offender.locations.length - 1
188
+ lines.push(chalk.gray(`(also used in ${more} other location${more === 1 ? '' : 's'})`))
189
+ }
190
+ } else {
191
+ lines.push(chalk.gray('Location: not found in views — likely from a controller or safelist'))
192
+ }
193
+
194
+ lines.push('')
195
+ lines.push(chalk.red(`Issue: ${offender.issue.issue}`))
196
+ lines.push(`${chalk.green('Fix:')} ${offender.issue.suggestion}`)
197
+
198
+ logger.block(chalk.red('Class Syntax Error'), ...lines)
199
+ }
200
+
201
+ const word = offenders.length === 1 ? 'class syntax error' : 'class syntax errors'
202
+ logger.block(
203
+ chalk.red(`Found ${offenders.length} ${word} — fix the ${offenders.length === 1 ? 'class' : 'classes'} above and re-run purgetss`)
204
+ )
205
+
206
+ const error = new Error('Class syntax errors detected')
207
+ error.isClassSyntaxError = true
208
+ throw error
209
+ }
@@ -23,15 +23,7 @@ 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
- 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
- },
26
+ logos: {}, // empty = auto-discovers from purgetss/brand/
35
27
  padding: {
36
28
  ios: '4%', // iOS aesthetic. Range: 2% bold — 8% conservative. No launcher mask.
37
29
  androidLegacy: '10%', // legacy ic_launcher.png padding
@@ -41,15 +33,14 @@ const BRAND_BLOCK = ` brand: {
41
33
  splash: false, // also generate splash_icon.png × 5
42
34
  notification: false // also generate ic_stat_notify.png × 5
43
35
  },
36
+ ios: {
37
+ dark: true, // generate iOS 18+ Dark appearance icon
38
+ tinted: true, // generate iOS 18+ Tinted appearance icon
39
+ darkBackground: null // null = transparent per Apple HIG
40
+ },
44
41
  colors: {
45
42
  background: '#FFFFFF' // Android adaptive bg + iOS/marketplace flatten
46
43
  },
47
- // Optional iOS overrides:
48
- // ios: {
49
- // dark: false,
50
- // tinted: false,
51
- // darkBackground: '#111111'
52
- // },
53
44
  confirmOverwrites: true // prompt before overwriting files (set false to skip)
54
45
  },
55
46
  `
@@ -84,6 +84,10 @@ function printFullNotes(opts) {
84
84
  logger.section('Configuration reminders')
85
85
  console.log(' The tool does NOT auto-edit tiapp.xml. Snippets below are optional —')
86
86
  console.log(' paste only what you need, after reviewing.')
87
+ console.log(' If your app already uses a custom Android theme, merge these changes')
88
+ console.log(' into that theme instead of replacing it blindly.')
89
+ console.log(' brand is designed around the modern Titanium icon pipeline, not around')
90
+ console.log(' older Android splash themes such as background.png / background.9.png.')
87
91
  console.log()
88
92
  console.log(` ${chalk.yellow('⚠')} ${chalk.yellow('tiapp.xml <application> tag may be self-closing')}`)
89
93
  console.log(' If yours looks like:')
@@ -114,14 +118,18 @@ function printFullNotes(opts) {
114
118
  console.log(' If you want the Android 12+ splash to use splash_icon instead of')
115
119
  console.log(' ic_launcher, add a custom theme and point')
116
120
  console.log(code(' <item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>'))
121
+ console.log(' If you still see a brief flash during splash exit, the artifact may')
122
+ console.log(' come from Titanium or the system splash transition rather than from')
123
+ console.log(' the generated PNG assets themselves.')
117
124
  }
118
125
 
119
126
  console.log()
120
127
  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.')
128
+ console.log(' PurgeTSS brand still regenerates app/assets/android/default.png as')
129
+ console.log(' a compatibility fallback while Titanium continues to recognize it.')
130
+ console.log(' It is not the primary modern path. If your app uses a custom')
131
+ console.log(' windowBackground / background.9.png theme, that custom theme still')
132
+ console.log(' takes precedence and should be managed manually.')
125
133
 
126
134
  if (withNotification) {
127
135
  const colorsDir = projectType === 'classic'