@maccesar/titools 4.3.0 → 4.4.2

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 +15 -12
  2. package/package.json +1 -1
  3. package/skills/purgetss/SKILL.md +4 -9
  4. package/skills/ti-api/references/api-core.md +24 -69
  5. package/skills/ti-api/references/api-data-network.md +18 -63
  6. package/skills/ti-api/references/api-media.md +15 -49
  7. package/skills/ti-api/references/api-modules-coremotion-urlsession.md +6 -20
  8. package/skills/ti-api/references/api-modules-map.md +17 -40
  9. package/skills/ti-api/references/api-modules-nfc.md +23 -56
  10. package/skills/ti-api/references/api-ui-android.md +16 -37
  11. package/skills/ti-api/references/api-ui-extras.md +28 -72
  12. package/skills/ti-api/references/api-ui-lists.md +14 -35
  13. package/skills/ti-api/references/api-ui-text-input.md +13 -39
  14. package/skills/ti-api/references/api-ui-windows-navigation.md +17 -43
  15. package/skills/ti-api/references/api-xml-global.md +25 -60
  16. package/skills/ti-expert/SKILL.md +35 -5
  17. package/skills/ti-expert/references/adaptive-layouts.md +15 -43
  18. package/skills/ti-expert/references/alloy-builtins.md +8 -22
  19. package/skills/ti-expert/references/alloy-structure.md +34 -18
  20. package/skills/ti-expert/references/anti-patterns.md +18 -39
  21. package/skills/ti-expert/references/code-conventions.md +5 -3
  22. package/skills/ti-expert/references/controller-patterns.md +31 -26
  23. package/skills/ti-expert/references/examples.md +41 -21
  24. package/skills/ti-expert/references/feedback-migration.md +143 -0
  25. package/skills/ti-expert/references/feedback-surfaces.md +111 -0
  26. package/skills/ti-expert/references/feedback-widget-contracts.md +175 -0
  27. package/skills/ti-expert/references/file-type-association.md +265 -0
  28. package/skills/ti-expert/references/migration-patterns.md +37 -25
  29. package/skills/ti-expert/references/security-device.md +58 -56
  30. package/skills/ti-expert/references/sharing.md +227 -0
  31. package/skills/ti-expert/references/state-management.md +5 -5
  32. package/skills/ti-expert/references/theming.md +109 -392
  33. package/skills/ti-guides/references/coding-best-practices.md +1 -2
  34. package/skills/ti-guides/references/style-and-conventions.md +1 -2
  35. package/skills/ti-howtos/references/debugging-profiling.md +1 -2
  36. package/skills/ti-howtos/references/location-and-maps.md +1 -2
  37. package/skills/ti-howtos/references/using-modules.md +1 -2
  38. package/skills/ti-howtos/references/web-content-integration.md +1 -2
  39. package/skills/ti-howtos/references/webpack-build-pipeline.md +1 -2
  40. package/skills/ti-ui/references/listviews-and-performance.md +5 -10
  41. package/skills/ti-ui/references/orientation.md +1 -2
@@ -0,0 +1,175 @@
1
+ # Contracts for feedback Widgets
2
+
3
+ Use these contracts for app-owned Snackbar, Dialog, and Bottom Sheet surfaces in Alloy. Namespaces and visual tokens belong to each app; behavior stays stable.
4
+
5
+ ## Contents
6
+
7
+ - [Boundary and registration](#boundary-and-registration)
8
+ - [Snackbar](#snackbar)
9
+ - [Dialog](#dialog)
10
+ - [Bottom Sheet](#bottom-sheet)
11
+ - [Shared lifecycle contract](#shared-lifecycle-contract)
12
+ - [PurgeTSS integration](#purgetss-integration)
13
+ - [Widget, Require, state, or native UI](#widget-require-state-or-native-ui)
14
+
15
+ ## Boundary and registration
16
+
17
+ Implement each surface as an independent Alloy Widget under `app/widgets/`. Each Widget owns its XML, controller, optional styles, assets, state, listeners, timers, and `widget.json`. Keep them independent from any third-party component library — including TiKit, the Alloy/PurgeTSS component set. A feedback surface must not require one to work, and must not be folded into one: its value comes from being installable and testable on its own.
18
+
19
+ Register every Widget in `app/config.json`:
20
+
21
+ ```json
22
+ {
23
+ "dependencies": {
24
+ "com.example.ui.bottomsheet": "1.0.0",
25
+ "com.example.ui.dialog": "1.0.0",
26
+ "com.example.ui.snackbar": "1.0.0"
27
+ }
28
+ }
29
+ ```
30
+
31
+ Use the app's reverse-domain namespace. The dependency key must match the `id` in that Widget's `widget.json`.
32
+
33
+ Declare Widgets in XML, normally as the last visual children of the owning window so overlays render above screen content:
34
+
35
+ ```xml
36
+ <Widget id="snackbar" src="com.example.ui.snackbar" />
37
+ <Widget id="dialog" src="com.example.ui.dialog" />
38
+ <Widget id="bottomSheet" src="com.example.ui.bottomsheet" />
39
+ ```
40
+
41
+ Build the stable proxy tree in XML. JavaScript may update properties and supply `ListView` data; it must not reconstruct the complete surface imperatively.
42
+
43
+ ## Snackbar
44
+
45
+ ```javascript
46
+ $.snackbar.show({
47
+ message,
48
+ tone: 'neutral', // neutral | success | warning | error
49
+ duration: 4500,
50
+ actionTitle,
51
+ onAction,
52
+ onDismiss
53
+ })
54
+
55
+ $.snackbar.dismiss()
56
+ $.snackbar.destroy()
57
+ ```
58
+
59
+ Contract:
60
+
61
+ - A new message replaces the visible message and restarts the timer.
62
+ - Default `duration` is 4500 ms; `duration: 0` remains visible until an action, `dismiss()`, replacement, or destruction.
63
+ - Allow at most one text action, such as Undo.
64
+ - Invoke `onAction` once, then close the Snackbar.
65
+ - Invoke `onDismiss` once with the reason `timeout`, `action`, `replaced`, `programmatic`, or `destroy`.
66
+ - Clear the old timer before replacement and on every close path.
67
+ - Position above bottom navigation and inside the current safe area.
68
+ - Queueing Snackbars is not the default. The newest relevant message wins.
69
+
70
+ ## Dialog
71
+
72
+ ```javascript
73
+ $.dialog.attach(window)
74
+
75
+ $.dialog.show({
76
+ title,
77
+ message,
78
+ tone: 'neutral', // neutral | warning | error
79
+ confirmTitle,
80
+ cancelTitle,
81
+ destructive: false,
82
+ onConfirm,
83
+ onCancel
84
+ })
85
+
86
+ $.dialog.dismiss('programmatic')
87
+ $.dialog.destroy()
88
+ ```
89
+
90
+ Contract:
91
+
92
+ - Without `cancelTitle`, present an acknowledgement. With it, present a confirmation.
93
+ - Enqueue additional `show()` calls FIFO. Never stack two Dialogs visually.
94
+ - Disable both buttons on the first accepted tap.
95
+ - Invoke `onConfirm` or `onCancel` at most once; never both.
96
+ - Cancel button, Android Back, or allowed backdrop dismissal invokes `onCancel`. Android Back closes an acknowledgement as its single dismissal action.
97
+ - A destructive confirmation must not close from a backdrop tap.
98
+ - `dismiss('programmatic')` closes without treating the operation as a user confirmation or cancellation.
99
+ - `destroy()` clears the active request and queue without running business callbacks.
100
+ - Use platform-familiar safe/cancel ordering and a distinct destructive role; do not hard-code one platform's button order for both.
101
+
102
+ ## Bottom Sheet
103
+
104
+ ```javascript
105
+ $.bottomSheet.attach(window)
106
+
107
+ $.bottomSheet.show({
108
+ title,
109
+ message,
110
+ items: [{
111
+ id,
112
+ title,
113
+ subtitle,
114
+ icon,
115
+ tone: 'neutral',
116
+ enabled: true
117
+ }],
118
+ selectedId,
119
+ onSelect,
120
+ onCancel
121
+ })
122
+
123
+ $.bottomSheet.dismiss('cancel')
124
+ $.bottomSheet.destroy()
125
+ ```
126
+
127
+ Contract:
128
+
129
+ - Render items with a fixed `ListView` template and data objects. Support long lists, disabled rows, optional subtitles/icons, and a selected marker.
130
+ - Enqueue additional `show()` calls FIFO. Never stack sheets.
131
+ - Accept the first enabled selection only, invoke `onSelect(item)` once, and close.
132
+ - Backdrop, Android Back, Cancel, or `dismiss('cancel')` invokes `onCancel` once.
133
+ - Other programmatic dismissal and `destroy()` do not imply cancellation and do not execute business callbacks.
134
+ - Present from the bottom on phones. On tablets and large screens, use a centered, width-limited action surface. Reserve `anchor` for a future anchored variant; do not expose behavior that is not implemented.
135
+
136
+ ## Shared lifecycle contract
137
+
138
+ - Prefer one instance of each needed Widget per window. Add a global coordinator only after real cross-window requirements exist.
139
+ - Call `attach(window)` after the host window/controller exists. Call `destroy()` from the host's close cleanup.
140
+ - Make `destroy()` idempotent. Remove listeners, clear timers, cancel pending animation callbacks, clear queues, and release callback references.
141
+ - Guard every asynchronous completion with the current presentation generation so a stale timer or animation cannot dismiss a newer request.
142
+ - Consume backdrop events and prevent them from bubbling to underlying controls.
143
+ - Save the host's previous accessibility state, exclude underlying content while modal, move focus to the surface, and restore the saved state on close.
144
+ - Keep localized copy outside the Widget. Pass strings and business callbacks in the public API.
145
+ - Keep business logic out of the Widget. It reports a choice; the host performs domain work.
146
+ - Verify every callback path for exactly-once behavior: action, confirm, cancel, Back, backdrop, timeout, replacement, programmatic dismissal, and destruction.
147
+
148
+ ## PurgeTSS integration
149
+
150
+ When `purgetss/` or `purgetss/config.cjs` exists, load the `purgetss` skill before writing styles. `ti-expert` decides the component boundary; `purgetss` decides the verified utilities and configuration.
151
+
152
+ Apply these architectural rules:
153
+
154
+ - Set PurgeTSS `options.widgets` to `true` so Widget XML is scanned.
155
+ - Express stable anatomy and utility classes in Widget XML.
156
+ - Use semantic color names for surfaces, text, borders, accent, overlay, success, warning, and error. Let Titanium resolve light/dark variants.
157
+ - Do not edit generated `app/styles/app.tss`.
158
+ - Use `widget.tss` only for properties that have no verified utility or that are inherently specific to the Widget.
159
+ - Declare animations as XML `Animation` proxies using `purgetss.ui`. Use explicit `open()` and `close()` calls instead of toggle behavior.
160
+ - After generation, inspect the final `app.tss` section for unused or unsupported classes and resolve every new entry.
161
+
162
+ Do not copy class inventories into this skill and do not guess Tailwind-like names. The installed PurgeTSS skill remains the source of truth.
163
+
164
+ ## Widget, Require, state, or native UI
165
+
166
+ | Choose | When |
167
+ | --- | --- |
168
+ | Alloy Widget | Self-contained public API, state/lifecycle ownership, portable boundary, or reuse across apps/screens |
169
+ | `<Require>` controller | App-specific view/controller composition that shares the host's domain and lifecycle |
170
+ | Screen state in XML | Loading, empty, content, or error presentation belongs only to that screen |
171
+ | TSS or PurgeTSS utilities | Repeated visual treatment has no behavior, state, or lifecycle |
172
+ | Native/system UI | The operating system owns permissions, trust, files, sharing, identity, purchases, or another protected flow |
173
+
174
+ Usage count is evidence, not a threshold. A component can deserve a Widget on its first use if it has a stable portable API and non-trivial lifecycle; three similar labels still do not deserve one.
175
+
@@ -0,0 +1,265 @@
1
+ # File type association
2
+
3
+ How to make the OS hand your app a file — so a document with your extension opens your app when the user taps it, arrives over AirDrop or Quick Share, or is picked from another app's share sheet.
4
+
5
+ This is a configuration problem more than a coding one, and it fails in a specific way that wastes days: the system gives no error, no warning, and no log line when a key is in the wrong place. The app simply never gets the file, and every symptom looks like "iOS/Android doesn't allow this", which is the wrong conclusion. Most of this file is about telling those failures apart.
6
+
7
+ ## Contents
8
+
9
+ 1. [Decide what you are declaring](#1-decide-what-you-are-declaring)
10
+ 2. [iOS: declaring the type](#2-ios-declaring-the-type)
11
+ 3. [iOS: claiming the documents](#3-ios-claiming-the-documents)
12
+ 4. [iOS: the root-level keys](#4-ios-the-root-level-keys)
13
+ 5. [iOS: receiving the file](#5-ios-receiving-the-file)
14
+ 6. [Android: the intent filter](#6-android-the-intent-filter)
15
+ 7. [Android: receiving the file](#7-android-receiving-the-file)
16
+ 8. [Document icons](#8-document-icons)
17
+ 9. [Verify instead of assuming](#9-verify-instead-of-assuming)
18
+ 10. [Symptom to cause](#10-symptom-to-cause)
19
+
20
+ ## 1. Decide what you are declaring
21
+
22
+ Two questions decide everything downstream.
23
+
24
+ **Is the type yours or someone else's?** A format your app invented is *exported* — you own the identifier and define what it means. A format that already exists (PDF, CSV, a competitor's format) is *imported* — you are declaring that you can handle a type someone else defined. On iOS these are different keys (`UTExportedTypeDeclarations` vs `UTImportedTypeDeclarations`); declaring someone else's type as exported is how two apps end up fighting over an identifier.
25
+
26
+ **What is the file, as far as the system is concerned?** This matters more than it looks, and it is the first place people go wrong. A backup archive that happens to be a ZIP internally is *not* a ZIP as far as the user is concerned — it is your document. If you declare it as conforming to `public.zip-archive`, iOS Files treats it as an archive: tapping it lists the contents instead of handing it to your app, because that is what the system does with archives, and it is doing exactly what you told it.
27
+
28
+ Conform to `public.data` for an opaque document whose internals are your business. The internal format is an implementation detail; the declared type is a user-facing contract.
29
+
30
+ The same reasoning applies to the extension. `.zip` makes your app volunteer for every ZIP on the device; a distinct extension keeps the association narrow.
31
+
32
+ ## 2. iOS: declaring the type
33
+
34
+ Goes inside `<ios><plist><dict>` in `tiapp.xml`:
35
+
36
+ ```xml
37
+ <key>UTExportedTypeDeclarations</key>
38
+ <array>
39
+ <dict>
40
+ <key>UTTypeIdentifier</key>
41
+ <string>com.example.myapp.mytype</string>
42
+ <key>UTTypeDescription</key>
43
+ <string>My App Document</string>
44
+ <key>UTTypeConformsTo</key>
45
+ <array>
46
+ <string>public.data</string>
47
+ </array>
48
+ <key>UTTypeTagSpecification</key>
49
+ <dict>
50
+ <key>public.filename-extension</key>
51
+ <array>
52
+ <string>mytype</string>
53
+ </array>
54
+ </dict>
55
+ </dict>
56
+ </array>
57
+ ```
58
+
59
+ Use reverse-DNS under your bundle id for the identifier. The extension in `UTTypeTagSpecification` carries no leading dot.
60
+
61
+ For a type you did not invent, use `UTImportedTypeDeclarations` with the same structure and the identifier its owner published.
62
+
63
+ ## 3. iOS: claiming the documents
64
+
65
+ Declaring a type says the type exists. `CFBundleDocumentTypes` says your app handles it:
66
+
67
+ ```xml
68
+ <key>CFBundleDocumentTypes</key>
69
+ <array>
70
+ <dict>
71
+ <key>CFBundleTypeName</key>
72
+ <string>My App Document</string>
73
+ <key>CFBundleTypeRole</key>
74
+ <string>Editor</string>
75
+ <key>LSHandlerRank</key>
76
+ <string>Owner</string>
77
+ <key>LSItemContentTypes</key>
78
+ <array>
79
+ <string>com.example.myapp.mytype</string>
80
+ </array>
81
+ <key>CFBundleTypeExtensions</key>
82
+ <array>
83
+ <string>mytype</string>
84
+ </array>
85
+ </dict>
86
+ </array>
87
+ ```
88
+
89
+ `LSHandlerRank` decides who wins when several apps claim the same type: `Owner` for a format you defined, `Alternate` for one you merely support, `Default` when no other app claims it. `CFBundleTypeRole` is `Editor` if your app can modify the file, `Viewer` if it only reads it.
90
+
91
+ ## 4. iOS: the root-level keys
92
+
93
+ This is where the day gets lost, so it is worth stating plainly: **`LSSupportsOpeningDocumentsInPlace` is a root-level `Info.plist` key.** It is a sibling of `CFBundleDocumentTypes`, not a key inside its dictionary.
94
+
95
+ Nesting it reads as natural — it describes how documents of that type are opened, so it looks like it belongs with the type. iOS does not read it there and says nothing about it. The resulting state is deceptive: the app *is* registered as owner of the type, so the system knows it exists, but nothing ever declared that the app can open it in place. Files falls back to previewing the document, and the behavior is indistinguishable from "iOS won't launch third-party apps from Files" — a conclusion people reach, write down, and design around.
96
+
97
+ ```xml
98
+ <!-- correct: sibling of CFBundleDocumentTypes, at the root of the plist dict -->
99
+ <key>CFBundleDocumentTypes</key>
100
+ <array>...</array>
101
+
102
+ <key>LSSupportsOpeningDocumentsInPlace</key>
103
+ <true/>
104
+ ```
105
+
106
+ Apple's own requirement makes this non-optional: when you declare `CFBundleDocumentTypes`, you must also set either `LSSupportsOpeningDocumentsInPlace` or `UISupportsDocumentBrowser` (the latter only if the app uses `UIDocumentBrowserViewController`). Omitting both also triggers the *Missing Document Configuration* warning from App Store Connect, which is often the first hint anyone gets that something is wrong.
107
+
108
+ There is a second reason to set it beyond launching. Without it, iOS copies the document into the app's Inbox before delivering it — a full second copy on disk. For a document that carries photos or video, that is a real cost paid every time the user opens one.
109
+
110
+ ### The neighboring key that is not the same thing
111
+
112
+ `UIFileSharingEnabled` (also root-level) exposes the app's entire `Documents` directory inside the Files app. It is unrelated to opening a document your app was handed, and it is not required for this to work.
113
+
114
+ Enabling it makes every JSON file, cache and media asset the app keeps in `Documents` browsable by the user. That is a product decision — sometimes the right one, for an app whose documents *are* the user's files — but reaching for it while debugging a file association exposes the app's internals as a side effect of a fix that was never about that.
115
+
116
+ ## 5. iOS: receiving the file
117
+
118
+ The app receives the document URL through the `handleurl` event on `Ti.App.iOS`:
119
+
120
+ ```javascript
121
+ Ti.App.iOS.addEventListener('handleurl', event => {
122
+ const url = event.launchOptions && event.launchOptions.url
123
+ // resolve, validate, then adopt
124
+ })
125
+ ```
126
+
127
+ Register the listener at a point that survives the app being launched *by* the file rather than merely resumed — a cold launch delivers the event early. Store whatever identifies the last handled document, because the same launch payload can be read more than once during a lifecycle and reprocessing it silently imports the same file twice.
128
+
129
+ **Validate before acting.** An intent filter or a UTI claim is a public invitation: any file with the right extension reaches your app, including one that is malformed or came from somewhere unexpected. Read the manifest, check the version, and fail with a message the user understands — never start writing over existing data on the strength of a filename.
130
+
131
+ ### Saving a file back out
132
+
133
+ Two APIs are easy to confuse:
134
+
135
+ - `Ti.UI.iOS.DocumentViewer` is a **previewer**. If the file type is not previewable it does not present and does not report an error.
136
+ - `ti.documentpicker.export()` opens the system destination dialog — this is the one for "save a copy where the user chooses".
137
+
138
+ ## 6. Android: the intent filter
139
+
140
+ Android associates by intent filter on the activity that should receive the file, declared inside `<android><manifest><application>` in `tiapp.xml`:
141
+
142
+ ```xml
143
+ <activity android:name=".MyAppActivity">
144
+ <intent-filter>
145
+ <action android:name="android.intent.action.VIEW"/>
146
+ <category android:name="android.intent.category.DEFAULT"/>
147
+ <category android:name="android.intent.category.BROWSABLE"/>
148
+ <data android:scheme="content"/>
149
+ <data android:scheme="file"/>
150
+ <data android:mimeType="*/*"/>
151
+ <data android:host="*"/>
152
+ <data android:pathPattern=".*\\.mytype"/>
153
+ <data android:pathPattern=".*\\..*\\.mytype"/>
154
+ <data android:pathPattern=".*\\..*\\..*\\.mytype"/>
155
+ </intent-filter>
156
+ </activity>
157
+ ```
158
+
159
+ Three things in that block are not obvious.
160
+
161
+ **`pathPattern` is not a regular expression.** It supports only `*` (zero or more of the preceding character) and `.` (any character), and the matching is greedy without backtracking. `.*\\.mytype` therefore fails on any filename containing an earlier dot — `my.backup.mytype` does not match — so the conventional fix is to repeat the pattern with one extra `.*\\.` per additional dot you want to tolerate. Three variants covers most real filenames. The backslash is doubled because the manifest is XML.
162
+
163
+ **A custom extension usually has no registered MIME type**, so the file arrives as `application/octet-stream` or with whatever type the sending app guessed. `android:mimeType="*/*"` is what makes the filter reachable at all; the `pathPattern` entries are what keep it from volunteering for every file on the device. Both halves are load-bearing — `*/*` alone makes your app an option for opening anything.
164
+
165
+ **Matching is by name, which has a consequence.** A `content://` URI whose path does not carry the filename will not match, no matter what its MIME type says. This is a real limit of the mechanism, not a misconfiguration to hunt down.
166
+
167
+ ### Whether to also filter ACTION_SEND
168
+
169
+ `ACTION_VIEW` covers tapping the file — in a file manager, a messaging app, or a notification. `ACTION_SEND` covers another app sharing content *to* you, which is how Quick Share and most share buttons deliver.
170
+
171
+ Adding `ACTION_SEND` has a side effect worth predicting: your app then appears in its own share sheet, offering to receive the file it just produced. If the app's only outbound flow is exporting a document, that is confusing enough to be worth skipping — tapping the received file still works through `ACTION_VIEW`.
172
+
173
+ ## 7. Android: receiving the file
174
+
175
+ Read the intent from the current activity, falling back to the launch intent:
176
+
177
+ ```javascript
178
+ const activity = Ti.Android.currentActivity || Ti.Android.rootActivity
179
+ const intent = (activity && activity.intent) || Ti.App.Android.launchIntent
180
+ const action = intent.getAction()
181
+
182
+ // ACTION_VIEW → intent.getData()
183
+ // ACTION_SEND → intent.getBlobExtra(Ti.Android.EXTRA_STREAM)
184
+ ```
185
+
186
+ **A `content://` URI is not a path.** It is a handle into another app's content provider, valid only for as long as the grant lasts. Read the blob and write it to a file you own under `applicationDataDirectory` before doing anything else; code that treats it as a filesystem path works on some devices and fails on others, which is the worst kind of bug to inherit.
187
+
188
+ Give that landing spot its own directory, separate from the working area your import routine clears at the start of a run — otherwise the incoming file deletes itself moments before it is read.
189
+
190
+ As on iOS, the same launch intent can be read more than once during a lifecycle. Remember the last one handled.
191
+
192
+ ## 8. Document icons
193
+
194
+ A document icon is what lets someone find their file months later without reading filenames. It is also the part most likely to be quietly ignored by the system, so it is worth confirming before producing a full set of images.
195
+
196
+ **iOS derives one for free — check that before drawing anything.** With no icon keys declared at all, the Files app renders the document as a page with the app icon inside it (verified 2026-08-05 on a Titanium 13.4.0 app: no `UTTypeIconFiles`, no `CFBundleTypeIconFiles`, not a single document PNG in the project). For a type only your app produces, that is usually enough — it reads as "a document belonging to this app", which is the whole job, and it costs nothing to maintain.
197
+
198
+ Note the sequencing this implies: the derived icon is only meaningful once the app icon is final, since it is built from it. Producing a document icon set before app identity is settled means judging a fallback that is itself a placeholder.
199
+
200
+ If you do want a custom one, declare `UTTypeIconFiles` in the type declaration and `CFBundleTypeIconFiles` in the document type entry, both arrays of PNG filenames (with extension) placed in `app/assets/iphone/`. Confirm it is actually honored on a device before producing the full size set — there are long-standing developer reports of the system preferring its derived version anyway.
201
+
202
+ **Android.** The icon belongs on the `intent-filter`, not on the data element:
203
+
204
+ ```xml
205
+ <intent-filter android:icon="@drawable/mytype">
206
+ ```
207
+
208
+ with the drawable under `platform/android/res/drawable/`. Android does not oblige file managers to use it, and several show their own icon based on MIME type — with `application/octet-stream`, expect some to render it generic no matter what you ship.
209
+
210
+ Android has no derived-icon fallback, so the two platforms are not symmetrical: on iOS doing nothing produces something reasonable, while on Android doing nothing leaves the file generic. Decide them separately rather than as one task.
211
+
212
+ ## 9. Verify instead of assuming
213
+
214
+ Every claim in this file is checkable in seconds, and the failure modes are silent enough that checking is the only way to know.
215
+
216
+ **Confirm the built binary, not `tiapp.xml`.** The build normalizes and merges plists, so what you wrote is not necessarily what shipped:
217
+
218
+ ```bash
219
+ # what the installed app actually declares
220
+ plutil -extract LSSupportsOpeningDocumentsInPlace raw -o - "<App>.app/Info.plist"
221
+ plutil -extract CFBundleDocumentTypes xml1 -o - "<App>.app/Info.plist"
222
+ ```
223
+
224
+ A key in the wrong place returns *"No value at that key path"* — the same result as not declaring it at all, which is precisely the point.
225
+
226
+ For a simulator build, locate the bundle with:
227
+
228
+ ```bash
229
+ xcrun simctl get_app_container <device-udid> <bundle-id> app
230
+ ```
231
+
232
+ **On Android, ask the package manager who claims the type:**
233
+
234
+ ```bash
235
+ adb shell cmd package query-activities -a android.intent.action.VIEW \
236
+ -d "file:///sdcard/Download/test.mytype"
237
+ ```
238
+
239
+ Run it twice — once with your extension and once with a neighbouring one (`.zip`, `.txt`) — to confirm the filter is both reachable *and* narrow. A filter that matches everything looks identical to a correct one until you test the negative case.
240
+
241
+ **The final check is a person tapping the file.** Nothing above proves the OS routes it; it only proves the declaration is well formed. Test a cold launch (app not running) separately from a warm one — they take different paths through the launch handling and only one of them may be wired up.
242
+
243
+ ## 10. Symptom to cause
244
+
245
+ | Symptom | Likely cause |
246
+ | --- | --- |
247
+ | iOS Files previews the document instead of launching the app | `LSSupportsOpeningDocumentsInPlace` missing from the **root** of `Info.plist`, or nested inside `CFBundleDocumentTypes` |
248
+ | iOS Files lists the file's contents like a folder | Type conforms to `public.zip-archive` (or another archive type) instead of `public.data` |
249
+ | App is offered for unrelated files of other apps | Extension or MIME type too broad (`.zip`, `application/zip`); on Android, `mimeType="*/*"` with no `pathPattern` |
250
+ | App offers itself in its own share sheet | An `ACTION_SEND` filter that the app's own outbound flow matches |
251
+ | Android: file with several dots in its name never opens the app | `pathPattern` variants missing for the extra dots |
252
+ | Android: a `content://` file never opens the app | The URI path carries no filename — a limit of name-based matching, not a bug |
253
+ | iOS: the same document imports twice | The launch payload was read more than once; no "last handled" guard |
254
+ | *Missing Document Configuration* on App Store upload | `CFBundleDocumentTypes` declared without `LSSupportsOpeningDocumentsInPlace` or `UISupportsDocumentBrowser` |
255
+ | Document icon stays generic | App icon not final (the fallback derives from it), or the file manager ignores the declared icon |
256
+
257
+ ## Sources
258
+
259
+ - Verified 2026-08-05 on a shipping Titanium 13.4.0 app: moving `LSSupportsOpeningDocumentsInPlace` from inside the `CFBundleDocumentTypes` dict to the root of the plist changed `plutil -extract` from *"No value at that key path"* to `true`, and changed Files behavior from previewing the document to launching the app.
260
+ - Verified on the same app: an exported type conforming to `public.zip-archive` made iOS Files list the archive contents; `public.data` did not.
261
+ - Verified with `cmd package query-activities`: a narrow extension plus `pathPattern` stops the app being offered for unrelated archives.
262
+ - Verified 2026-08-05 in the iOS Files app: with no icon keys declared, the document renders as a page with the app icon inside it.
263
+ - `Ti.App.iOS` `handleurl` event — see `ti-api/references/api-app-platform.md`.
264
+ - Apple, *Information Property List* reference, for `LSSupportsOpeningDocumentsInPlace`, `UIFileSharingEnabled` and `UISupportsDocumentBrowser`.
265
+ - Icon key names and sizes come from Apple's archived *Registering the File Types Your App Supports*; confirm sizes against current documentation before producing a full set.
@@ -4,7 +4,8 @@
4
4
  ## Contents
5
5
 
6
6
  - [Assessment checklist](#assessment-checklist)
7
- - [Phase 1: TSS organization](#phase-1-tss-organization)
7
+ - [Phase 1: styling-system organization](#phase-1-styling-system-organization)
8
+ - [Phase 1B: feedback surfaces](#phase-1b-feedback-surfaces)
8
9
  - [Phase 2: service layer extraction](#phase-2-service-layer-extraction)
9
10
  - [Phase 3: event system migration](#phase-3-event-system-migration)
10
11
  - [Phase 4: navigation service](#phase-4-navigation-service)
@@ -21,24 +22,31 @@ Before migrating, evaluate the current codebase:
21
22
 
22
23
  | Area | Signs of Legacy Code | Target State |
23
24
  | ----------- | -------------------------------------------------- | --------------------------------------------- |
24
- | Styling | Inline attributes, scattered styles | Organized TSS files (per-controller + global) |
25
+ | Styling | Inline attributes, scattered styles | PurgeTSS utilities when detected; otherwise organized Alloy TSS |
25
26
  | Controllers | 200+ lines, API calls, business logic | Thin orchestrators (<100 lines) |
26
27
  | Events | `Ti.App.fireEvent` everywhere | Backbone.Events or StateStore |
27
28
  | Navigation | Direct `Alloy.createController().getView().open()` | Navigation service |
28
29
  | Data | Scattered `Ti.App.Properties`, no collections | Centralized state + Collections |
29
30
 
30
- ## Phase 1: TSS organization
31
+ ## Phase 1: styling-system organization
31
32
 
32
- **Goal**: Migrate from inline styling and scattered styles to well-organized TSS files.
33
+ **Goal**: Migrate from inline and scattered styling into the styling system the project actually uses.
34
+
35
+ ### Step 0: detect PurgeTSS
36
+
37
+ Check for `purgetss/` or `purgetss/config.cjs` before editing styles.
38
+
39
+ - **PurgeTSS detected:** invoke the `purgetss` skill, use verified utilities and semantic colors, configure reusable rules in `config.cjs`, and never edit the generated `app/styles/app.tss`.
40
+ - **No PurgeTSS:** use standard per-controller TSS and a manually maintained `app/styles/app.tss` for genuinely global selectors.
41
+
42
+ Do not migrate a PurgeTSS application back into manual TSS by following a generic Alloy example.
33
43
 
34
44
  ### Step 1: audit existing styles
35
45
 
36
- Find all inline attributes and consolidate into TSS files:
46
+ Find inline attributes and classify them for the detected styling system:
37
47
  ```bash
38
48
  # Find inline styling in XML views
39
- grep -r 'backgroundColor=' app/views/
40
- grep -r 'font=' app/views/
41
- grep -r 'color=' app/views/
49
+ rg -n 'backgroundColor=|font=|color=' app/views/
42
50
  ```
43
51
 
44
52
  ### Step 2: move inline attributes to TSS
@@ -51,7 +59,7 @@ grep -r 'color=' app/views/
51
59
  ```
52
60
 
53
61
  ```xml
54
- <!-- AFTER: Clean XML + TSS file -->
62
+ <!-- AFTER in standard Alloy: clean XML + TSS file -->
55
63
  <View id="header">
56
64
  <Label id="title" text="L('welcome')" />
57
65
  </View>
@@ -63,9 +71,11 @@ grep -r 'color=' app/views/
63
71
  "#title": { color: "#333", font: { fontSize: 18, fontWeight: "bold" } }
64
72
  ```
65
73
 
66
- ### Step 3: organize TSS structure
74
+ ### Step 3: organize standard Alloy TSS
75
+
76
+ This step applies only when PurgeTSS is not present.
67
77
 
68
- 1. Use `app.tss` for global styles (shared across all views)
78
+ 1. Use `app.tss` for genuinely global styles shared across views
69
79
  2. Use per-controller TSS files for view-specific styles
70
80
  3. Use class selectors for reusable style patterns:
71
81
  ```tss
@@ -74,6 +84,14 @@ grep -r 'color=' app/views/
74
84
  ".btn-primary": { backgroundColor: '#2563eb', color: '#fff', height: 44, borderRadius: 8 }
75
85
  ```
76
86
 
87
+ In either styling path, replace hard-coded visual colors with semantic roles as part of the migration. See [Theming and Dark Mode](theming.md).
88
+
89
+ ## Phase 1B: feedback surfaces
90
+
91
+ Inventory native alerts, option dialogs, Toasts, and local overlays. Classify each occurrence before replacing it, then migrate one window at a time. Preserve system-owned pickers, permission prompts, sharing, and biometric UI.
92
+
93
+ Use [Feedback Migration](feedback-migration.md) for the sequence, deliberate silence rules, and verification contract.
94
+
77
95
  ## Phase 2: service layer extraction
78
96
 
79
97
  **Goal**: Move business logic from controllers to services.
@@ -280,11 +298,12 @@ const { user } = appStore.getState()
280
298
 
281
299
  Recommended sequence to minimize risk:
282
300
 
283
- 1. **TSS Organization** - Visual only, no logic changes
284
- 2. **Service Layer** - Extract logic without changing behavior
285
- 3. **Navigation Service** - Centralize with cleanup
286
- 4. **Event System** - Replace Ti.App events
287
- 5. **State Management** - Last, as it touches everything
301
+ 1. **Styling-system organization** - Establish semantic tokens and the correct PurgeTSS/Alloy path
302
+ 2. **Feedback surfaces** - Migrate one window without changing domain outcomes
303
+ 3. **Service Layer** - Extract logic without changing behavior
304
+ 4. **Navigation Service** - Centralize with cleanup
305
+ 5. **Event System** - Replace Ti.App events
306
+ 6. **State Management** - Last, as it touches everything
288
307
 
289
308
  ## Rollback strategy
290
309
 
@@ -293,16 +312,9 @@ For each phase:
293
312
  1. **Create a branch** before starting
294
313
  2. **Migrate one feature** completely
295
314
  3. **Test thoroughly** before moving to next feature
296
- 4. **Keep legacy code** commented until stable
315
+ 4. **Delete the replaced implementation** after the vertical slice passes
297
316
 
298
- ```javascript
299
- // Temporary: Keep both during migration
300
- // Legacy:
301
- // Ti.App.fireEvent('user:updated', data)
302
-
303
- // New:
304
- Alloy.Events.trigger('user:updated', data)
305
- ```
317
+ Use version control for rollback. Do not keep commented legacy implementations: they conceal duplicate listeners, stale callbacks, and incomplete migrations.
306
318
 
307
319
  ## Common migration pitfalls
308
320