purgetss 7.7.1 → 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
@@ -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`.
package/bin/purgetss CHANGED
@@ -353,6 +353,13 @@ Writes directly to the project (auto-detects Alloy vs Classic):
353
353
  Defaults come from the ${chalk.cyan('images:')} section in ${chalk.cyan('purgetss/config.cjs')}.
354
354
  CLI flags always win over config values.
355
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
+
356
363
  Examples:
357
364
  ${chalk.cyan('purgetss images')} # uses purgetss/images/ + config
358
365
  ${chalk.cyan('purgetss images')} ./docs/screenshots # scope to one folder
@@ -361,12 +368,15 @@ Examples:
361
368
  ${chalk.cyan('purgetss images')} --ios # iPhone scales only
362
369
  ${chalk.cyan('purgetss images')} --format webp # convert all outputs to WebP
363
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
364
373
  ${chalk.cyan('purgetss images')} --dry-run
365
374
  `)
366
375
  .option('--android', 'Generate only Android density variants (skip iPhone)')
367
376
  .option('--ios', 'Generate only iPhone scale variants (skip Android)')
368
377
  .option('--format <ext>', 'Convert all outputs to this format: webp|jpeg|png|avif|gif|tiff')
369
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))
370
380
  .option('--project <path>', 'Project root (default: cwd)')
371
381
  .option('--dry-run', 'Preview without writing any files')
372
382
  .option('-y, --yes', 'Skip the overwrite confirmation prompt')
@@ -1,4 +1,4 @@
1
- // PurgeTSS v7.7.1
1
+ // PurgeTSS v7.8.0
2
2
  // Created by César Estrada
3
3
  // https://purgetss.com
4
4
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "purgetss",
4
- "version": "7.7.1",
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
+ }
@@ -7,28 +7,21 @@ import path from 'path'
7
7
  import { fileURLToPath } from 'url'
8
8
  import { createRequire } from 'module'
9
9
  import _ from 'lodash'
10
- import chalk from 'chalk'
11
10
  let saveGlossary = false
12
11
 
13
12
  const __filename = fileURLToPath(import.meta.url)
14
13
  const __dirname = path.dirname(__filename)
15
14
  const require = createRequire(import.meta.url)
16
15
  const cwd = process.cwd()
17
- import { colores } from '../src/shared/brand-colors.js'
16
+ import { colores } from '../../shared/brand-colors.js'
18
17
  export { colores }
19
- const purgeLabel = colores.purgeLabel
20
18
 
21
- import * as helpers from '../src/shared/helpers.js'
22
- import { getConfigFile } from '../src/shared/config-manager.js'
23
- import { projectsConfigJS } from '../src/shared/constants.js'
24
- const tiCompletionsFile = require('../lib/completions/titanium/completions-v3.json')
25
-
26
- const logger = {
27
- info: (...args) => console.log(purgeLabel, args.join(' ')),
28
- warn: (...args) => console.log(purgeLabel, chalk.yellow(args.join(' '))),
29
- error: (...args) => console.log(purgeLabel, chalk.red(args.join(' '))),
30
- file: (...args) => console.log(purgeLabel, chalk.yellow(args.join(' ')), 'file created!')
31
- }
19
+ import * as helpers from '../../shared/helpers.js'
20
+ import { getConfigFile } from '../../shared/config-manager.js'
21
+ import { projectsConfigJS } from '../../shared/constants.js'
22
+ import { logger } from '../../shared/logger.js'
23
+ import { registerSemanticName } from '../../shared/semantic-helpers.js'
24
+ const tiCompletionsFile = require('../../../lib/completions/titanium/completions-v3.json')
32
25
 
33
26
  // Keys whose numeric values are interpreted with `ti.ui.defaultunit` from tiapp.xml.
34
27
  // The glossary .md files for these keys receive an inline "// Unit: ..." note
@@ -174,7 +167,7 @@ function buildSubfolderIndex(folder) {
174
167
  }
175
168
 
176
169
  function glossaryBaseFolder() {
177
- if (!fs.existsSync(projectsConfigJS)) return path.resolve(__dirname, '../dist/glossary/')
170
+ if (!fs.existsSync(projectsConfigJS)) return path.resolve(__dirname, '../../../dist/glossary/')
178
171
  if (saveGlossary) return cwd + '/purgetss/glossary/'
179
172
  return ''
180
173
  }
@@ -236,7 +229,7 @@ function autoBuildUtilitiesTSS(options = {}) {
236
229
 
237
230
  saveGlossary = options.glossary ?? false
238
231
  scaffoldGlossary()
239
- let tailwindStyles = fs.readFileSync(path.resolve(__dirname, '../lib/templates/tailwind/custom-template.tss'), 'utf8')
232
+ let tailwindStyles = fs.readFileSync(path.resolve(__dirname, '../../../lib/templates/tailwind/custom-template.tss'), 'utf8')
240
233
  tailwindStyles += (fs.existsSync(projectsConfigJS)) ? `// config.js file updated on: ${getFileUpdatedDate(projectsConfigJS)}\n` : '// default config.js file\n'
241
234
 
242
235
  const baseValues = combineDefaultThemeWithConfigFile()
@@ -255,7 +248,7 @@ function autoBuildUtilitiesTSS(options = {}) {
255
248
  saveFile(cwd + '/purgetss/styles/utilities.tss', tailwindStyles)
256
249
  logger.file('./purgetss/styles/utilities.tss')
257
250
  } else {
258
- saveFile(path.resolve(__dirname, '../dist/utilities.tss'), tailwindStyles)
251
+ saveFile(path.resolve(__dirname, '../../../dist/utilities.tss'), tailwindStyles)
259
252
  logger.file('./dist/utilities.tss')
260
253
  }
261
254
  }
@@ -311,7 +304,7 @@ function processCompletionsClasses(_completionsWithBaseValues) {
311
304
 
312
305
  function generateGlossary(_key, _theClasses, _keyName = null) {
313
306
  let baseDestinationFolder = ''
314
- if (!fs.existsSync(projectsConfigJS)) baseDestinationFolder = path.resolve(__dirname, '../dist/glossary/')
307
+ if (!fs.existsSync(projectsConfigJS)) baseDestinationFolder = path.resolve(__dirname, '../../../dist/glossary/')
315
308
  else if (saveGlossary) baseDestinationFolder = cwd + '/purgetss/glossary/'
316
309
 
317
310
  if (baseDestinationFolder !== '') {
@@ -378,7 +371,7 @@ function getTiUIComponents(_base) {
378
371
 
379
372
  function processCompoundClasses({ ..._base }) {
380
373
  let compoundClasses = ''
381
- const compoundTemplate = require('../lib/templates/tailwind/compoundTemplate.json')
374
+ const compoundTemplate = require('../../../lib/templates/tailwind/compoundTemplate.json')
382
375
 
383
376
  _.each(compoundTemplate, (value, key) => {
384
377
  compoundClasses += generateGlossary(key, helpers.processProperties(value.description, value.template, value.base ?? { default: _base[key] }))
@@ -534,6 +527,11 @@ function combineDefaultThemeWithConfigFile() {
534
527
  }
535
528
 
536
529
  _.merge(base.colors, themeOrDefaultValues.colors, configFile.theme.extend.colors)
530
+ // Track semantic color names so opacity modifiers (bg-X/65) can later
531
+ // auto-derive an alpha-applied entry in semantic.colors.json. A value is
532
+ // "semantic" when it's a string that isn't a hex literal or a Ti reserved
533
+ // keyword.
534
+ _.each(base.colors, value => collectSemanticReferences(value))
537
535
  _.merge(base.size, themeOrDefaultValues.spacing, configFile.theme.extend.spacing)
538
536
  _.merge(base.spacing, themeOrDefaultValues.spacing, configFile.theme.extend.spacing)
539
537
 
@@ -572,8 +570,16 @@ function combineDefaultThemeWithConfigFile() {
572
570
  delete base.zIndex.auto
573
571
 
574
572
  // ! Process custom Window, View and ImageView
575
- // Merge extend values into theme (same as colors, spacing, etc.)
573
+ // Track whether the user defined each Ti Element at the theme.X (replace) level
574
+ // BEFORE merging extend into theme. This mirrors the Tailwind convention:
575
+ // theme.X → REPLACE the framework's defaults entirely
576
+ // theme.extend.X → MERGE with the framework's defaults
577
+ // Without this distinction, presets (like Window's backgroundColor: '#FFFFFF')
578
+ // leak into a strict-replace config and surface as ghost properties in app.tss.
579
+ const userReplaced = {}
576
580
  _.each(['Window', 'View', 'ImageView'], comp => {
581
+ userReplaced[comp] = !!configFile.theme[comp] && !configFile.theme.extend[comp]
582
+
577
583
  if (configFile.theme.extend[comp]) {
578
584
  configFile.theme[comp] = _.merge({}, configFile.theme[comp], configFile.theme.extend[comp])
579
585
  delete configFile.theme.extend[comp]
@@ -584,11 +590,18 @@ function combineDefaultThemeWithConfigFile() {
584
590
  }
585
591
  })
586
592
 
587
- // Merge user config WITH defaults, then write back to configFile.theme
588
- // so that getTiUIComponents/combineKeys picks up the full merged object
589
- configFile.theme.Window = _.merge({ default: { backgroundColor: '#FFFFFF' } }, configFile.theme.Window)
590
- configFile.theme.ImageView = _.merge({ ios: { hires: true } }, configFile.theme.ImageView)
591
- configFile.theme.View = _.merge({ default: { width: 'Ti.UI.SIZE', height: 'Ti.UI.SIZE' } }, configFile.theme.View)
593
+ // Apply framework defaults only when NOT in strict-replace mode. In replace
594
+ // mode the user's config is the single source of truth — presets must not be
595
+ // silently re-injected.
596
+ if (!userReplaced.Window) {
597
+ configFile.theme.Window = _.merge({ default: { backgroundColor: '#FFFFFF' } }, configFile.theme.Window)
598
+ }
599
+ if (!userReplaced.ImageView) {
600
+ configFile.theme.ImageView = _.merge({ ios: { hires: true } }, configFile.theme.ImageView)
601
+ }
602
+ if (!userReplaced.View) {
603
+ configFile.theme.View = _.merge({ default: { width: 'Ti.UI.SIZE', height: 'Ti.UI.SIZE' } }, configFile.theme.View)
604
+ }
592
605
 
593
606
  base.Window = configFile.theme.Window
594
607
  base.ImageView = configFile.theme.ImageView
@@ -610,6 +623,22 @@ function checkDeletePlugins() {
610
623
  return Array.isArray(deletePlugins) ? deletePlugins : Object.keys(deletePlugins).map(key => key)
611
624
  }
612
625
 
626
+ // Walk a color config value (possibly nested object of shades) and register
627
+ // any leaf string that points to a semantic color name in
628
+ // `semantic.colors.json` (e.g. `surface: 'surfaceColor'`,
629
+ // `brand: { DEFAULT: 'brandColor' }`).
630
+ const _semanticReservedValues = new Set(['transparent', 'currentColor', 'inherit'])
631
+ function collectSemanticReferences(value) {
632
+ if (typeof value === 'string') {
633
+ if (value.startsWith('#') || _semanticReservedValues.has(value)) return
634
+ registerSemanticName(value)
635
+ return
636
+ }
637
+ if (value && typeof value === 'object') {
638
+ _.each(value, v => collectSemanticReferences(v))
639
+ }
640
+ }
641
+
613
642
  // ! Helper Functions
614
643
  function removeDeprecatedColors(theObject) {
615
644
  delete theObject.blueGray
@@ -830,8 +859,8 @@ function generateCombinedClasses(key, data) {
830
859
 
831
860
  function saveAutoTSS(key, classes) {
832
861
  if (fs.existsSync(projectsConfigJS) && saveGlossary) {
833
- makeSureFolderExists(cwd + '/purgetss/experimental/tailwind-classes/')
834
- saveFile(cwd + `/purgetss/experimental/tailwind-classes/${key}.tss`, classes)
862
+ makeSureFolderExists(cwd + '/purgetss/glossary/tailwind-classes/')
863
+ saveFile(cwd + `/purgetss/glossary/tailwind-classes/${key}.tss`, classes)
835
864
  }
836
865
  }
837
866
 
@@ -8,10 +8,10 @@
8
8
 
9
9
  // Import functions from their new modular locations
10
10
  import * as helpers from '../../shared/helpers.js'
11
- import { autoBuildUtilitiesTSS } from '../../../experimental/completions2.js'
11
+ import { autoBuildUtilitiesTSS } from './auto-utilities-builder.js'
12
12
 
13
13
  /**
14
- * Build Tailwind (AUTO mode using experimental completions engine)
14
+ * Build Tailwind (Auto-builds utilities.tss from config.cjs (active production path))
15
15
  * @param {Object} options - Build options
16
16
  */
17
17
  export function buildTailwind(options) {