@maccesar/titools 5.0.0 → 5.1.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.
Files changed (41) hide show
  1. package/README.md +26 -3
  2. package/package.json +1 -1
  3. package/skills/purgetss/SKILL.md +27 -24
  4. package/skills/purgetss/references/EXAMPLES.md +52 -41
  5. package/skills/purgetss/references/adopting-purgetss.md +137 -0
  6. package/skills/purgetss/references/animation-advanced.md +18 -5
  7. package/skills/purgetss/references/animation-system.md +21 -9
  8. package/skills/purgetss/references/app-branding.md +36 -97
  9. package/skills/purgetss/references/appearance-module.md +70 -5
  10. package/skills/purgetss/references/apply-directive.md +14 -6
  11. package/skills/purgetss/references/arbitrary-values.md +59 -23
  12. package/skills/purgetss/references/brand-padding-geometry.md +105 -0
  13. package/skills/purgetss/references/class-categories.md +684 -272
  14. package/skills/purgetss/references/class-index-properties.md +713 -607
  15. package/skills/purgetss/references/class-index.md +149 -84
  16. package/skills/purgetss/references/classic-projects.md +3 -3
  17. package/skills/purgetss/references/cli-commands.md +27 -213
  18. package/skills/purgetss/references/color-commands.md +218 -0
  19. package/skills/purgetss/references/configurable-properties.md +17 -2
  20. package/skills/purgetss/references/custom-fonts.md +21 -16
  21. package/skills/purgetss/references/custom-rules.md +7 -7
  22. package/skills/purgetss/references/customization-deep-dive.md +14 -11
  23. package/skills/purgetss/references/dynamic-component-creation.md +104 -61
  24. package/skills/purgetss/references/grid-layout.md +21 -24
  25. package/skills/purgetss/references/icon-fonts.md +37 -20
  26. package/skills/purgetss/references/installation-setup.md +1 -1
  27. package/skills/purgetss/references/ios-large-titles.md +12 -11
  28. package/skills/purgetss/references/migration-guide.md +101 -10
  29. package/skills/purgetss/references/multi-density-images.md +44 -24
  30. package/skills/purgetss/references/opacity-modifier.md +1 -1
  31. package/skills/purgetss/references/performance-tips.md +5 -4
  32. package/skills/purgetss/references/platform-modifiers.md +2 -2
  33. package/skills/purgetss/references/purgetss-ui-classic.md +21 -5
  34. package/skills/purgetss/references/semantic-colors.md +20 -13
  35. package/skills/purgetss/references/smart-mappings.md +8 -7
  36. package/skills/purgetss/references/svg-pipeline.md +21 -13
  37. package/skills/purgetss/references/tikit-components.md +2 -2
  38. package/skills/purgetss/references/titanium-resets.md +27 -17
  39. package/skills/purgetss/references/ui-ux-design.md +38 -26
  40. package/skills/purgetss/references/values-and-units.md +1 -2
  41. package/skills/purgetss/references/version-history.md +32 -4
@@ -65,7 +65,7 @@ app/assets/
65
65
  ├── android/images/
66
66
  │ ├── res-mdpi/
67
67
  │ │ ├── my-hero-illustration.png
68
- │ │ └── buttons/primary.svg
68
+ │ │ └── buttons/primary.png
69
69
  │ ├── res-hdpi/…
70
70
  │ ├── res-xhdpi/…
71
71
  │ ├── res-xxhdpi/…
@@ -75,12 +75,12 @@ app/assets/
75
75
  ├── my-hero-illustration@2x.png
76
76
  ├── my-hero-illustration@3x.png
77
77
  └── buttons/
78
- ├── primary.svg (@1x)
79
- ├── primary@2x.svg
80
- └── primary@3x.svg
78
+ ├── primary.png (@1x)
79
+ ├── primary@2x.png
80
+ └── primary@3x.png
81
81
  ```
82
82
 
83
- Classic projects output to `Resources/android/images/res-*/` and `Resources/iphone/images/` — the command auto-detects the layout.
83
+ SVG sources are written as `.png` (Sharp cannot write SVG) unless `--format` or `images.format` picks another format. Classic projects output to `Resources/android/images/res-*/` and `Resources/iphone/images/` — the command auto-detects the layout.
84
84
 
85
85
  ## The `purgetss/images/` convention
86
86
 
@@ -119,7 +119,11 @@ That means:
119
119
  | 200×200 | Button, inline icon |
120
120
  | 96×96 | Small inline icon |
121
121
 
122
- If your source is smaller than 4×, the tool still runs but the larger density outputs are essentially upscaled — quality drops on high-DPI devices.
122
+ For example, a 1024×512 `logos/hero.webp` produces eight files: `res-xxxhdpi` 1024×512 (the master, untouched), `res-xxhdpi` and `@3x` 768×384, `res-xhdpi` and `@2x` 512×256, `res-hdpi` 384×192, and `res-mdpi` and `@1x` 256×128. The intended `@1x` display size is the source ÷ 4.
123
+
124
+ In this default mode every density is the source or a reduction of it, so nothing is upscaled: a source that is too small simply yields a smaller `@1x` than you wanted. Upscaling only happens when a width is pinned (`--width` or `images.files`); then, for raster sources, the run warns `source is Xpx wide but xxxhdpi needs Ypx` and the output may look blurry. SVGs are exempt from that warning.
125
+
126
+ Every generated file is capped at **4096 px per side**. A source (or pinned width) that would exceed it aborts the run with `… would render W×Hpx, which exceeds the 4096px cap`. In practice the largest usable raster master is 4096 px wide, and the largest usable `--width` is 1024.
123
127
 
124
128
  Recommended sizes for common UI elements (in source pixels, assumed 4×):
125
129
 
@@ -130,7 +134,7 @@ Recommended sizes for common UI elements (in source pixels, assumed 4×):
130
134
 
131
135
  ## Pinning the output width with `--width`
132
136
 
133
- The 4× master convention works well for raster sources (`.png`, `.jpg`, `.webp`) because the file's pixel dimensions usually reflect the intended 4× size. **SVGs are different.** Their logical size comes from the `viewBox`, and vector editors (Affinity Designer, Illustrator, Figma exports) frequently emit viewBoxes in points or with disproportionate values — a logo can ship with `viewBox="0 0 29559 13542"` and `purgetss images` would happily scale every density from that base, producing files far too large for any UI surface.
137
+ The 4× master convention works well for raster sources (`.png`, `.jpg`, `.webp`) because the file's pixel dimensions usually reflect the intended 4× size. **SVGs are different.** Their logical size comes from the `viewBox`, and vector editors (Affinity Designer, Illustrator, Figma exports) frequently emit viewBoxes in points or with disproportionate values — a logo can ship with `viewBox="0 0 29559 13542"`, and `purgetss images` would scale every density from that base: even `res-mdpi` would be 7390×3386 px, so the run aborts on the 4096 px cap.
134
138
 
135
139
  `--width <n>` (added in PurgeTSS v7.8.0) is the escape hatch: it pins the **`mdpi` / `@1x`** output width to exactly `n` pixels, then derives every other density from that base. Height stays proportional to the source aspect ratio — you only specify width.
136
140
 
@@ -165,20 +169,18 @@ purgetss images logo.svg --width abc
165
169
  # Invalid --width 'NaN'. Must be an integer between 1 and 8192.
166
170
  ```
167
171
 
168
- The upper bound of `8192` exists because `--width 8192` already produces a `xxxhdpi` output of 32 768 px — that's Sharp's render ceiling and well beyond anything a Titanium UI needs.
172
+ Passing validation does not guarantee a run: every output is capped at 4096 px per side, and `xxxhdpi` is 4× the pinned width, so any `--width` above `1024` aborts with `… exceeds the 4096px cap`.
169
173
 
170
174
  ### The hint message for unflagged SVGs
171
175
 
172
- Whenever you run `purgetss images` against an SVG **without** `--width`, PurgeTSS prints a one-time hint:
176
+ Whenever you run `purgetss images` against an SVG that has neither `--width` nor an entry in `images.files`, PurgeTSS prints a one-time hint:
173
177
 
174
178
  ```text
175
- ⚠ SVG source detected without --width. Output sizes will be derived from
176
- each SVG's viewBox (treated as a 4× master).
177
- For SVGs from vector editors with disproportionate viewBoxes, pass
178
- --width <n> (e.g. --width 256) to pin the @1x/mdpi width.
179
+ ⚠ SVG source detected without --width and no entry in config.cjs > images.files. Output sizes will be derived from each SVG's viewBox (treated as a 4× master).
180
+ For SVGs from vector editors with disproportionate viewBoxes, pass --width <n> (e.g. --width 256) or add an entry to images.files to pin the @1x/mdpi width.
179
181
  ```
180
182
 
181
- This is a hint, **not an error**. The legacy 4×-from-viewBox behavior still runs in the same invocation. If your SVG has a sensible viewBox (`300×150` for a 300px-wide logo at 1×, etc.), the default is fine. If the viewBox is in points or noticeably larger than expected, re-run with `--width <n>` for predictable scaling.
183
+ This is a hint, **not an error**. The legacy 4×-from-viewBox behavior still runs in the same invocation. If your SVG has a sensible viewBox (`1200×600` for a logo meant to display 300 px wide at @1x, since the viewBox is read as the 4× size), the default is fine. If the viewBox is in points or noticeably larger than expected, re-run with `--width <n>` for predictable scaling.
182
184
 
183
185
  ### Why CLI-only (no `images:` config equivalent)
184
186
 
@@ -186,7 +188,7 @@ This is a hint, **not an error**. The legacy 4×-from-viewBox behavior still run
186
188
 
187
189
  ## The `images:` config section
188
190
 
189
- On the first run, `purgetss images` injects an `images:` block into your existing `purgetss/config.cjs` (between `brand:` and `theme:`) with these defaults:
191
+ When `purgetss images` runs without a positional source and an existing `purgetss/config.cjs` has no `images:` section, it injects this block before `theme:` (after `brand:` when present). A run with a positional source never touches the config:
190
192
 
191
193
  ```javascript
192
194
  // Sources in purgetss/images/ are 4x masters: a 1024px file yields
@@ -259,6 +261,8 @@ Precedence is the same for every file processed:
259
261
  2. Otherwise, the matching entry in `images.files` (if any).
260
262
  3. Otherwise, the source's natural size: `viewBox` for SVG, intrinsic pixel size for rasters (treated as 4× master).
261
263
 
264
+ `purgetss images` only applies an entry that has a numeric `width` (plus `height` when present). An entry with only `height`, which the SVG pipeline writes for `h-*`-only references, is not used by this command.
265
+
262
266
  | Source | In `files`? | Width source | Output format |
263
267
  | --- | --- | --- | --- |
264
268
  | `logos/logo.svg` | yes | 256 px @1x (from `files`) | `png` (forced; see below) |
@@ -273,7 +277,7 @@ Raster entries you add by hand survive subsequent runs untouched. For SVGs detec
273
277
 
274
278
  ## Output layouts
275
279
 
276
- **Alloy layout** (auto-detected when `app/assets/` exists):
280
+ **Alloy layout** (auto-detected when `app/views/` exists):
277
281
 
278
282
  ```text
279
283
  <project>/
@@ -290,7 +294,7 @@ Raster entries you add by hand survive subsequent runs untouched. For SVGs detec
290
294
  └── <name>@3x.<ext>
291
295
  ```
292
296
 
293
- **Classic layout** (auto-detected otherwise):
297
+ **Classic layout** (auto-detected when there is no `app/views/` but a `Resources/` folder exists; with neither, the command stops with `Could not detect a Titanium Alloy or Classic project`):
294
298
 
295
299
  ```text
296
300
  <project>/
@@ -375,7 +379,7 @@ Useful when:
375
379
  - You're iterating on an iOS-only screen and don't need to regenerate Android assets every time.
376
380
  - You want to tune JPEG quality differently for the two platforms (run the command twice with different flags).
377
381
 
378
- The two flags are mutually exclusive. Passing neither means “use the project's enabled targets,” not “always generate both.”
382
+ The two flags are mutually exclusive. Passing neither means “use the project's enabled targets,” not “always generate both.” If `tiapp.xml` enables neither platform, the command stops with `No Android or iOS deployment target is enabled in tiapp.xml.`
379
383
 
380
384
  ## Format conversion
381
385
 
@@ -386,7 +390,7 @@ purgetss images --format webp # convert every output to WebP
386
390
  purgetss images --format jpeg --quality 90
387
391
  ```
388
392
 
389
- Valid targets: `webp`, `jpeg`, `png`, `avif`, `gif`, `tiff`.
393
+ Valid targets: `webp`, `jpeg`, `png`, `avif`, `gif`, `tiff` (`jpg` is also accepted).
390
394
 
391
395
  ### Why WebP?
392
396
 
@@ -425,7 +429,7 @@ purgetss images logo.svg --opacity 50 --format png
425
429
 
426
430
  ### Format interactions
427
431
 
428
- - **PNG / WebP / AVIF:** alpha is preserved.
432
+ - **PNG / WebP / AVIF / TIFF / GIF:** alpha is preserved.
429
433
  - **JPEG:** JPEG has no alpha. The existing flatten-on-white step composites the semi-transparent image onto white before writing, so `--opacity 50 --format jpeg` produces a 50%-faded version **on white**, not a transparent JPEG.
430
434
 
431
435
  ## Adding breathing room with `--padding` (v7.10.0)
@@ -458,6 +462,12 @@ purgetss images purgetss/brand/logo.svg --output logos/default-image
458
462
 
459
463
  That writes `images/logos/default-image.png` (or `.jpg` / `.webp` depending on `--format`) across all densities, regardless of the source's location.
460
464
 
465
+ ### Constraints
466
+
467
+ - No extension: pass the basename only. Any extension in `--output` is stripped; the extension comes from `--format` if set, otherwise from the source, or `.png` when the source is SVG.
468
+ - Relative path only: absolute paths and paths containing `..` are rejected, so the output stays inside the platform `images/` folders.
469
+ - Single source only: `--output` with a directory source aborts, because one basename cannot apply to multiple files.
470
+
461
471
  ### Combining `--opacity`, `--padding`, and `--output`
462
472
 
463
473
  The three flags compose for the canonical "transparent placeholder with padding under a custom path" use case:
@@ -470,7 +480,11 @@ purgetss images purgetss/brand/logo.svg \
470
480
  --format png
471
481
  ```
472
482
 
473
- Produces a 30% opacity, 15%-padded version of the brand logo at `images/logos/default-image.png` for every Android density and iPhone scale — a one-command way to ship a default ImageView placeholder.
483
+ Produces a 30% opacity, 15%-padded version of the brand logo at `images/logos/default-image.png` for every Android density and iPhone scale — a one-command way to ship a default ImageView placeholder. Reference it from the view:
484
+
485
+ ```xml
486
+ <ImageView defaultImage="/images/logos/default-image.png" image="{remoteUrl}" />
487
+ ```
474
488
 
475
489
  ## Full pipeline alongside `build`
476
490
 
@@ -490,6 +504,8 @@ ti build -p android -T emulator
490
504
 
491
505
  If you only tweaked CSS classes (no image changes), you don't need to re-run `purgetss images`. It's safe to skip.
492
506
 
507
+ Classic projects have no PurgeTSS build step: generate the images, then run `ti build` as usual — the files under `Resources/` are ordinary Titanium resources.
508
+
493
509
  ## Cleaning up
494
510
 
495
511
  `purgetss images` never deletes files. It only creates them. If you remove a source, generated copies under `app/assets/` (Alloy) or `Resources/` (Classic) stay in place. Remove them manually or with git when you clean up.
@@ -515,7 +531,7 @@ If you only tweaked CSS classes (no image changes), you don't need to re-run `pu
515
531
  | --- | --- |
516
532
  | `--format <ext>` | Convert all outputs to: `webp`, `jpeg`, `png`, `avif`, `gif`, `tiff`. Default: keep source format. |
517
533
  | `--quality <n>` | Quality `0–100` for `webp`, `jpeg`, `avif` and `tiff`. PNG and GIF ignore it. Default `85`. |
518
- | `--width <n>` | (v7.8.0) Pin `mdpi` / `@1x` output width to `n` pixels; `[1, 8192]`. Other densities derive from this base (×1.5 / ×2 / ×3 / ×4). Most useful for SVG sources with non-standard viewBoxes. CLI-only — no `config.cjs` equivalent because width is per-asset. |
534
+ | `--width <n>` | (v7.8.0) Pin `mdpi` / `@1x` output width to `n` pixels; validated to `[1, 8192]`, but outputs are capped at 4096 px so values above `1024` abort. Other densities derive from this base (×1.5 / ×2 / ×3 / ×4). Most useful for SVG sources with non-standard viewBoxes. CLI-only — no `config.cjs` equivalent because width is per-asset. |
519
535
  | `--opacity <n>` | (v7.10.0) Multiply the alpha channel of every generated density by `n/100`. Range `[0, 100]`. Combine with `--format jpeg` and the alpha is flattened on white instead of producing a transparent JPEG. CLI-only. |
520
536
  | `--padding <n>` | (v7.10.0) Shrink the rendered image inside each density canvas by `n%` symmetric borders. Range `[0, 40]`. CLI-only. |
521
537
  | `--output <relpath>` | (v7.10.0) Override the basename and subpath relative to each platform's `images/` root. Lets a source from outside `purgetss/images/` (e.g. `purgetss/brand/`) write into a custom output folder like `images/logos/`. CLI-only. |
@@ -565,6 +581,10 @@ purgetss images --format webp # supports alpha
565
581
  purgetss images --format png # keeps alpha
566
582
  ```
567
583
 
584
+ ### My `<ImageView image="/.../foo.svg" />` shows nothing
585
+
586
+ Titanium falls back from a `.svg` reference to a `.png` sibling only. Either the `.png` was never generated, or a `.webp` (or other non-png) of the same basename sits next to it and breaks the fallback. See [SVG pipeline → Troubleshooting](./svg-pipeline.md#troubleshooting).
587
+
568
588
  ### My subdirectories aren't preserved in the output
569
589
 
570
590
  Verify your source path is inside `purgetss/images/`. When passing sources from outside the convention (e.g. `./docs/screenshots`), the directory of the source file is used as the root, so a file at `./docs/screenshots/hero.png` outputs to `images/hero.png` (flat), not `images/screenshots/hero.png`.
@@ -587,8 +607,8 @@ Shows every file that would be written, no side effects.
587
607
 
588
608
  ## Community-Discovered Patterns
589
609
 
590
- - **4× master convention predates PurgeTSS.** Treating the source as a 4× master (`xxxhdpi` on Android, `@4x` equivalent on iOS) is the same convention Titanium Alloy has always used internally when resolving density-qualified image paths. `purgetss images` formalizes the workflow but doesn't invent a new rule — any Alloy project already ships with the same assumption.
591
- - **Short-path fallback order matters for monorepos.** When you pass `buttons/primary.png`, the command tries `purgetss/images/buttons/primary.png` first, then `./buttons/primary.png` relative to cwd. In a monorepo where a design folder might shadow the convention, the convention wins. To force the cwd-relative interpretation, pass `./buttons/primary.png` with the explicit `./` prefix or an absolute path.
610
+ - **4× master convention predates PurgeTSS.** Treating the source as a 4× master (`xxxhdpi` on Android, `@4x` equivalent on iOS) is described in the PurgeTSS source as a convention inherited from Titanium Alloy. `purgetss images` formalizes the workflow rather than inventing a new rule.
611
+ - **Short-path fallback order matters for monorepos.** When you pass `buttons/primary.png`, the command tries `purgetss/images/buttons/primary.png` first, then `./buttons/primary.png` relative to the project root (`--project`, default cwd). In a monorepo where a design folder might shadow the convention, the convention wins. A `./` prefix does not change that — `./buttons/primary.png` is still looked up in `purgetss/images/` first. To force the other file, pass an absolute path.
592
612
 
593
613
  ## See also
594
614
 
@@ -70,4 +70,4 @@ See [Semantic colors — Opacity modifier auto-derivation](./semantic-colors.md#
70
70
  The following note reflects community observations about how the opacity modifier interacts with gradient utilities.
71
71
 
72
72
  > **Gradients**
73
- > The same modifier logic applies to color-based gradient utilities such as `from-*` and `to-*`. When you define custom `backgroundGradient.colors` arrays of `{ color, offset }` objects in `config.cjs`, PurgeTSS v7.4.0 correctly serializes those nested objects in `utilities.tss`.
73
+ > The same modifier logic applies to color-based gradient utilities such as `from-*` and `to-*`.
@@ -187,13 +187,14 @@ $.nameLabel.applyProperties({
187
187
  ### Dynamic Class Changes
188
188
 
189
189
  ```javascript
190
- // Change styles dynamically using classes array
191
- $.statusLabel.applyProperties({
192
- classes: isActive ? ['text-green-500'] : ['text-red-500'],
190
+ // Swap PurgeTSS classes on an existing view; the third argument sets inline properties
191
+ $.resetClass($.statusLabel, isActive ? 'text-green-500' : 'text-red-500', {
193
192
  text: isActive ? L('active') : L('inactive')
194
193
  })
195
194
  ```
196
195
 
196
+ `applyProperties({ classes })` does not restyle a view: Alloy resolves classes only through `$.UI.create()`, `$.createStyle()`, and `$.addClass()` / `$.removeClass()` / `$.resetClass()`. Alloy recommends enabling `autoStyle` on views whose classes change this way. An equivalent one-off form is `$.statusLabel.applyProperties($.createStyle({ apiName: 'Label', classes: ['text-green-500'] }))`.
197
+
197
198
  ## Debouncing and Throttling
198
199
 
199
200
  ### Debounce (Wait for User to Stop)
@@ -257,7 +258,7 @@ exports.throttle = function(fn, limit = 100) {
257
258
  | **Bridge** | Cached `Ti.Platform` properties |
258
259
  | **Bridge** | Using `applyProperties` for batch updates |
259
260
  | **Bridge** | PurgeTSS classes instead of inline styles |
260
- | **Memory** | All global listeners cleaned up in `$.cleanup` |
261
+ | **Memory** | All global listeners removed when the window closes (e.g. a `cleanup` function — project convention, not Alloy API); `$.destroy()` frees data bindings |
261
262
  | **Memory** | Heavy objects nulled in cleanup |
262
263
  | **Memory** | Images resized appropriately |
263
264
  | **Animation** | Using PurgeTSS Animation component |
@@ -9,7 +9,7 @@ Platform and device modifiers (also called variants or prefixes) apply styles co
9
9
  - `tablet:`
10
10
  - `handheld:`
11
11
 
12
- You can set different background colors and font sizes per platform and device, and combine them with arbitrary values. Examples: `ios:bg-(#53606b)`, `ios:text-(20px)`, `android:bg-(#8fb63e)`, and `android:text-(24px)`.
12
+ You can set different background colors and font sizes per platform and device, and combine them with arbitrary values. Examples: `ios:bg-(#53606b)`, `ios:text-(20)`, `android:bg-(#8fb63e)`, and `android:text-(24)`.
13
13
 
14
14
  ```xml
15
15
  <Alloy>
@@ -48,4 +48,4 @@ You can set different background colors and font sizes per platform and device,
48
48
  The following guidance comes from community experience combining platform modifiers with platform-specific Titanium constants.
49
49
 
50
50
  > **Platform-Specific Properties**
51
- > If a style relies on `Ti.UI.iOS.*` or `Ti.UI.Android.*` constants, scope it with the matching `ios:` or `android:` modifier, or with the equivalent platform block in `config.cjs`.
51
+ > If a style relies on `Ti.UI.iOS.*` or `Ti.UI.Android.*` constants, scope it with the matching `ios:` or `android:` modifier, or with the equivalent platform block in `config.cjs`. Built-in classes that use those constants already ship scoped in `utilities.tss` (for example `'.clip-enabled[platform=ios]'` and `'.large-title-display-mode-never[platform=ios]'`), so this matters most for custom rules and arbitrary values.
@@ -40,7 +40,7 @@ const {
40
40
  } = require('lib/purgetss.ui')
41
41
  ```
42
42
 
43
- Use `createAnimation(args)` for ordinary Classic code. It returns a zero-size, touch-disabled `Ti.UI.View` decorated with animation methods. Treat this view as a behavior object; do not add it to the window.
43
+ Use `createAnimation(args)` for ordinary Classic code. It returns a zero-size, touch-disabled `Ti.UI.View` decorated with animation methods. Treat this view as a behavior object; it does not need to be added to the window.
44
44
 
45
45
  ## Public exports
46
46
 
@@ -125,7 +125,9 @@ window.open()
125
125
 
126
126
  This is a newly constructed object, not the original Titanium event. For array playback, the configured base delay accumulates for successive views. `sequence()` instead waits for each native completion and invokes its callback once after the last view. Passing an empty array to `sequence()` produces no callback.
127
127
 
128
- `animationProperties.complete` starts after the active `play`, `toggle`, `open`, or `close` base phase, and is applied immediately after `apply`. The public play callback does not wait for the second top-level `complete` animation.
128
+ `animationProperties.complete` starts after the active `play`, `toggle`, `open`, or `close` base phase and after each view of a `sequence()`, and is applied immediately after `apply`. The public play callback does not wait for the second top-level `complete` animation.
129
+
130
+ `play` and `toggle` do nothing while the object's internal `playing` flag is set. The flag turns on when a native animation starts and off at the first completion event, so with an array it clears when the first view finishes. `open`, `close`, `apply`, and `sequence` do not check it.
129
131
 
130
132
  ## Native animation configuration
131
133
 
@@ -148,12 +150,23 @@ const motion = createAnimation({
148
150
  })
149
151
  ```
150
152
 
151
- The constructor removes `id` from the native animation object. It converts top-level `scale`, `rotate`, and `anchorPoint` into one `Ti.UI.Matrix2D`; it performs the same conversion separately for `animationProperties.open` and `.close`. Helpers such as `pulse`, `shake`, `swap`, `snapTo`, `reorder`, and `transition` can replace or reset that transform.
153
+ The constructor removes `id` from the native animation object. Add `debug: true` to log every method call and internal step with `console.warn('::ti.animation:: …')`. It converts top-level `scale`, `rotate`, and `anchorPoint` into one `Ti.UI.Matrix2D`; it performs the same conversion separately for `animationProperties.open` and `.close`. Helpers such as `pulse`, `shake`, `swap`, `snapTo`, `reorder`, and `transition` can replace or reset that transform.
152
154
 
153
155
  Most helpers spread the base object before their method-specific assignments, so later helper fields win. There is no shared `200ms` timing fallback for position/layout helpers. Only `shake()` computes `(duration ?? 400) / 6`, rounded to the nearest millisecond; `pulse()` supplies a default scale of `1.2`. Both force `ANIMATION_CURVE_EASE_IN_OUT`.
154
156
 
155
157
  Titanium exposes `anchorPoint` differently across platforms. Test pivot-dependent animation on both platforms; in Classic, set the view's iOS anchor point before animation and pass an Android animation anchor point only inside a platform guard.
156
158
 
159
+ ```javascript
160
+ const pivot = { x: 0, y: 0.5 }
161
+ const properties = { duration: 220, rotate: 12 }
162
+ const isIOS = ['iphone', 'ipad'].includes(Ti.Platform.osname)
163
+
164
+ if (isIOS) card.anchorPoint = pivot
165
+ if (Ti.Platform.osname === 'android') properties.anchorPoint = pivot
166
+
167
+ const pivotMotion = createAnimation(properties)
168
+ ```
169
+
157
170
  ## States and direct children
158
171
 
159
172
  ```javascript
@@ -237,9 +250,10 @@ Runtime precedence and behavior:
237
250
  - Every resolved drag/drop property is forwarded, including size, transform, and anchor-point fields.
238
251
  - `snap.center` calls `snapTo()` after a valid drop; `snap.back` returns a missed drop to its origin.
239
252
  - There is no `snap.magnet` behavior.
240
- - Collision detection tests the dragged view's center against registered targets. The last non-null hover target is a fallback at release.
253
+ - Collision detection tests the dragged view's center against registered targets, and only for views registered with `detectCollisions()`: include the dragged view in that list, or drop callbacks and snapping never run.
254
+ - Hit testing during movement runs only when a hover callback was supplied; then the last non-null hover target is a fallback at release. The hover callback also receives `(source, null)` whenever a registered view is released.
241
255
 
242
- Call drag/collision setup after the views are attached and laid out. `draggable(array)` assigns each view `zIndex` from its array index before registration. `keepZIndex` only prevents later touch-start promotion; register views individually if their existing z-index values must not be overwritten.
256
+ Call drag/collision setup after the views are attached and laid out, for example from the window's `open` or `postlayout` event. `draggable(array)` assigns each view `zIndex` from its array index before registration. `keepZIndex` only prevents later touch-start promotion; register views individually if their existing z-index values must not be overwritten.
243
257
 
244
258
  ## Drag lifecycle and cleanup
245
259
 
@@ -301,6 +315,8 @@ A view without a matching layout fades to opacity `0`, receives `zIndex: 0`, and
301
315
 
302
316
  On Mac Catalyst, use a fixed-size parent rather than `Ti.UI.FILL` for rotated layouts; a resizable parent can distort the matrix.
303
317
 
318
+ Animated `zIndex` is documented differently across Titanium platforms. Use a composite layout where stacking order matters and verify the result on both targets.
319
+
304
320
  ## Appearance in Classic
305
321
 
306
322
  Put semantic colors in `Resources/semantic.colors.json`, use their names directly in native color properties, and call `Appearance.init()` before opening the first window. Classic does not need utility classes, TSS, `purgetss/config.cjs`, or Alloy runtime files. Run a full native build after adding or changing semantic keys; LiveView alone does not rebuild the native catalog.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Semantic colors let your app respond to Light / Dark mode changes without any extra runtime code. You define color **names** once, give each name a `light` and `dark` hex value, and Titanium resolves the right one at render time based on `Ti.UI.overrideUserInterfaceStyle`. PurgeTSS's contribution is the `theme.extend.colors` mapping in `config.cjs` that turns those names into utility classes like `bg-surface` or `text-on-surface`.
4
4
 
5
- This file covers the full workflow: the JSON schema, the `config.cjs` mapping, nesting rules (and the `[object Object]` trap), the numeric 11-step tonal-inversion pattern, alpha transparency, the `purgetss semantic` CLI, and three patterns for consuming semantic colors from controllers at runtime.
5
+ This file covers the full workflow: the JSON schema, the `config.cjs` mapping, nesting rules (and the missing-`DEFAULT` trap), the numeric 11-step tonal-inversion pattern, alpha transparency, the `purgetss semantic` CLI, and three patterns for consuming semantic colors from controllers at runtime.
6
6
 
7
7
  For the mode-switching runtime that drives all of this (`Appearance.init()`, `Appearance.set(...)`, persistence), see [appearance-module.md](./appearance-module.md).
8
8
 
@@ -57,6 +57,10 @@ Each top-level key is a color name; each value is an object with `light` and `da
57
57
  "light": "#6B7280",
58
58
  "dark": "#94a3b8"
59
59
  },
60
+ "textMutedColor": {
61
+ "light": "#9CA3AF",
62
+ "dark": "#64748b"
63
+ },
60
64
  "borderColor": {
61
65
  "light": "#E5E7EB",
62
66
  "dark": "#334155"
@@ -68,11 +72,11 @@ Each top-level key is a color name; each value is an object with `light` and `da
68
72
  }
69
73
  ```
70
74
 
71
- This is a reasonable 6-color starter palette: background, elevated surfaces, primary text, secondary text, borders, and an accent. Most apps can ship with just these and extend later.
75
+ This is a reasonable 7-color starter palette: background, elevated surfaces, primary text, secondary text, muted text, borders, and an accent. Most apps can ship with just these and extend later.
72
76
 
73
77
  ### Alpha transparency — the 8-digit format
74
78
 
75
- Titanium accepts the `#RRGGBBAA` 8-digit hex format anywhere a color is expected. Use it in either mode to produce translucent surfaces like overlays or glass sheets:
79
+ In `semantic.colors.json`, the official docs use the 8-digit `#RRGGBBAA` hex format for transparency. Use it in either mode to produce translucent surfaces like overlays or glass sheets:
76
80
 
77
81
  ```json
78
82
  {
@@ -87,7 +91,7 @@ Titanium accepts the `#RRGGBBAA` 8-digit hex format anywhere a color is expected
87
91
  }
88
92
  ```
89
93
 
90
- The last two hex digits are the alpha channel: `00` is fully transparent, `FF` is fully opaque, `80` is ~50%, `B3` is ~70%, `CC` is ~80%.
94
+ The last two hex digits are the alpha channel: `00` is fully transparent, `FF` is fully opaque, `80` is ~50%, `B3` is ~70%, `CC` is ~80%. This order applies to the JSON file only: the hex colors PurgeTSS writes into TSS put alpha first (`#AARRGGBB`), as in `'.bg-sky-500/50': { backgroundColor: '#800ea5e9' }`, and its own warning suggests `bg-(#AARRGGBB)` for arbitrary values.
91
95
 
92
96
  ## Registering in `config.cjs`
93
97
 
@@ -105,6 +109,7 @@ module.exports = {
105
109
  },
106
110
  'on-surface': 'textColor',
107
111
  'on-surface-variant': 'textSecondaryColor',
112
+ muted: 'textMutedColor',
108
113
  border: 'borderColor',
109
114
  accent: 'accentColor'
110
115
  }
@@ -113,11 +118,11 @@ module.exports = {
113
118
  }
114
119
  ```
115
120
 
116
- This generates `bg-surface`, `bg-surface-high`, `text-on-surface`, `text-on-surface-variant`, `bg-border`, `text-accent`, `bg-accent`, etc. Any utility that takes a color — `bg-*`, `text-*`, `border-*`, `placeholder-*`, tint classes — can reference the name.
121
+ This generates `bg-surface`, `bg-surface-high`, `text-on-surface`, `text-on-surface-variant`, `text-muted`, `bg-border`, `text-accent`, `bg-accent`, etc. Any utility that takes a color — `bg-*`, `text-*`, `border-*`, `placeholder-*`, tint classes — can reference the name.
117
122
 
118
123
  ### Nested pattern with `DEFAULT`
119
124
 
120
- One level of nesting is supported, and it must include a `DEFAULT` key for the base variant:
125
+ Each key of a nested object becomes a class suffix. The `DEFAULT` key (lowercase `default` works too) produces the base class with no suffix:
121
126
 
122
127
  ```js
123
128
  // Correct -- generates bg-surface and bg-surface-high
@@ -127,21 +132,21 @@ surface: {
127
132
  }
128
133
  ```
129
134
 
130
- `bg-surface` resolves to `surfaceColor` (the `DEFAULT`), and `bg-surface-high` resolves to `surfaceHighColor`.
135
+ `bg-surface` resolves to `surfaceColor` (the `DEFAULT`), and `bg-surface-high` resolves to `surfaceHighColor`. Since v7.10.0, deeper nesting flattens the same way: `brand: { primary: { 500: 'brandColor' } }` generates `bg-brand-primary-500`.
131
136
 
132
137
  > **DANGER**
133
138
  >
134
139
  > Common error: nested objects without `DEFAULT`
135
140
  >
136
141
  > ```js
137
- > // Wrong -- generates [object Object] instead of a color
142
+ > // Wrong -- generates bg-surface-regular and bg-surface-high, but no bg-surface
138
143
  > surface: {
139
144
  > regular: 'surfaceColor',
140
145
  > high: 'surfaceHighColor'
141
146
  > }
142
147
  > ```
143
148
  >
144
- > If you nest without a `DEFAULT` key and then use the base class (`bg-surface`), PurgeTSS serializes the whole nested object with `String(...)`, producing the literal string `[object Object]` as the color value. Titanium can't parse that, and the view renders with whatever fallback the platform has. Always include `DEFAULT` for the base variant, or use a flat structure.
149
+ > Without a `DEFAULT` key there is no base class. A view that uses `bg-surface` gets no rule for it in `app.tss`, so the class has no effect. The official docs describe the result as `[object Object]`; PurgeTSS 7.17.1 emits no rule at all. Always include `DEFAULT` for the base variant, or use a flat structure.
145
150
 
146
151
  ### Flat structure alternative
147
152
 
@@ -163,6 +168,8 @@ Both approaches work. The nested form groups related shades under one namespace;
163
168
 
164
169
  Instead of purpose-based names, you can model a color as an 11-step tonal scale (`50` through `950`) where each light-mode value **inverts** in dark mode. This gives you a full tonal range from a single palette and keeps dark-mode contrast in lockstep with light mode.
165
170
 
171
+ The example below uses a neutral gray palette for clarity, but the same inversion pattern works with any hue — blues, greens, warm tones, or a custom brand palette — as long as the 11 stops share a consistent tonal progression.
172
+
166
173
  `app/assets/semantic.colors.json`
167
174
  ```json
168
175
  {
@@ -240,7 +247,7 @@ In Alloy, this:
240
247
  1. Generates `amazon50` through `amazon950` using the same algorithm as the `shades` command.
241
248
  2. Writes `semantic.colors.json` under `app/assets/` with mirror-by-index values — 50 ↔ 950, 100 ↔ 900, …, 500 as the identical anchor.
242
249
  3. Writes the `{ 50: 'amazon50', 100: 'amazon100', ... }` mapping into `config.cjs`.
243
- 4. Strips any prior keys for the `amazon` family before writing — re-runs cleanly replace, never duplicate.
250
+ 4. Strips any prior keys for the `amazon` family before writing, including a bare `amazon` key and the 11 shade keys — re-runs cleanly replace, never duplicate.
244
251
 
245
252
  In Classic, the same command writes only the native `Resources/semantic.colors.json` entries. It does not create `purgetss/`, `config.cjs`, utility mappings, TSS, `app/`, or an Alloy hook. Use keys such as `amazon50` directly in Titanium color properties.
246
253
 
@@ -361,7 +368,7 @@ Re-runs are idempotent: existing derived keys are reused, never duplicated. If y
361
368
  >
362
369
  > `semantic.colors.json` is read at **native build time**, not at runtime. The first time a brand-new opacity variant is auto-derived (a class like `bg-surface/65` you've never used before), the running app **will not see it** until the next full Titanium build. Liveview hot-reload alone does **not** refresh `semantic.colors.json` for the running app — only the native binary does.
363
370
  >
364
- > In practice: after introducing a new opacity class, run `purgetss build` once, then start a fresh native build (`appc run` / `ti build`) before resuming your usual Liveview cycle. Subsequent runs of the *same* `/N` value reuse the existing derived key and need no extra rebuild.
371
+ > In practice: after introducing a new opacity class, run `purgetss build` once, then start a fresh native build (`ti build`) before resuming your usual Liveview cycle. Subsequent runs of the *same* `/N` value reuse the existing derived key and need no extra rebuild.
365
372
 
366
373
  #### Constraints
367
374
 
@@ -444,11 +451,11 @@ Start with these 6-7 colors and add more only when the design requires it. Fewer
444
451
  ## Related
445
452
 
446
453
  - [appearance-module.md](./appearance-module.md) — `Appearance.init()`, `set(...)`, `get()`, `toggle()` — the runtime that switches the whole palette.
447
- - [cli-commands.md#semantic-command](./cli-commands.md#semantic-command) — full reference for `purgetss semantic` (palette and single modes, all flags).
454
+ - [color-commands.md#semantic-command](./color-commands.md#semantic-command) — full reference for `purgetss semantic` (palette and single modes, all flags).
448
455
  - [customization-deep-dive.md](./customization-deep-dive.md) — full `config.cjs` structure, including `theme.extend` vs `theme.colors` and the rest of the extendable keys.
449
456
 
450
457
  ## Community-Discovered Patterns
451
458
 
452
- - **The `[object Object]` crash is always a missing `DEFAULT`.** Any time a view renders with a mysterious fallback color and the class was something like `bg-surface`, check `config.cjs` first: if `surface` is a nested object without a `DEFAULT` key, PurgeTSS serializes the whole object and emits `[object Object]` as the color value. The fix is either to add `DEFAULT: 'surfaceColor'` inside the nested object or flatten the structure.
459
+ - **A base class that does nothing is always a missing `DEFAULT`.** Any time a class like `bg-surface` has no visible effect, check `config.cjs` first: if `surface` is a nested object without a `DEFAULT` key, PurgeTSS 7.17.1 generates only the suffixed classes (`bg-surface-high`, ...) and no `bg-surface` rule. The official docs describe this case as `[object Object]`. The fix is either to add `DEFAULT: 'surfaceColor'` inside the nested object or flatten the structure.
453
460
  - **Semantic resolution is Titanium-native, not PurgeTSS.** This matters when debugging: if a semantic name doesn't resolve, the problem is usually the `semantic.colors.json` file (wrong filename, wrong key, malformed JSON, or wrong location — Alloy expects `app/assets/`, Classic expects `Resources/`). PurgeTSS generates the file in both layouts and an additional utility mapping only in Alloy.
454
461
  - **Re-running `purgetss semantic` on the same family is safe.** The CLI strips prior keys for that family from both the JSON and `config.cjs` before writing, so switching between palette and single forms — or changing the base hex — does not leave orphans. Other palettes and manually-defined entries are untouched.
@@ -16,7 +16,7 @@ In PurgeTSS, `gap` is mapped to **external margins** on the element itself (`top
16
16
  - `.gap-x-{size}` — emits `right` and `left` only (horizontal margins).
17
17
  - `.gap-y-{size}` — emits `top` and `bottom` only (vertical margins).
18
18
  - `.gap-{t|r|b|l}-{size}` — single-side gap (e.g. `.gap-t-4`).
19
- - **Evidence**: Official [`purgetss-docs-context7/docs/grid-system.md`](https://github.com/macCesar/purgeTSS) § "Gutter utilities".
19
+ - **Evidence**: Official [`docs/grid-system.md`](https://github.com/macCesar/purgetss-docs/blob/main/docs/grid-system.md) § "Gutter utilities".
20
20
 
21
21
  ### 2. Hybrid Shadows (iOS vs. Android in One Class)
22
22
 
@@ -32,16 +32,17 @@ Shadow utilities (`.shadow-xs`, `.shadow-sm`, `.shadow`, `.shadow-md`, `.shadow-
32
32
  The `.grid` and `.grid-flow-col` classes emit **both** `layout: 'horizontal'` **and** `width: '100%'` (`.grid-flow-row` emits `layout: 'vertical', height: '100%'`).
33
33
 
34
34
  - **Why the width is baked in**: PurgeTSS resets `View` defaults to `SIZE`. A grid container needs an explicit dimension so that percentage-based children (e.g. `.col-span-6` emitting `width: '50%'`) have a parent measurement to resolve against.
35
- - **Official docs** ([`grid-system.md`](https://github.com/macCesar/purgeTSS)) only document the `layout: 'horizontal'` half. The `width: '100%'` behavior is **verified against `lib/templates/` → `dist/utilities.tss`** (lines emit `'.grid': { layout: 'horizontal', width: '100%' }`).
35
+ - **Official docs** ([`grid-system.md`](https://github.com/macCesar/purgetss-docs/blob/main/docs/grid-system.md)) only document the `layout: 'horizontal'` half. The `width: '100%'` behavior is **verified against `lib/templates/` → `dist/utilities.tss`** (lines emit `'.grid': { layout: 'horizontal', width: '100%' }`).
36
36
  - **Manual override**: If you need a grid sized to its content only, pair it with `.wh-auto` or an explicit sizing utility.
37
37
 
38
- ### 4. Native Rotations via `rotate` (Not CSS Transform)
38
+ ### 4. Rotations via `rotate` Are Matrix2D Properties (Not CSS Transform)
39
39
 
40
- The `.rotate-{n}` utilities (`rotate-0`, `rotate-1`, `rotate-2`, `rotate-3`, `rotate-6`, `rotate-12`, `rotate-45`, `rotate-90`, `rotate-135`, `rotate-180`, `rotate-225`, `rotate-270`, `rotate-315`, `rotate-360`) map **directly** to Titanium's `rotate` property — a plain numeric degree value on the view.
40
+ The `.rotate-{n}` utilities (`rotate-0`, `rotate-1`, `rotate-2`, `rotate-3`, `rotate-6`, `rotate-12`, `rotate-45`, `rotate-90`, `rotate-135`, `rotate-180`, `rotate-225`, `rotate-270`, `rotate-315`, `rotate-360`) emit `rotate: <degrees>`, which is a `Matrix2DCreationDict` key, not a `Ti.UI.View` property. They take effect through the `purgetss.ui` Animation module (`<Animation module="purgetss.ui" class="rotate-90" />`).
41
41
 
42
- - **Default anchor**: Titanium rotates around the view's **center**. To change this, use an `.origin-*` utility (`.origin-top`, `.origin-top-left`, `.origin-center`, `.origin-bottom-right`, etc.) which emits `anchorPoint: { x, y }`.
43
- - **No chained transforms**: Unlike CSS (`transform: rotate(45deg) scale(1.5)`), Titanium treats each transformation as an independent view property. For combined transforms you must build a `Ti.UI.2DMatrix`/`Ti.UI.Matrix2D` and assign it to `transform`.
44
- - **Evidence**: `dist/utilities.tss` lines `'.rotate-45': { rotate: 45 }` and `'.origin-center': { anchorPoint: { x: 0.5, y: 0.5 } }`.
42
+ - **How it is applied**: at construction time the Animation module converts top-level `scale`, `rotate`, and `anchorPoint` into one `Ti.UI.Matrix2D` (via `Ti.UI.createMatrix2D()`) and removes them from the base object; `animationProperties.open` / `.close` get their own matrices the same way. So `rotate-*`, `scale-*`, and `origin-*` combine without building the matrix by hand.
43
+ - **Anchor**: an `.origin-*` utility (`.origin-top`, `.origin-top-left`, `.origin-center`, `.origin-bottom-right`, etc.) emits `anchorPoint: { x, y }`, which feeds the same matrix.
44
+ - **Outside the Animation module**: to rotate a view yourself, assign a `Ti.UI.createMatrix2D({ rotate: 45 })` to its `transform`.
45
+ - **Evidence**: `dist/utilities.tss` (`// Property: rotate` … `// Component(s): Matrix2DCreationDict`, `'.rotate-45': { rotate: 45 }`, `'.origin-center': { anchorPoint: { x: 0.5, y: 0.5 } }`), `dist/purgetss.ui.js` `handleTransformations()`, and official [`purgetss-ui/9-implementation-rules.md`](https://github.com/macCesar/purgetss-docs/blob/main/docs/purgetss-ui/9-implementation-rules.md).
45
46
 
46
47
  ### 5. Z-Index Uses the Full `z-index-*` Prefix
47
48
 
@@ -38,7 +38,7 @@ The pipeline only touches SVGs you actually reference from views or controllers.
38
38
 
39
39
  ## What gets detected
40
40
 
41
- **XML views:** any node attribute ending in `.svg` is captured along with its `class=""` attribute:
41
+ **XML views:** an `image` or `backgroundImage` attribute ending in `.svg` is captured along with the node's `class=""` attribute:
42
42
 
43
43
  ```xml
44
44
  <ImageView class="w-32 h-auto" image="/images/logos/logo.svg" />
@@ -46,7 +46,7 @@ The pipeline only touches SVGs you actually reference from views or controllers.
46
46
  <View backgroundImage="/images/textures/grain.svg" class="wh-screen" />
47
47
  ```
48
48
 
49
- Both `image` and `backgroundImage` (and any other attribute whose value ends in `.svg`) are picked up.
49
+ Only `image` and `backgroundImage` are scanned, and only paths under `/images/` (`/images/<sub>/<name>.svg`) are handled; the file must exist at `purgetss/images/<sub>/<name>.svg`, or the run warns `SVG not found: … — skipping`.
50
50
 
51
51
  **Controllers:** objects passed to `$.UI.create(...)` (or similar) that mix `image: '...svg'` and `classes: '...'` are captured by an AST walker:
52
52
 
@@ -66,7 +66,7 @@ The scanner sees **literal string values only**, not computed paths. Dynamically
66
66
  image: '/images/' + variant + '.svg'
67
67
  ```
68
68
 
69
- For dynamic references, pin the dimensions manually under `images.files` (see [Manual pinning](#manual-pinning-for-undetected-references)) so the pipeline still generates the PNGs.
69
+ For dynamic references, pin the dimensions under `images.files` and generate the PNGs with `purgetss images` (see [Manual pinning](#manual-pinning-for-undetected-references)). If acorn cannot parse a controller, a conservative regex scan looks for the same `image`/`backgroundImage` + `classes` pairs.
70
70
 
71
71
  ## How dimensions get resolved
72
72
 
@@ -79,7 +79,7 @@ For each SVG reference, the cascade of classes is applied against `app.tss` in d
79
79
  | `class="w-(300)"` | `width: 300` (arbitrary value) | `widthDp = 300` |
80
80
  | `class="h-44"` (no `w-*`) | `height: 176` | `heightDp = 176`; width derived from viewBox at generation time |
81
81
  | `class="w-32 h-auto"` | `width: 128`, `height: Ti.UI.SIZE` | `widthDp = 128`; height derived from viewBox |
82
- | `class="w-full"` (non-numeric) | `width: Ti.UI.FILL` | skipped with a warning; no usable dim |
82
+ | `class="w-full"` (non-numeric) | `width: '100%'` | skipped with a warning; no usable dim |
83
83
 
84
84
  Note the spacing convention: PurgeTSS multiplies the Tailwind-style numeric scale, so `w-32` resolves to `width: 128` (128 dp), not 32.
85
85
 
@@ -140,8 +140,8 @@ images: {
140
140
 
141
141
  With `autoSync: false`:
142
142
 
143
- - The pipeline still derives dimensions and generates PNGs, using the values you wrote in `images.files` — or the cascade fallback for files not listed.
144
- - `config.cjs` is **never** written. Those entries are yours to maintain.
143
+ - The pipeline still derives dimensions from the class cascade and generates PNGs at those sizes. It does not read the values you wrote in `images.files`; it only reports what it would have inserted or updated.
144
+ - `config.cjs` is **never** written. Those entries are yours to maintain, and `purgetss images` is the command that applies them.
145
145
 
146
146
  Use this when you want to pin sizes by hand and not have PurgeTSS overwrite your work.
147
147
 
@@ -152,7 +152,7 @@ The SVG pipeline **always emits `.png`**, regardless of `images.format`. This is
152
152
  1. Titanium's `image="/foo.svg"` fallback resolves to `.png` only. `.webp`, `.jpeg`, and `.avif` are not picked up through the `.svg` reference.
153
153
  2. Having a sibling `.webp` (or any non-`.png`) next to `.png` on disk for the same basename **breaks the fallback entirely**. Titanium then shows nothing.
154
154
 
155
- So even if you set `format: 'webp'` (which is honored by `purgetss images` for raster sources and unlisted SVGs), the post-purge pipeline emits `.png` for every SVG it processes. The `--debug` output line reads `png (forced; ignores format: webp)`.
155
+ So even if you set `format: 'webp'` (which is honored by `purgetss images` for raster sources and unlisted SVGs), the post-purge pipeline emits `.png` for every SVG it processes. The pipeline logs each generated file as `✓ <sub>/<name>.svg → <W>×<H>dp`; the `png (forced; ignores format: webp)` line belongs to the standalone `purgetss images` command.
156
156
 
157
157
  If you genuinely want a `.webp` of an SVG, reference it as `image="/.../foo.webp"` in your XML, not as `.svg`. The standalone `purgetss images` command will then generate `.webp` for that file. See the [WebP and SVG warning](./multi-density-images.md) in the multi-density reference.
158
158
 
@@ -188,7 +188,7 @@ Otherwise, PurgeTSS regenerates the image. If you `git clean`, delete a PNG by h
188
188
 
189
189
  ## Manual pinning for undetected references
190
190
 
191
- When the static scanner cannot see a reference (dynamic/concatenated paths) or a view has no resolvable `w-*` / `h-*` class, pin the entry by hand so the pipeline still generates the PNGs:
191
+ When the static scanner cannot see a reference (dynamic/concatenated paths) or a view has no resolvable `w-*` / `h-*` class, pin the entry by hand and run `purgetss images`. The post-purge pipeline only generates what it detected and resolved; pinned entries are applied by the standalone command:
192
192
 
193
193
  ```javascript title="./purgetss/config.cjs"
194
194
  images: {
@@ -210,11 +210,19 @@ images: {
210
210
 
211
211
  Both share the same `images.files` array and the same generation engine (`gen-scales`). They do not fight each other: the pipeline edits `images.files`; the standalone command reads it as overrides. Run either, or both.
212
212
 
213
+ ## Scope and limits
214
+
215
+ - The pipeline runs inside `purgetss` (the purge), which requires an Alloy project, so it does not run in Classic projects. It does nothing when `purgetss/images/` does not exist or no SVG reference is found.
216
+ - It always generates both platforms (5 Android + 3 iPhone PNGs); it does not read `<deployment-targets>` from `tiapp.xml` the way `purgetss images` does.
217
+ - An SVG with neither a `viewBox` nor explicit `width` and `height` attributes is skipped with a warning.
218
+ - Every PNG is capped at 4096 px per side, and `xxxhdpi` is 4× the resolved dp size, so an SVG resolved wider or taller than 1024 dp (for example `w-(1100)`) fails with `✗ <sub>/<name>.svg: … exceeds the 4096px cap`. The other SVGs in the run still generate.
219
+ - A run that generated or reused any PNG ends with `SVG pipeline: N generated, M cached.`
220
+
213
221
  ## Troubleshooting
214
222
 
215
- ### "no class resolved width or height to a number; skipping"
223
+ ### "no class resolved width or height to a number — skipping"
216
224
 
217
- The SVG is referenced from a view with no resolvable `w-*` or `h-*` class (just `w-full`, `w-screen`, `bg-*`, etc.). Add a numeric `w-*` or `h-*` utility, or pin the size manually in `images.files`:
225
+ The SVG is referenced from a view with no resolvable `w-*` or `h-*` class (just `w-full`, `w-screen`, `bg-*`, etc.). Add a numeric `w-*` or `h-*` utility, or pin the size manually in `images.files` and run `purgetss images`:
218
226
 
219
227
  ```javascript
220
228
  files: [
@@ -224,13 +232,13 @@ files: [
224
232
 
225
233
  ### My change to a class didn't update the PNG
226
234
 
227
- Check that the class actually exists in `app.tss` after purging. The Tailwind-style spacing scale skips numbers like `h-50`; only `h-48`, `h-52`, `h-56` are emitted by default. Add custom values under `theme.extend.spacing` if you need them, or use arbitrary values: `h-(50)`, `h-(200px)`, `h-(12.5rem)`.
235
+ Check that the class actually exists in `app.tss` after purging. The Tailwind-style spacing scale skips numbers like `h-50`; only `h-48`, `h-52`, `h-56` are emitted by default. Add custom values under `theme.extend.spacing` if you need them, or use arbitrary values: `h-(50)`, `h-(200)`, `h-(12.5rem)`.
228
236
 
229
237
  If the class is correct but the image didn't refresh on device, it might be the Titanium simulator's image cache. Do a clean build.
230
238
 
231
239
  ### The pipeline didn't detect my SVG reference
232
240
 
233
- The static scanner sees literal string values, not computed paths. Concatenated strings (`'/images/' + variant + '.svg'`) are not detected. Pin the entry in `images.files` manually so the pipeline still generates the PNGs.
241
+ The static scanner sees literal string values, not computed paths. Concatenated strings (`'/images/' + variant + '.svg'`) are not detected. Pin the entry in `images.files` manually and run `purgetss images` to generate the PNGs.
234
242
 
235
243
  ### `<ImageView image="/.../foo.svg" />` shows nothing
236
244
 
@@ -241,7 +249,7 @@ Two common causes:
241
249
 
242
250
  ### I want to opt out for a specific run
243
251
 
244
- Switch `autoSync: false` and manage the `images.files` entries yourself, or delete the SVG cache entry. The pipeline still derives dimensions and generates the PNGs so views keep working, but it does not write to `config.cjs`.
252
+ Switch `autoSync: false` and manage the `images.files` entries yourself. The pipeline still derives dimensions and generates the PNGs so views keep working, but it does not write to `config.cjs`. Deleting the SVG cache entry does not opt out; it only forces that SVG to regenerate on the next run.
245
253
 
246
254
  ## See also
247
255