@maccesar/titools 4.23.1 → 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 (56) hide show
  1. package/README.md +26 -3
  2. package/package.json +1 -1
  3. package/skills/alloy-guides/references/MODELS.md +2 -2
  4. package/skills/alloy-guides/references/MODELS_ADVANCED.md +1 -1
  5. package/skills/alloy-guides/references/VIEWS_XML.md +13 -13
  6. package/skills/alloy-howtos/references/best_practices.md +2 -2
  7. package/skills/alloy-howtos/references/cli_reference.md +1 -1
  8. package/skills/alloy-howtos/references/debugging_troubleshooting.md +1 -1
  9. package/skills/purgetss/SKILL.md +27 -24
  10. package/skills/purgetss/references/EXAMPLES.md +52 -41
  11. package/skills/purgetss/references/adopting-purgetss.md +137 -0
  12. package/skills/purgetss/references/animation-advanced.md +18 -5
  13. package/skills/purgetss/references/animation-system.md +21 -9
  14. package/skills/purgetss/references/app-branding.md +36 -97
  15. package/skills/purgetss/references/appearance-module.md +70 -5
  16. package/skills/purgetss/references/apply-directive.md +14 -6
  17. package/skills/purgetss/references/arbitrary-values.md +59 -23
  18. package/skills/purgetss/references/brand-padding-geometry.md +105 -0
  19. package/skills/purgetss/references/class-categories.md +684 -272
  20. package/skills/purgetss/references/class-index-properties.md +713 -607
  21. package/skills/purgetss/references/class-index.md +149 -84
  22. package/skills/purgetss/references/classic-projects.md +2 -2
  23. package/skills/purgetss/references/cli-commands.md +27 -213
  24. package/skills/purgetss/references/color-commands.md +218 -0
  25. package/skills/purgetss/references/configurable-properties.md +17 -2
  26. package/skills/purgetss/references/custom-fonts.md +21 -16
  27. package/skills/purgetss/references/custom-rules.md +7 -7
  28. package/skills/purgetss/references/customization-deep-dive.md +14 -11
  29. package/skills/purgetss/references/dynamic-component-creation.md +104 -61
  30. package/skills/purgetss/references/grid-layout.md +21 -24
  31. package/skills/purgetss/references/icon-fonts.md +37 -20
  32. package/skills/purgetss/references/installation-setup.md +1 -1
  33. package/skills/purgetss/references/ios-large-titles.md +12 -11
  34. package/skills/purgetss/references/migration-guide.md +101 -10
  35. package/skills/purgetss/references/multi-density-images.md +44 -24
  36. package/skills/purgetss/references/opacity-modifier.md +1 -1
  37. package/skills/purgetss/references/performance-tips.md +5 -4
  38. package/skills/purgetss/references/platform-modifiers.md +2 -2
  39. package/skills/purgetss/references/purgetss-ui-classic.md +21 -5
  40. package/skills/purgetss/references/semantic-colors.md +20 -13
  41. package/skills/purgetss/references/smart-mappings.md +8 -7
  42. package/skills/purgetss/references/svg-pipeline.md +21 -13
  43. package/skills/purgetss/references/tikit-components.md +2 -2
  44. package/skills/purgetss/references/titanium-resets.md +27 -17
  45. package/skills/purgetss/references/ui-ux-design.md +38 -26
  46. package/skills/purgetss/references/values-and-units.md +1 -2
  47. package/skills/purgetss/references/version-history.md +32 -4
  48. package/skills/ti-api/references/api-data-network.md +1 -1
  49. package/skills/ti-api/references/api-media.md +1 -1
  50. package/skills/ti-api/references/api-services.md +8 -5
  51. package/skills/ti-api/references/api-ui-lists.md +3 -2
  52. package/skills/ti-api/references/api-ui-text-input.md +1 -1
  53. package/skills/ti-expert/references/feedback-surfaces.md +1 -1
  54. package/skills/ti-guides/references/resources.md +1 -1
  55. package/skills/ti-guides/references/sdk-release-notes.md +22 -11
  56. package/skills/ti-howtos/references/extending-titanium.md +77 -0
@@ -0,0 +1,137 @@
1
+ # Adopting PurgeTSS in an Existing Alloy App
2
+
3
+ How to move an Alloy app whose styles live in hand-written `.tss` files (per-controller files, `#id` rules, inline XML attributes) to PurgeTSS utility classes. For upgrading a project that already uses PurgeTSS from one version to another, see [migration-guide.md](migration-guide.md) instead.
4
+
5
+ > **Source note:** the official PurgeTSS docs have no adoption guide. This page is TiTools author guidance, assembled from behavior verified in the PurgeTSS source (v7.17.1), the Alloy style-priority rules and `utilities.tss`. Every class in the tables below exists in `utilities.tss` with the value shown.
6
+
7
+ ## Ground Rules
8
+
9
+ - **Never delete the user's `.tss` files without explicit consent.** Convert a view, show the result, and wait for confirmation before removing the old rules (see `SKILL.md` § "RESPECT USER FILES").
10
+ - **One view at a time.** Convert, compile, check on both platforms, then move on.
11
+ - **Alloy only.** The utility-class workflow does not exist in Classic projects; see [classic-projects.md](classic-projects.md).
12
+
13
+ ## What PurgeTSS Does to Your Styles
14
+
15
+ | File | What happens |
16
+ | --- | --- |
17
+ | `app/styles/app.tss` | On the first run it is backed up to `app/styles/_app.tss`. From then on PurgeTSS **rewrites `app.tss` on every run**: resets, then the contents of `_app.tss`, then only the utility classes your views use. Never edit `app.tss` directly. |
18
+ | `app/styles/_app.tss` | Your original global styles. Keep custom global classes here; they are copied into `app.tss` on each run. |
19
+ | `app/styles/<controller>.tss` | Not touched. PurgeTSS reads these files only so the classes they define are not reported as missing. |
20
+ | `alloy.jmk` | The first run adds a hook so `purgetss` runs on every Alloy compile. See [installation-setup.md](installation-setup.md). |
21
+
22
+ ### The Priority Trap
23
+
24
+ Alloy applies styles from lowest to highest priority: `styles/app.tss` (global), then `styles/<controller>.tss`, then attributes in the XML markup, with theme and platform-specific files layered in between (Alloy guide, "Style Priorities"). PurgeTSS classes land in `app.tss`, the **lowest** layer. During a partial migration this means:
25
+
26
+ - A leftover rule in `styles/index.tss` for the same element beats the utility class, silently.
27
+ - An inline attribute such as `backgroundColor="#fff"` beats both.
28
+
29
+ When a class "does nothing" mid-migration, look for a leftover rule in the controller's `.tss` or an attribute on the XML tag before suspecting PurgeTSS.
30
+
31
+ ## Workflow
32
+
33
+ ### 1. Set up
34
+
35
+ ```bash
36
+ cd your-app
37
+ purgetss init # creates ./purgetss/config.cjs
38
+ purgetss # first purge: backs up app.tss to _app.tss and installs the alloy.jmk hook
39
+ ```
40
+
41
+ Commit (or branch) before this step so the backup and the hook are easy to review.
42
+
43
+ ### 2. Convert one view
44
+
45
+ Before:
46
+
47
+ ```xml
48
+ <!-- app/views/index.xml -->
49
+ <View id="header">
50
+ <Label id="title" text="Welcome" />
51
+ </View>
52
+ ```
53
+
54
+ ```tss
55
+ // app/styles/index.tss
56
+ '#header': { backgroundColor: '#ffffff', height: 64, top: 0 }
57
+ '#title': { color: '#1f2937', font: { fontSize: 18, fontWeight: 'bold' } }
58
+ ```
59
+
60
+ After:
61
+
62
+ ```xml
63
+ <!-- app/views/index.xml -->
64
+ <View class="top-0 h-16 bg-white">
65
+ <Label class="text-lg font-bold text-gray-800" text="Welcome" />
66
+ </View>
67
+ ```
68
+
69
+ Keep the `id` attributes if controller code uses them (`$.header`); only the style rules move.
70
+
71
+ ### 3. Remove the old rules (with consent)
72
+
73
+ Once the converted view looks right on both platforms, and the user agrees:
74
+
75
+ 1. Delete the converted rules from `app/styles/index.tss`. Remove the file only if nothing is left in it.
76
+ 2. Move any global class still needed from `_app.tss` into utility classes, or leave it in `_app.tss`.
77
+ 3. Run `purgetss` (or compile) to regenerate `app.tss`.
78
+
79
+ ## Translation Tables
80
+
81
+ ### Properties
82
+
83
+ | Hand-written TSS | PurgeTSS class |
84
+ | --- | --- |
85
+ | `backgroundColor: '#ffffff'` | `bg-white` |
86
+ | `backgroundColor: '#f3f4f6'` | `bg-gray-100` |
87
+ | `color: '#1f2937'` | `text-gray-800` (sets `color` and `textColor`) |
88
+ | `font: { fontSize: 18 }` | `text-lg` |
89
+ | `font: { fontWeight: 'bold' }` | `font-bold` |
90
+ | `width: Ti.UI.FILL` | `w-screen` |
91
+ | `width: '100%'` | `w-full` |
92
+ | `height: Ti.UI.SIZE` | `h-auto` |
93
+ | `height: 64` | `h-16` |
94
+ | `height: 60` (not on the scale) | `h-(60)` |
95
+ | `top: 0` | `top-0` |
96
+ | `top: 16` | `mt-4` |
97
+ | `left: 16, right: 16` | `mx-4` |
98
+ | `borderRadius: 8` | `rounded-lg` |
99
+ | `borderWidth: 1, borderColor: '#e5e7eb'` | `border border-gray-200` |
100
+ | `opacity: 0.5` | `opacity-50` |
101
+ | `visible: false` | `hidden` |
102
+ | `layout: 'horizontal'` | `horizontal` |
103
+ | `layout: 'vertical'` | `vertical` |
104
+ | `elevation: 4` | `elevation-1` (the scale is ×4: `elevation-4` is `16`) |
105
+
106
+ Spacing classes multiply by 4 (`mt-4` = 16), and the numbers are unitless: Titanium resolves them with `ti.ui.defaultunit` (see [values-and-units.md](values-and-units.md)). For a value off the scale use an arbitrary value **without a unit**: `h-(60)`, not `h-(60px)`, which stops the build since v7.8.0 (see [arbitrary-values.md](arbitrary-values.md)). Colors in your brand palette belong in `config.cjs` (see [customization-deep-dive.md](customization-deep-dive.md)), not in arbitrary `bg-(#hex)` classes repeated across views.
107
+
108
+ ### Platform and device conditionals
109
+
110
+ | Hand-written code | PurgeTSS |
111
+ | --- | --- |
112
+ | `if (OS_IOS) { $.box.top = 40 } else { $.box.top = 20 }` | `class="ios:mt-10 mt-5"` |
113
+ | `if (OS_ANDROID) { $.card.elevation = 4 }` | `class="android:elevation-1"` |
114
+ | Tablet width set in the controller | `class="tablet:w-6/12 w-screen"` |
115
+
116
+ See [platform-modifiers.md](platform-modifiers.md) for the full modifier list.
117
+
118
+ ### Animations
119
+
120
+ | Hand-written animation | PurgeTSS Animation module |
121
+ | --- | --- |
122
+ | `Ti.UI.createAnimation({ opacity: 0, duration: 300 })` toggled by hand | `<Animation module="purgetss.ui" id="fade" class="close:opacity-0 open:opacity-100 duration-300" />` and `$.fade.play($.box)` |
123
+ | A scale-down press effect | `class="close:scale-100 open:scale-95 duration-150"` |
124
+
125
+ See [animation-system.md](animation-system.md).
126
+
127
+ ## Pitfalls
128
+
129
+ | Pitfall | Consequence | Prevention |
130
+ | --- | --- | --- |
131
+ | Editing `app.tss` directly | Overwritten on the next run | Put custom global styles in `_app.tss` |
132
+ | Leaving the old rule in `<controller>.tss` | It overrides the utility class | Remove converted rules once the user agrees |
133
+ | Keeping inline attributes (`backgroundColor="…"`) | They override every class | Move them into classes |
134
+ | Flexbox classes (`flex-row`, `justify-between`) | Classes don't exist | `horizontal` / `vertical` layouts |
135
+ | `p-*` on a `View` or `Window` | No effect: `padding` exists only on `TextField`, `TextArea` and Android `CardView` | Margins on the children |
136
+ | `w-[100]` or `w-(100px)` | Stops the build | `w-(100)` |
137
+ | Converting every view at once | Hard to find what broke | One view per step, tested on both platforms |
@@ -138,7 +138,7 @@ Values: `repeat-0` through `repeat-12`.
138
138
 
139
139
  ### rotate-* and -rotate-*
140
140
 
141
- Rotation in degrees. Range: **0** to **180**, positive and negative.
141
+ Rotation in degrees. Range: **0** to **360**, positive and negative.
142
142
 
143
143
  ```css
144
144
  '.rotate-0': { rotate: 0 }
@@ -149,7 +149,12 @@ Rotation in degrees. Range: **0** to **180**, positive and negative.
149
149
  '.rotate-12': { rotate: 12 }
150
150
  '.rotate-45': { rotate: 45 }
151
151
  '.rotate-90': { rotate: 90 }
152
+ '.rotate-135': { rotate: 135 }
152
153
  '.rotate-180': { rotate: 180 }
154
+ '.rotate-225': { rotate: 225 }
155
+ '.rotate-270': { rotate: 270 }
156
+ '.rotate-315': { rotate: 315 }
157
+ '.rotate-360': { rotate: 360 }
153
158
  '.-rotate-0': { rotate: 0 }
154
159
  '.-rotate-1': { rotate: -1 }
155
160
  '.-rotate-2': { rotate: -2 }
@@ -158,7 +163,12 @@ Rotation in degrees. Range: **0** to **180**, positive and negative.
158
163
  '.-rotate-12': { rotate: -12 }
159
164
  '.-rotate-45': { rotate: -45 }
160
165
  '.-rotate-90': { rotate: -90 }
166
+ '.-rotate-135': { rotate: -135 }
161
167
  '.-rotate-180': { rotate: -180 }
168
+ '.-rotate-225': { rotate: -225 }
169
+ '.-rotate-270': { rotate: -270 }
170
+ '.-rotate-315': { rotate: -315 }
171
+ '.-rotate-360': { rotate: -360 }
162
172
  ```
163
173
 
164
174
  ### scale-*
@@ -180,6 +190,7 @@ Scales the matrix by the specified factor. Same value for horizontal and vertica
180
190
  '.scale-110': { scale: 1.1 }
181
191
  '.scale-125': { scale: 1.25 }
182
192
  '.scale-150': { scale: 1.5 }
193
+ '.scale-200': { scale: 2 }
183
194
  ```
184
195
 
185
196
  ### Snap Classes
@@ -221,7 +232,7 @@ Prevents the drag system from promoting the active view on touch start. It does
221
232
 
222
233
  ### zoom-in-* / zoom-out-*
223
234
 
224
- Animates zoom in and zoom out. Sets the View's scale to the specified value, then animates it back to 1. Range: **0** to **150**.
235
+ Animates zoom in and zoom out. Sets the View's scale to the specified value, then animates it back to 1. Range: **0** to **200**.
225
236
 
226
237
  ```css
227
238
  '.zoom-in-0': { animationProperties: { open: { scale: 0 }, complete: { scale: 1 } } }
@@ -234,7 +245,7 @@ Animates zoom in and zoom out. Sets the View's scale to the specified value, the
234
245
  '.zoom-out-110': { animationProperties: { close: { scale: 1.1 }, complete: { scale: 1 } } }
235
246
  ```
236
247
 
237
- Full range: `zoom-in-0`, `zoom-in-1`, `zoom-in-5`, `zoom-in-10`, `zoom-in-25`, `zoom-in-50`, `zoom-in-75`, `zoom-in-90`, `zoom-in-95`, `zoom-in-100`, `zoom-in-105`, `zoom-in-110`, `zoom-in-125`, `zoom-in-150` (and same for `zoom-out-*`).
248
+ Full range: `zoom-in-0`, `zoom-in-1`, `zoom-in-5`, `zoom-in-10`, `zoom-in-25`, `zoom-in-50`, `zoom-in-75`, `zoom-in-90`, `zoom-in-95`, `zoom-in-100`, `zoom-in-105`, `zoom-in-110`, `zoom-in-125`, `zoom-in-150`, `zoom-in-200` (and same for `zoom-out-*`).
238
249
 
239
250
  ---
240
251
 
@@ -278,7 +289,7 @@ At construction time, top-level `scale`, `rotate`, and `anchorPoint` are convert
278
289
 
279
290
  `play()`, `toggle()`, `apply()`, and `sequence()` toggle internal open/close state. `open()` and `close()` set it explicitly. The active state is merged into the current base object when `animationProperties` exists.
280
291
 
281
- After a top-level `play`, `toggle`, `open`, or `close`, `animationProperties.complete` starts as a second animation. `apply()` applies `complete` immediately. The public callback belongs to the base play/apply operation; it does not wait for the second top-level `complete` animation.
292
+ After a top-level `play`, `toggle`, `open`, or `close`, and after each view of a `sequence()`, `animationProperties.complete` starts as a second animation. `apply()` applies `complete` immediately. The public callback belongs to the base play/apply operation; it does not wait for the second top-level `complete` animation.
282
293
 
283
294
  For each direct child with an active state, merge precedence is:
284
295
 
@@ -308,7 +319,7 @@ Drag precedence is precise:
308
319
  - `snap.center` invokes `snapTo()` after a valid drop; `snap.back` returns a missed drop to its captured origin.
309
320
  - No `snap.magnet` behavior exists.
310
321
 
311
- Collision detection uses the dragged view's center. On release, the last non-null hover target is a fallback when the final hit test returns `null`.
322
+ Collision detection uses the dragged view's center and runs only for views registered with `detectCollisions()`, so register the dragged views as well as the targets. Hit testing during movement runs only when a `dragCB` was supplied; in that case the last non-null hover target is a fallback when the final hit test at release returns `null`. `dragCB(source, null)` is also called whenever a registered view is released.
312
323
 
313
324
  `draggable(array)` immediately assigns each view `zIndex` from its array index. `keepZIndex` only disables later touch-start promotion. `swap()` restores z-order by the current draggable-registry order, not by arbitrary values present before registration.
314
325
 
@@ -423,3 +434,5 @@ function doAction(event) {
423
434
  ```
424
435
 
425
436
  This demonstrates: `play` for sidebar/card toggle, `open:`/`close:` modifiers for different states, `draggable` with `bounds:` modifier, and multiple Animation objects controlling different UI elements.
437
+
438
+ If this window can close, add an `onClose` handler that calls `$.draggableAnimation.undraggable($.myCard)` so the global `orientationchange` listener is removed.
@@ -84,7 +84,7 @@ The `play`, `toggle`, `open`, `close`, `apply`, and `sequence` methods accept an
84
84
  | --- | --- |
85
85
  | `open:` | Properties applied during open state |
86
86
  | `close:` | Properties applied during close state |
87
- | `complete:` | Additional properties applied after the active `play`/`toggle`/`open`/`close` state, or immediately after `apply` |
87
+ | `complete:` | Additional properties applied after the active `play`/`toggle`/`open`/`close` state (and after each view of a `sequence`), or immediately after `apply` |
88
88
  | `children:` | Global properties for all children of a View |
89
89
  | `child:` | Individual properties for specific children |
90
90
  | `bounds:` | Drag boundaries within parent |
@@ -93,7 +93,9 @@ The `play`, `toggle`, `open`, `close`, `apply`, and `sequence` methods accept an
93
93
 
94
94
  ### Timing and Special Classes
95
95
 
96
- `delay-*`, `duration-*`, `rotate-*`, `scale-*`, `repeat-*`, `zoom-in-*`, `zoom-out-*`, `drag-apply`, `drag-animate`, `ease-in`, `ease-out`, `ease-linear`, `ease-in-out`, `vertical-constraint`, `horizontal-constraint`.
96
+ `delay-*`, `duration-*`, `rotate-*`, `scale-*`, `repeat-*`, `zoom-in-*`, `zoom-out-*`, `drag-apply`, `drag-animate`, `ease-in`, `ease-out`, `ease-linear`, `ease-in-out`, `vertical-constraint`, `horizontal-constraint`, `debug`.
97
+
98
+ The `debug` class sets `debug: true` (in Classic, pass `debug: true` to `createAnimation()`). The object then logs every method call and internal step with `console.warn('::ti.animation:: …')`.
97
99
 
98
100
  ---
99
101
 
@@ -101,6 +103,8 @@ The `play`, `toggle`, `open`, `close`, `apply`, and `sequence` methods accept an
101
103
 
102
104
  Runs the animation for a single view or an array of views. Toggles between open and close states on repeated calls.
103
105
 
106
+ `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.
107
+
104
108
  ```javascript
105
109
  $.myAnimation.play($.myView)
106
110
  ```
@@ -262,6 +266,10 @@ function showPanel() {
262
266
  function closePanel() {
263
267
  $.panelAnim.close($.panel, () => { $.panel.hide() })
264
268
  }
269
+
270
+ function onOverlayTap({ source }) {
271
+ if (source.id === 'panel') closePanel()
272
+ }
265
273
  ```
266
274
 
267
275
  Key classes: `zoom-in-110` (pop effect), `opacity-to-100` (fade overlay), `close:duration-0 open:duration-100` (instant close, animated open).
@@ -341,6 +349,8 @@ function onClose() {
341
349
 
342
350
  After calling `draggable()`, enable collision detection based on center-point hit testing.
343
351
 
352
+ Include the dragged views themselves in the `views` list: drop callbacks, `snap-center`, and `snap-back` run only for a view registered with `detectCollisions()`.
353
+
344
354
  ```javascript
345
355
  $.myAnimation.draggable(views)
346
356
  $.myAnimation.detectCollisions(views, dragCB, dropCB)
@@ -351,9 +361,11 @@ $.myAnimation.detectCollisions(views, dragCB, dropCB)
351
361
  | Parameter | Type | Description |
352
362
  | --- | --- | --- |
353
363
  | `views` | `View/Array` | Views to register for collision detection |
354
- | `dragCB` | `Function(source, target)` | Called during drag; `target` is the view under the drag center, or `null` when leaving |
364
+ | `dragCB` | `Function(source, target)` | Called during drag; `target` is the view under the drag center, or `null` when leaving. Also called with `null` whenever a registered view is released |
355
365
  | `dropCB` | `Function(source, target)` | Called on drop when a collision target is found |
356
366
 
367
+ Hit testing during movement runs only when a `dragCB` was supplied. Without one, the drop uses only the final hit test at release; the last-hovered target is not available as a fallback.
368
+
357
369
  ### Snap Classes
358
370
 
359
371
  Off by default -- opt-in via classes on the `<Animation>` object:
@@ -436,12 +448,12 @@ $.myAnimation.sequence(views, callback)
436
448
  ```
437
449
 
438
450
  ```javascript
439
- $.fadeIn.sequence([$.title, $.subtitle, $.cta], () => {
451
+ $.revealAnim.sequence([$.title, $.subtitle, $.cta], () => {
440
452
  console.log('All views animated in sequence')
441
453
  })
442
454
 
443
455
  // Use close() to force state back before revealing again
444
- $.revealAnim.close(views)
456
+ $.revealAnim.close([$.title, $.subtitle, $.cta])
445
457
  ```
446
458
 
447
459
  ---
@@ -459,7 +471,7 @@ $.myAnimation.swap(view1, view2)
459
471
  - Resets the source transform while persisting the exchanged positions
460
472
  - Temporarily elevates z-index; then assigns z-index from the current draggable-registry order rather than restoring arbitrary original values
461
473
  - Updates `_originLeft`/`_originTop` for subsequent drag operations
462
- - **Automatic position normalization**: no explicit `top`/`left` needed -- resolved via `view.rect`
474
+ - **Position resolution**: `view2`'s destination is read from `view2.rect`, but `view1`'s home comes from its stored drag origin or its `top`/`left`, with no `rect` fallback. Give `view1` explicit `top`/`left` unless it has already been dragged or moved by a helper
463
475
  - **Bounce-back safe**: completes any in-progress bounce-back before swapping
464
476
 
465
477
  ### Memory Card Game Example
@@ -469,7 +481,7 @@ let firstCard = null
469
481
  function onCardTap({ source }) {
470
482
  $.flipAnim.open(source)
471
483
  if (!firstCard) { firstCard = source; return }
472
- if (firstCard.valor === source.valor) {
484
+ if (firstCard.value === source.value) {
473
485
  firstCard = null
474
486
  } else {
475
487
  $.gameAnim.swap(firstCard, source)
@@ -552,7 +564,7 @@ $.puzzleAnim.draggable(pieces)
552
564
  $.puzzleAnim.detectCollisions(pieces.concat(slots),
553
565
  null,
554
566
  (source, target) => {
555
- if (source.valor === target.valor) {
567
+ if (source.value === target.value) {
556
568
  $.puzzleAnim.undraggable(source)
557
569
  source.applyProperties({ opacity: 0.6 })
558
570
  }
@@ -643,7 +655,7 @@ $.anim.transition(screensB, fanOut)
643
655
 
644
656
  ```javascript
645
657
  const photos = [$.photo1, $.photo2, $.photo3]
646
- function doFan() { $.galleryAnim.transition(photos, fan) }
658
+ function doFan() { $.galleryAnim.transition(photos, fanOut) }
647
659
  function doStack() { $.galleryAnim.transition(photos, stack) }
648
660
  $.galleryAnim.draggable(photos) // photos keep rotation/scale while dragging
649
661
  ```
@@ -1,8 +1,8 @@
1
1
  # App Icons & Branding
2
2
 
3
- The `purgetss brand` command generates the complete Titanium branding set from one main SVG or PNG logo: launcher icons, adaptive icons, iOS 18+ Dark/Tinted variants, marketplace artwork, and both splash sets. Per-piece overrides are available when needed. Alloy and Classic layouts are auto-detected, and since v7.14.0 a normal run follows the platforms enabled in `tiapp.xml`.
3
+ The `purgetss brand` command generates the complete Titanium branding set from one main SVG or PNG logo: launcher icons, adaptive icons, iOS 18+ Dark/Tinted variants, marketplace artwork, and both splash sets. Per-piece overrides are available when needed. Alloy and Classic layouts are auto-detected, and since v7.14.0 a normal run follows the platforms enabled in `tiapp.xml`. The rule: if an enabled platform can consume the file, `brand` generates or updates it.
4
4
 
5
- The command also creates Titanium-consumed Classic paths that a fresh Classic template does not seed, including the 11 Android `Resources/android/images/res-*` splash variants. An explicit `--only` is an intentional override that can prepare assets for a disabled deployment target.
5
+ The command also creates Titanium-consumed Classic paths that a fresh Classic template does not seed, including the 11 Android `Resources/android/images/res-*` splash variants. An explicit `--only` is an intentional override that can prepare assets for a disabled deployment target. A normal run names the pieces it left out (`Skipped for disabled deployment target(s): …`), and stops with a warning when no selected piece matches an enabled target.
6
6
 
7
7
  For the terse flag reference, see the [`brand` command reference](./cli-commands.md#brand-command). For sibling UI assets, see [Multi-Density Images](./multi-density-images.md).
8
8
 
@@ -30,8 +30,7 @@ For the terse flag reference, see the [`brand` command reference](./cli-commands
30
30
  - [iPhone launch images](#iphone-launch-images)
31
31
  - [iOS 18+ Dark and Tinted variants](#ios-18-dark-and-tinted-variants)
32
32
  - [Brand color](#brand-color)
33
- - [Padding guidance](#padding-guidance)
34
- - [Rounded non-icon artwork](#rounded-non-icon-artwork)
33
+ - [Padding and geometry](#padding-and-geometry)
35
34
  - [Cleanup legacy branding artifacts](#cleanup-legacy-branding-artifacts)
36
35
  - [Troubleshooting](#troubleshooting)
37
36
  - [Flag reference](#flag-reference)
@@ -64,7 +63,7 @@ Pass `--dry-run` to preview without writing any files:
64
63
  purgetss brand --dry-run
65
64
  ```
66
65
 
67
- You may also pass a source directly. When a standalone Classic project has no canonical `purgetss/brand/logo.{svg,png}`, the confirmed run moves that positional source into the convention and reports the destination. An existing canonical logo is never replaced silently.
66
+ You may also pass a source directly. When the project (Alloy or Classic) has no canonical `purgetss/brand/logo.{svg,png}`, the confirmed in-place run **moves** that positional `.svg`/`.png` to `purgetss/brand/logo.<ext>` and reports the destination; `--dry-run` only reports where it would go. An existing canonical logo is never replaced, and the positional source is then used from where it is.
68
67
 
69
68
  ## The `purgetss/brand/` convention
70
69
 
@@ -121,6 +120,8 @@ You can also pass a path directly or point to a logo from the config. Useful whe
121
120
  purgetss brand ./docs/snap-logo.svg
122
121
  ```
123
122
 
123
+ A positional logo is moved into `purgetss/brand/logo.<ext>` when no canonical logo exists yet (see [Quick start](#quick-start)). To keep a master in `docs/`, point to it from the config instead, or use a `--<piece>-logo` flag.
124
+
124
125
  Or in `purgetss/config.cjs`, where each piece takes its own `logo`:
125
126
 
126
127
  ```javascript
@@ -236,7 +237,7 @@ And these live at the top level:
236
237
 
237
238
  | Key | Default | Purpose |
238
239
  | --- | --- | --- |
239
- | `background` | `'#FFFFFF'` | The background every piece inherits unless it declares its own: the Android adaptive background layer, the `DefaultIcon-ios.png` flatten, the splash canvases, and the marketplace flatten when a background is explicitly configured. |
240
+ | `background` | `'#FFFFFF'` | The background every piece inherits unless it declares its own: the Android adaptive background layer, the `DefaultIcon-ios.png` flatten, the splash canvases, and the marketplace flatten. |
240
241
  | `artworkCornerRadius` | `'0%'` | Shared radius for the four supported non-icon artwork pieces. It never changes store or launcher icons. |
241
242
  | `splashCornerRadius` | — | Optional splash-only override for `iosSplash` and `androidSplash`. |
242
243
  | `confirmOverwrites` | `true` | Whether `brand` asks before overwriting project files in place. |
@@ -304,7 +305,7 @@ The output is automatically routed to the right directory for your project layou
304
305
 
305
306
  ```text
306
307
  <project>/
307
- ├── DefaultIcon.png ← 1024×1024, universal fallback (brand.icon.padding)
308
+ ├── DefaultIcon.png ← 1024×1024, universal fallback, opaque (brand.icon.padding)
308
309
  ├── DefaultIcon-ios.png ← 1024×1024, iOS flattened on brand.background
309
310
  ├── DefaultIcon-Dark.png ← 1024×1024, iOS 18+ dark (transparent per Apple HIG)
310
311
  ├── DefaultIcon-Tinted.png ← 1024×1024, iOS 18+ tinted (grayscale on black)
@@ -531,7 +532,7 @@ The second one colors the notification's accent, not the icon. Create or merge `
531
532
 
532
533
  Titanium builds `LaunchLogo.imageset` itself on every iOS build, resizing one source into the five sizes it needs. It looks for `LaunchLogo.png` first and falls back to `DefaultIcon.png`.
533
534
 
534
- So there is nothing for PurgeTSS to generate there, but there is something to choose. With only `DefaultIcon.png` around, the launch screen shows your app icon, safe-zone padding and all. Dropping a `logo-launch.svg` (or `.png`) into `purgetss/brand/` makes `brand` write a `LaunchLogo.png`, and the launch screen shows the full logotype instead:
535
+ So there is nothing for PurgeTSS to generate there, but there is something to choose. With only `DefaultIcon.png` around, the launch screen shows your app icon with its configured icon inset (`0%` by default). Dropping a `logo-launch.svg` (or `.png`) into `purgetss/brand/` makes `brand` write a `LaunchLogo.png`, and the launch screen shows the full logotype instead:
535
536
 
536
537
  ```bash
537
538
  cp docs/my-wordmark.svg purgetss/brand/logo-launch.svg
@@ -580,7 +581,7 @@ purgetss brand --no-dark --no-tinted
580
581
  > Upstream work in progress
581
582
  > As of April 2026, Titanium SDK picks up `DefaultIcon-ios.png` automatically but does **not** yet wire `DefaultIcon-Dark.png` / `DefaultIcon-Tinted.png` into the generated iOS appiconset. Upstream tracking: [tidev/titanium-sdk#14122](https://github.com/tidev/titanium-sdk/issues/14122).
582
583
  >
583
- > Until that PR lands, after your first iOS build you may need to add the two PNGs manually into `build/iphone/Assets.xcassets/AppIcon.appiconset/` in Xcode (via the "Appearance" column in the asset catalog editor). Once #14122 merges, the command becomes fully end-to-end.
584
+ > Until that issue is resolved, after your first iOS build you may need to add the two PNGs manually into `build/iphone/Assets.xcassets/AppIcon.appiconset/` in Xcode (via the "Appearance" column in the asset catalog editor). Once #14122 is resolved, the command becomes fully end-to-end.
584
585
 
585
586
  ## Brand color
586
587
 
@@ -598,7 +599,7 @@ purgetss brand --bg-color "#0B1326"
598
599
 
599
600
  Any piece can opt out with its own `background`. That is what `dark: { background: null }` does in the default config, which keeps `DefaultIcon-Dark.png` transparent per Apple HIG.
600
601
 
601
- If you never pass the flag, background stays `#FFFFFF`. `iTunesConnect.png` and `MarketplaceArtwork.png` keep their alpha channel to match Titanium's default; `MarketplaceArtworkFeature.png` is always flattened for Google Play.
602
+ If you never pass the flag, background stays `#FFFFFF`. `iTunesConnect.png`, `MarketplaceArtwork.png` and `MarketplaceArtworkFeature.png` are always flattened to opaque output on the resolved background.
602
603
 
603
604
  ### Matching the launch background
604
605
 
@@ -606,93 +607,9 @@ If you never pass the flag, background stays `#FFFFFF`. `iTunesConnect.png` and
606
607
 
607
608
  `purgetss brand --notes` prints that setup with the project's current `brand.background` already filled in.
608
609
 
610
+ ## Padding and geometry
609
611
 
610
- ## Padding guidance
611
-
612
- Padding belongs to the piece, not to the project. Each piece has its own key and its own flag:
613
-
614
- | Piece | Config | Flag | Default |
615
- | --- | --- | --- | --- |
616
- | `adaptive` | `brand.adaptive.padding` | `--android-adaptive-padding` | `18%` |
617
- | `legacy-icon` | `brand.legacyIcon.padding` | `--android-legacy-padding` | `10%` |
618
- | `appicon` | `brand.appicon.padding` | `--appicon-padding` | `10%` |
619
- | `icon` | `brand.icon.padding` | `--ios-padding` | `0%` |
620
- | `feature-graphic` | `brand.featureGraphic.padding` | `--feature-graphic-padding` | `12%` |
621
- | `launch-logo` | `brand.launchLogo.padding` | `--launch-logo-padding` | `12%` |
622
- | `android-splash` | `brand.androidSplash.padding` | `--android-splash-padding` | `26%` |
623
- | `ios-splash` | `brand.iosSplash.padding` | `--ios-splash-padding` | `26%` |
624
-
625
- `--padding` is a shortcut for the two Android launcher paddings in a single run, and `--splash-padding` for the two splash paddings. `--ios-padding` moves the four square iOS/marketplace pieces together (`icon`, `dark`, `tinted`, `marketplace`); in config each of them has its own key.
626
-
627
- There is deliberately **no global padding value that cascades down**. The defaults answer to different constraints: `18%` answers to the Android launcher mask, while finished square iOS/store artwork stays full-bleed at `0%`. One inherited number could silently break the launcher mask or add an unwanted frame to finished artwork. `background`, which has no such trap, is inherited from `brand.background`.
628
-
629
- ### How the source is read, and how sharp the output is
630
-
631
- Two things are worth knowing about what happens to your `logo.svg` or `logo.png` before any padding is applied.
632
-
633
- **The container is what counts, not the artwork's bounding box.** An SVG is read at its `viewBox`, a raster at its full canvas, and neither is trimmed to where the pixels actually are. So whatever margin a designer baked into the file **adds** to the padding configured per piece. A round logo exported inside a 2048×2048 PNG with 25% of its own air, generated at `adaptive: { padding: '18%' }`, ends up covering about 32% of the icon canvas, not 64%. If a mark comes out smaller than the numbers suggest, that is almost always why: crop the source or lower the padding.
634
-
635
- **The masters are sized to the run.** The source is rasterized once into two intermediate masters, and every piece scales down from them, so their resolution is the ceiling on output sharpness. Rather than a fixed size, `brand` measures the largest number of pixels any selected piece will ask for and builds the masters at exactly that. A default run reports it:
636
-
637
- ```text
638
- • Masters at 942 px — the largest any selected piece asks for
639
- ```
640
-
641
- Lower a padding and the figure rises with it (`--splash-padding 4` needs 1413 px), so output never goes soft against a fixed ceiling. Every destination is a reduction, never an upscale.
642
-
643
- The one case this cannot fix is a raster source that is simply too small: a 512-px PNG cannot produce a sharp 942-px icon. Prefer SVG, or a PNG of at least 1024×1024.
644
-
645
- ### Splash padding
646
-
647
- The 28 splash images (`default.png`, the 11 `res-*`, and the 16 iPhone launch images) share one rule: the logo is fitted into a square whose side is a share of the canvas's **shorter** side.
648
-
649
- Measuring against the shorter side is what lets a single number work across canvases as different as 1440×2560 and 800×480: at 800×480 the limit comes from the height, at 240×400 from the width, and the logo keeps the same visual weight in portrait and in landscape.
650
-
651
- | `androidSplash.padding` / `iosSplash.padding` | Logo | `default.png` (1440×2560) | `res-notlong-port-mdpi` (320×480) |
652
- | --- | --- | --- | --- |
653
- | `20%` | 60% of the shorter side | 864 px | 192 px |
654
- | `26%` (default) | 48% | 691 px | 153 px |
655
- | `30%` | 40% | 576 px | 128 px |
656
- | `35%` | 30% | 432 px | 96 px |
657
-
658
- The `26%` default is calibrated against the Titanium template itself: the Alloy logo in the stock `default.png` measures 665×488 px on a 1440×2560 canvas, so `26%` lands within 4% of the size Titanium ships.
659
-
660
- Before v7.13.0 none of this was configurable: `default.png` used a hardcoded box of 72% × 26% of its own canvas, and the `res-*` set a separate hardcoded 60%. Two rules for the same piece, neither adjustable.
661
-
662
- ## Rounded non-icon artwork
663
-
664
- `brand.artworkCornerRadius` rounds artwork only in the 16 iPhone launch images, Android `default.png` plus its 11 qualifier variants, `MarketplaceArtworkFeature.png`, and `LaunchLogo.png`. A piece may override it with `cornerRadius`; `brand.splashCornerRadius` is an optional shared override for the two legacy splash pieces.
665
-
666
- Values are integer numbers or percentage strings from `0` through `50`, measured against the shorter side of the resized artwork. `0%` preserves the previous output byte for byte; `50%` makes square artwork circular and a wordmark capsule-shaped. Normal and `--dry-run` summaries report the effective padding and radius.
667
-
668
- Precedence is:
669
-
670
- - Feature Graphic / LaunchLogo: piece-specific flag → `--artwork-corner-radius` → piece config → `brand.artworkCornerRadius` → `0%`.
671
- - Legacy splashes: platform flag → `--splash-corner-radius` → `--artwork-corner-radius` → piece config → `brand.splashCornerRadius` → `brand.artworkCornerRadius` → `0%`.
672
-
673
- Store and launcher icons remain unmasked for platform processing. `cornerRadius` is rejected in `DefaultIcon*`, `iTunesConnect.png`, `MarketplaceArtwork.png`, adaptive, legacy, and app icons, Android 12+ `splash_icon.png`, notification icons, and any other unsupported piece; invalid, fractional, negative, non-numeric, or greater-than-50 values also abort before files are written.
674
-
675
- ### Adaptive icon padding
676
-
677
- Android's adaptive canvas is 108 dp. The mask leaves roughly 72 dp visible, and the **guaranteed** safe area is a 66 dp circle inscribed in it. What each padding means in those terms:
678
-
679
- | Padding | Logo | vs. the 66 dp safe circle | vs. the ~72 dp the mask shows |
680
- | --- | --- | --- | --- |
681
- | `15%` | 75.6 dp | outside | **outside** — clipped on any launcher |
682
- | `16%` | 73.4 dp | outside | **outside** |
683
- | `18%` | 69.1 dp | corners outside | inside — **the default** |
684
- | `19.44%` | 66.0 dp | exactly on it | inside |
685
- | `20%` | 64.8 dp | inside | inside — most conservative |
686
-
687
- `18%` sits between the guaranteed circle and the mask edge: a logo that carries its own margin never reaches those corners, which is why it is a safe default in practice. Drop to `20%` if your mark runs edge to edge and you see clipping on a circular launcher.
688
-
689
- A useful visual check is the "corners" heuristic: imagine a circle inscribed in your 1024×1024 canvas with the given padding. If your logo's outermost corners fit inside that circle, you're safe on circular launchers (Pixel default, Oppo Android 15). If they poke out, they'll be clipped.
690
-
691
- The official Android spec floor is `19.44%` (108dp canvas, 66dp inscribed safe-zone circle). That is the theoretical worst-case for aggressive adaptive masks, which is why the adaptive default sits close to it.
692
-
693
- ### Legacy icon padding
694
-
695
- Legacy `ic_launcher.png` does not go through the same adaptive mask, so it can usually run tighter. That is why the default for `brand.legacyIcon.padding` is `10%`.
612
+ Per-piece padding, how the source is read, the shared splash sizing rule, rounded non-icon artwork and the adaptive-mask math are in [Brand Padding & Geometry](./brand-padding-geometry.md).
696
613
 
697
614
  ## Cleanup legacy branding artifacts
698
615
 
@@ -765,6 +682,28 @@ Adaptive padding is probably too generous. Lower it:
765
682
  purgetss brand --android-adaptive-padding 17
766
683
  ```
767
684
 
685
+ ### A white frame appears around a dark icon
686
+
687
+ PurgeTSS does not require a white frame. Square iOS/store pieces (`icon`, `dark`, `tinted`, `marketplace`) default to `0%` since v7.14.0, so finished edge-to-edge artwork stays full-bleed. If an older or customized config still has `brand.icon.padding: '4%'`, change it to `0%` and regenerate.
688
+
689
+ Android launcher pieces keep safe-zone padding on purpose (`18%` adaptive, `10%` legacy and appicon). If the source is already a finished opaque icon, that inset exposes `brand.background`. Set the inherited background once to a color matching the artwork's perimeter, or give the Android pieces a transparent mark instead of the finished icon canvas:
690
+
691
+ ```javascript
692
+ brand: {
693
+ background: '#020109',
694
+ icon: { padding: '0%' },
695
+ adaptive: { logo: './purgetss/brand/logo-adaptive.png', padding: '18%' },
696
+ legacyIcon: { padding: '10%' },
697
+ appicon: { padding: '10%' }
698
+ }
699
+ ```
700
+
701
+ When the main logo is opaque to its edges and a padded piece sits on a contrasting background, `brand` warns before generating the pieces (it checks `icon`, `marketplace`, `adaptive`, `legacy-icon` and `appicon`): `The source is opaque to its edges (edge color ≈ #…)` followed by `A contrasting frame will be visible in: …`, naming each affected piece with its padding and background. Pieces with their own `logo` are not checked.
702
+
703
+ ### "The source logo is not close to square"
704
+
705
+ `brand` prints this warning when the main logo's aspect ratio is wider than 1.25:1 or taller than 1:1.25. Launcher icons and the Android 12+ system splash center the mark inside a mask, so a wordmark comes out cramped. Drop a square `logo-adaptive.svg` (and `logo-splash-icon.svg` if you use `--splash-icon`) as described in [Community-Discovered Patterns](#community-discovered-patterns).
706
+
768
707
  ### The monochrome version looks like a white blob
769
708
 
770
709
  Your colored logo likely has multi-color detail that does not survive automatic whitening. Provide a dedicated silhouette:
@@ -784,7 +723,7 @@ Apple requires App Store icons to have no alpha channel. `DefaultIcon-ios.png` i
784
723
 
785
724
  ### The dark variant doesn't show on my iPhone
786
725
 
787
- Dark variants require iOS 18+ and Titanium SDK automatic wiring (tracked upstream in [titanium-sdk#14122](https://github.com/tidev/titanium-sdk/issues/14122)). Until that PR merges, you may need to add `DefaultIcon-Dark.png` and `DefaultIcon-Tinted.png` manually into the Xcode appiconset after the first iOS build.
726
+ Dark variants require iOS 18+ and Titanium SDK automatic wiring (tracked upstream in [titanium-sdk#14122](https://github.com/tidev/titanium-sdk/issues/14122)). Until that issue is resolved, you may need to add `DefaultIcon-Dark.png` and `DefaultIcon-Tinted.png` manually into the Xcode appiconset after the first iOS build.
788
727
 
789
728
  ### I get "Input image exceeds pixel limit" on an SVG from Affinity / Illustrator
790
729