@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/package.json CHANGED
@@ -1,6 +1,91 @@
1
1
  {
2
+ "version": "2.0.0-1",
3
+ "author": "Masahiko Sakakibara <sakakibara@rdlabo.jp>",
4
+ "license": "MIT",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/rdlabo-dev/ionic-theme-ios27.git",
8
+ "directory": "packages/capacitor-native-ui"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/rdlabo-dev/ionic-theme-ios27/issues"
12
+ },
13
+ "type": "module",
2
14
  "name": "@rdlabo/capacitor-native-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
15
+ "description": "Web-owned native UI projection for Capacitor",
16
+ "keywords": [
17
+ "capacitor",
18
+ "plugin",
19
+ "native",
20
+ "ios",
21
+ "android"
22
+ ],
23
+ "main": "./dist/native/index.js",
24
+ "module": "./dist/native/index.js",
25
+ "types": "./dist/native/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/native/index.d.ts",
29
+ "import": "./dist/native/index.js"
30
+ },
31
+ "./vertical-bars": {
32
+ "types": "./dist/vertical-bars.d.ts",
33
+ "import": "./dist/vertical-bars.js"
34
+ },
35
+ "./integration": {
36
+ "types": "./dist/native-integration/index.d.ts",
37
+ "import": "./dist/native-integration/index.js"
38
+ },
39
+ "./package.json": "./package.json",
40
+ "./core": {
41
+ "types": "./dist/core.d.ts",
42
+ "import": "./dist/core.js"
43
+ }
44
+ },
45
+ "files": [
46
+ "android/src/main/",
47
+ "android/build.gradle",
48
+ "android/proguard-rules.pro",
49
+ "dist/",
50
+ "src/",
51
+ "ios/Sources",
52
+ "ios/Tests",
53
+ "Package.swift",
54
+ "RdlaboCapacitorNativeUi.podspec",
55
+ "LICENSE",
56
+ "README.md",
57
+ "docs"
58
+ ],
59
+ "scripts": {
60
+ "build": "npm run clean && npm run docgen && tsc --noEmit && rdlabo-build-theme",
61
+ "clean": "rm -rf dist",
62
+ "docgen": "docgen --project tsconfig.docgen.json --api NativeUIHandle --output-readme docs/native-ui.md && docgen --project tsconfig.docgen.json --api VerticalControlAreaHandle --output-readme docs/vertical-bars.md",
63
+ "prepublishOnly": "npm run build"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public"
67
+ },
68
+ "peerDependencies": {
69
+ "@capacitor/core": ">=8.0.0 <9",
70
+ "@ionic/core": ">=8.8.1 <10"
71
+ },
72
+ "capacitor": {
73
+ "ios": {
74
+ "src": "ios"
75
+ },
76
+ "android": {
77
+ "src": "android"
78
+ }
79
+ },
80
+ "peerDependenciesMeta": {
81
+ "@ionic/core": {
82
+ "optional": true
83
+ },
84
+ "@capacitor/core": {
85
+ "optional": true
86
+ }
87
+ },
88
+ "dependencies": {
89
+ "@rdlabo/ionic-theme-utils": "0.1.4"
90
+ }
91
+ }
package/src/core.ts ADDED
@@ -0,0 +1,14 @@
1
+ /** Standard native UI and app-local SwiftUI and Compose extensions. No Ionic runtime import. */
2
+ export { enableNativeUI } from './native/startup';
3
+ export { defineNativeUI } from './native/extensions';
4
+ export type { NativeUIExtension, NativeUIValue, NativeUIReadContext, NativeUIImageReference } from './native/extensions';
5
+ export type {
6
+ NativeUIOptions,
7
+ NativeUIVerticalBarsOptions,
8
+ NativeUIControls,
9
+ ControlProjection,
10
+ IonicProjectionOptions,
11
+ NativeUIHandle,
12
+ NativeUIStatus,
13
+ NativeUISuspension,
14
+ } from './native/definitions';
@@ -0,0 +1,385 @@
1
+ import { controlDefault } from './shared/control-options';
2
+ import { toolbarOptions } from './shared/toolbar-options';
3
+ import { createDiagnostics } from './shared/diagnostics';
4
+ import { createHTMLWebProjection } from './html';
5
+ import { validateExtensions, type NativeUIExtension } from './extensions';
6
+ import { Capacitor, registerPlugin } from '@capacitor/core';
7
+ import { nativeWebViewMetrics } from '../native-integration';
8
+ import type { NativeUIRuntimeHandle, NativeUIRuntimeOptions, NativeUIPlugin, VerticalBarEdge, WebViewMetrics } from './definitions';
9
+ import { createRuntime } from './runtime';
10
+ import { bounded, boundedListener } from './shared/bridge';
11
+ interface OverlayController {
12
+ suspend(): Promise<() => void>;
13
+ destroy(): Promise<void>;
14
+ }
15
+ type OverlayFactory = (
16
+ doc: Document,
17
+ plugin: NativeUIPlugin,
18
+ options: NativeUIRuntimeOptions,
19
+ retain: (deferActivation?: (id: string) => boolean) => () => void,
20
+ nativeRail: () => boolean,
21
+ ) => Promise<OverlayController>;
22
+ import { createVerticalBarsWebProjection } from './vertical-bars-web';
23
+ import { prehideVerticalBarsToolbarSources } from './prehide';
24
+ import { observeVerticalBarsModals } from './shared/modal';
25
+ export { defineNativeUI } from './extensions';
26
+ export type { NativeUIExtension, NativeUIValue } from './extensions';
27
+ export type {
28
+ NativeUIComponent,
29
+ NativeUIControls,
30
+ NativeUIRuntimeHandle,
31
+ NativeUIRuntimeOptions,
32
+ NativeUIStatus,
33
+ NativeUISuspension,
34
+ VerticalBarEdge,
35
+ WebViewMetrics,
36
+ } from './definitions';
37
+
38
+ const plugin = registerPlugin<NativeUIPlugin>('NativeUI');
39
+ export const NativeUI = plugin;
40
+ let active: Promise<NativeUIRuntimeHandle> | undefined;
41
+ let activeFailed = false;
42
+ let activeRoot: HTMLElement | undefined;
43
+ let activeRailHost: HTMLElement | undefined;
44
+ let activeConfiguration: string | undefined;
45
+ let activeExtensions: readonly NativeUIExtension[] = [];
46
+ let stopping: Promise<void> | undefined;
47
+
48
+ // Every stop entry point releases the activation immediately but reserves the
49
+ // shared native/DOM resources until its teardown has settled.
50
+ const stopActivation = (owner: Promise<NativeUIRuntimeHandle>, destruction: Promise<void>) => {
51
+ if (active !== owner) return;
52
+ active = undefined;
53
+ activeFailed = false;
54
+ activeConfiguration = undefined;
55
+ activeRoot = undefined;
56
+ activeRailHost = undefined;
57
+ activeExtensions = [];
58
+ stopping = destruction;
59
+ const release = () => {
60
+ if (stopping === destruction) stopping = undefined;
61
+ };
62
+ void destruction.then(release, release);
63
+ };
64
+ const web = (reason: string): NativeUIRuntimeHandle => ({
65
+ markForCheck: () => {},
66
+ getStatus: () => ({ state: 'web', projected: 0, updates: 0, reason }),
67
+ suspend: async () => ({ resume: async () => {} }),
68
+ destroy: async () => {},
69
+ });
70
+
71
+ const combine = (native: NativeUIRuntimeHandle, fallback: NativeUIRuntimeHandle): NativeUIRuntimeHandle => ({
72
+ markForCheck() {
73
+ native.markForCheck();
74
+ fallback.markForCheck();
75
+ },
76
+ getStatus() {
77
+ const nativeStatus = native.getStatus();
78
+ const fallbackStatus = fallback.getStatus();
79
+ return {
80
+ ...nativeStatus,
81
+ state:
82
+ nativeStatus.state === 'stopped' && fallbackStatus.state === 'stopped' ? 'stopped' : nativeStatus.projected > 0 ? 'native' : 'web',
83
+ projected: nativeStatus.projected + fallbackStatus.projected,
84
+ updates: nativeStatus.updates + fallbackStatus.updates,
85
+ };
86
+ },
87
+ async suspend() {
88
+ const [nativeLease, fallbackLease] = await Promise.all([native.suspend(), fallback.suspend()]);
89
+ return { resume: async () => void (await Promise.all([nativeLease.resume(), fallbackLease.resume()])) };
90
+ },
91
+ async destroy() {
92
+ await Promise.all([native.destroy(), fallback.destroy()]);
93
+ },
94
+ });
95
+
96
+ /** Wraps a runtime handle with the shared enable-lifecycle: reason, suspend hooks and idempotent destroy. */
97
+ const manage = (
98
+ handle: NativeUIRuntimeHandle,
99
+ lifecycle: {
100
+ reason?: string;
101
+ suspend?: () => (() => void) | undefined | Promise<(() => void) | undefined>;
102
+ /** Registers teardown before it starts so direct destroy and disable share the same queue. */
103
+ stopping?: (destruction: Promise<void>) => void;
104
+ destroy?: () => void | Promise<void>;
105
+ } = {},
106
+ ): NativeUIRuntimeHandle => {
107
+ let destruction: Promise<void> | undefined;
108
+ return {
109
+ markForCheck: () => {
110
+ if (!destruction) handle.markForCheck();
111
+ },
112
+ getStatus: () => (lifecycle.reason ? { ...handle.getStatus(), reason: lifecycle.reason } : handle.getStatus()),
113
+ async suspend() {
114
+ const lease = await handle.suspend();
115
+ const resume = await lifecycle.suspend?.();
116
+ return {
117
+ async resume() {
118
+ await lease.resume();
119
+ resume?.();
120
+ },
121
+ };
122
+ },
123
+ destroy() {
124
+ if (!destruction) {
125
+ destruction = Promise.resolve().then(async () => {
126
+ try {
127
+ await handle.destroy();
128
+ } finally {
129
+ await lifecycle.destroy?.();
130
+ }
131
+ });
132
+ lifecycle.stopping?.(destruction);
133
+ }
134
+ return destruction;
135
+ },
136
+ };
137
+ };
138
+
139
+ /** Reads the current native WebView geometry and applies it to page transitions. */
140
+ export const configureNativeTransition = async (): Promise<WebViewMetrics> => {
141
+ const metrics =
142
+ typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' && Capacitor.isPluginAvailable('NativeUI')
143
+ ? await bounded(plugin.getWebViewMetrics())
144
+ : { radius: 0 };
145
+ nativeWebViewMetrics.radius = metrics.radius;
146
+ return metrics;
147
+ };
148
+
149
+ /** Resolves a logical vertical-bar edge to the physical side for the given direction. */
150
+ const physicalVerticalBarEdge = (edge: Exclude<VerticalBarEdge, null>, rtl: boolean): 'left' | 'right' =>
151
+ (edge === 'leading') !== rtl ? 'left' : 'right';
152
+
153
+ const elementRtl = (element: Element): boolean => element.closest('[dir]')?.getAttribute('dir') === 'rtl';
154
+
155
+ const webProjection = (doc: Document, options: NativeUIRuntimeOptions, enabled: () => boolean = () => true) => {
156
+ const ionic = () => createVerticalBarsWebProjection(doc, options, () => options.verticalBars?.enabled !== false && enabled());
157
+ if (options.source === 'html') return createHTMLWebProjection(doc, options);
158
+ if (options.source === 'auto') return combine(ionic(), createHTMLWebProjection(doc, options));
159
+ return ionic();
160
+ };
161
+
162
+ /** Call once at application startup. Ionic markup remains the source of truth. */
163
+ export const enableNativeUI = (options: NativeUIRuntimeOptions = {}, overlayFactory?: OverlayFactory): Promise<NativeUIRuntimeHandle> => {
164
+ if (typeof document === 'undefined') return Promise.resolve(web('Requires a document'));
165
+ const extensions = [...(options.extensions ?? [])];
166
+ const nativeState: NonNullable<NativeUIRuntimeOptions['nativeState']> = (options.nativeState = {});
167
+ try {
168
+ validateExtensions(extensions, document);
169
+ } catch (error) {
170
+ return Promise.reject(error);
171
+ }
172
+ const configuration = JSON.stringify([
173
+ options.source ?? 'ionic',
174
+ options.debug === true,
175
+ options.ionic?.auto !== false,
176
+ options.verticalBars?.enabled !== false,
177
+ options.verticalBars?.placement?.edge,
178
+ options.rail?.edge,
179
+ toolbarOptions(options, false),
180
+ toolbarOptions(options, true),
181
+ options.ionic?.verticalBars?.buttonProjection ?? 'system',
182
+ // Local source overrides can use this even when the startup projection is system.
183
+ options.ionic?.verticalBars?.buttonDefaultFill ?? 'glass',
184
+ ...(['tabs', 'segment', 'fab'] as const).flatMap((kind) => [controlDefault(options, kind, false), controlDefault(options, kind, true)]),
185
+ options.ionic?.verticalBars?.popover === true,
186
+ ]);
187
+ if (active && activeFailed) {
188
+ const previous = active;
189
+ stopActivation(
190
+ previous,
191
+ previous.then((handle) => handle.destroy()),
192
+ );
193
+ }
194
+ if (
195
+ active &&
196
+ (activeConfiguration !== configuration ||
197
+ activeRoot !== options.root ||
198
+ activeRailHost !== options.rail?.host ||
199
+ extensions.length !== activeExtensions.length ||
200
+ extensions.some((extension, index) => extension !== activeExtensions[index]))
201
+ )
202
+ return Promise.reject(
203
+ new Error('Native UI is already running with different controls or extensions; destroy it before changing configuration.'),
204
+ );
205
+ if (active) return active;
206
+ activeConfiguration = configuration;
207
+ activeRoot = options.root;
208
+ activeRailHost = options.rail?.host;
209
+ activeExtensions = extensions;
210
+ const previousStop = stopping;
211
+ let start: Promise<NativeUIRuntimeHandle>;
212
+ const stop = (destruction: Promise<void>) => stopActivation(start, destruction);
213
+ let releaseNative = async () => {};
214
+ let retired = false;
215
+ const pagehide = () => runtimeStopped('pagehide');
216
+ const runtimeStopped = (cause: 'failure' | 'pagehide') => {
217
+ if (cause === 'failure') {
218
+ // Keep the Web rail usable, but replace this degraded activation on the next enable.
219
+ if (active === start) activeFailed = true;
220
+ void start
221
+ .then(async () => {
222
+ if (!retired) document.defaultView?.addEventListener('pagehide', pagehide, { once: true });
223
+ await releaseNative();
224
+ })
225
+ .catch(() => {});
226
+ return;
227
+ }
228
+ const destruction = start.then((handle) => handle.destroy());
229
+ stop(destruction);
230
+ void destruction.catch(() => {});
231
+ };
232
+ return (start = active =
233
+ (async () => {
234
+ // Native controls and DOM ownership are shared. A replacement activation
235
+ // must wait until the old handle has finished clearing both surfaces.
236
+ if (previousStop) await previousStop.catch(() => {});
237
+ const platform = Capacitor.getPlatform();
238
+ const diagnose = createDiagnostics(options.debug);
239
+ const stopModals = options.source === 'html' ? () => {} : observeVerticalBarsModals(document);
240
+ const prehide =
241
+ platform !== 'android' && options.source !== 'html' && options.ionic?.auto !== false
242
+ ? prehideVerticalBarsToolbarSources(document, () => options.verticalBars?.enabled !== false, options.root, options)
243
+ : undefined;
244
+ const fallback = (reason: string) => {
245
+ diagnose(`web-fallback: ${reason}`);
246
+ return manage(webProjection(document, options), {
247
+ reason,
248
+ suspend: () => prehide?.suspend(),
249
+ destroy: () => {
250
+ retired = true;
251
+ document.defaultView?.removeEventListener('pagehide', pagehide);
252
+ prehide?.stop();
253
+ stopModals();
254
+ },
255
+ stopping: stop,
256
+ });
257
+ };
258
+ if (platform !== 'ios' && platform !== 'android') return fallback('Requires Capacitor iOS or Android');
259
+ if (!Capacitor.isPluginAvailable('NativeUI')) return fallback('Native UI plugin is not registered');
260
+ let runtime: NativeUIRuntimeHandle | undefined;
261
+ let overlays: OverlayController | undefined;
262
+ let metricsListener: Awaited<ReturnType<typeof plugin.addListener>> | undefined;
263
+ let nativeRelease: Promise<void> | undefined;
264
+ releaseNative = () =>
265
+ (nativeRelease ??= (async () => {
266
+ try {
267
+ await overlays?.destroy();
268
+ } finally {
269
+ if (metricsListener) await bounded(metricsListener.remove()).catch(() => {});
270
+ }
271
+ })());
272
+ const cleanup = async () => {
273
+ retired = true;
274
+ try {
275
+ await releaseNative();
276
+ } finally {
277
+ document.defaultView?.removeEventListener('pagehide', pagehide);
278
+ prehide?.stop();
279
+ stopModals();
280
+ }
281
+ };
282
+ try {
283
+ const ordinaryControls = (['button', 'backButton', 'tabs', 'segment', 'fab'] as const).some((kind) =>
284
+ controlDefault(options, kind, false),
285
+ );
286
+ const capabilities = await bounded(plugin.configure({ suppressScrollEdgeEffect: ordinaryControls }));
287
+ if (!capabilities.supported) return fallback('Native UI is unavailable on this device');
288
+ const unavailableExtensions = new Map<string, string>();
289
+ for (const extension of extensions) {
290
+ if (platform === 'android' && extension.placement === 'tab-accessory')
291
+ unavailableExtensions.set(extension.name, 'unsupported-placement: tab-accessory requires a supported iOS tab host.');
292
+ else if (!capabilities.extensions?.includes(extension.name))
293
+ unavailableExtensions.set(
294
+ extension.name,
295
+ 'unregistered-extension: register this name in the native app before enabling the shell, then rebuild.',
296
+ );
297
+ }
298
+ if (platform === 'android') {
299
+ runtime = await createRuntime(
300
+ document,
301
+ plugin,
302
+ {
303
+ ...options,
304
+ source: 'extensions',
305
+ extensions,
306
+ unavailableExtensions,
307
+ },
308
+ () => false,
309
+ runtimeStopped,
310
+ );
311
+ runtime = combine(runtime, webProjection(document, options));
312
+ metricsListener = await boundedListener(
313
+ plugin.addListener('webViewMetricsChange', () => {
314
+ document.defaultView?.dispatchEvent(new Event('nativeUIRefresh'));
315
+ }),
316
+ );
317
+ return manage(runtime, {
318
+ stopping: stop,
319
+ destroy: cleanup,
320
+ });
321
+ }
322
+ let deviceEdge: VerticalBarEdge = null;
323
+ const ionicRoot = () => {
324
+ if (options.source === 'html') return null;
325
+ const root = options.root ?? document.body;
326
+ return root.closest<HTMLElement>('ion-app') ?? root.querySelector<HTMLElement>('ion-app');
327
+ };
328
+ const updateNativeEdge = () => {
329
+ const requested = options.verticalBars?.placement?.edge;
330
+ const root = ionicRoot() ?? options.root ?? document.body;
331
+ nativeState.edge =
332
+ deviceEdge && (!requested || requested === deviceEdge) ? physicalVerticalBarEdge(deviceEdge, elementRtl(root)) : undefined;
333
+ };
334
+ // Ionic layout is read-only input; device metrics determine native availability.
335
+ const nativeVerticalBars = () => {
336
+ updateNativeEdge();
337
+ if (options.verticalBars?.enabled === false) return false;
338
+ const app = ionicRoot();
339
+ if (!app?.classList.contains('ios-theme-vertical-bars')) return !!nativeState.edge;
340
+ const physicalEdge = app.classList.contains('ios-theme-vertical-bars-left') ? 'left' : 'right';
341
+ return physicalEdge === nativeState.edge;
342
+ };
343
+ const applyMetrics = (metrics: WebViewMetrics) => {
344
+ if (ordinaryControls) nativeWebViewMetrics.radius = metrics.radius;
345
+ deviceEdge = metrics.verticalBarEdge ?? null;
346
+ updateNativeEdge();
347
+ document.defaultView?.dispatchEvent(new Event('nativeUIRefresh'));
348
+ };
349
+ metricsListener = await boundedListener(plugin.addListener('webViewMetricsChange', applyMetrics));
350
+ await bounded(Promise.resolve().then(() => plugin.getWebViewMetrics()))
351
+ .then(applyMetrics)
352
+ .catch(() => undefined);
353
+ const native = await createRuntime(
354
+ document,
355
+ plugin,
356
+ { ...options, extensions, unavailableExtensions },
357
+ nativeVerticalBars,
358
+ runtimeStopped,
359
+ );
360
+ runtime = combine(
361
+ native,
362
+ webProjection(document, options, () => !nativeVerticalBars() || native.getStatus().projected === 0),
363
+ );
364
+ if (options.ionic?.verticalBars?.popover === true && overlayFactory)
365
+ overlays = await overlayFactory(document, plugin, options, native.retain, nativeVerticalBars);
366
+ return manage(runtime, {
367
+ suspend: async () => {
368
+ const restorePrehide = prehide?.suspend();
369
+ const resumeOverlays = await overlays?.suspend();
370
+ return () => {
371
+ restorePrehide?.();
372
+ resumeOverlays?.();
373
+ };
374
+ },
375
+ stopping: stop,
376
+ destroy: cleanup,
377
+ });
378
+ } catch (error) {
379
+ await overlays?.destroy().catch(() => {});
380
+ await runtime?.destroy().catch(() => {});
381
+ if (metricsListener) await bounded(metricsListener.remove()).catch(() => {});
382
+ return fallback(error instanceof Error ? error.message : String(error));
383
+ }
384
+ })());
385
+ };
@@ -0,0 +1,45 @@
1
+ import type { VerticalControlAreaOptions } from '../definitions';
2
+ import * as button from './ion-button';
3
+ import * as buttons from './ion-buttons';
4
+ import * as backButton from './ion-back-button';
5
+ import * as menuButton from './ion-menu-button';
6
+ import * as tabBar from './ion-tab-bar';
7
+ import * as segment from './ion-segment';
8
+ import * as fab from './ion-fab';
9
+ import { isVerticalBarsSource, visible, withoutPrehide } from '../shared/dom';
10
+ import type { Candidate, Identify } from '../shared/candidate';
11
+
12
+ // Static composition only. Each component declares its own tag, discovery and reader.
13
+ export const components = [button, buttons, backButton, menuButton, tabBar, segment, fab] as const;
14
+ export type NativeUIComponent = (typeof components)[number]['tag'];
15
+ export const selector = components
16
+ .map((component) => ('selector' in component ? component.selector : component.tag))
17
+ .filter(Boolean)
18
+ .join(', ');
19
+ export const shadowSelector = [
20
+ 'ion-icon',
21
+ ...components.flatMap((component) => ('shadowSelector' in component ? [component.shadowSelector] : [])),
22
+ ].join(', ');
23
+ export const motionSelector = [
24
+ '.ion-page',
25
+ 'ion-header',
26
+ 'ion-footer',
27
+ 'ion-toolbar',
28
+ ...components.filter((component) => 'tracksMotion' in component && component.tracksMotion).map((component) => component.tag),
29
+ ].join(', ');
30
+
31
+ export const isVerticalBarsCandidate = isVerticalBarsSource;
32
+
33
+ export const readCandidate = (element: HTMLElement, id: Identify, options: VerticalControlAreaOptions = {}): Candidate | undefined => {
34
+ const verticalBars = isVerticalBarsCandidate(element);
35
+ if (
36
+ (!element.classList.contains('ios') && !verticalBars) ||
37
+ !visible(element, verticalBars) ||
38
+ element.closest('ion-popover') ||
39
+ (element.closest('ion-modal') && !verticalBars)
40
+ )
41
+ return;
42
+ if (element.contains(element.ownerDocument.activeElement)) return;
43
+ // Read icon and control geometry before restoring prehide, which can collapse the toolbar.
44
+ return withoutPrehide(element, () => components.find((component) => component.tag === element.localName)?.read(element, id, options));
45
+ };
@@ -0,0 +1,22 @@
1
+ import type { VerticalControlAreaOptions } from '../definitions';
2
+ import { horizontalSelected, inVerticalToolbarLayout } from '../shared/toolbar-options';
3
+ import { createCandidate, appendItem } from '../shared/candidate';
4
+ import type { Candidate, Identify } from '../shared/candidate';
5
+ import { inFixedToolbar, isVerticalBarsSource } from '../shared/dom';
6
+
7
+ export const tag = 'ion-back-button';
8
+
9
+ export const read = (element: HTMLElement, id: Identify, options: VerticalControlAreaOptions = {}): Candidate | undefined => {
10
+ const button = element as HTMLIonBackButtonElement;
11
+ if (
12
+ (inVerticalToolbarLayout(element) ? !isVerticalBarsSource(element) : !horizontalSelected(element, options)) ||
13
+ !button.shadowRoot ||
14
+ button.icon !== undefined ||
15
+ button.color !== undefined ||
16
+ (!isVerticalBarsSource(element) && !inFixedToolbar(element))
17
+ )
18
+ return;
19
+ const candidate = createCandidate(element, tag, id);
20
+ const label = button.shadowRoot.querySelector('[part="text"]')?.textContent?.trim() ?? '';
21
+ return appendItem(candidate, element, id, button.shadowRoot, label) ? candidate : undefined;
22
+ };
@@ -0,0 +1,25 @@
1
+ import { horizontalSelected, horizontalToolbarGroup, inVerticalToolbarLayout } from '../shared/toolbar-options';
2
+ import type { VerticalControlAreaOptions } from '../definitions';
3
+ import { createCandidate, appendItem } from '../shared/candidate';
4
+ import type { Candidate, Identify } from '../shared/candidate';
5
+ import { inFixedToolbar, isVerticalBarsToolbarAction, isVerticalBarsToolbarGroup } from '../shared/dom';
6
+
7
+ export const tag = 'ion-button';
8
+
9
+ export const read = (element: HTMLElement, id: Identify, options: VerticalControlAreaOptions = {}): Candidate | undefined => {
10
+ const button = element as HTMLIonButtonElement;
11
+ const verticalBars = inVerticalToolbarLayout(element);
12
+ if (verticalBars && element.parentElement && isVerticalBarsToolbarGroup(element.parentElement)) return;
13
+ const fill = button.fill ?? 'default';
14
+ const group = element.parentElement?.matches('ion-buttons') ? element.parentElement : undefined;
15
+ if (!verticalBars && (!horizontalSelected(element, options) || (group && horizontalToolbarGroup(group, options)))) return;
16
+ if (
17
+ !inFixedToolbar(element) ||
18
+ (verticalBars && !isVerticalBarsToolbarAction(element)) ||
19
+ (!verticalBars && fill !== 'default' && !(group && fill === 'clear')) ||
20
+ (!verticalBars && button.classList.contains('ion-color'))
21
+ )
22
+ return;
23
+ const candidate = createCandidate(element, tag, id, options);
24
+ return appendItem(candidate, element, id) ? candidate : undefined;
25
+ };
@@ -0,0 +1,37 @@
1
+ import { horizontalToolbarGroup, inVerticalToolbarLayout } from '../shared/toolbar-options';
2
+ import type { VerticalControlAreaOptions } from '../definitions';
3
+ import { createCandidate, appendItem } from '../shared/candidate';
4
+ import type { Candidate, Identify } from '../shared/candidate';
5
+ import { childElements, verticalBarsToolbarActions, inFixedToolbar, isVerticalBarsToolbarGroup } from '../shared/dom';
6
+ import * as menuButton from './ion-menu-button';
7
+
8
+ export const tag = 'ion-buttons';
9
+ export const tracksMotion = true;
10
+
11
+ export const read = (element: HTMLElement, id: Identify, options: VerticalControlAreaOptions = {}): Candidate | undefined => {
12
+ if (!inFixedToolbar(element)) return;
13
+ let children = childElements(element);
14
+ const verticalBars = inVerticalToolbarLayout(element);
15
+ if (verticalBars ? !isVerticalBarsToolbarGroup(element) : !horizontalToolbarGroup(element, options)) return;
16
+ if (children.length === 1) return menuButton.read(element, id, options);
17
+ if (verticalBars) children = verticalBarsToolbarActions(element);
18
+ if (
19
+ !children.length ||
20
+ children.some(
21
+ (child) =>
22
+ !child.matches(`${menuButton.tag}${verticalBars ? '' : '.ios'}`) &&
23
+ (!child.matches(`ion-button${verticalBars ? '' : '.ios.button-clear'}`) ||
24
+ (!verticalBars && (child as HTMLIonButtonElement).fill !== 'clear')),
25
+ )
26
+ )
27
+ return;
28
+ const candidate = createCandidate(element, tag, id, options);
29
+ if (verticalBars && children.length !== childElements(element).length) candidate.sources = children;
30
+ for (const child of children) {
31
+ const supported = child.matches(menuButton.tag)
32
+ ? menuButton.append(candidate, child as HTMLIonMenuButtonElement, id)
33
+ : appendItem(candidate, child, id);
34
+ if (!supported) return;
35
+ }
36
+ return candidate;
37
+ };