@maccesar/titools 4.4.0 → 4.5.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 (45) hide show
  1. package/README.md +10 -13
  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 +225 -107
  7. package/skills/ti-api/references/api-modules-coremotion-urlsession.md +6 -20
  8. package/skills/ti-api/references/api-modules-map.md +45 -58
  9. package/skills/ti-api/references/api-modules-nfc.md +23 -56
  10. package/skills/ti-api/references/api-modules-social-misc.md +23 -3
  11. package/skills/ti-api/references/api-ui-android.md +16 -37
  12. package/skills/ti-api/references/api-ui-extras.md +106 -87
  13. package/skills/ti-api/references/api-ui-lists.md +103 -57
  14. package/skills/ti-api/references/api-ui-text-input.md +13 -39
  15. package/skills/ti-api/references/api-ui-windows-navigation.md +95 -93
  16. package/skills/ti-api/references/api-xml-global.md +25 -60
  17. package/skills/ti-expert/SKILL.md +8 -11
  18. package/skills/ti-expert/references/adaptive-layouts.md +15 -43
  19. package/skills/ti-expert/references/alloy-builtins.md +1 -4
  20. package/skills/ti-expert/references/alloy-structure.md +4 -12
  21. package/skills/ti-expert/references/anti-patterns.md +16 -45
  22. package/skills/ti-expert/references/code-conventions.md +2 -6
  23. package/skills/ti-expert/references/controller-patterns.md +1 -3
  24. package/skills/ti-expert/references/examples.md +3 -10
  25. package/skills/ti-expert/references/feedback-migration.md +17 -39
  26. package/skills/ti-expert/references/feedback-surfaces.md +19 -48
  27. package/skills/ti-expert/references/feedback-widget-contracts.md +31 -69
  28. package/skills/ti-expert/references/file-type-association.md +265 -0
  29. package/skills/ti-expert/references/migration-patterns.md +8 -18
  30. package/skills/ti-expert/references/sharing.md +227 -0
  31. package/skills/ti-expert/references/theming.md +20 -45
  32. package/skills/ti-guides/SKILL.md +5 -1
  33. package/skills/ti-guides/references/coding-best-practices.md +1 -2
  34. package/skills/ti-guides/references/compatibility-matrix.md +89 -0
  35. package/skills/ti-guides/references/resources.md +7 -1
  36. package/skills/ti-guides/references/sdk-release-notes.md +75 -0
  37. package/skills/ti-guides/references/style-and-conventions.md +1 -2
  38. package/skills/ti-howtos/references/debugging-profiling.md +1 -2
  39. package/skills/ti-howtos/references/location-and-maps.md +1 -2
  40. package/skills/ti-howtos/references/using-modules.md +1 -2
  41. package/skills/ti-howtos/references/web-content-integration.md +1 -2
  42. package/skills/ti-howtos/references/webpack-build-pipeline.md +1 -2
  43. package/skills/ti-ui/references/listviews-and-performance.md +5 -10
  44. package/skills/ti-ui/references/orientation.md +1 -2
  45. package/skills/ti-ui/references/scrolling-views.md +7 -1
@@ -1,7 +1,6 @@
1
1
  # Feedback surfaces and app-owned UI
2
2
 
3
- Use this guide before choosing a Titanium proxy or drawing a component. The
4
- surface follows the meaning of the event: style comes afterwards.
3
+ Use this guide before choosing a Titanium proxy or drawing a component. The surface follows the meaning of the event: style comes afterwards.
5
4
 
6
5
  ## Contents
7
6
 
@@ -21,12 +20,9 @@ Classify the event in this order:
21
20
  3. **Recovery**: is the action reversible or is data at risk?
22
21
  4. **Location**: does the message belong to a field, a screen, or the whole app?
23
22
  5. **Persistence**: is it transient, persistent until state changes, or modal?
24
- 6. **Choice shape**: is there one acknowledgement, a binary decision, or a list
25
- of related actions?
23
+ 6. **Choice shape**: is there one acknowledgement, a binary decision, or a list of related actions?
26
24
 
27
- Do not begin with "which native dialog can display this string?" That turns
28
- different meanings into the same interruption and makes the app feel visually
29
- inconsistent.
25
+ Do not begin with "which native dialog can display this string?" That turns different meanings into the same interruption and makes the app feel visually inconsistent.
30
26
 
31
27
  ## Feedback scale
32
28
 
@@ -46,20 +42,13 @@ inconsistent.
46
42
  | No content yet | Empty State inside the screen | This is screen state, not transient feedback |
47
43
  | Initial or blocking work | Loading/Progress state in context | Communicates progress without inventing a message |
48
44
 
49
- Reserve Dialogs for critical information. Do not use them merely because they
50
- are easy to create. If an action is common and undoable, prefer immediate
51
- execution plus Undo over confirmation.
45
+ Reserve Dialogs for critical information. Do not use them merely because they are easy to create. If an action is common and undoable, prefer immediate execution plus Undo over confirmation.
52
46
 
53
47
  ### Snackbar versus Toast
54
48
 
55
- For app-owned foreground feedback, standardize on Snackbar. It supports the
56
- app's semantic colors, accessibility, safe-area positioning, duration policy,
57
- and one optional recovery action. Do not maintain both a custom Toast and a
58
- Snackbar for the same job.
49
+ For app-owned foreground feedback, standardize on Snackbar. It supports the app's semantic colors, accessibility, safe-area positioning, duration policy, and one optional recovery action. Do not maintain both a custom Toast and a Snackbar for the same job.
59
50
 
60
- A platform Toast can remain only when the operating system owns the behavior or
61
- when a documented platform fallback is necessary. It is not the default design
62
- surface.
51
+ A platform Toast can remain only when the operating system owns the behavior or when a documented platform fallback is necessary. It is not the default design surface.
63
52
 
64
53
  ### Banner versus Snackbar
65
54
 
@@ -69,17 +58,13 @@ surface.
69
58
 
70
59
  ### Bottom Sheet versus Dialog
71
60
 
72
- - Use Bottom Sheet for related choices: sorting, exporting, selecting a category,
73
- or choosing an action for an item.
61
+ - Use Bottom Sheet for related choices: sorting, exporting, selecting a category, or choosing an action for an item.
74
62
  - Use Dialog for acknowledgement or a consequential confirm/cancel decision.
75
- - Move long prose, structured data, search, or complex input into a sheet or
76
- dedicated screen.
63
+ - Move long prose, structured data, search, or complex input into a sheet or dedicated screen.
77
64
 
78
65
  ## App-owned versus system-owned
79
66
 
80
- Recreate and brand a surface only when the app owns its content and behavior.
81
- Keep the platform UI when the system owns trust, permissions, identity, files,
82
- or another protected workflow.
67
+ Recreate and brand a surface only when the app owns its content and behavior. Keep the platform UI when the system owns trust, permissions, identity, files, or another protected workflow.
83
68
 
84
69
  | Keep native/system-owned | May be app-owned and styled |
85
70
  | --- | --- |
@@ -91,43 +76,29 @@ or another protected workflow.
91
76
  | Biometric authentication prompt | Banners and inline validation |
92
77
  | Keyboard, autofill, purchases, notifications, and OS settings | App-specific onboarding or education |
93
78
 
94
- An app may explain why a system surface is about to appear, but it must not
95
- imitate the trusted system prompt. Use the official Titanium API or native module
96
- for the system-owned part.
79
+ An app may explain why a system surface is about to appear, but it must not imitate the trusted system prompt. Use the official Titanium API or native module for the system-owned part.
97
80
 
98
- Cancellation is often a normal system outcome. Do not turn every cancellation
99
- into an error, a "Cancelled" message, or an unsupported success claim. Document
100
- silence explicitly at the call site when it is meaningful.
81
+ Cancellation is often a normal system outcome. Do not turn every cancellation into an error, a "Cancelled" message, or an unsupported success claim. Document silence explicitly at the call site when it is meaningful.
101
82
 
102
83
  ## What to standardize next
103
84
 
104
85
  After Snackbar, Dialog, and Bottom Sheet, the highest-value shared patterns are:
105
86
 
106
87
  1. **Banner / Inline Notice** for persistent app state.
107
- 2. **Loading / Progress wrapper** that uses the native activity indicator but
108
- standardizes placement, text, blocking behavior, and cleanup.
88
+ 2. **Loading / Progress wrapper** that uses the native activity indicator but standardizes placement, text, blocking behavior, and cleanup.
109
89
  3. **Empty / Error State** for consistent screen-level recovery.
110
90
  4. **Field Validation** for message placement, focus, accessibility, and colors.
111
91
 
112
- Standardize buttons, cards, badges, chips, skeletons, and coach marks only when
113
- real product repetition exposes a stable contract. A visual resemblance alone
114
- does not justify a Widget.
92
+ Standardize buttons, cards, badges, chips, skeletons, and coach marks only when real product repetition exposes a stable contract. A visual resemblance alone does not justify a Widget.
115
93
 
116
94
  ## Phone, tablet, and accessibility
117
95
 
118
- - On phones, present contextual choices from the bottom and keep touch targets
119
- reachable.
120
- - On tablets and large screens, use a centered, width-limited action surface or
121
- a platform-appropriate anchored presentation; do not stretch phone geometry
122
- edge to edge.
123
- - Respect safe areas, bottom navigation, orientation, split screen, text scaling,
124
- and localized strings.
125
- - Announce transient feedback appropriately and move accessibility focus into a
126
- modal surface when it opens.
127
- - While a modal is active, prevent interaction with and accessibility traversal
128
- into the underlying content. Restore the exact previous state on close.
129
- - Back, backdrop, Cancel, and destructive actions must have predictable semantics
130
- and must never execute callbacks more than once.
96
+ - On phones, present contextual choices from the bottom and keep touch targets reachable.
97
+ - On tablets and large screens, use a centered, width-limited action surface or a platform-appropriate anchored presentation; do not stretch phone geometry edge to edge.
98
+ - Respect safe areas, bottom navigation, orientation, split screen, text scaling, and localized strings.
99
+ - Announce transient feedback appropriately and move accessibility focus into a modal surface when it opens.
100
+ - While a modal is active, prevent interaction with and accessibility traversal into the underlying content. Restore the exact previous state on close.
101
+ - Back, backdrop, Cancel, and destructive actions must have predictable semantics and must never execute callbacks more than once.
131
102
 
132
103
  ## References
133
104
 
@@ -1,7 +1,6 @@
1
1
  # Contracts for feedback Widgets
2
2
 
3
- Use these contracts for app-owned Snackbar, Dialog, and Bottom Sheet surfaces in
4
- Alloy. Namespaces and visual tokens belong to each app; behavior stays stable.
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.
5
4
 
6
5
  ## Contents
7
6
 
@@ -15,12 +14,7 @@ Alloy. Namespaces and visual tokens belong to each app; behavior stays stable.
15
14
 
16
15
  ## Boundary and registration
17
16
 
18
- Implement each surface as an independent Alloy Widget under `app/widgets/`.
19
- Each Widget owns its XML, controller, optional styles, assets, state, listeners,
20
- timers, and `widget.json`. Keep them independent from any third-party component
21
- library — including TiKit, the Alloy/PurgeTSS component set. A feedback surface
22
- must not require one to work, and must not be folded into one: its value comes
23
- from being installable and testable on its own.
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.
24
18
 
25
19
  Register every Widget in `app/config.json`:
26
20
 
@@ -34,11 +28,9 @@ Register every Widget in `app/config.json`:
34
28
  }
35
29
  ```
36
30
 
37
- Use the app's reverse-domain namespace. The dependency key must match the `id`
38
- in that Widget's `widget.json`.
31
+ Use the app's reverse-domain namespace. The dependency key must match the `id` in that Widget's `widget.json`.
39
32
 
40
- Declare Widgets in XML, normally as the last visual children of the owning
41
- window so overlays render above screen content:
33
+ Declare Widgets in XML, normally as the last visual children of the owning window so overlays render above screen content:
42
34
 
43
35
  ```xml
44
36
  <Widget id="snackbar" src="com.example.ui.snackbar" />
@@ -46,8 +38,7 @@ window so overlays render above screen content:
46
38
  <Widget id="bottomSheet" src="com.example.ui.bottomsheet" />
47
39
  ```
48
40
 
49
- Build the stable proxy tree in XML. JavaScript may update properties and supply
50
- `ListView` data; it must not reconstruct the complete surface imperatively.
41
+ Build the stable proxy tree in XML. JavaScript may update properties and supply `ListView` data; it must not reconstruct the complete surface imperatively.
51
42
 
52
43
  ## Snackbar
53
44
 
@@ -68,12 +59,10 @@ $.snackbar.destroy()
68
59
  Contract:
69
60
 
70
61
  - A new message replaces the visible message and restarts the timer.
71
- - Default `duration` is 4500 ms; `duration: 0` remains visible until an action,
72
- `dismiss()`, replacement, or destruction.
62
+ - Default `duration` is 4500 ms; `duration: 0` remains visible until an action, `dismiss()`, replacement, or destruction.
73
63
  - Allow at most one text action, such as Undo.
74
64
  - Invoke `onAction` once, then close the Snackbar.
75
- - Invoke `onDismiss` once with the reason `timeout`, `action`, `replaced`,
76
- `programmatic`, or `destroy`.
65
+ - Invoke `onDismiss` once with the reason `timeout`, `action`, `replaced`, `programmatic`, or `destroy`.
77
66
  - Clear the old timer before replacement and on every close path.
78
67
  - Position above bottom navigation and inside the current safe area.
79
68
  - Queueing Snackbars is not the default. The newest relevant message wins.
@@ -100,20 +89,15 @@ $.dialog.destroy()
100
89
 
101
90
  Contract:
102
91
 
103
- - Without `cancelTitle`, present an acknowledgement. With it, present a
104
- confirmation.
92
+ - Without `cancelTitle`, present an acknowledgement. With it, present a confirmation.
105
93
  - Enqueue additional `show()` calls FIFO. Never stack two Dialogs visually.
106
94
  - Disable both buttons on the first accepted tap.
107
95
  - Invoke `onConfirm` or `onCancel` at most once; never both.
108
- - Cancel button, Android Back, or allowed backdrop dismissal invokes `onCancel`.
109
- Android Back closes an acknowledgement as its single dismissal action.
96
+ - Cancel button, Android Back, or allowed backdrop dismissal invokes `onCancel`. Android Back closes an acknowledgement as its single dismissal action.
110
97
  - A destructive confirmation must not close from a backdrop tap.
111
- - `dismiss('programmatic')` closes without treating the operation as a user
112
- confirmation or cancellation.
113
- - `destroy()` clears the active request and queue without running business
114
- callbacks.
115
- - Use platform-familiar safe/cancel ordering and a distinct destructive role;
116
- do not hard-code one platform's button order for both.
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.
117
101
 
118
102
  ## Bottom Sheet
119
103
 
@@ -142,60 +126,40 @@ $.bottomSheet.destroy()
142
126
 
143
127
  Contract:
144
128
 
145
- - Render items with a fixed `ListView` template and data objects. Support long
146
- lists, disabled rows, optional subtitles/icons, and a selected marker.
129
+ - Render items with a fixed `ListView` template and data objects. Support long lists, disabled rows, optional subtitles/icons, and a selected marker.
147
130
  - Enqueue additional `show()` calls FIFO. Never stack sheets.
148
- - Accept the first enabled selection only, invoke `onSelect(item)` once, and
149
- close.
131
+ - Accept the first enabled selection only, invoke `onSelect(item)` once, and close.
150
132
  - Backdrop, Android Back, Cancel, or `dismiss('cancel')` invokes `onCancel` once.
151
- - Other programmatic dismissal and `destroy()` do not imply cancellation and do
152
- not execute business callbacks.
153
- - Present from the bottom on phones. On tablets and large screens, use a centered,
154
- width-limited action surface. Reserve `anchor` for a future anchored variant;
155
- do not expose behavior that is not implemented.
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.
156
135
 
157
136
  ## Shared lifecycle contract
158
137
 
159
- - Prefer one instance of each needed Widget per window. Add a global coordinator
160
- only after real cross-window requirements exist.
161
- - Call `attach(window)` after the host window/controller exists. Call `destroy()`
162
- from the host's close cleanup.
163
- - Make `destroy()` idempotent. Remove listeners, clear timers, cancel pending
164
- animation callbacks, clear queues, and release callback references.
165
- - Guard every asynchronous completion with the current presentation generation
166
- so a stale timer or animation cannot dismiss a newer request.
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.
167
142
  - Consume backdrop events and prevent them from bubbling to underlying controls.
168
- - Save the host's previous accessibility state, exclude underlying content while
169
- modal, move focus to the surface, and restore the saved state on close.
170
- - Keep localized copy outside the Widget. Pass strings and business callbacks in
171
- the public API.
172
- - Keep business logic out of the Widget. It reports a choice; the host performs
173
- domain work.
174
- - Verify every callback path for exactly-once behavior: action, confirm, cancel,
175
- Back, backdrop, timeout, replacement, programmatic dismissal, and destruction.
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.
176
147
 
177
148
  ## PurgeTSS integration
178
149
 
179
- When `purgetss/` or `purgetss/config.cjs` exists, load the `purgetss` skill before
180
- writing styles. `ti-expert` decides the component boundary; `purgetss` decides
181
- the verified utilities and configuration.
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.
182
151
 
183
152
  Apply these architectural rules:
184
153
 
185
154
  - Set PurgeTSS `options.widgets` to `true` so Widget XML is scanned.
186
155
  - Express stable anatomy and utility classes in Widget XML.
187
- - Use semantic color names for surfaces, text, borders, accent, overlay, success,
188
- warning, and error. Let Titanium resolve light/dark variants.
156
+ - Use semantic color names for surfaces, text, borders, accent, overlay, success, warning, and error. Let Titanium resolve light/dark variants.
189
157
  - Do not edit generated `app/styles/app.tss`.
190
- - Use `widget.tss` only for properties that have no verified utility or that are
191
- inherently specific to the Widget.
192
- - Declare animations as XML `Animation` proxies using `purgetss.ui`. Use explicit
193
- `open()` and `close()` calls instead of toggle behavior.
194
- - After generation, inspect the final `app.tss` section for unused or unsupported
195
- classes and resolve every new entry.
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.
196
161
 
197
- Do not copy class inventories into this skill and do not guess Tailwind-like
198
- names. The installed PurgeTSS skill remains the source of truth.
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.
199
163
 
200
164
  ## Widget, Require, state, or native UI
201
165
 
@@ -207,7 +171,5 @@ names. The installed PurgeTSS skill remains the source of truth.
207
171
  | TSS or PurgeTSS utilities | Repeated visual treatment has no behavior, state, or lifecycle |
208
172
  | Native/system UI | The operating system owns permissions, trust, files, sharing, identity, purchases, or another protected flow |
209
173
 
210
- Usage count is evidence, not a threshold. A component can deserve a Widget on
211
- its first use if it has a stable portable API and non-trivial lifecycle; three
212
- similar labels still do not deserve one.
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.
213
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.
@@ -30,21 +30,16 @@ Before migrating, evaluate the current codebase:
30
30
 
31
31
  ## Phase 1: styling-system organization
32
32
 
33
- **Goal**: Migrate from inline and scattered styling into the styling system the
34
- project actually uses.
33
+ **Goal**: Migrate from inline and scattered styling into the styling system the project actually uses.
35
34
 
36
35
  ### Step 0: detect PurgeTSS
37
36
 
38
37
  Check for `purgetss/` or `purgetss/config.cjs` before editing styles.
39
38
 
40
- - **PurgeTSS detected:** invoke the `purgetss` skill, use verified utilities and
41
- semantic colors, configure reusable rules in `config.cjs`, and never edit the
42
- generated `app/styles/app.tss`.
43
- - **No PurgeTSS:** use standard per-controller TSS and a manually maintained
44
- `app/styles/app.tss` for genuinely global selectors.
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.
45
41
 
46
- Do not migrate a PurgeTSS application back into manual TSS by following a generic
47
- Alloy example.
42
+ Do not migrate a PurgeTSS application back into manual TSS by following a generic Alloy example.
48
43
 
49
44
  ### Step 1: audit existing styles
50
45
 
@@ -89,17 +84,13 @@ This step applies only when PurgeTSS is not present.
89
84
  ".btn-primary": { backgroundColor: '#2563eb', color: '#fff', height: 44, borderRadius: 8 }
90
85
  ```
91
86
 
92
- In either styling path, replace hard-coded visual colors with semantic roles as
93
- part of the migration. See [Theming and Dark Mode](theming.md).
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).
94
88
 
95
89
  ## Phase 1B: feedback surfaces
96
90
 
97
- Inventory native alerts, option dialogs, Toasts, and local overlays. Classify
98
- each occurrence before replacing it, then migrate one window at a time. Preserve
99
- system-owned pickers, permission prompts, sharing, and biometric UI.
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.
100
92
 
101
- Use [Feedback Migration](feedback-migration.md) for the sequence, deliberate
102
- silence rules, and verification contract.
93
+ Use [Feedback Migration](feedback-migration.md) for the sequence, deliberate silence rules, and verification contract.
103
94
 
104
95
  ## Phase 2: service layer extraction
105
96
 
@@ -323,8 +314,7 @@ For each phase:
323
314
  3. **Test thoroughly** before moving to next feature
324
315
  4. **Delete the replaced implementation** after the vertical slice passes
325
316
 
326
- Use version control for rollback. Do not keep commented legacy implementations:
327
- they conceal duplicate listeners, stale callbacks, and incomplete migrations.
317
+ Use version control for rollback. Do not keep commented legacy implementations: they conceal duplicate listeners, stale callbacks, and incomplete migrations.
328
318
 
329
319
  ## Common migration pitfalls
330
320