@rdlabo/capacitor-native-ui 0.0.0-stage → 2.0.0-1

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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/Package.swift +23 -0
  3. package/README.md +66 -2
  4. package/RdlaboCapacitorNativeUi.podspec +17 -0
  5. package/android/build.gradle +69 -0
  6. package/android/proguard-rules.pro +0 -0
  7. package/android/src/main/AndroidManifest.xml +1 -0
  8. package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUI.kt +51 -0
  9. package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUIPlugin.kt +192 -0
  10. package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUISnapshot.kt +57 -0
  11. package/dist/_virtual/_rolldown/runtime.js +13 -0
  12. package/dist/core.d.ts +4 -0
  13. package/dist/core.js +3 -0
  14. package/dist/native/activation.d.ts +14 -0
  15. package/dist/native/activation.d.ts.map +1 -0
  16. package/dist/native/activation.js +305 -0
  17. package/dist/native/activation.js.map +1 -0
  18. package/dist/native/components/index.d.ts +19 -0
  19. package/dist/native/components/index.d.ts.map +1 -0
  20. package/dist/native/components/index.js +38 -0
  21. package/dist/native/components/index.js.map +1 -0
  22. package/dist/native/components/ion-back-button.d.ts +10 -0
  23. package/dist/native/components/ion-back-button.d.ts.map +1 -0
  24. package/dist/native/components/ion-back-button.js +21 -0
  25. package/dist/native/components/ion-back-button.js.map +1 -0
  26. package/dist/native/components/ion-button.d.ts +10 -0
  27. package/dist/native/components/ion-button.d.ts.map +1 -0
  28. package/dist/native/components/ion-button.js +25 -0
  29. package/dist/native/components/ion-button.js.map +1 -0
  30. package/dist/native/components/ion-buttons.d.ts +11 -0
  31. package/dist/native/components/ion-buttons.d.ts.map +1 -0
  32. package/dist/native/components/ion-buttons.js +30 -0
  33. package/dist/native/components/ion-buttons.js.map +1 -0
  34. package/dist/native/components/ion-fab.d.ts +10 -0
  35. package/dist/native/components/ion-fab.d.ts.map +1 -0
  36. package/dist/native/components/ion-fab.js +132 -0
  37. package/dist/native/components/ion-fab.js.map +1 -0
  38. package/dist/native/components/ion-menu-button.d.ts +13 -0
  39. package/dist/native/components/ion-menu-button.d.ts.map +1 -0
  40. package/dist/native/components/ion-menu-button.js +44 -0
  41. package/dist/native/components/ion-menu-button.js.map +1 -0
  42. package/dist/native/components/ion-segment.d.ts +11 -0
  43. package/dist/native/components/ion-segment.d.ts.map +1 -0
  44. package/dist/native/components/ion-segment.js +30 -0
  45. package/dist/native/components/ion-segment.js.map +1 -0
  46. package/dist/native/components/ion-tab-bar.d.ts +11 -0
  47. package/dist/native/components/ion-tab-bar.d.ts.map +1 -0
  48. package/dist/native/components/ion-tab-bar.js +50 -0
  49. package/dist/native/components/ion-tab-bar.js.map +1 -0
  50. package/dist/native/components/searchable-tabs.d.ts +20 -0
  51. package/dist/native/components/searchable-tabs.d.ts.map +1 -0
  52. package/dist/native/components/searchable-tabs.js +413 -0
  53. package/dist/native/components/searchable-tabs.js.map +1 -0
  54. package/dist/native/definitions.d.ts +237 -0
  55. package/dist/native/definitions.d.ts.map +1 -0
  56. package/dist/native/definitions.js +0 -0
  57. package/dist/native/extensions.d.ts +31 -0
  58. package/dist/native/extensions.d.ts.map +1 -0
  59. package/dist/native/extensions.js +122 -0
  60. package/dist/native/extensions.js.map +1 -0
  61. package/dist/native/foldable/fold-state.d.ts +5 -0
  62. package/dist/native/foldable/fold-state.d.ts.map +1 -0
  63. package/dist/native/foldable/fold-state.js +15 -0
  64. package/dist/native/foldable/fold-state.js.map +1 -0
  65. package/dist/native/foldable/types.d.ts +12 -0
  66. package/dist/native/foldable/types.d.ts.map +1 -0
  67. package/dist/native/foldable/types.js +0 -0
  68. package/dist/native/html.d.ts +9 -0
  69. package/dist/native/html.d.ts.map +1 -0
  70. package/dist/native/html.js +197 -0
  71. package/dist/native/html.js.map +1 -0
  72. package/dist/native/index.d.ts +10 -0
  73. package/dist/native/index.d.ts.map +1 -0
  74. package/dist/native/index.js +19 -0
  75. package/dist/native/index.js.map +1 -0
  76. package/dist/native/legacy-layout.d.ts +5 -0
  77. package/dist/native/legacy-layout.d.ts.map +1 -0
  78. package/dist/native/legacy-layout.js +27 -0
  79. package/dist/native/legacy-layout.js.map +1 -0
  80. package/dist/native/overlays/content.d.ts +4 -0
  81. package/dist/native/overlays/content.d.ts.map +1 -0
  82. package/dist/native/overlays/content.js +51 -0
  83. package/dist/native/overlays/content.js.map +1 -0
  84. package/dist/native/overlays/controller.d.ts +8 -0
  85. package/dist/native/overlays/controller.d.ts.map +1 -0
  86. package/dist/native/overlays/controller.js +285 -0
  87. package/dist/native/overlays/controller.js.map +1 -0
  88. package/dist/native/overlays/dialog-animation.d.ts +9 -0
  89. package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
  90. package/dist/native/overlays/dialog-animation.js +53 -0
  91. package/dist/native/overlays/dialog-animation.js.map +1 -0
  92. package/dist/native/overlays/dismiss-animation.d.ts +4 -0
  93. package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
  94. package/dist/native/overlays/dismiss-animation.js +18 -0
  95. package/dist/native/overlays/dismiss-animation.js.map +1 -0
  96. package/dist/native/overlays/focus.d.ts +5 -0
  97. package/dist/native/overlays/focus.d.ts.map +1 -0
  98. package/dist/native/overlays/focus.js +59 -0
  99. package/dist/native/overlays/focus.js.map +1 -0
  100. package/dist/native/overlays/popover.d.ts +12 -0
  101. package/dist/native/overlays/popover.d.ts.map +1 -0
  102. package/dist/native/overlays/popover.js +110 -0
  103. package/dist/native/overlays/popover.js.map +1 -0
  104. package/dist/native/overlays/styles.d.ts +11 -0
  105. package/dist/native/overlays/styles.d.ts.map +1 -0
  106. package/dist/native/overlays/styles.js +167 -0
  107. package/dist/native/overlays/styles.js.map +1 -0
  108. package/dist/native/prehide.d.ts +8 -0
  109. package/dist/native/prehide.d.ts.map +1 -0
  110. package/dist/native/prehide.js +238 -0
  111. package/dist/native/prehide.js.map +1 -0
  112. package/dist/native/runtime.d.ts +7 -0
  113. package/dist/native/runtime.d.ts.map +1 -0
  114. package/dist/native/runtime.js +776 -0
  115. package/dist/native/runtime.js.map +1 -0
  116. package/dist/native/shared/bridge.d.ts +6 -0
  117. package/dist/native/shared/bridge.d.ts.map +1 -0
  118. package/dist/native/shared/bridge.js +18 -0
  119. package/dist/native/shared/bridge.js.map +1 -0
  120. package/dist/native/shared/candidate.d.ts +25 -0
  121. package/dist/native/shared/candidate.d.ts.map +1 -0
  122. package/dist/native/shared/candidate.js +106 -0
  123. package/dist/native/shared/candidate.js.map +1 -0
  124. package/dist/native/shared/clipping.d.ts +5 -0
  125. package/dist/native/shared/clipping.d.ts.map +1 -0
  126. package/dist/native/shared/clipping.js +26 -0
  127. package/dist/native/shared/clipping.js.map +1 -0
  128. package/dist/native/shared/control-options.d.ts +6 -0
  129. package/dist/native/shared/control-options.d.ts.map +1 -0
  130. package/dist/native/shared/control-options.js +17 -0
  131. package/dist/native/shared/control-options.js.map +1 -0
  132. package/dist/native/shared/crossfade.d.ts +12 -0
  133. package/dist/native/shared/crossfade.d.ts.map +1 -0
  134. package/dist/native/shared/crossfade.js +67 -0
  135. package/dist/native/shared/crossfade.js.map +1 -0
  136. package/dist/native/shared/diagnostics.d.ts +4 -0
  137. package/dist/native/shared/diagnostics.d.ts.map +1 -0
  138. package/dist/native/shared/diagnostics.js +18 -0
  139. package/dist/native/shared/diagnostics.js.map +1 -0
  140. package/dist/native/shared/dom.d.ts +46 -0
  141. package/dist/native/shared/dom.d.ts.map +1 -0
  142. package/dist/native/shared/dom.js +180 -0
  143. package/dist/native/shared/dom.js.map +1 -0
  144. package/dist/native/shared/events.d.ts +15 -0
  145. package/dist/native/shared/events.d.ts.map +1 -0
  146. package/dist/native/shared/events.js +17 -0
  147. package/dist/native/shared/events.js.map +1 -0
  148. package/dist/native/shared/icons.d.ts +8 -0
  149. package/dist/native/shared/icons.d.ts.map +1 -0
  150. package/dist/native/shared/icons.js +61 -0
  151. package/dist/native/shared/icons.js.map +1 -0
  152. package/dist/native/shared/modal.d.ts +10 -0
  153. package/dist/native/shared/modal.d.ts.map +1 -0
  154. package/dist/native/shared/modal.js +160 -0
  155. package/dist/native/shared/modal.js.map +1 -0
  156. package/dist/native/shared/projection-id.d.ts +4 -0
  157. package/dist/native/shared/projection-id.d.ts.map +1 -0
  158. package/dist/native/shared/projection-id.js +7 -0
  159. package/dist/native/shared/projection-id.js.map +1 -0
  160. package/dist/native/shared/toolbar-options.d.ts +13 -0
  161. package/dist/native/shared/toolbar-options.d.ts.map +1 -0
  162. package/dist/native/shared/toolbar-options.js +28 -0
  163. package/dist/native/shared/toolbar-options.js.map +1 -0
  164. package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
  165. package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
  166. package/dist/native/shared/vertical-bars-geometry.js +85 -0
  167. package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
  168. package/dist/native/startup.d.ts +8 -0
  169. package/dist/native/startup.d.ts.map +1 -0
  170. package/dist/native/startup.js +70 -0
  171. package/dist/native/startup.js.map +1 -0
  172. package/dist/native/vertical-bars-web-search.d.ts +10 -0
  173. package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
  174. package/dist/native/vertical-bars-web-search.js +90 -0
  175. package/dist/native/vertical-bars-web-search.js.map +1 -0
  176. package/dist/native/vertical-bars-web.d.ts +5 -0
  177. package/dist/native/vertical-bars-web.d.ts.map +1 -0
  178. package/dist/native/vertical-bars-web.js +422 -0
  179. package/dist/native/vertical-bars-web.js.map +1 -0
  180. package/dist/native-integration/index.d.ts +25 -0
  181. package/dist/native-integration/index.d.ts.map +1 -0
  182. package/dist/native-integration/index.js +33 -0
  183. package/dist/native-integration/index.js.map +1 -0
  184. package/dist/vertical-bars.d.ts +6 -0
  185. package/dist/vertical-bars.js +5 -0
  186. package/docs/appearance.md +45 -0
  187. package/docs/configuration.md +219 -0
  188. package/docs/framework-independent.md +107 -0
  189. package/docs/ionic-vertical-bars.md +173 -0
  190. package/docs/migration.md +184 -0
  191. package/docs/native-ui-extensions.md +394 -0
  192. package/docs/native-ui.md +528 -0
  193. package/docs/setup.md +224 -0
  194. package/docs/vertical-bars.md +431 -0
  195. package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
  196. package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
  197. package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
  198. package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
  199. package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
  200. package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
  201. package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
  202. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
  203. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
  204. package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
  205. package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
  206. package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
  207. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
  208. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
  209. package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
  210. package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
  211. package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
  212. package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
  213. package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
  214. package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
  215. package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
  216. package/package.json +89 -4
  217. package/src/core.ts +14 -0
  218. package/src/native/activation.ts +385 -0
  219. package/src/native/components/index.ts +45 -0
  220. package/src/native/components/ion-back-button.ts +22 -0
  221. package/src/native/components/ion-button.ts +25 -0
  222. package/src/native/components/ion-buttons.ts +37 -0
  223. package/src/native/components/ion-fab.ts +151 -0
  224. package/src/native/components/ion-menu-button.ts +37 -0
  225. package/src/native/components/ion-segment.ts +27 -0
  226. package/src/native/components/ion-tab-bar.ts +50 -0
  227. package/src/native/components/searchable-tabs.ts +499 -0
  228. package/src/native/definitions.ts +274 -0
  229. package/src/native/extensions.ts +176 -0
  230. package/src/native/foldable/fold-state.ts +10 -0
  231. package/src/native/foldable/types.ts +4 -0
  232. package/src/native/html.ts +199 -0
  233. package/src/native/index.ts +32 -0
  234. package/src/native/legacy-layout.ts +21 -0
  235. package/src/native/overlays/content.ts +51 -0
  236. package/src/native/overlays/controller.ts +349 -0
  237. package/src/native/overlays/dialog-animation.ts +53 -0
  238. package/src/native/overlays/dismiss-animation.ts +15 -0
  239. package/src/native/overlays/focus.ts +64 -0
  240. package/src/native/overlays/popover.ts +120 -0
  241. package/src/native/overlays/styles.ts +181 -0
  242. package/src/native/prehide.ts +285 -0
  243. package/src/native/runtime.ts +979 -0
  244. package/src/native/shared/bridge.ts +18 -0
  245. package/src/native/shared/candidate.ts +162 -0
  246. package/src/native/shared/clipping.ts +24 -0
  247. package/src/native/shared/control-options.ts +20 -0
  248. package/src/native/shared/crossfade.ts +67 -0
  249. package/src/native/shared/diagnostics.ts +13 -0
  250. package/src/native/shared/dom.ts +304 -0
  251. package/src/native/shared/events.ts +14 -0
  252. package/src/native/shared/icons.ts +57 -0
  253. package/src/native/shared/modal.ts +157 -0
  254. package/src/native/shared/projection-id.ts +2 -0
  255. package/src/native/shared/toolbar-options.ts +49 -0
  256. package/src/native/shared/vertical-bars-geometry.ts +80 -0
  257. package/src/native/startup.ts +68 -0
  258. package/src/native/vertical-bars-web-search.ts +86 -0
  259. package/src/native/vertical-bars-web.ts +500 -0
  260. package/src/native-integration/index.ts +49 -0
  261. package/src/vertical-bars.ts +12 -0
@@ -0,0 +1,184 @@
1
+ ---
2
+ title: Migration
3
+ ---
4
+
5
+ # Migration
6
+
7
+ ## Upgrading from 1.2.1
8
+
9
+ This guide is for applications using `@rdlabo/ionic-theme-ios27` **1.2.1**, including its Native UI Shell Preview and standalone Vertical Control Area.
10
+
11
+ - **Theme only:** update the theme package. Existing theme imports, CSS/Sass paths and animation imports remain valid; no native startup is needed.
12
+ - **Native UI or Vertical Control Area:** install the native package directly, replace imports and startup settings below, then sync Capacitor and rebuild the native app.
13
+
14
+ Native UI remains Preview. Experimental Extensions are documented in the [current extension guide](native-ui-extensions.md); this guide covers the published 1.2.1 controls and setup.
15
+
16
+ ## Native UI package separation
17
+
18
+ For a new native integration, follow [Native UI setup](setup.md) or, if you already use the iOS theme, [Add Native UI to your theme](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/add-native-ui.md). The steps below cover existing applications migrating package names and native projects.
19
+
20
+ The theme and native controls now live in separate packages. Choose dependencies by the behavior your app uses:
21
+
22
+ | App behavior | Package to use directly |
23
+ | ------------------------------------------------------ | ----------------------------- |
24
+ | Theme styles, Ionic Web behavior, and theme animations | `@rdlabo/ionic-theme-ios27` |
25
+ | Native UI or standalone Vertical Control Area | `@rdlabo/capacitor-native-ui` |
26
+ | Theme styling together with native controls | Both packages |
27
+
28
+ The theme owns the appearance and behavior of Ionic's Web elements. Native UI owns projecting supported controls into native iOS views and coordinating their lifecycle with Web navigation. Native UI can be used without the iOS 27 theme; installing it does not apply theme styles. Standard controls on Android and browsers retain Web presentation; Android can render [app-local Compose extensions](native-ui-extensions.md#2b-register-compose-content-on-android).
29
+
30
+ The theme uses the native package's shared transition/search integration internally. This dependency does not enable native controls automatically. Apps using native controls must declare the native package directly so Capacitor can discover the plugin.
31
+
32
+ ### 1. Update application dependencies
33
+
34
+ When using both packages, update them together to keep their versions aligned.
35
+
36
+ For theme-only apps, update the theme package. Keep existing theme imports and CSS/Sass paths; no native startup or plugin setup is needed for theme-only use.
37
+
38
+ ```sh
39
+ npm install @rdlabo/ionic-theme-ios27
40
+ ```
41
+
42
+ For apps using both the theme and native controls, install both as direct application dependencies:
43
+
44
+ ```sh
45
+ npm install @rdlabo/ionic-theme-ios27 @rdlabo/capacitor-native-ui
46
+ ```
47
+
48
+ For native controls with your existing Web appearance, follow [Native UI only](setup.md). Install the native package directly and keep your existing Web styles. The plugin no longer requires theme variables to select supported controls; Web layout remains the responsibility of your app or theme. Native startup requires Capacitor 8. The Ionic adapter requires Ionic 8.8.1 or later within Ionic 8 or 9; the `/core` entry does not require Ionic.
49
+
50
+ ### 2. Replace native imports
51
+
52
+ | Previous import | Replacement |
53
+ | ----------------------------------------- | ------------------------------------------- |
54
+ | `@rdlabo/ionic-theme-ios27/native` | `@rdlabo/capacitor-native-ui` |
55
+ | `@rdlabo/ionic-theme-ios27/vertical-bars` | `@rdlabo/capacitor-native-ui/vertical-bars` |
56
+
57
+ Update both value and type imports, including lazy imports, test aliases, and bundler configuration. The old theme `/native` and `/vertical-bars` entries have been removed; there are no compatibility aliases.
58
+
59
+ ```diff
60
+ - import { enableNativeUIShell } from '@rdlabo/ionic-theme-ios27/native';
61
+ + import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
62
+ ```
63
+
64
+ Keep existing control markup; review the [startup option changes](#startup-options), including partial `controls` defaults. The ordinary `@rdlabo/ionic-theme-ios27` entry, theme CSS/Sass paths, and theme animation imports stay unchanged. For custom navigation animations, see the [transition adapter instructions](#native-ui-transition-adapter) below.
65
+
66
+ ### 3. Sync the native project
67
+
68
+ After updating dependencies and imports, rebuild your app's Web assets with its usual build command, then run:
69
+
70
+ ```sh
71
+ npx cap sync ios
72
+ ```
73
+
74
+ The Capacitor plugin is now supplied by the native package. Remove manually linked copies of the old theme Swift package so only the new package supplies the plugin. The Swift package/product and CocoaPods pod are now `RdlaboCapacitorNativeUi`; the Swift module is `NativeUIPlugin`. The Capacitor bridge name is now `NativeUI` on JavaScript, iOS and Android. Run Capacitor sync for each native platform you use and rebuild the native app. An OTA/Web-only update cannot migrate an installed binary containing only the old plugin. Use matching JavaScript and native package versions.
75
+
76
+ Use Capacitor's generated integration for the npm dependency; applications do not need a checkout of this repository or a manually linked Swift package.
77
+
78
+ ### 4. Verify the migration
79
+
80
+ - Check that native-enabled apps declare `@rdlabo/capacitor-native-ui` directly, then build and confirm no imports of the removed theme entries remain.
81
+ - Build and launch the iOS app after sync. Check native controls, navigation/back gestures, and restored Web controls after teardown.
82
+ - Check browser and Android Web presentation where supported by your app, and confirm the theme still looks correct.
83
+ - Check that previously disabled control kinds remain disabled after expanding partial Preview settings. Test toolbar grouping, individually selected actions and back navigation in both horizontal and vertical layouts.
84
+
85
+ ## Native UI naming changes
86
+
87
+ The split package uses Native UI names throughout. These are breaking renames; the old names are not aliases. The previous names below refer to the published 1.2.1 Preview.
88
+
89
+ | Previous name | New name |
90
+ | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
91
+ | `enableNativeUIShell()` | `enableNativeUI()` |
92
+ | `NativeUIShellOptions`, `NativeUIShellHandle`, `NativeUIShellStatus`, `NativeUIShellSuspension`, `NativeUIShellControls`, `NativeUIShellComponent` | `NativeUIOptions`, `NativeUIHandle`, `NativeUIStatus`, `NativeUISuspension`, `NativeUIControls`, `NativeUIComponent` |
93
+ | `IonicNativeUIShell` (plugin export and bridge registration) | `NativeUI` |
94
+ | `withNativeUIShellTransition()` | `withNativeUITransition()` (still exported by the theme package) |
95
+ | `IonicNativeUIShellPlugin` (Swift module/native class) | `NativeUIPlugin` |
96
+ | `RdlaboIonicThemeIos27` (SPM package/product) | `RdlaboCapacitorNativeUi` |
97
+ | `data-shell="disabled"` | `data-native-ui="disabled"` |
98
+ | `.ios-theme-shell-disabled` | `.native-ui-disabled` |
99
+
100
+ Runtime-owned attributes/events are internal, not configuration: `data-native-ui-shell` is now `data-native-ui-projected`, other `data-native-ui-shell-*` attributes use `data-native-ui-*`, `nativeUIShell*` events use `nativeUI*`, and `.ios-theme-native-ui-shell-*` classes use `.native-ui-*`. Update application tests or diagnostics that inspect them. **Keep explicit source declarations such as `data-native-ui="button"` unchanged**; the projected marker is separate and is managed by the runtime.
101
+
102
+ ## Native UI transition adapter
103
+
104
+ The Ionic transition adapter now belongs to `@rdlabo/ionic-theme-ios27`. Replace the 1.2.1 import of `withNativeUIShellTransition` from `@rdlabo/ionic-theme-ios27/vertical-bars` with `withNativeUITransition` from the theme root. Native-only applications can use `shell.suspend()` and the returned `resume()` directly.
105
+
106
+ Use `withNativeUITransition()` to keep your existing Ionic navigation animation while coordinating Native UI controls.
107
+
108
+ - If you already import `iosTransitionAnimation` from `@rdlabo/ionic-theme-ios27`, keep the [package animation setup](ionic-vertical-bars.md#use-the-themes-ios-animation); no configuration change is needed. It now uses the shared adapter internally; do not add another wrapper.
109
+ - If you use Ionic's default animation without a `navAnimation` option, follow [Keep Ionic's default animation](ionic-vertical-bars.md#keep-ionics-default-animation). The example selects Ionic's standard iOS or MD builder from the transition mode.
110
+ - If you use a custom navigation animation with Native UI or the standalone Vertical Control Area, wrap your existing builder when configuring Ionic:
111
+
112
+ ```diff
113
+ + import { withNativeUITransition } from '@rdlabo/ionic-theme-ios27';
114
+
115
+ const ionicConfig = {
116
+ - navAnimation: existingTransition,
117
+ + navAnimation: withNativeUITransition(existingTransition),
118
+ };
119
+ ```
120
+
121
+ Merge this option into your existing Ionic configuration before initialization. The adapter preserves the animation's effects, duration, and easing while coordinating native retirement, swipe progress, and cancellation. Keep your existing theme stylesheet imports and Native UI or Vertical Control Area startup.
122
+
123
+ Use the adapter only for navigation; leave modal and popover animations unchanged. Your builder must create a fresh `Animation` for each navigation because Ionic destroys it afterward. Keep lifecycle events for control registration and transitions without animation. If the custom builder animates a horizontal back button separately, exclude that effect while `.ios-theme-vertical-bars` is active.
124
+
125
+ See [Connect your navigation animation](ionic-vertical-bars.md#3-connect-your-navigation-animation) for the setup and supported scope.
126
+
127
+ ## Startup options
128
+
129
+ In 1.2.1, providing `controls` enabled **only fields set to `true`**. In the new API, omitted fields default to `true`, even in a partial object. Expand your old selection when migrating.
130
+
131
+ | 1.2.1 | New configuration |
132
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
133
+ | No `controls` | No `controls`: all supported kinds remain selected by default. |
134
+ | `controls: {}` | `controls: false` to disable default selection, or `ionic: { auto: false }` to disable Ionic discovery altogether. |
135
+ | `controls.toolbar` | Set `controls.buttonGroup`, `controls.button`, and `controls.backButton` to the old toolbar value. |
136
+ | `controls.tabs`, `.segment`, `.fab` | Same names, but explicitly use `false` for previously omitted fields. |
137
+ | `buttonProjection` | `ionic.verticalBars.buttonProjection` |
138
+ | `buttonDefaultFill: null` | `ionic.verticalBars.buttonDefaultFill: 'glass'` (also the default) |
139
+ | `buttonDefaultFill: 'solid'` | `ionic.verticalBars.buttonDefaultFill: 'solid'` |
140
+ | `verticalBarsOnly: true` | `controls: 'verticalOnly'` for default selection in the rail. For a partial selection, use `'verticalOnly'` on each enabled kind and `false` on the rest. |
141
+ | `enabled: false` | Do not start the runtime, or call the active handle's `destroy()`. |
142
+
143
+ The old startup option names are removed, without aliases. The standalone `enableVerticalControlArea()` entry remains available and defaults `controls` to `'verticalOnly'`; its appearance settings also move under `ionic.verticalBars`. Use only one startup entry per app.
144
+
145
+ ### Preserve a tabs-only configuration
146
+
147
+ ```diff
148
+ - import { enableNativeUIShell } from '@rdlabo/ionic-theme-ios27/native';
149
+ + import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
150
+
151
+ - const shell = await enableNativeUIShell({
152
+ - controls: { tabs: true },
153
+ - buttonProjection: 'source',
154
+ - buttonDefaultFill: null,
155
+ + const shell = await enableNativeUI({
156
+ + controls: {
157
+ + buttonGroup: false,
158
+ + button: false,
159
+ + backButton: false,
160
+ + tabs: true,
161
+ + segment: false,
162
+ + fab: false,
163
+ + },
164
+ + ionic: {
165
+ + verticalBars: { buttonProjection: 'source', buttonDefaultFill: 'glass' },
166
+ + },
167
+ });
168
+ ```
169
+
170
+ ### Select controls by placement
171
+
172
+ All six control fields accept `boolean | 'horizontalOnly' | 'verticalOnly'`. A scalar `controls` value applies to every field. Horizontal and vertical mean ordinary placement and the rail, not device orientation or model.
173
+
174
+ ```ts
175
+ const shell = await enableNativeUI({ controls: 'verticalOnly' });
176
+ ```
177
+
178
+ These are selection defaults, not a hard projection scope. Explicit `data-native-ui` sources and local `data-horizontal-bars="true"` / `data-vertical-bars="true"` can opt in even when a matching default is `false`. Remove the explicit marker or use `data-native-ui="disabled"` to leave an individual source on the Web. `ionic.auto: false` disables automatic Ionic discovery, including local layout opt-ins, but leaves explicit source declarations enabled.
179
+
180
+ `buttonGroup` controls grouping only. It does not select children: `buttonGroup: true, button: false` projects no ordinary action without explicit selection. `backButton` controls back navigation independently, including inside an `ion-buttons` container. See [all settings](configuration.md) for the complete selection matrix and [toolbar grouping](native-ui.md#toolbar-selection-and-grouping) for local overrides.
181
+
182
+ ## Web layout ownership
183
+
184
+ Import `setVerticalBarsLayout` and `applyFoldStateClasses` from `@rdlabo/ionic-theme-ios27` for theme CSS. Replace `setVerticalControlAreaPlacement({ edge, nativeEdge, inset })` with `setVerticalBarsLayout({ edge, inset })`. Native UI now reads availability from its own device metrics; `nativeEdge` is ignored by the deprecated compatibility helper. Normal startup, disabled rails and teardown do not remove theme classes or alter reserved Web spacing. An unavailable native rail keeps the application’s Web rail layout; applications wanting a horizontal layout must select it themselves. Native-only applications can continue to use their own layout CSS.
@@ -0,0 +1,394 @@
1
+ ---
2
+ title: App-local Native UI extensions (experimental)
3
+ ---
4
+
5
+ # App-local Native UI extensions (experimental)
6
+
7
+ > **Experimental:** The extension API, native registration interfaces and behavior may change in future releases. This applies to both SwiftUI and Kotlin/Compose extensions, including `element` and `tab-accessory` placement.
8
+
9
+ An extension projects your existing Web UI into a SwiftUI View on iOS or Jetpack Compose content on Android. Keep both definitions in your application: no npm package, separate library, or Capacitor plugin is required. The Web element remains the source of truth and the fallback UI.
10
+
11
+ Extensions use the same [projection model](../README.md#how-projection-works) as built-in controls. Define the DOM data and actions to expose, then provide their native presentation.
12
+
13
+ ## Before you start
14
+
15
+ Start with an existing Capacitor 8 app and install the plugin if you have not already:
16
+
17
+ ```sh
18
+ npm install @rdlabo/capacitor-native-ui
19
+ ```
20
+
21
+ Choose the native platform you want to support:
22
+
23
+ - **Android:** use your Capacitor Android project and an API 24+ device or emulator. Follow step 1, then the [Kotlin/Compose registration](#2b-register-compose-content-on-android), and steps 3–4. No iOS project, Xcode, Ionic, or theme package is required.
24
+ - **iOS:** use your Capacitor iOS project, Xcode 26+, and an iOS 26+ device or simulator. Follow step 1, the [SwiftUI registration](#2a-register-a-swiftui-view-on-ios), and steps 3–4. To also project standard controls, follow [Ionic setup](setup.md) or [HTML setup](framework-independent.md).
25
+ - **Both:** share the DOM reader from step 1 and register native content for the same name on each platform.
26
+
27
+ The TypeScript examples below use the Ionic entry. Without Ionic, import both `defineNativeUI` and `enableNativeUI` from `@rdlabo/capacitor-native-ui/core`.
28
+
29
+ ## 1. Define a DOM reader
30
+
31
+ ```ts
32
+ // src/native-ui/counter.ts
33
+ import { defineNativeUI } from '@rdlabo/capacitor-native-ui';
34
+
35
+ export const counter = defineNativeUI({
36
+ name: 'app.counter',
37
+ selector: '[data-counter]',
38
+ read(element) {
39
+ return {
40
+ props: { count: Number(element.getAttribute('data-count') ?? 0) },
41
+ actions: { increment: element.querySelector<HTMLButtonElement>('button') },
42
+ };
43
+ },
44
+ });
45
+ ```
46
+
47
+ For example, use this Web source. Its reserved space also accommodates native content and an action error:
48
+
49
+ ```html
50
+ <div data-counter data-count="0" style="width: 280px; min-height: 128px">
51
+ <p data-count-label>Count: 0</p>
52
+ <button type="button">Increment</button>
53
+ </div>
54
+ ```
55
+
56
+ Keep your framework's click handler updating the count and `data-count`. In plain HTML, this is enough:
57
+
58
+ ```ts
59
+ const source = document.querySelector<HTMLElement>('[data-counter]')!;
60
+ source.querySelector('button')!.addEventListener('click', () => {
61
+ const count = Number(source.dataset.count ?? 0) + 1;
62
+ source.dataset.count = String(count);
63
+ source.querySelector('[data-count-label]')!.textContent = `Count: ${count}`;
64
+ });
65
+ ```
66
+
67
+ The shell observes DOM changes and sends the new props to the native content. State rendered into DOM attributes or text is observed automatically. For properties or external state read by the reader that do not mutate the DOM, call `shell.markForCheck()` after changing them.
68
+
69
+ ### Reader contract
70
+
71
+ | Part | Requirement |
72
+ | -------------- | -------------------------------------------------------------------------------------------------------------- |
73
+ | Execution | Synchronous and read-only. |
74
+ | Web fallback | Return `null` when the source should stay on the Web. |
75
+ | `props` | JSON object containing finite numbers, strings, booleans, null, arrays and objects; limited to 64K characters. |
76
+ | Action targets | The source itself or its descendants. Missing, hidden and disabled targets cannot be activated. |
77
+ | Action count | Up to 30 per source. |
78
+
79
+ ## 2a. Register a SwiftUI View on iOS
80
+
81
+ Run `npx cap sync ios` after installing the plugin, then add a Swift file to the application's Xcode target. Import `NativeUIPlugin`, the module supplied by `@rdlabo/capacitor-native-ui`. Keep your existing SPM or CocoaPods integration.
82
+
83
+ ```swift
84
+ import SwiftUI
85
+ import NativeUIPlugin
86
+
87
+ struct CounterProps: Decodable {
88
+ let count: Int
89
+ }
90
+
91
+ struct CounterView: View {
92
+ let props: CounterProps
93
+ let actions: NativeUIActions
94
+ @State private var error: String?
95
+
96
+ var body: some View {
97
+ HStack {
98
+ Text("Count: \(props.count)")
99
+ Button("Increment") {
100
+ Task { @MainActor in
101
+ do {
102
+ try await actions.activate("increment")
103
+ error = nil
104
+ } catch {
105
+ self.error = "Action unavailable"
106
+ }
107
+ }
108
+ }
109
+ .disabled(!actions.isAvailable("increment"))
110
+ if let error { Text(error) }
111
+ }
112
+ }
113
+ }
114
+
115
+ @MainActor func registerAppNativeUI() throws {
116
+ try NativeUI.register("app.counter") {
117
+ (props: CounterProps, actions: NativeUIActions) in
118
+ CounterView(props: props, actions: actions)
119
+ }
120
+ }
121
+ ```
122
+
123
+ Call `registerAppNativeUI()` once from `application(_:didFinishLaunchingWithOptions:)`, before JavaScript enables the shell. Handle a registration error there; duplicate or empty names are rejected. Swift source is compiled into the app, so adding or changing a View requires an iOS rebuild. Registration is synchronous setup, not a native bridge command.
124
+
125
+ ## 2b. Register Compose content on Android
126
+
127
+ Android supports **element extensions only**. Standard Ionic and HTML controls stay on the Web, including explicitly marked buttons. Native Vertical Bars and `tab-accessory` are iOS features; Android can combine a Web side rail with element extensions. The same DOM reader, props, action targets, and `markForCheck()` work on both platforms.
128
+
129
+ Run `npx cap sync android` after installing the plugin. Use Kotlin in your Android app. In the root `build.gradle`, add the Kotlin and Compose compiler plugins to `buildscript.dependencies`, using the same Kotlin version as the app (the plugin defaults to `2.2.20`). Define `ext.kotlin_version = '2.2.20'` in the root `buildscript` if it is not already defined:
130
+
131
+ ```groovy
132
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
133
+ classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlin_version"
134
+ ```
135
+
136
+ In `app/build.gradle`, apply `kotlin-android` and `org.jetbrains.kotlin.plugin.compose`, and enable `android { buildFeatures { compose true } }`. The Native UI dependency exposes Compose UI and Foundation; add Material dependencies yourself if your extension uses them.
137
+
138
+ Register once from your `Application.onCreate()`, on the main thread, before JavaScript enables the shell. Use an `Application` subclass configured with `android:name=".App"` on `<application>` in `AndroidManifest.xml`; avoid registering on each Activity recreation.
139
+
140
+ ```kotlin
141
+ import android.app.Application
142
+ import androidx.compose.foundation.background
143
+ import androidx.compose.foundation.clickable
144
+ import androidx.compose.foundation.layout.*
145
+ import androidx.compose.foundation.text.BasicText
146
+ import androidx.compose.runtime.*
147
+ import androidx.compose.ui.Alignment
148
+ import androidx.compose.ui.Modifier
149
+ import androidx.compose.ui.draw.alpha
150
+ import androidx.compose.ui.graphics.Color
151
+ import androidx.compose.ui.semantics.*
152
+ import androidx.compose.ui.text.TextStyle
153
+ import androidx.compose.ui.unit.dp
154
+ import dev.rdlabo.capacitor.nativeui.NativeUI
155
+ import kotlinx.coroutines.CancellationException
156
+ import kotlinx.coroutines.launch
157
+
158
+ class App : Application() {
159
+ override fun onCreate() {
160
+ super.onCreate()
161
+ NativeUI.register(
162
+ name = "app.counter",
163
+ decode = { json -> json.getInt("count") },
164
+ ) { count, actions, context ->
165
+ val scope = rememberCoroutineScope()
166
+ var error by remember { mutableStateOf<String?>(null) }
167
+ val enabled = actions.isAvailable("increment")
168
+ val foreground = if (context.dark) Color.White else Color.Black
169
+ val background = if (context.dark) Color(0xFF202020) else Color.White
170
+ val textStyle = TextStyle(color = foreground)
171
+ Column(Modifier.fillMaxSize().background(background).padding(8.dp)) {
172
+ BasicText("Count: $count", style = textStyle)
173
+ Box(
174
+ Modifier.fillMaxWidth().heightIn(min = 48.dp)
175
+ .alpha(if (enabled) 1f else 0.5f)
176
+ .clickable(enabled = enabled, role = Role.Button, onClickLabel = "Increment count") {
177
+ scope.launch {
178
+ try {
179
+ actions.activate("increment")
180
+ error = null
181
+ } catch (cancelled: CancellationException) {
182
+ throw cancelled
183
+ } catch (unavailable: IllegalStateException) {
184
+ error = "Action unavailable. Try again."
185
+ }
186
+ }
187
+ },
188
+ contentAlignment = Alignment.Center,
189
+ ) { BasicText("Increment", style = textStyle) }
190
+ BasicText(
191
+ error ?: "",
192
+ modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
193
+ style = textStyle,
194
+ )
195
+ }
196
+ }
197
+ }
198
+ }
199
+ ```
200
+
201
+ The shell sizes and positions the Compose host from the source DOM rectangle. `context.sourceFrame` contains CSS pixels; `sourceFrameInDp` contains Compose dp, with `dpPerCSSPixel` as the conversion factor. `sourceID` and `viewportWidth` describe the current source and viewport. `rtl` is also applied through `LocalLayoutDirection`; use `dark` to select your app's Compose theme. Do not apply the source's `x` and `y` again inside your content.
202
+
203
+ The `decode` callback runs before accepting the projection. If it throws, that source stays on the Web. Empty or duplicate registration names are rejected. Compose content owns its styling and accessibility. Rebuild the Android app after changing Kotlin code.
204
+
205
+ ## 3. Enable with local definitions
206
+
207
+ ```ts
208
+ import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
209
+ import { counter } from './native-ui/counter';
210
+
211
+ const shell = await enableNativeUI({ ionic: { auto: false }, extensions: [counter] });
212
+ ```
213
+
214
+ This example enables your extensions without automatically projecting Ionic controls. Explicit `data-native-ui` sources remain opted in. To also project standard Ionic controls on supported iOS, omit `ionic: { auto: false }`. Android standard controls remain Web-rendered either way.
215
+
216
+ Replace your existing startup call with the one above; do not start a second shell. Connect it to your framework using the [app-root lifecycle examples](setup.md#place-startup-in-your-frameworks-app-root), putting these options in the shared startup call.
217
+
218
+ Keep extension objects stable across repeated enable calls. Changing extensions while running requires `await shell.destroy()` first. Extension names must be unique within an activation. Do not register overlapping selectors for the same source. An extension owns its source subtree, so built-in controls inside it are not projected separately.
219
+
220
+ ## 4. Build and try the extension
221
+
222
+ Build your Web assets, then sync and rebuild the platform you configured:
223
+
224
+ - **Android:** run `npx cap sync android`, then build and launch the app from Android Studio. The counter should render with Compose.
225
+ - **iOS:** run `npx cap sync ios`, then build and launch the app from Xcode. On iOS 26+, the counter should render with SwiftUI.
226
+
227
+ Tap Increment: the original Web handler should update `data-count`, and the native count should follow. In a browser, the original Web counter should still work.
228
+
229
+ For source-specific fallback reasons, temporarily enable [`debug: true`](configuration.md#understand-why-a-source-stays-on-the-web). If the source stays on the Web, confirm that registration runs before JavaScript enables the shell and that the names and props agree on both sides. On Android, check that your `Application` subclass is declared in the manifest; on iOS, check that the Swift file belongs to the app target.
230
+
231
+ ## Content and placement are independent
232
+
233
+ | `placement` | Behavior |
234
+ | ---------------------- | ------------------------------------------------------------------------------------- |
235
+ | Omitted or `'element'` | Hosts native content in the source element's Web frame on iOS 26+ or Android API 24+. |
236
+ | `'tab-accessory'` | Attaches the View to a projected tab bar's system SwiftUI tab accessory on iOS 26.1+. |
237
+
238
+ Extensions are independent of `controls` and `ionic.auto`, including with `enableVerticalControlArea()`. The application controls their presentation: return `null` from the reader when a source should stay on the Web, then call `markForCheck()` when that condition changes. Accessories still require their supported native tab host.
239
+
240
+ ### Tab accessory requirements
241
+
242
+ | Requirement | Supported configuration |
243
+ | ------------------- | ------------------------------------------------------------------------------------------------- |
244
+ | Source location | Inside its owning `ion-tabs`; the shell resolves that element's `ion-tab-bar`. |
245
+ | Native host | Vertical Bars SwiftUI tab host on iOS 26.1+. |
246
+ | Accessory count | A single accessory for the host. |
247
+ | Web fallback layout | Keep an equivalent Web layout, such as a fixed footer; the source retains its space while hidden. |
248
+
249
+ Ordinary UIKit tab bars, missing native tabs, iOS 26.0 or competing accessories keep the source on the Web. Applications do not pass native controller references, and the shell does not approximate a system accessory with a floating overlay.
250
+
251
+ ## Read source geometry and presentation state
252
+
253
+ The shell measures the source DOM element automatically. With the default `element` placement, it positions and sizes the SwiftUI host to match that rectangle. You do not need to send coordinates in `props` or position the native view yourself.
254
+
255
+ Use the three-argument registration closure to receive that information:
256
+
257
+ ```swift
258
+ try NativeUI.register("app.counter") {
259
+ (props: CounterProps, actions: NativeUIActions, context: NativeUIContext) in
260
+ CounterView(props: props, actions: actions)
261
+ .padding(.horizontal, context.sourceFrame.width < 240 ? 4 : 12)
262
+ }
263
+ ```
264
+
265
+ The existing `(props, actions)` closure remains supported. Both forms use the same registration names and duplicate-name checks.
266
+
267
+ | Context property | Meaning |
268
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
269
+ | `sourceID` | Opaque source identity for its current lifetime; not the HTML `id`. |
270
+ | `sourceFrame` | DOM `getBoundingClientRect()` in CSS pixels relative to the Web viewport, including `x`, `y`, `width`, and `height`. |
271
+ | `sourceFrameInWebView` | The source rectangle converted to UIKit points in the WebView's local coordinate system; not screen coordinates. |
272
+ | `viewportWidth` | CSS viewport width used for that snapshot's coordinate conversion. |
273
+ | `pointsPerCSSPixel` | UIKit points per CSS pixel; not device pixel density. |
274
+ | `placement` | `.element` or `.tabAccessory`. |
275
+ | `colorScheme` | `.light` or `.dark`, also applied to the SwiftUI environment. |
276
+ | `layoutDirection` | `.leftToRight` or `.rightToLeft`, also applied to the SwiftUI environment. |
277
+
278
+ The closure receives a fresh context when the shell applies an updated snapshot, including source movement, resizing, and appearance changes. Use the current context rather than caching its geometry in `@State`. Updates cross the Web/native bridge; they are not a synchronous DOM measurement from Swift.
279
+
280
+ For `tabAccessory`, `sourceFrame` still describes the original DOM element. The system assigns the accessory's actual location and size. Use SwiftUI's `GeometryReader` inside your View for the actual local space in either placement:
281
+
282
+ ```swift
283
+ GeometryReader { geometry in
284
+ CounterView(props: props, actions: actions)
285
+ .frame(width: geometry.size.width, height: geometry.size.height)
286
+ }
287
+ ```
288
+
289
+ Do not apply `sourceFrame.x` or `y` as another SwiftUI offset: the shell already positions the host for `element`. Your View lays out its contents within that host.
290
+
291
+ ## Use the shared projection capabilities
292
+
293
+ | Capability | How an extension uses it |
294
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
295
+ | Position and size | Automatic host placement plus `NativeUIContext`; actual local bounds through `GeometryReader`. |
296
+ | Dark mode and RTL | `context` or SwiftUI `@Environment(\.colorScheme)` and `@Environment(\.layoutDirection)`. |
297
+ | Reduce Motion and accessibility | SwiftUI environment values such as `@Environment(\.accessibilityReduceMotion)` and native accessibility modifiers. Give custom controls accessible names. |
298
+ | DOM-backed application state | Return JSON `props` from `read(element)`; DOM changes trigger synchronization. For changes without a DOM mutation, call `shell.markForCheck()`. |
299
+ | Enabled/visible action targets | `actions.availableNames` or `actions.isAvailable("increment")`; unavailable DOM targets are omitted. |
300
+ | Invoke the original Web action | `try await actions.activate("increment")`; completion and failure follow the contract below. |
301
+ | Temporary Web fallback and teardown | Managed automatically for unsupported content, overlays, navigation suspension, and source removal. |
302
+ | Application-controlled suspension | Use the shell handle's `suspend()`, the returned token's `resume()`, and `destroy()` from JavaScript. |
303
+
304
+ For example, disable a native button when its original action is unavailable:
305
+
306
+ ```swift
307
+ Button("Increment") {
308
+ Task { @MainActor in
309
+ do { try await actions.activate("increment") }
310
+ catch { /* Present an application-appropriate error. */ }
311
+ }
312
+ }
313
+ .disabled(!actions.isAvailable("increment"))
314
+ ```
315
+
316
+ Availability is a snapshot, not a reservation: a target can disappear before activation, so handle errors too. Custom text, selected state, fonts, colors, and badges belong in your extension's `props` and SwiftUI View; the shell does not infer a custom component's visual structure from CSS. Read the desired element's computed styles and attributes in your reader. Use the shared image and color conversion APIs below instead of reimplementing those transformations.
317
+
318
+ ## Schedule a source check
319
+
320
+ DOM attributes, text, children, observed element sizes, scrolling, and supported lifecycle events trigger automatic checks. The shell does not poll JavaScript variables or arbitrary DOM properties. After changing state that your reader uses without a DOM mutation, request a check:
321
+
322
+ ```ts
323
+ source.playbackPosition = newPosition; // An application-defined property read by your extension.
324
+ shell.markForCheck();
325
+ ```
326
+
327
+ `markForCheck(): void` schedules a re-read and geometry measurement; it does not update the application's DOM or force a native redraw. Repeated calls are batched, and unchanged snapshots are not sent. A call during a bridge update schedules another check afterwards. Suspended projection stays suspended until resumed. Calls after `destroy()` or in an environment without a projection runtime do nothing.
328
+
329
+ Call after your framework has committed any relevant DOM changes. This API does not trigger Angular change detection, React rendering, or Vue rendering, and it is not a promise that waits for native completion. It also works for standard controls when their properties change without an observable mutation.
330
+
331
+ ## Reuse native artwork and color conversion
332
+
333
+ A reader's second argument exposes the standard SVG capture and rasterization path. It accepts a local `svg` or a resolved `ion-icon` belonging to the source and returns an **opaque image reference**, or `null` for unsupported artwork. Return the reference in `props`; do not try to read image bytes synchronously:
334
+
335
+ ```ts
336
+ const badge = defineNativeUI({
337
+ name: 'app.badge',
338
+ selector: '[data-badge]',
339
+ read(element, context) {
340
+ return {
341
+ props: {
342
+ icon: context.image(element.querySelector<HTMLElement>('ion-icon')),
343
+ color: getComputedStyle(element).color,
344
+ },
345
+ };
346
+ },
347
+ });
348
+ ```
349
+
350
+ Before sending the snapshot, the shell resolves references into `{ data, width, height, template }`: base64 PNG data, CSS dimensions, and rendering mode. Nested references and copies are supported; the `$nativeUIImage` key is reserved for references returned by the current read context. Use `{ template: true }` as the second argument to `image()` when SwiftUI should tint the icon; the default preserves source colors. The same local SVG restrictions, RTL capture, display-scale rasterization, and image cache used by standard controls apply. External resources and unsupported SVG content return `null`; unresolved `ion-icon` content should retain your Web fallback until loaded. Rasterization failure or props exceeding the size limit keeps that extension on the Web.
351
+
352
+ On iOS, decode the resulting value with `NativeUIImage` and use the shared converters:
353
+
354
+ ```swift
355
+ struct BadgeProps: Decodable {
356
+ let icon: NativeUIImage?
357
+ let color: String
358
+ }
359
+
360
+ struct BadgeView: View {
361
+ let props: BadgeProps
362
+ var body: some View {
363
+ if let icon = props.icon, let image = NativeUIRendering.image(icon) {
364
+ Image(uiImage: image)
365
+ .foregroundStyle(Color(uiColor: NativeUIRendering.color(props.color)))
366
+ }
367
+ }
368
+ }
369
+ ```
370
+
371
+ `NativeUIRendering.image` returns `nil` for invalid image data. `NativeUIRendering.color` accepts computed `rgb()`/`rgba()` values; unsupported formats use `.label`, or the `fallback:` color you pass. Both are shared with the standard native renderers. They do not evaluate CSS selectors, variables, named colors, or arbitrary stylesheets.
372
+
373
+ ## Lifecycle and action completion
374
+
375
+ The shell owns discovery, geometry, state synchronization, native controller containment, overlay retirement, suspension and teardown. Extensions do not implement attach/detach hooks. Web sources are hidden only after native accepts the projection. An unregistered renderer, a failing props decoder, unsupported placement or failed reader leaves that instance on the Web. Existing shell overlay policy applies; extensions return to the Web while Web overlays cover the page.
376
+
377
+ Element placement requires the source to fit fully inside the viewport and its ancestors' overflow clipping bounds, including Ionic's shadow scroll containers. Partially clipped sources stay on the Web and become eligible again when scrolled fully into view. Tab accessories use the system host's placement instead.
378
+
379
+ ### Action completion
380
+
381
+ | Event | Result of `try await actions.activate(name)` |
382
+ | ------------------------------------------------------------------ | -------------------------------------------- |
383
+ | Original Web element receives its click | Completes. |
384
+ | Handler starts a network request, navigation or modal presentation | Does not wait for that application work. |
385
+ | Action is disabled, removed or stale | Rejects. |
386
+ | Teardown, native task cancellation or bridge timeout | Settles pending calls. |
387
+
388
+ The bridge acknowledgement is a Capacitor Promise method.
389
+
390
+ The [demo reader](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/src/native-ui/counter.ts) and [demo SwiftUI View](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/ios/App/App/NativeUICounter.swift) show both placements using app-local files. Open the Native UI demo page to compare native projection with its Web fallback.
391
+
392
+ ## Example: mini-player
393
+
394
+ The demo mini-player shows the same pattern with track metadata and playback actions: a [DOM reader and Web fallback](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/src/native-ui/mini-player.ts) paired with an [app-local SwiftUI View](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/demo/ios/App/App/NativeMiniPlayer.swift). The Web app owns playback state and modal presentation; native `open` and `toggle` actions activate its original buttons. This is a UI example, not an audio engine. Adapt the files after completing the extension setup above.