purgetss 7.5.2 → 7.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -11
- package/bin/purgetss +140 -1
- package/dist/purgetss.ui.js +65 -26
- package/dist/utilities.tss +21 -4
- package/experimental/completions2.js +1 -1
- package/lib/completions/titanium/completions-v3.json +62 -1
- package/lib/templates/purgetss.config.js.cjs +15 -1
- package/lib/templates/purgetss.ui.js.cjs +64 -25
- package/package.json +3 -1
- package/src/cli/commands/brand.js +69 -0
- package/src/cli/commands/create.js +11 -7
- package/src/cli/commands/fonts.js +9 -9
- package/src/cli/commands/icon-library.js +18 -16
- package/src/cli/commands/images.js +116 -0
- package/src/cli/commands/init.js +4 -0
- package/src/cli/commands/module.js +4 -2
- package/src/cli/commands/purge.js +77 -101
- package/src/cli/commands/semantic.js +180 -0
- package/src/cli/commands/shades.js +332 -13
- package/src/cli/utils/project-detection.js +4 -2
- package/src/core/analyzers/class-extractor.js +110 -3
- package/src/core/branding/brand-config.js +111 -0
- package/src/core/branding/branding-logger.js +40 -0
- package/src/core/branding/cleanup-legacy.js +220 -0
- package/src/core/branding/ensure-brand-section.js +80 -0
- package/src/core/branding/gen-android-adaptive.js +116 -0
- package/src/core/branding/gen-android-legacy.js +63 -0
- package/src/core/branding/gen-ic-launcher-xml.js +29 -0
- package/src/core/branding/gen-ios-dark.js +70 -0
- package/src/core/branding/gen-ios-tinted.js +55 -0
- package/src/core/branding/gen-ios.js +69 -0
- package/src/core/branding/gen-marketplace.js +71 -0
- package/src/core/branding/gen-notification.js +76 -0
- package/src/core/branding/gen-splash.js +64 -0
- package/src/core/branding/index.js +336 -0
- package/src/core/branding/post-gen-notes.js +145 -0
- package/src/core/branding/prepare-master.js +108 -0
- package/src/core/branding/tiapp-reader.js +110 -0
- package/src/core/builders/tailwind-helpers.js +1 -1
- package/src/core/images/ensure-images-section.js +57 -0
- package/src/core/images/gen-scales.js +181 -0
- package/src/core/images/index.js +171 -0
- package/src/shared/config-manager.js +46 -0
- package/src/shared/config-writer.js +84 -0
- package/src/shared/constants.js +3 -0
- package/src/shared/helpers/typography.js +38 -3
- package/src/shared/logger.js +69 -4
- package/src/shared/prompt.js +64 -0
- package/src/shared/svg-utils.js +80 -0
- package/src/shared/utils.js +8 -4
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PurgeTSS - `semantic` command
|
|
3
|
+
*
|
|
4
|
+
* Generates Titanium semantic colors (app/assets/semantic.colors.json) in two
|
|
5
|
+
* modes, dispatched by --single:
|
|
6
|
+
*
|
|
7
|
+
* Palette mode (no --single):
|
|
8
|
+
* One base hex → 11-step tonal palette with mirror-by-index Light/Dark
|
|
9
|
+
* inversion (anchored at shade 500). Writes JSON + config mapping (the
|
|
10
|
+
* 11 shade keys are mechanically derived from the family name, so the
|
|
11
|
+
* class mapping is unambiguous).
|
|
12
|
+
*
|
|
13
|
+
* Single mode (--single):
|
|
14
|
+
* Explicit light + optional dark + optional alpha → one purpose-based
|
|
15
|
+
* semantic color. Writes JSON ONLY. The class name mapping in config.cjs
|
|
16
|
+
* is a design-layer decision (designers pick `bg-surface` to point at
|
|
17
|
+
* `surfaceColor`, `text-on-surface` to point at `textColor`, etc.) and
|
|
18
|
+
* should not be auto-derived from the semantic key. The command tells
|
|
19
|
+
* the user how to add the mapping.
|
|
20
|
+
*
|
|
21
|
+
* If the name matches an existing palette shade (e.g. `amazon50` while
|
|
22
|
+
* palette `amazon` exists), the operation narrows to an in-place JSON
|
|
23
|
+
* value edit — no new top-level entry, no config touch.
|
|
24
|
+
*
|
|
25
|
+
* @author César Estrada
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import chalk from 'chalk'
|
|
29
|
+
import { alloyProject } from '../../shared/utils.js'
|
|
30
|
+
import { logger } from '../../shared/logger.js'
|
|
31
|
+
import { ensureConfig, getConfigFile } from '../../shared/config-manager.js'
|
|
32
|
+
import {
|
|
33
|
+
toCamelCase,
|
|
34
|
+
buildSemanticPalette,
|
|
35
|
+
buildSingleSemantic,
|
|
36
|
+
writeSemanticColors,
|
|
37
|
+
writeSemanticJSON,
|
|
38
|
+
writeConfigMapping,
|
|
39
|
+
updateSemanticEntry,
|
|
40
|
+
wrapHexWithAlpha,
|
|
41
|
+
detectFamilyShadeConflict,
|
|
42
|
+
normalizeAlpha,
|
|
43
|
+
checkIfColorModule,
|
|
44
|
+
missingHexMessage
|
|
45
|
+
} from './shades.js'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalize a user-supplied name into both kebab and camel forms while
|
|
49
|
+
* preserving the user's case. Handles three input shapes:
|
|
50
|
+
* - camelCase ('surfaceColor') → { kebab: 'surface-color', camel: 'surfaceColor' }
|
|
51
|
+
* - kebab ('surface-color') → { kebab: 'surface-color', camel: 'surfaceColor' }
|
|
52
|
+
* - natural ('Surface Color') → { kebab: 'surface-color', camel: 'surfaceColor' }
|
|
53
|
+
*/
|
|
54
|
+
function parseName(raw) {
|
|
55
|
+
const clean = String(raw).replace(/'/g, '').replace(/\//g, '').trim()
|
|
56
|
+
if (/\s/.test(clean)) {
|
|
57
|
+
const kebab = clean.toLowerCase().split(/\s+/).join('-')
|
|
58
|
+
return { kebab, camel: toCamelCase(kebab) }
|
|
59
|
+
}
|
|
60
|
+
if (clean.includes('-')) {
|
|
61
|
+
const kebab = clean.toLowerCase()
|
|
62
|
+
return { kebab, camel: toCamelCase(kebab) }
|
|
63
|
+
}
|
|
64
|
+
// No space, no hyphen: treat as camelCase or single word, preserve case
|
|
65
|
+
const camel = clean
|
|
66
|
+
const kebab = camel.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
67
|
+
return { kebab, camel }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function semantic(args, options) {
|
|
71
|
+
const silent = options.log
|
|
72
|
+
|
|
73
|
+
if (options.single) {
|
|
74
|
+
return runSingle(args, options, silent)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return runPalette(args, options, silent)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runPalette(args, options, silent) {
|
|
81
|
+
if (!args.hexcode && !options.random) {
|
|
82
|
+
logger.block(...missingHexMessage('semantic'))
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const chroma = (await import('chroma-js')).default
|
|
87
|
+
const referenceColorFamilies = (await import('../../../lib/color-shades/tailwindColors.js')).default
|
|
88
|
+
const generateColorShades = (await import('../../../lib/color-shades/generateColorShades.js')).default
|
|
89
|
+
|
|
90
|
+
const colorFamily = options.random
|
|
91
|
+
? generateColorShades(chroma.random(), referenceColorFamilies)
|
|
92
|
+
: generateColorShades(args.hexcode, referenceColorFamilies)
|
|
93
|
+
|
|
94
|
+
if (args.name) colorFamily.name = args.name
|
|
95
|
+
else if (options.name) colorFamily.name = options.name
|
|
96
|
+
|
|
97
|
+
colorFamily.name = colorFamily.name.replace(/'/g, '').replace(/\//g, '').replace(/\s+/g, ' ')
|
|
98
|
+
|
|
99
|
+
const { kebab: kebabName } = parseName(colorFamily.name)
|
|
100
|
+
const { semanticEntries, configMapping } = buildSemanticPalette(colorFamily, kebabName)
|
|
101
|
+
|
|
102
|
+
if (alloyProject(silent) && !silent) {
|
|
103
|
+
ensureConfig()
|
|
104
|
+
writeSemanticColors(semanticEntries, kebabName, configMapping, options)
|
|
105
|
+
logger.info(`${chalk.hex(colorFamily.hexcode).bold(`"${colorFamily.name}"`)} palette (11 shades) saved to`, chalk.yellow('app/assets/semantic.colors.json'))
|
|
106
|
+
} else {
|
|
107
|
+
logger.info(`${chalk.hex(colorFamily.hexcode).bold(`"${colorFamily.name}"`)} palette preview:\n${JSON.stringify(semanticEntries, null, 2)}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return true
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runSingle(args, options, silent) {
|
|
114
|
+
const lightHex = args.hexcode
|
|
115
|
+
const rawName = args.name || options.name
|
|
116
|
+
if (!lightHex) {
|
|
117
|
+
logger.info(`${chalk.red('Missing light hex.')} Usage: ${chalk.green("pt semantic --single '#F9FAFB' surfaceColor [--dark '#0f172a'] [--alpha 50]")}`)
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
if (!rawName) {
|
|
121
|
+
logger.info(`${chalk.red('Missing name.')} Usage: ${chalk.green("pt semantic --single '#F9FAFB' surfaceColor [--dark '#0f172a'] [--alpha 50]")}`)
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const darkHex = options.dark || lightHex
|
|
126
|
+
const alpha = normalizeAlpha(options.alpha)
|
|
127
|
+
const { kebab: kebabName, camel: camelName } = parseName(rawName)
|
|
128
|
+
|
|
129
|
+
// Preview mode: log the JSON, no writes, no Alloy check
|
|
130
|
+
if (silent) {
|
|
131
|
+
const { semanticEntries } = buildSingleSemantic(camelName, lightHex, darkHex, alpha)
|
|
132
|
+
logger.info(`${chalk.hex(lightHex).bold(`"${camelName}"`)} preview:\n${JSON.stringify(semanticEntries, null, 2)}`)
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!alloyProject(silent)) return false
|
|
137
|
+
ensureConfig()
|
|
138
|
+
|
|
139
|
+
// If the name matches an existing palette shade (e.g. `amazon50` when palette
|
|
140
|
+
// `amazon` is already declared), interpret as an in-place value edit. Update
|
|
141
|
+
// the JSON entry, leave config.cjs alone (palette already maps to this key).
|
|
142
|
+
const conflict = detectFamilyShadeConflict(getConfigFile(), kebabName, camelName)
|
|
143
|
+
if (conflict) {
|
|
144
|
+
const value = {
|
|
145
|
+
light: wrapHexWithAlpha(lightHex, alpha),
|
|
146
|
+
dark: wrapHexWithAlpha(darkHex, alpha)
|
|
147
|
+
}
|
|
148
|
+
updateSemanticEntry(conflict.camelKey, value)
|
|
149
|
+
checkIfColorModule()
|
|
150
|
+
logger.info(`${chalk.hex(lightHex).bold(conflict.camelKey)} updated in ${chalk.yellow('app/assets/semantic.colors.json')} — palette ${chalk.yellow(conflict.parentName)} already references this key, config.cjs left unchanged.`)
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Fresh single entry: write JSON + auto-map config.cjs to a sensible class name.
|
|
155
|
+
// Convention: strip the trailing 'Color' suffix (Titanium's semantic naming
|
|
156
|
+
// pattern) and kebab-case the rest. Users who want a different class name
|
|
157
|
+
// (`on-surface` for `textColor`, etc.) edit config.cjs after the fact —
|
|
158
|
+
// overriding the auto-mapping is one keystroke; typing it from scratch is many.
|
|
159
|
+
const { semanticEntries } = buildSingleSemantic(camelName, lightHex, darkHex, alpha)
|
|
160
|
+
const className = suggestClassName(camelName)
|
|
161
|
+
writeSemanticJSON(semanticEntries, camelName)
|
|
162
|
+
writeConfigMapping(className, camelName, options)
|
|
163
|
+
logger.info(`${chalk.hex(lightHex).bold(`"${camelName}"`)} saved to ${chalk.yellow('app/assets/semantic.colors.json')} and mapped to class ${chalk.green(className)} in ${chalk.yellow('config.cjs')}.`)
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Derive a class name from a Titanium semantic key by stripping the
|
|
169
|
+
* conventional `Color` suffix and kebab-casing the result.
|
|
170
|
+
* 'surfaceColor' → 'surface'
|
|
171
|
+
* 'surfaceHighColor' → 'surface-high'
|
|
172
|
+
* 'borderColor' → 'border'
|
|
173
|
+
* 'overlay' → 'overlay' (no Color suffix → unchanged)
|
|
174
|
+
*/
|
|
175
|
+
function suggestClassName(camelKey) {
|
|
176
|
+
const stripped = camelKey.replace(/Color$/, '') || camelKey
|
|
177
|
+
return stripped.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default { semantic }
|
|
@@ -15,7 +15,7 @@ import fs from 'fs'
|
|
|
15
15
|
import chalk from 'chalk'
|
|
16
16
|
import { createRequire } from 'module'
|
|
17
17
|
import { alloyProject, makeSureFolderExists } from '../../shared/utils.js'
|
|
18
|
-
import { projectsConfigJS, projectsLibFolder } from '../../shared/constants.js'
|
|
18
|
+
import { projectsConfigJS, projectsLibFolder, projectsSemanticColorsJSON } from '../../shared/constants.js'
|
|
19
19
|
import { logger } from '../../shared/logger.js'
|
|
20
20
|
import { ensureConfig, getConfigFile } from '../../shared/config-manager.js'
|
|
21
21
|
import { cleanDoubleQuotes } from '../utils/file-operations.js'
|
|
@@ -67,6 +67,27 @@ export function checkIfColorModule() {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Build the "missing hex" error message shown when the user invokes `shades`
|
|
72
|
+
* or `semantic` without a hex argument and without `--random`. The common
|
|
73
|
+
* cause is an unquoted `#` on the command line — bash/zsh treat everything
|
|
74
|
+
* from `#` onward as a comment, so `pt shades #6A2489` reaches the CLI as
|
|
75
|
+
* `pt shades` with no argument. We surface the shell behavior explicitly so
|
|
76
|
+
* the user isn't mystified by a silent random color.
|
|
77
|
+
*/
|
|
78
|
+
export function missingHexMessage(commandName) {
|
|
79
|
+
return [
|
|
80
|
+
chalk.red('No hex color provided.'),
|
|
81
|
+
`If you typed ${chalk.yellow(`pt ${commandName} #6A2489`)} (unquoted), your shell stripped ${chalk.yellow('#6A2489')}`,
|
|
82
|
+
'as a comment — the CLI never received it.',
|
|
83
|
+
'',
|
|
84
|
+
'Try one of these instead:',
|
|
85
|
+
` ${chalk.green(`pt ${commandName} '#6A2489'`)} ${chalk.gray('(quoted)')}`,
|
|
86
|
+
` ${chalk.green(`pt ${commandName} 6A2489`)} ${chalk.gray('(no hash)')}`,
|
|
87
|
+
` ${chalk.green(`pt ${commandName} --random`)} ${chalk.gray('(random color)')}`
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
/**
|
|
71
92
|
* Main shades command - generates color shades from hex codes
|
|
72
93
|
* Maintains exact same logic as original shades() function
|
|
@@ -86,11 +107,16 @@ export function checkIfColorModule() {
|
|
|
86
107
|
* @returns {Promise<boolean>} Success status
|
|
87
108
|
*/
|
|
88
109
|
export async function shades(args, options) {
|
|
110
|
+
if (!args.hexcode && !options.random) {
|
|
111
|
+
logger.block(...missingHexMessage('shades'))
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
89
115
|
const chroma = (await import('chroma-js')).default
|
|
90
116
|
const referenceColorFamilies = (await import('../../../lib/color-shades/tailwindColors.js')).default
|
|
91
117
|
const generateColorShades = (await import('../../../lib/color-shades/generateColorShades.js')).default
|
|
92
118
|
|
|
93
|
-
const colorFamily =
|
|
119
|
+
const colorFamily = options.random
|
|
94
120
|
? generateColorShades(chroma.random(), referenceColorFamilies)
|
|
95
121
|
: generateColorShades(args.hexcode, referenceColorFamilies)
|
|
96
122
|
|
|
@@ -99,19 +125,14 @@ export async function shades(args, options) {
|
|
|
99
125
|
|
|
100
126
|
colorFamily.name = colorFamily.name.replace(/'/g, '').replace(/\//g, '').replace(/\s+/g, ' ')
|
|
101
127
|
|
|
102
|
-
const colorObject = createColorObject(colorFamily, colorFamily.hexcode, options)
|
|
103
|
-
|
|
104
128
|
const silent = options.tailwind || options.json || options.log
|
|
129
|
+
const inAlloyProject = !silent && alloyProject(silent)
|
|
105
130
|
|
|
106
|
-
|
|
107
|
-
if (alloyProject(silent) && !silent) {
|
|
108
|
-
ensureConfig()
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Get config file (after potential migration)
|
|
112
|
-
const configFile = getConfigFile()
|
|
131
|
+
const colorObject = createColorObject(colorFamily, colorFamily.hexcode, options)
|
|
113
132
|
|
|
114
|
-
if (
|
|
133
|
+
if (inAlloyProject) {
|
|
134
|
+
ensureConfig()
|
|
135
|
+
const configFile = getConfigFile()
|
|
115
136
|
|
|
116
137
|
if (options.override) {
|
|
117
138
|
if (!configFile.theme.colors) configFile.theme.colors = {}
|
|
@@ -180,11 +201,309 @@ function createColorObject(family, hexcode, options) {
|
|
|
180
201
|
return colors
|
|
181
202
|
}
|
|
182
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Convert a kebab-case color name to camelCase for semantic JSON keys.
|
|
206
|
+
* Example: 'amazon-green' → 'amazonGreen'. Leaves single-word names untouched.
|
|
207
|
+
* Exported because the `semantic` command imports it for shade-conflict detection.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} kebabName - kebab-case color name
|
|
210
|
+
* @returns {string} camelCase name
|
|
211
|
+
*/
|
|
212
|
+
export function toCamelCase(kebabName) {
|
|
213
|
+
return kebabName.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Validate and normalize the --alpha CLI input.
|
|
218
|
+
* Returns undefined when no alpha was supplied; throws on invalid input.
|
|
219
|
+
* Per the Titanium semantic.colors.json spec, alpha is a string in 0.0-100.0
|
|
220
|
+
* (integer or float). See ti-expert/references/theming.md.
|
|
221
|
+
*
|
|
222
|
+
* @param {string|number|undefined} input - raw CLI value
|
|
223
|
+
* @returns {string|undefined} normalized alpha as string, or undefined
|
|
224
|
+
*/
|
|
225
|
+
export function normalizeAlpha(input) {
|
|
226
|
+
if (input === undefined || input === null || input === '') return undefined
|
|
227
|
+
const n = Number(input)
|
|
228
|
+
if (!Number.isFinite(n) || n < 0 || n > 100) {
|
|
229
|
+
throw new Error(`--alpha must be a number between 0 and 100 (got "${input}")`)
|
|
230
|
+
}
|
|
231
|
+
return String(n)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Wrap a hex value in the Titanium semantic-color extended form when alpha is
|
|
236
|
+
* present, or return the bare hex string otherwise. Both forms are valid per
|
|
237
|
+
* the Titanium spec (a single semantic.colors.json may mix them).
|
|
238
|
+
*
|
|
239
|
+
* @param {string} hex - hex color
|
|
240
|
+
* @param {string|undefined} alpha - normalized alpha string, or undefined
|
|
241
|
+
* @returns {string|{color: string, alpha: string}}
|
|
242
|
+
*/
|
|
243
|
+
function wrapValue(hex, alpha) {
|
|
244
|
+
return alpha === undefined ? hex : { color: hex, alpha }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const SHADE_NUMBERS = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950']
|
|
248
|
+
const SHADE_SUFFIX_RE = new RegExp(`^(.+?)(${SHADE_NUMBERS.join('|')})$`)
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Detect when a name like 'amazon50' refers to an existing palette shade.
|
|
252
|
+
* The `semantic --single` command uses this to decide between in-place update
|
|
253
|
+
* (when the name matches a shade of a palette already declared in config) and
|
|
254
|
+
* the normal full write flow (when the name is independent). Returns the
|
|
255
|
+
* conflict descriptor (parentName + shadeNum + camelKey) or null.
|
|
256
|
+
*
|
|
257
|
+
* Checks BOTH the kebab and camel forms — kebab matches config.cjs keys,
|
|
258
|
+
* camel matches semantic.colors.json keys for multi-word families like
|
|
259
|
+
* `amazonGreen500`.
|
|
260
|
+
*
|
|
261
|
+
* @param {Object} configFile - parsed purgetss/config.cjs
|
|
262
|
+
* @param {string} kebabName - kebab-case name (config.cjs key style)
|
|
263
|
+
* @param {string} camelName - camelCase name (semantic.colors.json key style)
|
|
264
|
+
* @returns {{ parentName: string, shadeNum: string, camelKey: string }|null}
|
|
265
|
+
*/
|
|
266
|
+
export function detectFamilyShadeConflict(configFile, kebabName, camelName) {
|
|
267
|
+
for (const candidate of [kebabName, camelName]) {
|
|
268
|
+
const match = candidate.match(SHADE_SUFFIX_RE)
|
|
269
|
+
if (!match) continue
|
|
270
|
+
const [, parentName, shadeNum] = match
|
|
271
|
+
|
|
272
|
+
const extend = configFile?.theme?.extend?.colors?.[parentName]
|
|
273
|
+
const main = configFile?.theme?.colors?.[parentName]
|
|
274
|
+
const parentMapping = extend ?? main
|
|
275
|
+
|
|
276
|
+
if (parentMapping && typeof parentMapping === 'object' && parentMapping[shadeNum] !== undefined) {
|
|
277
|
+
return { parentName, shadeNum, camelKey: candidate === kebabName ? camelName : candidate }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Remove all keys in `existing` that belong to the named semantic family —
|
|
285
|
+
* the bare camelName (single form) plus camelName + each of the 11 shade
|
|
286
|
+
* numbers (palette form). Keys outside the family are left untouched, so
|
|
287
|
+
* unrelated palettes and manually-defined entries (`surfaceColor`, etc.)
|
|
288
|
+
* survive a re-run. This makes `shades --semantic` consistent with the
|
|
289
|
+
* non-semantic flow: regenerating a family fully replaces it.
|
|
290
|
+
*
|
|
291
|
+
* @param {Object} existing - parsed semantic.colors.json
|
|
292
|
+
* @param {string} camelName - camelCase family name (e.g., 'amazon', 'glassSurface')
|
|
293
|
+
* @returns {Object} new object without the family's keys
|
|
294
|
+
*/
|
|
295
|
+
export function stripFamilyKeys(existing, camelName) {
|
|
296
|
+
const familyKeys = new Set([camelName, ...SHADE_NUMBERS.map(n => `${camelName}${n}`)])
|
|
297
|
+
const cleaned = {}
|
|
298
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
299
|
+
if (!familyKeys.has(key)) cleaned[key] = value
|
|
300
|
+
}
|
|
301
|
+
return cleaned
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Build a semantic palette with mirror-by-index light/dark inversion,
|
|
306
|
+
* anchored at shade 500. Pure function — no I/O.
|
|
307
|
+
*
|
|
308
|
+
* For each slot at index i in the sorted (50→950) shade list:
|
|
309
|
+
* light = shade[last - i].hexcode (inverted)
|
|
310
|
+
* dark = shade[i].hexcode (original)
|
|
311
|
+
* Slot 500 is the anchor (same light/dark).
|
|
312
|
+
*
|
|
313
|
+
* When alpha is provided, every value is wrapped as { color, alpha }.
|
|
314
|
+
*
|
|
315
|
+
* @param {Object} family - Color family from generateColorShades()
|
|
316
|
+
* @param {string} kebabName - Normalized color name (e.g., 'amazon-green')
|
|
317
|
+
* @param {string|undefined} alpha - Optional alpha string (0-100)
|
|
318
|
+
* @returns {{ semanticEntries: Object, configMapping: Object }}
|
|
319
|
+
*/
|
|
320
|
+
export function buildSemanticPalette(family, kebabName, alpha) {
|
|
321
|
+
const camelName = toCamelCase(kebabName)
|
|
322
|
+
const sorted = [...family.shades].sort((a, b) => a.number - b.number)
|
|
323
|
+
const semanticEntries = {}
|
|
324
|
+
const configMapping = {}
|
|
325
|
+
|
|
326
|
+
sorted.forEach((shade, i) => {
|
|
327
|
+
const mirror = sorted[sorted.length - 1 - i]
|
|
328
|
+
const key = `${camelName}${shade.number}`
|
|
329
|
+
semanticEntries[key] = {
|
|
330
|
+
light: wrapValue(mirror.hexcode, alpha),
|
|
331
|
+
dark: wrapValue(shade.hexcode, alpha)
|
|
332
|
+
}
|
|
333
|
+
configMapping[shade.number] = key
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
return { semanticEntries, configMapping }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Build a single-entry purpose-based semantic color from explicit per-mode
|
|
341
|
+
* hex values. Used by `pt semantic --single`. Light and dark are independent;
|
|
342
|
+
* if `darkHex` is omitted the same value is used for both modes (useful for
|
|
343
|
+
* overlays/glass surfaces where alpha is the variation, not hue).
|
|
344
|
+
*
|
|
345
|
+
* The JSON key is taken VERBATIM — caller is responsible for passing the exact
|
|
346
|
+
* camelCase form expected in semantic.colors.json (e.g. 'surfaceColor'), since
|
|
347
|
+
* Titanium's conventions are case-sensitive and the design-layer class name
|
|
348
|
+
* (config.cjs) is a separate decision that the caller owns.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} camelKey - exact JSON key (e.g. 'surfaceColor', 'overlay')
|
|
351
|
+
* @param {string} lightHex - hex for light mode (required)
|
|
352
|
+
* @param {string|undefined} darkHex - hex for dark mode (defaults to lightHex)
|
|
353
|
+
* @param {string|undefined} alpha - normalized alpha string (0-100), wraps both modes
|
|
354
|
+
* @returns {{ semanticEntries: Object }}
|
|
355
|
+
*/
|
|
356
|
+
export function buildSingleSemantic(camelKey, lightHex, darkHex, alpha) {
|
|
357
|
+
const dark = darkHex ?? lightHex
|
|
358
|
+
return {
|
|
359
|
+
semanticEntries: {
|
|
360
|
+
[camelKey]: {
|
|
361
|
+
light: wrapValue(lightHex, alpha),
|
|
362
|
+
dark: wrapValue(dark, alpha)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Wrap a hex value with optional alpha — exposed so the `semantic` command
|
|
370
|
+
* can format individual entries without re-importing the building blocks.
|
|
371
|
+
*
|
|
372
|
+
* @param {string} hex
|
|
373
|
+
* @param {string|undefined} alpha
|
|
374
|
+
* @returns {string|{color:string,alpha:string}}
|
|
375
|
+
*/
|
|
376
|
+
export function wrapHexWithAlpha(hex, alpha) {
|
|
377
|
+
return wrapValue(hex, alpha)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Read + parse semantic.colors.json if present. Returns {} for missing/empty.
|
|
382
|
+
* Rethrows on invalid JSON after logging, to protect user data.
|
|
383
|
+
*/
|
|
384
|
+
function readSemanticJSON() {
|
|
385
|
+
if (!fs.existsSync(projectsSemanticColorsJSON)) return {}
|
|
386
|
+
const raw = fs.readFileSync(projectsSemanticColorsJSON, 'utf8')
|
|
387
|
+
if (!raw.trim()) return {}
|
|
388
|
+
try {
|
|
389
|
+
return JSON.parse(raw)
|
|
390
|
+
} catch (err) {
|
|
391
|
+
logger.info(`${chalk.red('Warning:')} ${chalk.yellow('app/assets/semantic.colors.json')} is not valid JSON. Aborting to avoid data loss.`)
|
|
392
|
+
throw err
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Write the semantic-colors JSON (caller provides the fully-merged object).
|
|
398
|
+
* Ensures app/assets/ exists first.
|
|
399
|
+
*/
|
|
400
|
+
function persistSemanticJSON(data) {
|
|
401
|
+
const assetsFolder = projectsSemanticColorsJSON.replace(/\/semantic\.colors\.json$/, '')
|
|
402
|
+
makeSureFolderExists(assetsFolder)
|
|
403
|
+
fs.writeFileSync(projectsSemanticColorsJSON, JSON.stringify(data, null, 2) + '\n', 'utf8')
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Write new entries into semantic.colors.json, replacing any prior keys that
|
|
408
|
+
* belong to the same camelName family (single form + 11 shade form). Unrelated
|
|
409
|
+
* keys survive untouched. Used by both palette and single-mode fresh writes.
|
|
410
|
+
*
|
|
411
|
+
* @param {Object} semanticEntries - new entries to merge in
|
|
412
|
+
* @param {string} camelName - family name (for stripping)
|
|
413
|
+
*/
|
|
414
|
+
export function writeSemanticJSON(semanticEntries, camelName) {
|
|
415
|
+
const existing = readSemanticJSON()
|
|
416
|
+
const cleaned = stripFamilyKeys(existing, camelName)
|
|
417
|
+
const merged = { ...cleaned, ...semanticEntries }
|
|
418
|
+
persistSemanticJSON(merged)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Update one entry in semantic.colors.json in place, preserving the existing
|
|
423
|
+
* key order. Used when `pt semantic --single` is invoked with a name that
|
|
424
|
+
* matches an existing palette shade — the user is editing a shade value, not
|
|
425
|
+
* creating a new top-level color, so we must NOT touch config.cjs (the
|
|
426
|
+
* palette already maps to this key) and we must NOT shift the key to the end.
|
|
427
|
+
*
|
|
428
|
+
* Spread-merge with an existing key updates the value while keeping the
|
|
429
|
+
* original insertion position (V8/spec object key ordering).
|
|
430
|
+
*
|
|
431
|
+
* @param {string} camelKey - the JSON key to update (already camelCase)
|
|
432
|
+
* @param {string|{color:string,alpha:string}} value - new value (bare hex or wrapped form)
|
|
433
|
+
*/
|
|
434
|
+
export function updateSemanticEntry(camelKey, value) {
|
|
435
|
+
const existing = readSemanticJSON()
|
|
436
|
+
const updated = { ...existing, [camelKey]: value }
|
|
437
|
+
persistSemanticJSON(updated)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Write the family-name → configMapping entry into purgetss/config.cjs under
|
|
442
|
+
* theme.extend.colors (or theme.colors when --override is set). Cleans up the
|
|
443
|
+
* opposite branch so the mapping doesn't duplicate.
|
|
444
|
+
*
|
|
445
|
+
* @param {string} kebabName - key in theme.extend.colors / theme.colors
|
|
446
|
+
* @param {Object|string} configMapping - palette object or single string
|
|
447
|
+
* @param {Object} options - CLI options (reads .override and .quotes)
|
|
448
|
+
*/
|
|
449
|
+
export function writeConfigMapping(kebabName, configMapping, options) {
|
|
450
|
+
const configFile = getConfigFile()
|
|
451
|
+
|
|
452
|
+
if (options.override) {
|
|
453
|
+
if (!configFile.theme.colors) configFile.theme.colors = {}
|
|
454
|
+
configFile.theme.colors[kebabName] = configMapping
|
|
455
|
+
|
|
456
|
+
if (configFile.theme.extend.colors) {
|
|
457
|
+
if (configFile.theme.extend.colors[kebabName]) delete configFile.theme.extend.colors[kebabName]
|
|
458
|
+
if (Object.keys(configFile.theme.extend.colors).length === 0) delete configFile.theme.extend.colors
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
if (!configFile.theme.extend.colors) configFile.theme.extend.colors = {}
|
|
462
|
+
configFile.theme.extend.colors[kebabName] = configMapping
|
|
463
|
+
|
|
464
|
+
if (configFile.theme.colors) {
|
|
465
|
+
if (configFile.theme.colors[kebabName]) delete configFile.theme.colors[kebabName]
|
|
466
|
+
if (Object.keys(configFile.theme.colors).length === 0) delete configFile.theme.colors
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
fs.writeFileSync(projectsConfigJS, 'module.exports = ' + cleanDoubleQuotes(configFile, options), 'utf8', err => { throw err })
|
|
471
|
+
checkIfColorModule()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Combined write for palette mode (JSON entries + config mapping in one call).
|
|
476
|
+
* Single mode uses writeSemanticJSON alone — class naming is a design-layer
|
|
477
|
+
* decision that belongs to the user, not auto-derived from the semantic key.
|
|
478
|
+
*
|
|
479
|
+
* @param {Object} semanticEntries - Output of buildSemanticPalette().semanticEntries
|
|
480
|
+
* @param {string} kebabName - Color family name as config.cjs key
|
|
481
|
+
* @param {Object} configMapping - palette's 11-shade mapping
|
|
482
|
+
* @param {Object} options - CLI options (reads .override and .quotes)
|
|
483
|
+
*/
|
|
484
|
+
export function writeSemanticColors(semanticEntries, kebabName, configMapping, options) {
|
|
485
|
+
const camelName = toCamelCase(kebabName)
|
|
486
|
+
writeSemanticJSON(semanticEntries, camelName)
|
|
487
|
+
writeConfigMapping(kebabName, configMapping, options)
|
|
488
|
+
}
|
|
489
|
+
|
|
183
490
|
/**
|
|
184
491
|
* Export for CLI usage
|
|
185
492
|
*/
|
|
186
493
|
export default {
|
|
187
494
|
colorModule,
|
|
188
495
|
checkIfColorModule,
|
|
189
|
-
shades
|
|
496
|
+
shades,
|
|
497
|
+
missingHexMessage,
|
|
498
|
+
toCamelCase,
|
|
499
|
+
buildSemanticPalette,
|
|
500
|
+
buildSingleSemantic,
|
|
501
|
+
detectFamilyShadeConflict,
|
|
502
|
+
updateSemanticEntry,
|
|
503
|
+
writeSemanticJSON,
|
|
504
|
+
writeConfigMapping,
|
|
505
|
+
wrapHexWithAlpha,
|
|
506
|
+
normalizeAlpha,
|
|
507
|
+
stripFamilyKeys,
|
|
508
|
+
writeSemanticColors
|
|
190
509
|
}
|
|
@@ -64,8 +64,10 @@ export function validateProject(silent = false) {
|
|
|
64
64
|
|
|
65
65
|
if (projectType === 'unknown') {
|
|
66
66
|
if (!silent) {
|
|
67
|
-
logger.
|
|
68
|
-
|
|
67
|
+
logger.block(
|
|
68
|
+
'Please make sure you are running purgetss within an Alloy or Classic Project.',
|
|
69
|
+
'For more information, visit https://purgetss.com'
|
|
70
|
+
)
|
|
69
71
|
}
|
|
70
72
|
return false
|
|
71
73
|
}
|