purgetss 7.5.2 → 7.5.3

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
@@ -240,6 +240,67 @@ Fallback defaults when not set: `swap`/`reorder`/`snapTo` → 200ms; `shake` →
240
240
 
241
241
  See the full documentation at [purgetss.com/docs/animation-module/introduction](https://purgetss.com/docs/animation-module/introduction).
242
242
 
243
+ ### Appearance management
244
+
245
+ Switch between Light, Dark, and System modes with automatic persistence:
246
+
247
+ ```js
248
+ const { Appearance } = require('purgetss.ui')
249
+
250
+ // Call once at app startup (e.g., in index.js before opening the first window)
251
+ Appearance.init()
252
+ ```
253
+
254
+ | Method | Description |
255
+ | ----------- | ------------------------------------------------------------ |
256
+ | `init()` | Restore the saved mode from `Ti.App.Properties` |
257
+ | `get()` | Returns the current mode string |
258
+ | `set(mode)` | Apply and persist a mode: `'system'`, `'light'`, or `'dark'` |
259
+ | `toggle()` | Switch between `'light'` and `'dark'` |
260
+
261
+ Use it from any controller to respond to user actions:
262
+
263
+ ```js
264
+ const { Appearance } = require('purgetss.ui')
265
+
266
+ function selectDark() { Appearance.set('dark') }
267
+ function selectLight() { Appearance.set('light') }
268
+ function selectSystem() { Appearance.set('system') }
269
+ ```
270
+
271
+ Requires `semantic.colors.json` in `app/assets/` for views to respond to mode changes. See the [Titanium docs on semantic colors](https://titaniumsdk.com/guide/Titanium_SDK/Titanium_SDK_How-tos/User_Interface_Deep_Dives/iOS_Dark_Mode.html) for the file format.
272
+
273
+ ### Default font families
274
+
275
+ PurgeTSS generates `font-sans`, `font-serif`, and `font-mono` classes automatically with platform-appropriate values:
276
+
277
+ | Class | iOS | Android |
278
+ | ------------ | ------------------ | ------------- |
279
+ | `font-sans` | `Helvetica Neue` | `sans-serif` |
280
+ | `font-serif` | `Georgia` | `serif` |
281
+ | `font-mono` | `monospace` | `monospace` |
282
+
283
+ Override or add families in `config.cjs`:
284
+
285
+ ```js
286
+ // theme.extend.fontFamily → adds to defaults
287
+ extend: {
288
+ fontFamily: {
289
+ display: 'AlfaSlabOne-Regular',
290
+ body: 'BarlowSemiCondensed-Regular'
291
+ }
292
+ }
293
+
294
+ // theme.fontFamily → replaces defaults entirely
295
+ theme: {
296
+ fontFamily: {
297
+ sans: 'System',
298
+ mono: 'Courier',
299
+ display: 'AlfaSlabOne-Regular'
300
+ }
301
+ }
302
+ ```
303
+
243
304
  ---
244
305
 
245
306
  ## Customizing default components
@@ -1,4 +1,4 @@
1
- // PurgeTSS v7.5.2
1
+ // PurgeTSS v7.5.3
2
2
  // Created by César Estrada
3
3
  // https://purgetss.com
4
4
 
@@ -906,3 +906,45 @@ function saveComponent({ source, directory = Ti.Filesystem.tempDirectory }) {
906
906
  exports.saveComponent = saveComponent
907
907
 
908
908
  exports.createAnimation = (args) => new Animation(args)
909
+
910
+ // --- Appearance Management (Light/Dark/System) ---
911
+ function Appearance() {
912
+ const PROP_KEY = 'userInterfaceStyle'
913
+ const STYLES = {
914
+ dark: Ti.UI.USER_INTERFACE_STYLE_DARK,
915
+ light: Ti.UI.USER_INTERFACE_STYLE_LIGHT,
916
+ system: Ti.UI.USER_INTERFACE_STYLE_UNSPECIFIED
917
+ }
918
+
919
+ let currentMode = 'system'
920
+
921
+ function applyMode(mode) {
922
+ currentMode = mode
923
+ Ti.UI.overrideUserInterfaceStyle = STYLES[mode]
924
+ Ti.App.Properties.setInt(PROP_KEY, STYLES[mode])
925
+ }
926
+
927
+ return {
928
+ init() {
929
+ const saved = Ti.App.Properties.getInt(PROP_KEY, STYLES.system)
930
+ currentMode = Object.keys(STYLES).find(key => STYLES[key] === saved) || 'system'
931
+ Ti.UI.overrideUserInterfaceStyle = saved
932
+ },
933
+
934
+ set(mode) {
935
+ if (!STYLES.hasOwnProperty(mode)) return
936
+ applyMode(mode)
937
+ },
938
+
939
+ get() {
940
+ return currentMode
941
+ },
942
+
943
+ toggle() {
944
+ const next = (currentMode === 'dark') ? 'light' : 'dark'
945
+ applyMode(next)
946
+ }
947
+ }
948
+ }
949
+
950
+ exports.Appearance = Appearance()
@@ -2250,9 +2250,6 @@
2250
2250
  '.rounded-full-2.5': { width: 10, height: 10, borderRadius: 5 }
2251
2251
  '.rounded-full-3.5': { width: 14, height: 14, borderRadius: 7 }
2252
2252
 
2253
- // Property(ies): fontFamily
2254
- // Component(s): Ti.UI.ActivityIndicator, Ti.UI.Button, Ti.UI.Label, Ti.UI.ListItem, Ti.UI.Picker, Ti.UI.PickerColumn, Ti.UI.PickerRow, Ti.UI.ProgressBar, Ti.UI.Switch, Ti.UI.TableViewRow, Ti.UI.TextArea, Ti.UI.TextField
2255
-
2256
2253
  // Property(ies): fontSize
2257
2254
  // Component(s): Ti.UI.ActivityIndicator, Ti.UI.Button, Ti.UI.Label, Ti.UI.ListItem, Ti.UI.Picker, Ti.UI.PickerColumn, Ti.UI.PickerRow, Ti.UI.ProgressBar, Ti.UI.Switch, Ti.UI.TableViewRow, Ti.UI.TextArea, Ti.UI.TextField
2258
2255
  '.text-xs': { font: { fontSize: 12 } }
@@ -2281,6 +2278,14 @@
2281
2278
  '.font-extrabold': { font: { fontWeight: 'bold' } }
2282
2279
  '.font-black': { font: { fontWeight: 'bold' } }
2283
2280
 
2281
+ // Property(ies): fontFamily
2282
+ // Component(s): Ti.UI.ActivityIndicator, Ti.UI.Button, Ti.UI.Label, Ti.UI.ListItem, Ti.UI.Picker, Ti.UI.PickerColumn, Ti.UI.PickerRow, Ti.UI.ProgressBar, Ti.UI.Switch, Ti.UI.TableViewRow, Ti.UI.TextArea, Ti.UI.TextField
2283
+ '.font-mono': { font: { fontFamily: 'monospace' } }
2284
+ '.font-sans[platform=ios]': { font: { fontFamily: 'Helvetica Neue' } }
2285
+ '.font-serif[platform=ios]': { font: { fontFamily: 'Georgia' } }
2286
+ '.font-sans[platform=android]': { font: { fontFamily: 'sans-serif' } }
2287
+ '.font-serif[platform=android]': { font: { fontFamily: 'serif' } }
2288
+
2284
2289
  // Property(ies): top, right, bottom, left - Gap for Grid System
2285
2290
  // Component(s): Ti.UI.ActivityIndicator, Ti.UI.Animation, Ti.UI.View, Ti.UI.Window
2286
2291
  '.gap-0': { top: 0, right: 0, bottom: 0, left: 0 }
@@ -247,9 +247,9 @@ function processCompoundClasses({ ..._base }) {
247
247
  // ! Configurables
248
248
  compoundClasses += generateGlossary('borderRadius-alternative', helpers.borderRadius(_base.borderRadius))
249
249
  compoundClasses += generateGlossary('borderRadius-full', helpers.borderRadiusFull(_base.borderRadius))
250
- compoundClasses += generateGlossary('fontFamily', helpers.fontFamily(_base.fontFamily))
251
250
  compoundClasses += generateGlossary('fontSize', helpers.fontSize(_base.fontSize))
252
251
  compoundClasses += generateGlossary('fontWeight', helpers.fontWeight(_base.fontWeight))
252
+ compoundClasses += generateGlossary('fontFamily', helpers.fontFamily(_base.fontFamily))
253
253
  compoundClasses += generateGlossary('margin-alternative', helpers.gap(_base.margin))
254
254
  compoundClasses += generateGlossary('minimumFontSize', helpers.minimumFontSize(_base.fontSize))
255
255
  compoundClasses += generateGlossary('padding-alternative', helpers.padding(_base.padding))
@@ -905,3 +905,45 @@ function saveComponent({ source, directory = Ti.Filesystem.tempDirectory }) {
905
905
  exports.saveComponent = saveComponent
906
906
 
907
907
  exports.createAnimation = (args) => new Animation(args)
908
+
909
+ // --- Appearance Management (Light/Dark/System) ---
910
+ function Appearance() {
911
+ const PROP_KEY = 'userInterfaceStyle'
912
+ const STYLES = {
913
+ dark: Ti.UI.USER_INTERFACE_STYLE_DARK,
914
+ light: Ti.UI.USER_INTERFACE_STYLE_LIGHT,
915
+ system: Ti.UI.USER_INTERFACE_STYLE_UNSPECIFIED
916
+ }
917
+
918
+ let currentMode = 'system'
919
+
920
+ function applyMode(mode) {
921
+ currentMode = mode
922
+ Ti.UI.overrideUserInterfaceStyle = STYLES[mode]
923
+ Ti.App.Properties.setInt(PROP_KEY, STYLES[mode])
924
+ }
925
+
926
+ return {
927
+ init() {
928
+ const saved = Ti.App.Properties.getInt(PROP_KEY, STYLES.system)
929
+ currentMode = Object.keys(STYLES).find(key => STYLES[key] === saved) || 'system'
930
+ Ti.UI.overrideUserInterfaceStyle = saved
931
+ },
932
+
933
+ set(mode) {
934
+ if (!STYLES.hasOwnProperty(mode)) return
935
+ applyMode(mode)
936
+ },
937
+
938
+ get() {
939
+ return currentMode
940
+ },
941
+
942
+ toggle() {
943
+ const next = (currentMode === 'dark') ? 'light' : 'dark'
944
+ applyMode(next)
945
+ }
946
+ }
947
+ }
948
+
949
+ exports.Appearance = Appearance()
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "purgetss",
4
- "version": "7.5.2",
4
+ "version": "7.5.3",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "purgetss": "bin/purgetss"
@@ -191,7 +191,15 @@ function validateXML(xmlText, filePath) {
191
191
  function findSuspectLine(lines) {
192
192
  for (let i = 0; i < lines.length; i++) {
193
193
  const trimmed = lines[i].trim()
194
- if (!trimmed || trimmed.startsWith('<!--')) continue
194
+ if (!trimmed) continue
195
+
196
+ // "--" inside XML comments (illegal in XML spec)
197
+ if (trimmed.includes('<!--')) {
198
+ const commentBody = trimmed.replace(/<!--/, '').replace(/-->.*$/, '')
199
+ if (/--/.test(commentBody)) return i + 1
200
+ }
201
+
202
+ if (trimmed.startsWith('<!--')) continue
195
203
 
196
204
  // Opening tag without tag name: "< class=..."
197
205
  if (/^<\s+\w+=/.test(trimmed)) return i + 1
@@ -235,8 +243,26 @@ function preValidateXML(xmlText, filePath) {
235
243
  const line = lines[i]
236
244
  const trimmed = line.trim()
237
245
 
238
- // Skip empty lines and comments
239
- if (!trimmed || trimmed.startsWith('<!--') || trimmed.startsWith('<Alloy')) {
246
+ // Skip empty lines and Alloy root tag
247
+ if (!trimmed || trimmed.startsWith('<Alloy')) {
248
+ continue
249
+ }
250
+
251
+ // Check for "--" inside XML comments (illegal in XML spec)
252
+ // e.g. <!-- Section: --modules Option --> is invalid because of the "--" before "modules"
253
+ if (trimmed.includes('<!--')) {
254
+ const commentBody = trimmed.replace(/<!--/, '').replace(/-->.*$/, '')
255
+ if (/--/.test(commentBody)) {
256
+ const dashMatch = commentBody.match(/--(\S*)/)
257
+ const offender = dashMatch ? `--${dashMatch[1]}` : '--'
258
+ throwPreValidationError({
259
+ relativePath,
260
+ lineNumber: i + 1,
261
+ lineContent: trimmed,
262
+ message: `XML comment contains illegal "--" sequence ("${offender}")`,
263
+ fix: `Replace "--" with "—" (em-dash) or reword the comment to avoid double dashes`
264
+ })
265
+ }
240
266
  continue
241
267
  }
242
268
 
@@ -383,7 +383,7 @@ export function combineAllValues(base, defaultTheme) {
383
383
  allValues.contentWidth = combineKeys(configFile.theme, base.width, 'contentWidth')
384
384
  allValues.countDownDuration = combineKeys(configFile.theme, base.spacing, 'countDownDuration')
385
385
  allValues.elevation = combineKeys(configFile.theme, base.spacing, 'elevation')
386
- allValues.fontFamily = combineKeys(configFile.theme, defaultTheme.fontFamily, 'fontFamily')
386
+ allValues.fontFamily = combineKeys(configFile.theme, {}, 'fontFamily')
387
387
  allValues.fontSize = combineKeys(configFile.theme, base.fontSize, 'fontSize')
388
388
  allValues.fontWeight = combineKeys(configFile.theme, defaultTheme.fontWeight, 'fontWeight')
389
389
  allValues.gap = combineKeys(configFile.theme, base.spacing, 'gap')
@@ -26,18 +26,53 @@ function removeFractions(modifiersAndValues, extras = []) {
26
26
 
27
27
  /**
28
28
  * Font family property for text components
29
+ *
30
+ * Built-in platform defaults:
31
+ * - font-sans → Android: 'sans-serif', iOS: 'Helvetica Neue'
32
+ * - font-serif → Android: 'serif', iOS: 'Georgia'
33
+ * - font-mono → 'monospace' (both platforms)
34
+ *
35
+ * User values from config.cjs override defaults cross-platform.
36
+ *
29
37
  * @param {Object} modifiersAndValues - Modifier and value pairs
30
38
  * @returns {string} Generated styles
31
39
  */
32
40
  export function fontFamily(modifiersAndValues) {
41
+ const platformDefaults = {
42
+ sans: { ios: 'Helvetica Neue', android: 'sans-serif' },
43
+ serif: { ios: 'Georgia', android: 'serif' }
44
+ }
45
+
46
+ const crossPlatformDefaults = { mono: 'monospace' }
47
+
48
+ const defaults = { ...modifiersAndValues }
49
+ const ios = {}
50
+ const android = {}
51
+
52
+ _.each(crossPlatformDefaults, (value, key) => {
53
+ if (!(key in defaults)) {
54
+ defaults[key] = value
55
+ }
56
+ })
57
+
58
+ _.each(platformDefaults, (platforms, key) => {
59
+ if (!(key in defaults)) {
60
+ ios[key] = platforms.ios
61
+ android[key] = platforms.android
62
+ }
63
+ })
64
+
65
+ const selectorsAndValues = {}
66
+ if (!_.isEmpty(defaults)) selectorsAndValues.default = defaults
67
+ if (!_.isEmpty(ios)) selectorsAndValues.ios = ios
68
+ if (!_.isEmpty(android)) selectorsAndValues.android = android
69
+
33
70
  return processProperties({
34
71
  prop: 'fontFamily',
35
72
  modules: 'Ti.UI.ActivityIndicator, Ti.UI.Button, Ti.UI.Label, Ti.UI.ListItem, Ti.UI.Picker, Ti.UI.PickerColumn, Ti.UI.PickerRow, Ti.UI.ProgressBar, Ti.UI.Switch, Ti.UI.TableViewRow, Ti.UI.TextArea, Ti.UI.TextField'
36
73
  }, {
37
74
  font: '{ font: { fontFamily: {value} } }'
38
- }, {
39
- default: modifiersAndValues
40
- })
75
+ }, selectorsAndValues)
41
76
  }
42
77
 
43
78
  /**