purgetss 7.5.1 → 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.1
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.1",
4
+ "version": "7.5.3",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "purgetss": "bin/purgetss"
@@ -144,25 +144,30 @@ function validateXML(xmlText, filePath) {
144
144
  const columnMatch = error.message.match(/Column:\s*(\d+)/)
145
145
  const charMatch = error.message.match(/Char:\s*(.+)/)
146
146
 
147
+ const lines = xmlText.split('\n')
148
+ const parserLine = lineMatch ? parseInt(lineMatch[1]) : null
149
+
150
+ // Try to find the real source of the error by scanning for suspicious tags.
151
+ // xml2json often reports errors far from the actual problem (e.g. at EOF)
152
+ // because it only notices the mismatch when nesting fails to close.
153
+ const suspectLine = findSuspectLine(lines)
154
+
155
+ const reportLine = suspectLine || parserLine
156
+
147
157
  let errorMessage = chalk.red(`\n::PurgeTSS:: XML Syntax Error\n`) +
148
158
  chalk.yellow(`File: "${filePath}"\n`)
149
159
 
150
- if (lineMatch || columnMatch) {
151
- const lineNum = lineMatch ? parseInt(lineMatch[1]) : '?'
152
- const colNum = columnMatch ? parseInt(columnMatch[1]) : '?'
153
- const badChar = charMatch ? charMatch[1] : '?'
154
-
155
- errorMessage += chalk.yellow(`Error near line: ${lineNum}\n\n`)
160
+ if (reportLine) {
161
+ errorMessage += chalk.yellow(`Error near line: ${reportLine}\n\n`)
156
162
 
157
163
  // Extract and show context: line before, error line, and line after
158
- const lines = xmlText.split('\n')
159
- const startLine = Math.max(0, lineNum - 2)
160
- const endLine = Math.min(lines.length, lineNum + 2)
164
+ const startLine = Math.max(0, reportLine - 2)
165
+ const endLine = Math.min(lines.length, reportLine + 2)
161
166
 
162
167
  errorMessage += chalk.gray('Context:\n')
163
168
  for (let i = startLine; i < endLine; i++) {
164
169
  const lineNumDisplay = i + 1
165
- const isTargetLine = (lineNumDisplay === lineNum)
170
+ const isTargetLine = (lineNumDisplay === reportLine)
166
171
  const prefix = isTargetLine ? chalk.red('>>>') : chalk.gray(' ')
167
172
  const lineContent = lines[i] || ''
168
173
 
@@ -181,54 +186,215 @@ function validateXML(xmlText, filePath) {
181
186
  }
182
187
  }
183
188
 
189
+ // Scan XML lines for common malformations that xml2json can't pinpoint.
190
+ // Returns the 1-based line number of the first suspect, or null.
191
+ function findSuspectLine(lines) {
192
+ for (let i = 0; i < lines.length; i++) {
193
+ const trimmed = lines[i].trim()
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
203
+
204
+ // Opening tag without tag name: "< class=..."
205
+ if (/^<\s+\w+=/.test(trimmed)) return i + 1
206
+
207
+ // Double slash in closing tag: "<//View>"
208
+ if (/^<\/\/\w+>/.test(trimmed)) return i + 1
209
+
210
+ // Closing tag with extra characters: "</View/>" or "</View >>" etc.
211
+ if (/^<\/[a-zA-Z0-9_]+[^>]+>/.test(trimmed) && !/^<\/[a-zA-Z0-9_:]+\s*>/.test(trimmed)) return i + 1
212
+
213
+ // Opening < immediately followed by non-alpha, non-slash, non-! (e.g. "< >" or "<=>")
214
+ if (/^<[^a-zA-Z/!?\s]/.test(trimmed)) return i + 1
215
+
216
+ // Space between < and tag name: "< View"
217
+ if (/^<\s+[A-Z]/.test(trimmed)) return i + 1
218
+
219
+ // Backslash instead of forward slash: \>
220
+ if (/\\>/.test(trimmed)) return i + 1
221
+
222
+ // Double closing bracket: >>
223
+ if (/>>/.test(trimmed)) return i + 1
224
+
225
+ // Unclosed tag: starts with < but doesn't end with > and next line starts a new tag
226
+ if (trimmed.startsWith('<') && !trimmed.startsWith('</') && !trimmed.endsWith('>')) {
227
+ const nextTrimmed = (lines[i + 1] || '').trim()
228
+ if (nextTrimmed.startsWith('<')) return i + 1
229
+ }
230
+ }
231
+ return null
232
+ }
233
+
184
234
  /**
185
235
  * Pre-validate XML for common Alloy malformations
186
236
  * Returns Error if problem found, null if OK
187
237
  */
188
238
  function preValidateXML(xmlText, filePath) {
189
239
  const lines = xmlText.split('\n')
240
+ const relativePath = filePath.replace(process.cwd() + '/', '')
190
241
 
191
- // Check for tags without opening < (common mistake: Label, View, etc. without <)
192
242
  for (let i = 0; i < lines.length; i++) {
193
243
  const line = lines[i]
194
244
  const trimmed = line.trim()
195
245
 
196
- // Skip empty lines, comments, and closing tags
197
- if (!trimmed || trimmed.startsWith('<!--') || trimmed.startsWith('</') || trimmed.startsWith('<Alloy') || trimmed.startsWith('</')) {
246
+ // Skip empty lines and Alloy root tag
247
+ if (!trimmed || trimmed.startsWith('<Alloy')) {
198
248
  continue
199
249
  }
200
250
 
201
- // Check for line starting with uppercase letter followed by space and common Alloy attributes
202
- // Pattern: "Label id=", "View class=","Button onClick=", etc. WITHOUT opening <
203
- const tagWithoutOpening = trimmed.match(/^[A-Z][a-zA-Z0-9_]+\s+(id|class|onClick|onOpen|onClose|height|width|backgroundColor|color|font|text|hintText|imageUrl)=/)
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
+ }
266
+ continue
267
+ }
268
+
269
+ // Check for opening tag without tag name: "< class=..." or "< id=..."
270
+ if (/^<\s+(class|id|onClick|onOpen|onClose|height|width|backgroundColor|color|font|text|hintText|imageUrl)=/.test(trimmed)) {
271
+ throwPreValidationError({
272
+ relativePath,
273
+ lineNumber: i + 1,
274
+ lineContent: trimmed,
275
+ message: 'Opening tag is missing its tag name',
276
+ fix: `Add the tag name after "<", e.g. "<View ${trimmed.slice(1).trim()}"`
277
+ })
278
+ }
279
+
280
+ // Check for double slash in closing tags: "<//View>"
281
+ const doubleSlash = trimmed.match(/^<\/\/([a-zA-Z0-9_]+)>/)
282
+ if (doubleSlash) {
283
+ throwPreValidationError({
284
+ relativePath,
285
+ lineNumber: i + 1,
286
+ lineContent: trimmed,
287
+ message: `Closing tag "</${doubleSlash[1]}>" has an extra "/"`,
288
+ fix: `Change "${trimmed}" to "</${doubleSlash[1]}>"`
289
+ })
290
+ }
204
291
 
205
- if (tagWithoutOpening) {
292
+ // Check for tags without opening < (common mistake: Label, View, etc. without <)
293
+ // Pattern: "Label id=", "View class=","Button onClick=", etc. WITHOUT opening <
294
+ if (/^[A-Z][a-zA-Z0-9_]+\s+(id|class|onClick|onOpen|onClose|height|width|backgroundColor|color|font|text|hintText|imageUrl)=/.test(trimmed)) {
206
295
  const tagName = trimmed.split(/\s+|[>=]/)[0]
207
- const relativePath = filePath.replace(process.cwd() + '/', '')
208
-
209
- // Create a custom error with details for the caller to handle
210
- const error = new Error(`XML Syntax Error in ${relativePath}:${i + 1}`)
211
- error.isPreValidationError = true
212
- error.filePath = relativePath
213
- error.lineNumber = i + 1
214
- error.lineContent = line.trim()
215
- error.tagName = tagName
216
-
217
- // Print error using logger (still throw, but caller can catch and handle)
218
- logger.error('XML Syntax Error')
219
- logger.warn(`File: "${relativePath}"`)
220
- logger.warn(`Line: ${i + 1}`)
221
- logger.warn(`Content: "${line.trim()}"`)
222
- logger.warn(`Error: Tag "<${tagName}>" is missing opening "<"`)
223
- logger.warn(`Fix: Change "${tagName}" to "<${tagName}>"`)
296
+ throwPreValidationError({
297
+ relativePath,
298
+ lineNumber: i + 1,
299
+ lineContent: trimmed,
300
+ message: `Tag "<${tagName}>" is missing opening "<"`,
301
+ fix: `Change "${tagName}" to "<${tagName}>"`
302
+ })
303
+ }
224
304
 
225
- throw error
305
+ // Check for closing tag with extra slash: "</View/>"
306
+ const closingExtraSlash = trimmed.match(/^<\/([a-zA-Z0-9_]+)\/>/)
307
+ if (closingExtraSlash) {
308
+ throwPreValidationError({
309
+ relativePath,
310
+ lineNumber: i + 1,
311
+ lineContent: trimmed,
312
+ message: `Closing tag "</${closingExtraSlash[1]}>" has an extra "/" at the end`,
313
+ fix: `Change "${trimmed}" to "</${closingExtraSlash[1]}>"`
314
+ })
315
+ }
316
+
317
+ // Check for space between < and tag name: "< View class=..."
318
+ const spaceBeforeName = trimmed.match(/^<\s+([A-Z][a-zA-Z0-9_]+)\s/)
319
+ if (spaceBeforeName) {
320
+ throwPreValidationError({
321
+ relativePath,
322
+ lineNumber: i + 1,
323
+ lineContent: trimmed,
324
+ message: `Extra space between "<" and tag name "${spaceBeforeName[1]}"`,
325
+ fix: `Change "< ${spaceBeforeName[1]}" to "<${spaceBeforeName[1]}"`
326
+ })
327
+ }
328
+
329
+ // Check for attribute without = sign: <View class"foo">
330
+ const attrNoEquals = trimmed.match(/^<([a-zA-Z0-9_]+)\s+[a-zA-Z]+"/)
331
+ if (attrNoEquals && !trimmed.includes('=')) {
332
+ throwPreValidationError({
333
+ relativePath,
334
+ lineNumber: i + 1,
335
+ lineContent: trimmed,
336
+ message: `Attribute is missing "=" sign`,
337
+ fix: `Check attributes in this tag — each one needs an "=" before its value`
338
+ })
339
+ }
340
+
341
+ // Check for backslash in self-closing tag: <Label text="hi" \>
342
+ if (/\\>/.test(trimmed)) {
343
+ throwPreValidationError({
344
+ relativePath,
345
+ lineNumber: i + 1,
346
+ lineContent: trimmed,
347
+ message: `Tag has a backslash "\\>" instead of forward slash "/>"`,
348
+ fix: `Change "\\>" to "/>"`
349
+ })
350
+ }
351
+
352
+ // Check for double closing bracket: <View class="foo">>
353
+ if (/>>/.test(trimmed) && !trimmed.includes('<!--')) {
354
+ throwPreValidationError({
355
+ relativePath,
356
+ lineNumber: i + 1,
357
+ lineContent: trimmed,
358
+ message: `Tag has a double ">>" closing bracket`,
359
+ fix: `Remove the extra ">"`
360
+ })
361
+ }
362
+
363
+ // Check for unclosed opening tag (line ends without > and next line starts a new tag)
364
+ if (trimmed.startsWith('<') && !trimmed.startsWith('</') && !trimmed.startsWith('<!--') && !trimmed.endsWith('>') && !trimmed.endsWith('-->')) {
365
+ const nextTrimmed = (lines[i + 1] || '').trim()
366
+ if (nextTrimmed.startsWith('<')) {
367
+ throwPreValidationError({
368
+ relativePath,
369
+ lineNumber: i + 1,
370
+ lineContent: trimmed,
371
+ message: `Tag is missing its closing ">"`,
372
+ fix: `Add ">" at the end of this tag`
373
+ })
374
+ }
226
375
  }
227
376
  }
228
377
 
229
378
  return false
230
379
  }
231
380
 
381
+ function throwPreValidationError({ relativePath, lineNumber, lineContent, message, fix }) {
382
+ const error = new Error(`XML Syntax Error in ${relativePath}:${lineNumber}`)
383
+ error.isPreValidationError = true
384
+ error.filePath = relativePath
385
+ error.lineNumber = lineNumber
386
+ error.lineContent = lineContent
387
+
388
+ logger.error('XML Syntax Error\n')
389
+ logger.info(`File: "${relativePath}"`)
390
+ logger.info(`Line: ${lineNumber}`)
391
+ logger.info(`Content: "${lineContent}"\n`)
392
+ logger.error(message)
393
+ logger.info(chalk.green(`Fix: ${fix}\n`))
394
+
395
+ throw error
396
+ }
397
+
232
398
  /**
233
399
  * Extract classes from file content
234
400
  * COPIED exactly from original extractClasses() function
@@ -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
  /**