purgetss
Version:
A package that simplifies mobile app creation for Titanium developers.
451 lines (338 loc) • 27.6 kB
Markdown
<p align="center">
<img src="https://codigomovil.mx/images/logotipo-purgetss-gris.svg" height="230" width="230" alt="PurgeCSS logo"/>
</p>
<div align="center">



</div>
**PurgeTSS** is a toolkit for building mobile apps with the [Titanium framework](https://titaniumsdk.com). It gives you utility classes, icon font support, an Animation module, a grid system, and color generation commands (`shades` for tonal palettes, `semantic` for Light/Dark mode semantic colors).
---
- 23,300+ utility classes for styling Titanium views
- Parses XML files to generate a clean `app.tss` with only the classes your project uses
- Customizable defaults via `config.cjs`, with JIT classes for arbitrary values
- `brand` command for Titanium icons and branding assets, focused on the modern Titanium icon pipeline: Android adaptive icons, iOS icon variants, optional Android 12+ splash artwork, and a minimal `default.png` compatibility fallback
- Icon font support: Font Awesome, Material Icons, Material Symbols, Framework7-Icons
- `build-fonts` command generates `fonts.tss` with class definitions and fontFamily selectors
- `shades` command generates color shades from any hex color
- `semantic` command generates Titanium semantic colors (Light/Dark mode) — tonal palettes with mirror inversion, or single purpose-based colors with optional alpha
- Animation module for 2D matrix animations on views or arrays of views
- Grid system for aligning and distributing elements within views
---
## Animation Module (`purgetss.ui.js`)
Install with `purgetss module` (or `purgetss m`). This places `purgetss.ui.js` in your project's `lib` folder.
### Declaring an Animation object
```xml
<Animation id="myAnimation" module="purgetss.ui" class="opacity-0 duration-300 ease-in" />
```
You can use any position, size, color, opacity, or transformation class from `utilities.tss`.
### Available methods
| Method | Description |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `play(views, cb)` / `toggle(views, cb)` | Animate views from current state to the animation state. Toggles `open`/`close` on each call. |
| `open(views, cb)` | Explicitly run the `open` state animation. |
| `close(views, cb)` | Explicitly run the `close` state animation. |
| `apply(views, cb)` | Apply properties instantly without animation. |
| `draggable(views)` | Make a view or array of views draggable inside their parent. |
| `undraggable(views)` | Remove draggable behavior and clean up all listeners. |
| `detectCollisions(views, dragCB, dropCB)` | Enable collision detection on draggable views with hover and drop callbacks. |
| `swap(view1, view2)` | Animate two views exchanging positions. Auto-normalizes position from margins/right/center via `view.rect`. Inherits `duration`, `delay`, `curve`; fallback: 200ms. |
| `sequence(views, cb)` | Animate views one after another. Callback fires after the last view. |
| `shake(view, intensity)` | Bidirectional shake animation for error feedback. Inherits `duration`, `delay`; fallback: 400ms. Default intensity: 10px. |
| `pulse(view, count)` | Scale-up-and-back pulse animation. Scale from Animation object (default 1.2x). Count: number of pulses (default 1). |
| `snapTo(view, targets)` | Snap a view to the nearest target by center distance. Auto-normalizes target position. Inherits `duration`, `delay`, `curve`; fallback: 200ms. |
| `reorder(views, newOrder)` | Animate views to new positions based on index mapping. Auto-normalizes positions. Inherits `duration`, `delay`, `curve`; fallback: 200ms. |
| `transition(views, layouts)` | Multi-view layout transitions using `Matrix2D.translate().rotate().scale()`. Layout properties: `translation`, `rotate`, `scale`, `zIndex`. Compatible with TiDesigner presets. Views without a layout entry fade out; returning views fade in. |
### Callback event object
All callbacks (`play`, `open`, `close`, `apply`) receive an enriched event object:
```js
{
type: String, // event type ('complete' or 'applied')
bubbles: Boolean,
cancelBubble: Boolean,
action: String, // 'play' or 'apply'
state: String, // 'open' or 'close'
id: String, // Animation object ID
targetId: String, // ID of the animated view
index: Number, // position in array (0-based)
total: Number, // total views in array
getTarget: Function // returns the animated view object
}
```
When animating an **array of views**, the callback is called once per view with the corresponding `index` and `total` values.
```js
$.myAnimation.play([$.card1, $.card2, $.card3], (e) => {
console.log(`Animated ${e.index + 1} of ${e.total}`) // "Animated 1 of 3", etc.
if (e.index === e.total - 1) {
console.log('All done!')
}
})
```
### Multi-state animations
Use `open`, `close`, and `complete` modifiers inside `animationProperties` to define different states:
```xml
<Animation id="fadeToggle" module="purgetss.ui" class="duration-300"
animationProperties="{
open: { opacity: 1 },
close: { opacity: 0 }
}"
/>
```
### Draggable views
```js
$.myAnimation.draggable($.myView)
// or with constraints:
$.myAnimation.draggable([$.card1, $.card2])
```
Use `bounds` to restrict movement, and `drag`/`drop` modifiers for drag-state animations. Use `vertical-constraint` or `horizontal-constraint` classes to limit the drag axis.
### Collision detection
After calling `draggable()`, you can enable collision detection to know when a dragged view overlaps another:
```js
$.myAnimation.draggable(views)
$.myAnimation.detectCollisions(views, onHover, onDrop)
```
**`dragCB(source, target)`** is called during drag:
- `target` is the view under the drag center, or `null` when leaving all targets
- Use this to show visual feedback (highlights, borders, scaling)
**`dropCB(source, target)`** is called on drop:
- `target` is the view where the source was released
- If no target is found, the source automatically snaps back to its original position with a 200ms animation
### Swap animation
Animate two views exchanging positions:
```js
$.myAnimation.swap(view1, view2)
```
- Inherits `duration`, `delay`, and `curve` from the Animation object's classes
- Falls back to 200ms duration, 0ms delay, and ease-in-out curve if not set
- Handles iOS transform reset automatically
- Temporarily raises z-index so views animate above siblings
- Updates internal `_originLeft`/`_originTop` for subsequent drag operations
### Sequence animation
Animate views one after another (unlike `play(array)` which runs them in parallel):
```js
$.fadeIn.sequence([$.title, $.subtitle, $.button], () => {
console.log('All views animated')
})
```
- Each view fully completes before the next starts
- Callback fires once after the last view
- Respects `open`/`close` state (set once for the entire sequence)
### Shake animation
Quick horizontal shake for error feedback, using native `autoreverse` + `repeat` for smooth performance:
```js
$.myAnimation.shake($.loginButton) // default intensity: 10px
$.myAnimation.shake($.input, 6) // subtle: 6px
$.myAnimation.shake($.errorLabel, 20) // strong: 20px
```
### Snap to nearest target
After dragging, snap a view to the closest target by center-to-center distance:
```js
const matched = $.myAnimation.snapTo(draggedView, slotViews)
if (matched) {
console.log('Snapped to:', matched.id)
}
```
### Reorder animation
Animate an array of views to new positions based on index mapping:
```js
// Reverse order: view[0] goes to position of view[2], view[2] to position of view[0]
$.myAnimation.reorder(cardViews, [2, 1, 0])
```
- All views animate simultaneously
- Captures positions before animating to avoid conflicts
### Removing draggable behavior
Remove draggable behavior and clean up all event listeners:
```js
$.myAnimation.undraggable(cardViews)
```
### Property inheritance
The `swap`, `reorder`, `snapTo`, and `shake` methods inherit animation properties from the `<Animation>` object's classes. This means you can configure behavior declaratively in XML:
```xml
<Animation id="myAnim" module="purgetss.ui" class="curve-animation-ease-out delay-100 duration-150" />
```
| Property | `play`/`toggle`/`open`/`close`/`sequence` | `swap`/`reorder`/`snapTo` | `shake` |
| ------------- | :---------------------------------------: | :-----------------------: | :-----------------: |
| `duration` | ✅ | ✅ | ✅ (÷6) |
| `delay` | ✅ | ✅ | ✅ |
| `curve` | ✅ | ✅ | fixed `EASE_IN_OUT` |
| `autoreverse` | ✅ | — | fixed `true` |
| `repeat` | ✅ | — | fixed `3` |
Fallback defaults when not set: `swap`/`reorder`/`snapTo` → 200ms; `shake` → 400ms. All animation timing is controlled declaratively via the `<Animation>` object's classes.
- Removes touch and orientation listeners
- Removes views from collision detection registry
- Cleans up internal tracking properties
### Utility classes for animations
| Class pattern | Description |
| --------------------------------------------------- | ----------------------------- |
| `duration-{n}` | Animation duration in ms |
| `delay-{n}` | Delay before animation starts |
| `rotate-{n}` | 2D rotation in degrees |
| `scale-{n}` | Scale factor |
| `repeat-{n}` | Number of repeats |
| `ease-in`, `ease-out`, `ease-linear`, `ease-in-out` | Timing curve |
| `zoom-in-{n}`, `zoom-out-{n}` | Zoom animations |
| `drag-apply`, `drag-animate` | Drag interaction style |
| `vertical-constraint`, `horizontal-constraint` | Constrain drag axis |
### Utility functions
| Function | Description |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `deviceInfo()` | Logs detailed platform and display information to the console. Works in both Alloy and Classic projects. |
| `saveComponent({ source, directory })` | Saves a view snapshot as PNG to the photo gallery. |
See the full documentation at [purgetss.com/docs/animation-module/introduction](https://purgetss.com/docs/animation-module/introduction).
### Appearance management
Switch between Light, Dark, and System modes with automatic persistence:
```js
const { Appearance } = require('purgetss.ui')
// Call once at app startup (e.g., in index.js before opening the first window)
Appearance.init()
```
| Method | Description |
| ----------- | ------------------------------------------------------------ |
| `init()` | Restore the saved mode from `Ti.App.Properties` |
| `get()` | Returns the current mode string |
| `set(mode)` | Apply and persist a mode: `'system'`, `'light'`, or `'dark'` |
| `toggle()` | Switch between `'light'` and `'dark'` |
Use it from any controller to respond to user actions:
```js
const { Appearance } = require('purgetss.ui')
function selectDark() { Appearance.set('dark') }
function selectLight() { Appearance.set('light') }
function selectSystem() { Appearance.set('system') }
```
Requires `semantic.colors.json` in `app/assets/` for views to respond to mode changes. Generate it with the `semantic` command instead of writing it by hand:
```bash
# Tonal palette (11 shades with mirror-by-index Light/Dark inversion)
purgetss semantic '#15803d' amazon
# Purpose-based single colors (auto-mapped to classes in config.cjs)
purgetss semantic --single '#F9FAFB' surfaceColor --dark '#0f172a'
purgetss semantic --single '#111827' textColor --dark '#f1f5f9'
purgetss semantic --single '#000000' overlayColor --alpha 50
```
See the [Semantic Colors guide](https://purgetss.com/docs/best-practices/semantic-colors) for the full workflow.
### Default font families
PurgeTSS generates `font-sans`, `font-serif`, and `font-mono` classes automatically with platform-appropriate values:
| Class | iOS | Android |
| ------------ | ---------------- | ------------ |
| `font-sans` | `Helvetica Neue` | `sans-serif` |
| `font-serif` | `Georgia` | `serif` |
| `font-mono` | `monospace` | `monospace` |
Override or add families in `config.cjs`:
```js
// theme.extend.fontFamily → adds to defaults
extend: {
fontFamily: {
display: 'AlfaSlabOne-Regular',
body: 'BarlowSemiCondensed-Regular'
}
}
// theme.fontFamily → replaces defaults entirely
theme: {
fontFamily: {
sans: 'System',
mono: 'Courier',
display: 'AlfaSlabOne-Regular'
}
}
```
---
## Customizing default components
PurgeTSS sets defaults for three components out of the box:
| Component | Default |
| ----------- | --------------------------------------- |
| `Window` | `backgroundColor: '#FFFFFF'` |
| `View` | `width: Ti.UI.SIZE, height: Ti.UI.SIZE` |
| `ImageView` | `hires: true` (iOS only) |
Override or extend them in `config.cjs` with `theme.extend`:
```js
module.exports = {
theme: {
extend: {
Window: {
apply: 'exit-on-close-false bg-surface'
}
}
}
}
```
If an applied class sets a property that already exists in the defaults (e.g., `bg-surface` vs `backgroundColor: '#FFFFFF'`), the applied value wins. Array-type properties like `extendEdges` and `orientationModes` use bracket notation (`[ ]`) in the generated output.
### Shorthand and explicit forms
Both are equivalent:
```js
// Shorthand
Window: { apply: 'exit-on-close-false' }
// Explicit
Window: { default: { apply: 'exit-on-close-false' } }
```
Use the explicit form when you need platform-specific variants:
```js
Button: {
default: { apply: 'text-xl' },
ios: { apply: 'font-bold' },
android: { apply: 'text-2xl font-semibold', color: 'red' }
}
```
---
### Visit the official documentation site at [purgetss.com](https://purgetss.com) to learn more.
## Requirements
- **Titanium SDK** (Compatible with all versions; 13.1.1.GA recommended for full property support)
- **Alloy Framework** (for most commands)
- **Node.js 20+** (required for the CLI tool)
## Recent changes
### v7.11.0
- **SVG-aware compile-time image pipeline as a post-step of `purgetss`.** When views or controllers reference `image="/images/<sub>/<name>.svg"` (or `backgroundImage="..."`) alongside utility classes that resolve to numeric width/height (`w-32`, `w-(300)`, `h-auto`, …), purge now compiles those SVGs into the 8 Titanium density variants (5 Android + 3 iPhone PNGs) using dimensions resolved from `app.tss` after the regular purge finishes. Titanium loads the generated `.png` automatically at runtime when the XML/Controller references `.svg`. Cache lives at `purgetss/.cache/svg-images.json` (add to `.gitignore`). The SVG attribute stays untouched in your source — never rewritten.
- **`images.files` array in `config.cjs` as per-file override.** Pin width/height for individual files in `purgetss/images/`: `[{ filename: 'images/logos/logo.png', width: 128, height: 52 }, ...]`. When `purgetss images` runs, entries override the source's natural dimensions; CLI `--width` still wins over both. For SVGs detected by the purge SVG pipeline, entries populate automatically (subject to `images.autoSync`). Raster entries you add by hand survive subsequent runs untouched.
- **`images.autoSync` boolean (default `true`).** Opt-out for devs who manage `images.files` by hand — when `false`, purge still computes dimensions and generates PNGs, but never writes back to `config.cjs`.
- **Quality warning when a raster source is too small for its declared width.** Sources smaller than `width × 4` (the xxxhdpi/@4x requirement) trigger a non-blocking warning with exact numbers. SVGs are exempt (vector, no upscale penalty).
### The 4× source convention
Both `purgetss images` and `purgetss brand` treat each source file as the **xxxhdpi/@4x master**: your file's pixel dimensions ARE the largest density, and smaller densities derive at 1/4, 1.5/4, 2/4, 3/4. A 256 px PNG produces a 64 px `@1x/mdpi`. If you want 64 dp at `@1x`, drop a 256 px source.
Override per file via `images.files` in `config.cjs` (or the brand-specific fields under `brand.logos`/`brand.padding`). The convention is the default fallback — entries override it on a per-file basis. CLI flags like `--width` always win over both.
### v7.10.0
- **`MarketplaceArtworkFeature.png` (Google Play Feature Graphic) auto-generated by `purgetss brand`.** Every brand run now writes a 1024×500 banner to the project root alongside the existing `iTunesConnect.png` and `MarketplaceArtwork.png` submission assets. Master logo centered both axes inside the rectangular canvas with 12% vertical padding default, flattened on `brand.colors.background`. Override paths: CLI `--feature-graphic-padding <n>`, config `brand.padding.featureGraphic`, and optional `purgetss/brand/logo-feature.{svg,png}` (or CLI `--feature-logo` / `brand.logos.featureGraphic`) for a dedicated source. Submission artwork only — file goes to the project root for upload to the Play Console, not bundled into the APK.
- **`--opacity <n>`, `--padding <n>`, and `--output <relpath>` flags for `purgetss images`.** Three CLI-only per-asset transformations. `--opacity` multiplies the alpha channel of every generated density (0–100, integer). `--padding` shrinks the rendered image inside each density canvas with symmetric transparent borders (0–40%). `--output` overrides basename + subpath relative to the images output root (single-file source only; no extension, no absolute paths, no `..`). All three preserve the multi-density plural-output convention. Combines naturally for placeholder images: `purgetss images purgetss/brand/logo.svg --opacity 30 --padding 15 --output 'logos/default' --format png`.
- **`brand --padding <n>` shortcut bug fix.** Pre-existing bug: help text and docs described `--padding` as a shortcut that "sets BOTH Android paddings to the same value", but only `androidAdaptivePadding` read the shortcut from the fallback chain — `androidLegacyPadding` skipped it. `purgetss brand --padding 17` actually produced `androidAdaptive=17, androidLegacy=10` silently. Fix adds the missing fallback so `--padding` now sets both Android paddings to the same value as documented.
- **`purgetss images --format png` now writes truecolor RGBA, not palette-quantized PNG-8.** Passing `quality` to Sharp's `.png()` was silently triggering palette mode, banding subtle gradients in logos, placeholders, and any image with smooth color transitions. Drop of one parameter (`quality` is meaningless for PNG anyway — only `compressionLevel` matters, and that stays at 9 lossless). Brand outputs (icons, splash, marketplace) were never affected because their generators don't pass `quality` to `.png()`.
### v7.8.0
- **`--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.
- **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)`), square brackets (`top-[10px]` → `top-(10px)`), empty parentheses (`wh-()`), whitespace inside parentheses (`wh-( 200 )`), and redundant `px` unit (`top-(10px)` → `top-(10)`).
- All offenders are reported in a single run, so you can fix them in one pass.
- 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.
- **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.
### v7.7.0
- `brand` now uses grouped config sections: `brand.logos`, `brand.padding`, `brand.android`, `brand.ios`, and `brand.colors`.
- `brand` supports separate Android artwork through `brand.logos.androidLauncher` / `--icon-logo` and `brand.logos.androidSplash` / `--splash-logo`.
- `brand` regenerates `app/assets/android/default.png` in Alloy projects, or `Resources/android/default.png` in Classic projects, so older Android splash paths still have a fallback.
- `cleanup-legacy` no longer removes `default.png`.
- Branding help and docs now spell out the difference between Android launcher icons, Android 12+ `splash_icon.png`, and legacy Android splash assets.
- `--notes` is explicit that `tiapp.xml` is not auto-edited and that apps with an existing custom Android theme should merge splash snippets into that theme instead of replacing it.
- `brand` does not try to manage older Android splash theme assets such as `background.png` or `background.9.png`; if a project still depends on them, that remains manual by design.
See the full release notes in [CHANGELOG.md](./CHANGELOG.md).
## Table of Content
- [Installation](https://purgetss.com/docs/installation)
- [Commands](https://purgetss.com/docs/commands)
- App Assets
- [App icons and branding](https://purgetss.com/docs/app-assets/app-icons-and-branding)
- [Multi-density images](https://purgetss.com/docs/app-assets/multi-density-images)
- Customization
- [The Config File](https://purgetss.com/docs/customization/the-config-file)
- [Custom Rules](https://purgetss.com/docs/customization/custom-rules)
- [The `apply` Directive](https://purgetss.com/docs/customization/the-apply-directive)
- [The `opacity` Modifier](https://purgetss.com/docs/customization/the-opacity-modifier)
- [Arbitrary Values](https://purgetss.com/docs/customization/arbitrary-values)
- [Platform and Device Modifiers](https://purgetss.com/docs/customization/platform-and-device-modifiers)
- [Icon Fonts Libraries](https://purgetss.com/docs/customization/icon-fonts-libraries)
- The UI Module
- [Introduction](https://purgetss.com/docs/purgetss-ui/introduction)
- [The `play` Method](https://purgetss.com/docs/purgetss-ui/the-play-method)
- [The `apply` Method](https://purgetss.com/docs/purgetss-ui/the-apply-method)
- [The `open` and `close` Methods](https://purgetss.com/docs/purgetss-ui/the-open-and-close-methods)
- [The `draggable` Method](https://purgetss.com/docs/purgetss-ui/the-draggable-method)
- [Complex UI Elements](https://purgetss.com/docs/purgetss-ui/complex-ui-elements)
- [Additional Methods](https://purgetss.com/docs/purgetss-ui/additional-methods)
- [Available Utilities](https://purgetss.com/docs/purgetss-ui/available-utilities)
- [Implementation Rules](https://purgetss.com/docs/purgetss-ui/implementation-rules)
- [Appearance](https://purgetss.com/docs/purgetss-ui/appearance)
- Best Practices
- [Appearance Setup](https://purgetss.com/docs/best-practices/appearance-setup)
- [Semantic Colors](https://purgetss.com/docs/best-practices/semantic-colors)
- [Large Titles on iOS](https://purgetss.com/docs/best-practices/large-titles-on-ios)
- [Grid System](https://purgetss.com/docs/grid-system)