@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
@@ -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
 
@@ -81,7 +81,7 @@ $.navWin.open()
81
81
 
82
82
  > **WARNING**
83
83
  >
84
- > Call `Appearance.init()` **before** `$.navWin.open()` (or whichever window you open first). If the window opens first, it will render once against the system default and then flicker to the saved mode when `init()` runs — a visible flash on cold launch.
84
+ > Call `Appearance.init()` **before** `$.navWin.open()` (or whichever window you open first). Both official pages place the call before the first window opens.
85
85
 
86
86
  ### 3. Build an Appearance toggle
87
87
 
@@ -158,7 +158,61 @@ function updateUI(value) {
158
158
 
159
159
  The background, text, border, and icon colors in the XML come from semantic color classes (`bg-surface`, `bg-surface-high`, `text-on-surface`, `bg-border`, etc.) — see [semantic-colors.md](./semantic-colors.md) for how to wire those up so the whole view flips when `Appearance.set('dark')` fires.
160
160
 
161
- > **Why named functions in Alloy controllers?** Alloy resolves `onClick="selectDark"` by looking up a top-level identifier on the controller scope. Named function declarations (`function selectDark() { ... }`) hoist and are reliably resolvable; arrow functions assigned to `const` are not hoisted and tend to bite you when an XML attribute fires before the binding has executed. The official setup guide uses named functions for this reason.
161
+ Semantic colors apply anywhere you use `bg-*`, `text-*`, and `border-*` classes: Windows, Views, Labels, Buttons, TextFields, TextAreas, ListViews, and custom classes that reference a semantic name. One appearance switch updates the UI.
162
+
163
+ The official setup guide declares the `onClick` handlers (`selectDark`, `selectLight`, `selectSystem`) as named functions at the top level of the controller.
164
+
165
+ ### Minimal Alloy example
166
+
167
+ The API page uses a flat three-color mapping and a single toggle button:
168
+
169
+ `app/assets/semantic.colors.json`
170
+ ```json
171
+ {
172
+ "surfaceColor": { "light": "#F8FAFC", "dark": "#0F172A" },
173
+ "textColor": { "light": "#0F172A", "dark": "#F8FAFC" },
174
+ "accentColor": { "light": "#2563EB", "dark": "#60A5FA" }
175
+ }
176
+ ```
177
+
178
+ `purgetss/config.cjs`
179
+ ```js
180
+ module.exports = {
181
+ theme: {
182
+ extend: {
183
+ colors: {
184
+ surface: 'surfaceColor',
185
+ text: 'textColor',
186
+ accent: 'accentColor'
187
+ }
188
+ }
189
+ }
190
+ }
191
+ ```
192
+
193
+ `app/views/index.xml`
194
+ ```xml
195
+ <Alloy>
196
+ <Window class="bg-surface">
197
+ <Label class="text-text" text="Appearance" />
198
+ <Button class="bg-accent text-white" title="Toggle" onClick="toggleAppearance" />
199
+ </Window>
200
+ </Alloy>
201
+ ```
202
+
203
+ `app/controllers/index.js`
204
+ ```js
205
+ const { Appearance } = require('purgetss.ui')
206
+
207
+ Appearance.init()
208
+
209
+ function toggleAppearance() {
210
+ Appearance.toggle()
211
+ Ti.API.info(`Mode: ${Appearance.get()}`)
212
+ }
213
+
214
+ $.index.open()
215
+ ```
162
216
 
163
217
  ## Titanium Classic
164
218
 
@@ -190,9 +244,18 @@ const toggleButton = Ti.UI.createButton({
190
244
  backgroundColor: 'accentColor'
191
245
  })
192
246
 
247
+ function updateModeLabel() {
248
+ title.text = `Mode: ${Appearance.get()}`
249
+ }
250
+
193
251
  function toggleAppearance() {
194
252
  Appearance.toggle()
195
- title.text = `Mode: ${Appearance.get()}`
253
+ updateModeLabel()
254
+ }
255
+
256
+ function useSystemAppearance() {
257
+ Appearance.set('system')
258
+ updateModeLabel()
196
259
  }
197
260
 
198
261
  function disposeWindow() {
@@ -205,6 +268,8 @@ window.add(title)
205
268
  window.add(toggleButton)
206
269
  window.addEventListener('close', disposeWindow)
207
270
  window.open()
271
+
272
+ // Call useSystemAppearance() from your own System mode control.
208
273
  ```
209
274
 
210
275
  Run a full native build after adding or changing semantic keys. A LiveView reload alone does not refresh Titanium's native color catalog. The `purgetss semantic` command can create the JSON for a Classic project; it writes only `Resources/semantic.colors.json`, not `config.cjs`, TSS, or utility mappings.
@@ -261,7 +326,7 @@ app startup
261
326
  ## Related
262
327
 
263
328
  - [semantic-colors.md](./semantic-colors.md) — defining `app/assets/semantic.colors.json` and mapping the color names in `config.cjs` so views actually respond to mode changes.
264
- - [cli-commands.md#semantic-command](./cli-commands.md#semantic-command) — the `purgetss semantic` command; Alloy also receives `config.cjs` mappings, while Classic receives only the native JSON.
329
+ - [color-commands.md#semantic-command](./color-commands.md#semantic-command) — the `purgetss semantic` command; Alloy also receives `config.cjs` mappings, while Classic receives only the native JSON.
265
330
  - [customization-deep-dive.md](./customization-deep-dive.md) — full `config.cjs` structure, including the `theme.extend.colors` section where semantic names are registered.
266
331
  - [purgetss-ui-classic.md](./purgetss-ui-classic.md) — generated module path, Classic animation factory, method contracts, and cleanup.
267
332
 
@@ -269,5 +334,5 @@ app startup
269
334
 
270
335
  These field-tested patterns are now also confirmed by the official Appearance documentation:
271
336
 
272
- - **Always call `Appearance.init()` before opening the first window.** Opening the window first causes a visible flicker on cold launch as the UI repaints from the system default to the saved mode. The official best-practices guide puts `Appearance.init()` on line 3 of `app/controllers/index.js`, immediately before `$.navWin.open()`, for exactly this reason.
337
+ - **Always call `Appearance.init()` before opening the first window.** The official best-practices guide calls `Appearance.init()` right after the `require` in `app/controllers/index.js`, before `$.navWin.open()`. The docs give no reason beyond the ordering; the flicker on cold launch when the window opens first is a community observation.
273
338
  - **`toggle()` is not a three-state cycle.** It only alternates between `'light'` and `'dark'`. If the saved mode is `'system'`, the first `toggle()` call lands on `'dark'`. Build a cycle yourself with `get()` + `set(...)` when you need one.
@@ -273,7 +273,7 @@ module.exports = {
273
273
  extend: {
274
274
  ImageView: {
275
275
  ios: {
276
- apply: 'hires-true'
276
+ apply: 'hires'
277
277
  }
278
278
  },
279
279
  View: {
@@ -467,19 +467,27 @@ Constants like `Ti.UI.iOS.CLIP_MODE_ENABLED` or `Ti.UI.iOS.StatusBar.LIGHT_CONTE
467
467
 
468
468
  **See the dedicated reference: [`ios-large-titles.md`](./ios-large-titles.md)** — it covers the full pattern (three-property pairing, global-defaults recipe, TabGroup implicit NavigationWindow behavior, detail-window opt-out, and the ScrollView `content-w-screen` / `content-h-auto` pairing).
469
469
 
470
- Minimal recap — when Large Titles are in use, set the base iOS Window defaults once via `apply` in `config.cjs` rather than repeating them in every XML view:
470
+ Minimal recap — when Large Titles are in use, set the base iOS Window defaults once via `apply` in `config.cjs` rather than repeating them in every XML view. Put it under `theme.extend.Window` so the framework's white `backgroundColor` default is kept (a top-level `theme.Window` is replace mode and drops it):
471
471
 
472
472
  `./purgetss/config.cjs`
473
473
  ```javascript
474
474
  module.exports = {
475
475
  theme: {
476
- Window: {
477
- ios: {
478
- apply: 'auto-adjust-scroll-view-insets extend-edges-all large-title-enabled'
476
+ extend: {
477
+ Window: {
478
+ ios: {
479
+ apply: 'auto-adjust-scroll-view-insets extend-edges-all large-title-enabled'
480
+ }
479
481
  }
480
482
  }
481
483
  }
482
484
  };
483
485
  ```
484
486
 
485
- The `ios:` block (not an inline `ios:` prefix) is required because these classes only exist with a `[platform=ios]` suffix — see "Platform-Specific Classes" above. The *why* (rendering delay vs content-behind-nav-bar), the display-mode constants, and per-window overrides live in [`ios-large-titles.md`](./ios-large-titles.md) and in the official PurgeTSS docs at [Best Practices → Large Titles on iOS](https://purgetss.com/docs/best-practices/3-large-titles-on-ios).
487
+ `./purgetss/styles/utilities.tss`
488
+ ```tss
489
+ 'Window': { backgroundColor: '#FFFFFF' }
490
+ 'Window[platform=ios]': { autoAdjustScrollViewInsets: true, extendEdges: [ Ti.UI.EXTEND_EDGE_ALL ], largeTitleEnabled: true }
491
+ ```
492
+
493
+ These three classes exist without a platform suffix in `utilities.tss`, so they would resolve from a `default` block too; the `ios:` block is what scopes the generated rule to `[platform=ios]` so Android never receives these properties. The *why* (rendering delay vs content-behind-nav-bar), the display-mode constants, and per-window overrides live in [`ios-large-titles.md`](./ios-large-titles.md) and in the official PurgeTSS docs at [Best Practices → Large Titles on iOS](https://purgetss.com/docs/best-practices/large-titles-on-ios).