@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Package.swift ADDED
@@ -0,0 +1,23 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "RdlaboCapacitorNativeUi",
6
+ platforms: [.iOS(.v15)],
7
+ products: [
8
+ .library(name: "RdlaboCapacitorNativeUi", targets: ["NativeUIPlugin"])
9
+ ],
10
+ dependencies: [.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")],
11
+ targets: [.target(
12
+ name: "NativeUIPlugin",
13
+ dependencies: [
14
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
15
+ .product(name: "Cordova", package: "capacitor-swift-pm")
16
+ ],
17
+ path: "ios/Sources/NativeUIPlugin"
18
+ ), .testTarget(
19
+ name: "NativeUIPluginTests",
20
+ dependencies: ["NativeUIPlugin"],
21
+ path: "ios/Tests/NativeUIPluginTests"
22
+ )]
23
+ )
package/README.md CHANGED
@@ -1,3 +1,67 @@
1
- # Temporary Holding Version
1
+ # Capacitor Native UI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Render supported Web controls with UIKit and SwiftUI on iOS while keeping your application state, routing, and handlers in the WebView. Use existing Ionic markup or explicit HTML buttons; custom SwiftUI or Android Compose content can be added through experimental app-local extensions.
4
+
5
+ Native UI is opt-in and currently a preview. Standard controls on Android, browsers, older iOS, and unsupported controls retain Web presentation. Android supports experimental app-local Compose extensions. Keep your application's styles and layout; the iOS theme is optional.
6
+
7
+ ## How projection works
8
+
9
+ A **projection** is another presentation of a Web control. Your application keeps the original DOM element, state and event handlers. The plugin reads supported labels, icons, selected/disabled state and geometry, then associates the rendered control with that source element. It does not convert the page into a native screen or run a second copy of your application.
10
+
11
+ | Web source | Projected presentation |
12
+ | ----------------------------------------------- | ----------------------------------------------------------------------------------------- |
13
+ | Supported Ionic buttons, tabs and segments | Native UIKit/SwiftUI controls on supported iOS |
14
+ | Explicitly marked HTML buttons and back buttons | Native buttons on supported iOS |
15
+ | Eligible controls placed in Vertical Bars | SwiftUI controls in the available system rail; Web rail presentation otherwise |
16
+ | An app-local extension's DOM element | The SwiftUI or Compose content registered by your app, using props read from that element |
17
+
18
+ The original element **stays in the DOM** while projected. Its bindings and handlers remain attached. Once native presentation is accepted, the runtime hides the source visually, prevents direct pointer interaction and hides it from Web accessibility so there are not two interactive copies. Element projection retains the source's layout box; rail spacing is supplied by your app or theme. A Web rail similarly uses its adapter's Web presentation: Ionic layout/clones or HTML proxy buttons in your `webHost`.
19
+
20
+ For example, tapping a projected Save button activates the original Web button. Its existing handler performs the save and updates application state; supported DOM changes then update the projection. Tabs and other controls likewise route actions through their source adapters. Continue updating the source, rather than managing separate native state. For reader values that change without a DOM mutation, use `markForCheck()`.
21
+
22
+ Projection changes presentation; it does not translate arbitrary HTML or CSS into native views. Native controls use supported data and styling; a custom Web appearance is not guaranteed to be reproduced. Removing or disabling a source prevents further valid actions. If projection is unavailable or stopped, the runtime releases its hiding state and the source returns to Web presentation, or to an available Web rail. Your application's own hidden/disabled state still applies.
23
+
24
+ ## Choose what to build
25
+
26
+ | Goal | Start here |
27
+ | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
28
+ | Render existing Ionic controls natively | [Ionic setup](docs/setup.md) |
29
+ | Use native buttons without Ionic | [HTML setup](docs/framework-independent.md) |
30
+ | Add a side rail to Ionic while keeping your theme | [Vertical Bars step by step](docs/ionic-vertical-bars.md) |
31
+ | Add a side rail without Ionic | [HTML rail setup](docs/framework-independent.md) |
32
+ | Add native controls to an existing iOS 27 theme app | [Add Native UI to your theme](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/add-native-ui.md) |
33
+ | Render custom SwiftUI or Android Compose content | [App-local extensions (experimental)](docs/native-ui-extensions.md) |
34
+
35
+ ## Install from npm
36
+
37
+ Install in your Capacitor 8 application:
38
+
39
+ ```sh
40
+ npm install @rdlabo/capacitor-native-ui
41
+ ```
42
+
43
+ If you also use the iOS 27 theme, install or update both packages together:
44
+
45
+ ```sh
46
+ npm install @rdlabo/capacitor-native-ui @rdlabo/ionic-theme-ios27
47
+ ```
48
+
49
+ Both packages share the same release version. Declare Native UI as a direct dependency so Capacitor can discover the plugin. Theme-only apps can install just the theme; its dependency is resolved by npm and does not activate native controls.
50
+
51
+ Continue with [Ionic setup](docs/setup.md), [HTML setup](docs/framework-independent.md), or [extensions](docs/native-ui-extensions.md). After configuring startup, build your application's Web assets, run `npx cap sync` and rebuild the native app. No repository checkout or archive download is needed.
52
+
53
+ Upgrading from the native APIs bundled in theme 1.2.0 or 1.2.1? Follow the [migration guide](docs/migration.md) for import, configuration and native project changes.
54
+
55
+ ## Requirements
56
+
57
+ Native startup requires Capacitor 8 and an iOS or Android project. Android extensions require API 24+ and Kotlin/Compose. Ordinary native rendering requires iOS 26+ and Xcode 26+. The Ionic adapter supports Ionic `>=8.8.1 <10`; the `/core` HTML adapter requires no Ionic or theme package. [Vertical Bars](docs/vertical-bars.md) and [extension placement](docs/native-ui-extensions.md#content-and-placement-are-independent) have additional device requirements.
58
+
59
+ For standard controls, follow the setup guide for your markup; the Ionic and HTML adapters use different imports. For custom native content, start directly with the [extensions guide](docs/native-ui-extensions.md).
60
+
61
+ ## Reference
62
+
63
+ - [Supported Ionic controls](docs/native-ui.md#supported-markup) and fallback behavior.
64
+ - [Configuration](docs/configuration.md): defaults, selected controls, rail-only mode, and live changes.
65
+ - [Light and dark appearance](docs/appearance.md): inherit the Web theme, configure custom HTML, and understand shared rail appearance.
66
+ - [Vertical Bars](docs/vertical-bars.md): eligibility, appearance, and runtime API.
67
+ - [Migration](docs/migration.md): package names, imports, and native project updates.
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'RdlaboCapacitorNativeUi'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.homepage = package['repository']['url']
11
+ s.author = package['author']
12
+ s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
+ s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
+ s.ios.deployment_target = '15.0'
15
+ s.dependency 'Capacitor'
16
+ s.swift_version = '5.9'
17
+ end
@@ -0,0 +1,69 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
4
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
5
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
6
+ }
7
+
8
+ buildscript {
9
+ ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '2.2.20'
10
+ repositories {
11
+ google()
12
+ mavenCentral()
13
+ }
14
+ dependencies {
15
+ classpath 'com.android.tools.build:gradle:8.13.0'
16
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
17
+ classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlin_version"
18
+ }
19
+ }
20
+
21
+ apply plugin: 'com.android.library'
22
+ apply plugin: 'kotlin-android'
23
+ apply plugin: 'org.jetbrains.kotlin.plugin.compose'
24
+
25
+ android {
26
+ namespace = "dev.rdlabo.capacitor.nativeui"
27
+ compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
28
+ defaultConfig {
29
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
30
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
31
+ versionCode 1
32
+ versionName "1.0"
33
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
34
+ }
35
+ buildTypes {
36
+ release {
37
+ minifyEnabled false
38
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
39
+ }
40
+ }
41
+ lintOptions {
42
+ abortOnError = false
43
+ }
44
+ buildFeatures { compose true }
45
+ kotlinOptions { jvmTarget = '21' }
46
+ compileOptions {
47
+ sourceCompatibility JavaVersion.VERSION_21
48
+ targetCompatibility JavaVersion.VERSION_21
49
+ }
50
+ }
51
+
52
+ repositories {
53
+ google()
54
+ mavenCentral()
55
+ }
56
+
57
+ dependencies {
58
+ api platform("androidx.compose:compose-bom:2025.10.00")
59
+ api "androidx.compose.ui:ui"
60
+ api "androidx.compose.foundation:foundation"
61
+ api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
62
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
63
+ implementation project(':capacitor-android')
64
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
65
+ testImplementation "junit:junit:$junitVersion"
66
+ testImplementation "org.json:json:20250517"
67
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
68
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
69
+ }
File without changes
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"></manifest>
@@ -0,0 +1,51 @@
1
+ package dev.rdlabo.capacitor.nativeui
2
+
3
+ import android.os.Looper
4
+ import androidx.compose.runtime.Composable
5
+ import org.json.JSONObject
6
+ import java.util.Collections
7
+
8
+ /**
9
+ * App-local Compose renderers. Register on the main thread before starting the shell.
10
+ * Experimental API: registration interfaces and behavior may change in future releases.
11
+ */
12
+ object NativeUI {
13
+ internal val renderers = linkedMapOf<String, (JSONObject, NativeUIActions, NativeUIContext) -> (@Composable () -> Unit)>()
14
+
15
+ fun <Props> register(
16
+ name: String,
17
+ decode: (JSONObject) -> Props,
18
+ content: @Composable (Props, NativeUIActions, NativeUIContext) -> Unit,
19
+ ) {
20
+ check(Looper.myLooper() == Looper.getMainLooper()) { "Register Native UI on the main thread" }
21
+ require(name.isNotBlank() && !renderers.containsKey(name)) { "Native UI names must be nonempty and unique" }
22
+ renderers[name] = { json, actions, context ->
23
+ val props = decode(json)
24
+ val view: @Composable () -> Unit = { content(props, actions, context) }
25
+ view
26
+ }
27
+ }
28
+ }
29
+
30
+ data class NativeUIFrame(val x: Double, val y: Double, val width: Double, val height: Double)
31
+
32
+ /** Source geometry, not screen coordinates. Android currently supports element placement only. */
33
+ data class NativeUIContext(
34
+ val sourceID: String,
35
+ val sourceFrame: NativeUIFrame,
36
+ val sourceFrameInDp: NativeUIFrame,
37
+ val viewportWidth: Double,
38
+ val dpPerCSSPixel: Double,
39
+ val dark: Boolean,
40
+ val rtl: Boolean,
41
+ )
42
+
43
+ /** Completion acknowledges the DOM click dispatch, not the application's asynchronous work. */
44
+ class NativeUIActions internal constructor(names: Set<String>, private val dispatch: suspend (String) -> Unit) {
45
+ val availableNames: Set<String> = Collections.unmodifiableSet(names.toSet())
46
+ fun isAvailable(name: String): Boolean = name in availableNames
47
+ suspend fun activate(name: String) {
48
+ check(isAvailable(name)) { "Native UI action is unavailable" }
49
+ dispatch(name)
50
+ }
51
+ }
@@ -0,0 +1,192 @@
1
+ package dev.rdlabo.capacitor.nativeui
2
+
3
+ import android.os.Handler
4
+ import android.os.Looper
5
+ import android.view.View
6
+ import android.view.ViewGroup
7
+ import android.widget.FrameLayout
8
+ import androidx.compose.runtime.Composable
9
+ import androidx.compose.runtime.CompositionLocalProvider
10
+ import androidx.compose.runtime.mutableStateOf
11
+ import androidx.compose.ui.platform.ComposeView
12
+ import androidx.compose.ui.platform.LocalLayoutDirection
13
+ import androidx.compose.ui.platform.ViewCompositionStrategy
14
+ import androidx.compose.ui.unit.LayoutDirection
15
+ import com.getcapacitor.JSArray
16
+ import com.getcapacitor.JSObject
17
+ import com.getcapacitor.Plugin
18
+ import com.getcapacitor.PluginCall
19
+ import com.getcapacitor.PluginMethod
20
+ import com.getcapacitor.annotation.CapacitorPlugin
21
+ import kotlinx.coroutines.Dispatchers
22
+ import kotlinx.coroutines.suspendCancellableCoroutine
23
+ import kotlinx.coroutines.withContext
24
+ import org.json.JSONObject
25
+ import java.util.UUID
26
+ import kotlin.coroutines.resume
27
+ import kotlin.coroutines.resumeWithException
28
+ import kotlin.math.roundToInt
29
+
30
+ @CapacitorPlugin(name = "NativeUI")
31
+ class NativeUIPlugin : Plugin() {
32
+ private val handler = Handler(Looper.getMainLooper())
33
+ private var host: FrameLayout? = null
34
+ private var revision = 0L
35
+ private var sequence = 0L
36
+ private var paused = false
37
+ private val entries = linkedMapOf<String, Entry>()
38
+ private val pending = mutableMapOf<String, (Boolean) -> Unit>()
39
+ private val layoutListener = View.OnLayoutChangeListener { _, l, t, r, b, oldL, oldT, oldR, oldB ->
40
+ if (l != oldL || t != oldT || r != oldR || b != oldB) {
41
+ host?.visibility = View.INVISIBLE
42
+ failActions()
43
+ notifyListeners("webViewMetricsChange", JSObject().put("radius", 0))
44
+ }
45
+ }
46
+
47
+ private class Entry(val view: ComposeView, var node: ExtensionNode) {
48
+ val content = mutableStateOf<(@Composable () -> Unit)?>(null)
49
+ }
50
+
51
+ override fun load() {
52
+ activity.runOnUiThread { bridge.webView.addOnLayoutChangeListener(layoutListener) }
53
+ }
54
+
55
+ @PluginMethod
56
+ fun configure(call: PluginCall) = onMain(call) {
57
+ clearViews()
58
+ revision = 0
59
+ sequence = 0
60
+ call.resolve(JSObject().put("supported", true).put("extensions", JSArray(NativeUI.renderers.keys.toList())))
61
+ }
62
+
63
+ @PluginMethod
64
+ fun getWebViewMetrics(call: PluginCall) = call.resolve(JSObject().put("radius", 0))
65
+
66
+ @PluginMethod
67
+ fun update(call: PluginCall) = onMain(call) {
68
+ val snapshot = NativeUISnapshot.parse(call.data)
69
+ require(snapshot.revision > revision) { "Stale Native UI snapshot" }
70
+ val web = bridge.webView
71
+ val parent = web.parent as? ViewGroup ?: error("WebView has no host")
72
+ require(web.width > 0 && web.height > 0 && web.scaleX == 1f && web.scaleY == 1f) { "WebView geometry unavailable" }
73
+ val scale = web.width / snapshot.viewportWidth
74
+ val dpScale = scale / web.resources.displayMetrics.density
75
+ val accepted = linkedMapOf<String, Pair<ExtensionNode, @Composable () -> Unit>>()
76
+ val rejected = snapshot.rejected.toMutableList()
77
+ for (node in snapshot.nodes) {
78
+ val renderer = NativeUI.renderers[node.name]
79
+ if (renderer == null || paused) {
80
+ rejected.add(node.id)
81
+ continue
82
+ }
83
+ try {
84
+ val f = node.frame
85
+ require(listOf(f.x * scale, f.y * scale, f.width * scale, f.height * scale).all { it in -1_000_000.0..1_000_000.0 })
86
+ val context = NativeUIContext(node.id, f, NativeUIFrame(f.x * dpScale, f.y * dpScale, f.width * dpScale, f.height * dpScale), snapshot.viewportWidth, dpScale, node.dark, node.rtl)
87
+ val actions = NativeUIActions(node.actions.keys) { action -> activate(node, action) }
88
+ accepted[node.id] = node to renderer(JSONObject(node.props), actions, context)
89
+ } catch (_: Exception) {
90
+ rejected.add(node.id)
91
+ }
92
+ }
93
+ revision = snapshot.revision
94
+ val container = host ?: FrameLayout(activity).also {
95
+ it.clipChildren = true
96
+ it.clipToPadding = true
97
+ parent.addView(it, ViewGroup.LayoutParams(web.width, web.height))
98
+ host = it
99
+ }
100
+ container.layoutParams = container.layoutParams.apply { width = web.width; height = web.height }
101
+ container.x = web.x
102
+ container.y = web.y
103
+ for (id in entries.keys.toList()) if (id !in accepted) removeEntry(id)
104
+ for ((id, pair) in accepted) {
105
+ val (node, content) = pair
106
+ val entry = entries[id] ?: Entry(ComposeView(activity), node).also { entry ->
107
+ entry.view.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
108
+ container.addView(entry.view)
109
+ entry.view.setContent { entry.content.value?.invoke() }
110
+ entries[id] = entry
111
+ }
112
+ entry.node = node
113
+ entry.content.value = {
114
+ CompositionLocalProvider(LocalLayoutDirection provides if (node.rtl) LayoutDirection.Rtl else LayoutDirection.Ltr) { content() }
115
+ }
116
+ entry.view.layoutParams = FrameLayout.LayoutParams((node.frame.width * scale).roundToInt().coerceAtLeast(1), (node.frame.height * scale).roundToInt().coerceAtLeast(1)).apply {
117
+ leftMargin = (node.frame.x * scale).roundToInt()
118
+ topMargin = (node.frame.y * scale).roundToInt()
119
+ }
120
+ }
121
+ container.visibility = if (paused) View.INVISIBLE else View.VISIBLE
122
+ call.resolve(JSObject().put("revision", revision).put("rejectedControls", JSArray(rejected)))
123
+ }
124
+
125
+ private suspend fun activate(node: ExtensionNode, name: String): Unit = withContext(Dispatchers.Main.immediate) {
126
+ check(!paused && host?.visibility == View.VISIBLE && entries[node.id]?.node == node) { "Native UI action is stale" }
127
+ val target = node.actions[name] ?: error("Native UI action is unavailable")
128
+ suspendCancellableCoroutine { continuation ->
129
+ val request = UUID.randomUUID().toString()
130
+ val timeout = Runnable { pending.remove(request)?.invoke(false) }
131
+ pending[request] = { accepted ->
132
+ handler.removeCallbacks(timeout)
133
+ if (continuation.isActive) {
134
+ if (accepted) continuation.resume(Unit)
135
+ else continuation.resumeWithException(IllegalStateException("Native UI action was not accepted"))
136
+ }
137
+ }
138
+ continuation.invokeOnCancellation { handler.post { pending.remove(request); handler.removeCallbacks(timeout) } }
139
+ handler.postDelayed(timeout, 5000)
140
+ notifyListeners("activate", JSObject().put("revision", revision).put("id", target).put("sequence", ++sequence).put("requestId", request))
141
+ }
142
+ }
143
+
144
+ @PluginMethod
145
+ fun completeExtensionAction(call: PluginCall) = onMain(call) {
146
+ val request = call.getString("requestId") ?: error("Missing requestId")
147
+ pending.remove(request)?.invoke(call.getBoolean("accepted", false) == true)
148
+ call.resolve()
149
+ }
150
+
151
+ @PluginMethod
152
+ fun clear(call: PluginCall) = onMain(call) {
153
+ val next = call.data.getLong("revision")
154
+ require(next > revision) { "Stale Native UI clear" }
155
+ revision = next
156
+ clearViews()
157
+ call.resolve()
158
+ }
159
+
160
+ private fun onMain(call: PluginCall, block: () -> Unit) {
161
+ activity.runOnUiThread {
162
+ try { block() } catch (error: Exception) { call.reject(error.message ?: "Native UI failed", error) }
163
+ }
164
+ }
165
+ private fun failActions() {
166
+ val callbacks = pending.values.toList()
167
+ pending.clear()
168
+ callbacks.forEach { it(false) }
169
+ }
170
+ private fun removeEntry(id: String) {
171
+ entries.remove(id)?.let { it.view.disposeComposition(); host?.removeView(it.view) }
172
+ }
173
+ private fun clearViews() {
174
+ failActions()
175
+ entries.keys.toList().forEach(::removeEntry)
176
+ (host?.parent as? ViewGroup)?.removeView(host)
177
+ host = null
178
+ }
179
+ override fun handleOnPause() {
180
+ paused = true
181
+ host?.visibility = View.INVISIBLE
182
+ failActions()
183
+ }
184
+ override fun handleOnResume() {
185
+ paused = false
186
+ notifyListeners("webViewMetricsChange", JSObject().put("radius", 0))
187
+ }
188
+ override fun handleOnDestroy() {
189
+ bridge.webView.removeOnLayoutChangeListener(layoutListener)
190
+ clearViews()
191
+ }
192
+ }
@@ -0,0 +1,57 @@
1
+ package dev.rdlabo.capacitor.nativeui
2
+
3
+ import org.json.JSONObject
4
+
5
+ internal data class ExtensionNode(
6
+ val id: String,
7
+ val frame: NativeUIFrame,
8
+ val dark: Boolean,
9
+ val rtl: Boolean,
10
+ val name: String,
11
+ val props: String,
12
+ val actions: Map<String, String>,
13
+ )
14
+
15
+ internal data class NativeUISnapshot(val revision: Long, val viewportWidth: Double, val nodes: List<ExtensionNode>, val rejected: List<String>) {
16
+ companion object {
17
+ fun parse(json: JSONObject): NativeUISnapshot {
18
+ val revisionValue = json.getDouble("revision")
19
+ require(revisionValue.isFinite() && revisionValue >= 0 && revisionValue % 1.0 == 0.0 && revisionValue <= 9007199254740991.0)
20
+ val width = json.getDouble("viewportWidth")
21
+ require(width.isFinite() && width > 0)
22
+ val controls = json.getJSONArray("controls")
23
+ require(controls.length() <= 1000)
24
+ val ids = mutableSetOf<String>()
25
+ val nodes = mutableListOf<ExtensionNode>()
26
+ val rejected = mutableListOf<String>()
27
+ for (index in 0 until controls.length()) {
28
+ val item = controls.getJSONObject(index)
29
+ val id = item.getString("id")
30
+ require(id.isNotBlank() && ids.add(id))
31
+ if (item.getString("kind") != "extension") {
32
+ rejected.add(id)
33
+ continue
34
+ }
35
+ val extension = item.getJSONObject("extension")
36
+ if (!extension.isNull("tabBarId") || item.optString("placement") == "vertical-bars") {
37
+ rejected.add(id)
38
+ continue
39
+ }
40
+ val frame = NativeUIFrame(item.getDouble("x"), item.getDouble("y"), item.getDouble("width"), item.getDouble("height"))
41
+ require(listOf(frame.x, frame.y, frame.width, frame.height).all { it.isFinite() })
42
+ require(frame.width > 0 && frame.height > 0)
43
+ val props = extension.getString("props")
44
+ require(props.length <= 65536)
45
+ JSONObject(props)
46
+ val rawActions = extension.getJSONObject("actions")
47
+ require(rawActions.length() <= 30)
48
+ val actions = rawActions.keys().asSequence().associateWith { key ->
49
+ require(key.isNotBlank())
50
+ rawActions.getString(key).also { require(it.isNotBlank()) }
51
+ }
52
+ nodes.add(ExtensionNode(id, frame, item.optBoolean("dark"), item.optBoolean("rtl"), extension.getString("name"), props, actions))
53
+ }
54
+ return NativeUISnapshot(revisionValue.toLong(), width, nodes, rejected)
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll };
package/dist/core.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { NativeUIExtension, NativeUIImageReference, NativeUIReadContext, NativeUIValue, defineNativeUI } from "./native/extensions.js";
2
+ import { ControlProjection, IonicProjectionOptions, NativeUIControls, NativeUIHandle, NativeUIOptions, NativeUIStatus, NativeUISuspension, NativeUIVerticalBarsOptions } from "./native/definitions.js";
3
+ import { enableNativeUI } from "./native/startup.js";
4
+ export { type ControlProjection, type IonicProjectionOptions, type NativeUIControls, type NativeUIExtension, type NativeUIHandle, type NativeUIImageReference, type NativeUIOptions, type NativeUIReadContext, type NativeUIStatus, type NativeUISuspension, type NativeUIValue, type NativeUIVerticalBarsOptions, defineNativeUI, enableNativeUI };
package/dist/core.js ADDED
@@ -0,0 +1,3 @@
1
+ import { defineNativeUI } from "./native/extensions.js";
2
+ import { enableNativeUI } from "./native/startup.js";
3
+ export { defineNativeUI, enableNativeUI };
@@ -0,0 +1,14 @@
1
+ import { NativeUIExtension, NativeUIValue, defineNativeUI } from "./extensions.js";
2
+ import { NativeUIComponent, NativeUIControls, NativeUIPlugin, NativeUIRuntimeHandle, NativeUIRuntimeOptions, NativeUIStatus, NativeUISuspension, VerticalBarEdge, WebViewMetrics } from "./definitions.js";
3
+ //#region src/native/activation.d.ts
4
+ interface OverlayController {
5
+ suspend(): Promise<() => void>;
6
+ destroy(): Promise<void>;
7
+ }
8
+ type OverlayFactory = (doc: Document, plugin: NativeUIPlugin, options: NativeUIRuntimeOptions, retain: (deferActivation?: (id: string) => boolean) => () => void, nativeRail: () => boolean) => Promise<OverlayController>;
9
+ export declare const NativeUI: NativeUIPlugin;
10
+ export declare const configureNativeTransition: () => Promise<WebViewMetrics>;
11
+ export declare const enableNativeUI: (options?: NativeUIRuntimeOptions, overlayFactory?: OverlayFactory) => Promise<NativeUIRuntimeHandle>;
12
+ //#endregion
13
+ export { type NativeUIComponent, type NativeUIControls, type NativeUIExtension, type NativeUIRuntimeHandle, type NativeUIRuntimeOptions, type NativeUIStatus, type NativeUISuspension, type NativeUIValue, type VerticalBarEdge, type WebViewMetrics, defineNativeUI };
14
+ //# sourceMappingURL=activation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activation.d.ts","names":[],"sources":["../../src/native/activation.ts"],"mappings":";;;UAUU;EACR,WAAW;EACX,WAAW;;KAER,kBACH,KAAK,UACL,QAAQ,gBACR,SAAS,wBACT,SAAS,mBAAmB,uCAC5B,8BACG,QAAQ;qBAkBA,UAAQ;qBAqGR,iCAAsC,QAAQ;qBAuB9C,iBAAkB,UAAS,wBAA6B,iBAAiB,mBAAiB,QAAQ"}