@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,500 @@
1
+ import { controlSelected } from './shared/control-options';
2
+ import { createCrossfade, fadeMarker } from './shared/crossfade';
3
+ import { alignVerticalBarClone, verticalBarFrame } from './shared/vertical-bars-geometry';
4
+ import { createVerticalBarsWebSearch, searchProjectionClass } from './vertical-bars-web-search';
5
+ import { inVerticalBarsSurface, topModal } from './shared/modal';
6
+ import type { NativeUIRuntimeHandle, NativeUIOptions, NativeUIStatus } from './definitions';
7
+ import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
8
+ import {
9
+ activateProjectedElement,
10
+ createVerticalBarsPageState,
11
+ verticalBarsActionCandidate,
12
+ verticalBarsBackCandidate,
13
+ verticalBarsEnteringPage,
14
+ verticalBarsToolbarActions,
15
+ isExcluded,
16
+ isNativeUIDisabled,
17
+ isVerticalBarsToolbarGroup,
18
+ preferredVerticalBarsBack,
19
+ verticalBarsOwned,
20
+ marker,
21
+ syncToolbarText,
22
+ toolbarTextMarker,
23
+ prehideOnlyMutation,
24
+ prehiddenClass,
25
+ unprojected,
26
+ withoutPrehide,
27
+ } from './shared/dom';
28
+
29
+ const backProjectionClass = 'ios-theme-vertical-bars-back-button-projection';
30
+ const toolbarProjectionClass = 'ios-theme-vertical-bars-toolbar-projection';
31
+ const readyClass = 'ios-theme-vertical-bars-toolbar-ready';
32
+ const toolbarControlSize = 46;
33
+ const toolbarControlGap = 10;
34
+
35
+ interface ToolbarProjection {
36
+ source: HTMLElement;
37
+ projection: HTMLElement;
38
+ actions: { source: HTMLElement; projection: HTMLElement; previousAriaHidden: string | null }[];
39
+ }
40
+
41
+ interface ToolbarSource {
42
+ group: HTMLElement;
43
+ actions: HTMLElement[];
44
+ }
45
+
46
+ export const createVerticalBarsWebProjection = (
47
+ doc: Document,
48
+ options: NativeUIOptions,
49
+ enabled: () => boolean = () => true,
50
+ ): NativeUIRuntimeHandle => {
51
+ const win = doc.defaultView!;
52
+ const crossfade = createCrossfade(win);
53
+ let incomingInstant: boolean | undefined;
54
+ const toolbarEnabled = options.ionic?.auto !== false;
55
+ let root: HTMLElement | undefined;
56
+ let backSource: HTMLIonBackButtonElement | undefined;
57
+ let backProjection: HTMLIonBackButtonElement | undefined;
58
+ let previousBackAriaHidden: string | null = null;
59
+ let toolbarProjections: ToolbarProjection[] = [];
60
+ let suspended = 0;
61
+ let stopped = false;
62
+ let frame = 0;
63
+ let updates = 0;
64
+ let observingVerticalBars = false;
65
+ const verticalBarsPages = createVerticalBarsPageState();
66
+ let sourceObserver: MutationObserver | undefined;
67
+ let waiters: (() => void)[] = [];
68
+ const listeners = new AbortController();
69
+ const verticalBarsRoot = () => doc.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');
70
+ const projectedSources = () =>
71
+ [backSource, search.source, ...toolbarProjections.flatMap(({ actions }) => actions.map(({ source }) => source))].filter(
72
+ (source): source is HTMLElement => !!source,
73
+ );
74
+ const isRendered = (element: HTMLElement) =>
75
+ withoutPrehide(element, () => {
76
+ const style = getComputedStyle(element);
77
+ const rect = element.getBoundingClientRect();
78
+ return element.isConnected && style.display !== 'none' && style.visibility === 'visible' && rect.width > 0 && rect.height > 0;
79
+ });
80
+ const search = createVerticalBarsWebSearch(
81
+ doc,
82
+ (element) =>
83
+ options.ionic?.auto !== false &&
84
+ controlSelected(element, options, 'tabs', true) &&
85
+ !!verticalBarsRoot()?.contains(element) &&
86
+ (!options.root || options.root.contains(element)) &&
87
+ !element.closest('[data-native-ui-placement="element"]') &&
88
+ inVerticalBarsSurface(element) &&
89
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
90
+ !isNativeUIDisabled(element) &&
91
+ !verticalBarsPages.isDeparted(element) &&
92
+ isRendered(element),
93
+ () => {
94
+ void schedule();
95
+ },
96
+ );
97
+ const inEligibleToolbar = (element: HTMLElement) =>
98
+ !!verticalBarsRoot()?.contains(element) &&
99
+ (!options.root || options.root.contains(element)) &&
100
+ !element.closest('[data-native-ui-placement="element"]') &&
101
+ inVerticalBarsSurface(element) &&
102
+ verticalBarsActionCandidate(element) &&
103
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
104
+ !verticalBarsPages.isDeparted(element);
105
+ const isEligibleBack = (element: HTMLIonBackButtonElement) =>
106
+ !!verticalBarsRoot()?.contains(element) &&
107
+ (!options.root || options.root.contains(element)) &&
108
+ !element.closest('[data-native-ui-placement="element"]') &&
109
+ inVerticalBarsSurface(element) &&
110
+ verticalBarsOwned(element) &&
111
+ verticalBarsBackCandidate(element) &&
112
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
113
+ !verticalBarsPages.isDeparted(element) &&
114
+ unprojected(projectedSources(), () => isRendered(element));
115
+ const pageOrder = (element: Element) => Array.from(doc.querySelectorAll('.ion-page')).indexOf(element.closest('.ion-page')!);
116
+ const findBack = () => {
117
+ const candidates = Array.from(doc.querySelectorAll<HTMLIonBackButtonElement>(`ion-back-button:not(.${backProjectionClass})`)).filter(
118
+ isEligibleBack,
119
+ );
120
+ return preferredVerticalBarsBack(candidates, doc);
121
+ };
122
+ const findToolbarGroups = () => {
123
+ const candidates = Array.from(doc.querySelectorAll<HTMLIonButtonsElement>(`ion-buttons:not(.${toolbarProjectionClass})`)).flatMap(
124
+ (group): ToolbarSource[] => {
125
+ const actions = verticalBarsToolbarActions(group).filter((action) => unprojected(projectedSources(), () => isRendered(action)));
126
+ if (!actions.length) return [];
127
+ if (!isVerticalBarsToolbarGroup(group)) return actions.filter(inEligibleToolbar).map((action) => ({ group, actions: [action] }));
128
+ return inEligibleToolbar(group) && unprojected(projectedSources(), () => isRendered(group)) ? [{ group, actions }] : [];
129
+ },
130
+ );
131
+ for (const action of Array.from(doc.querySelectorAll<HTMLElement>('ion-button, ion-menu-button'))) {
132
+ if (action.closest('ion-buttons') || !verticalBarsOwned(action) || !inEligibleToolbar(action)) continue;
133
+ if (unprojected(projectedSources(), () => isRendered(action))) candidates.push({ group: action, actions: [action] });
134
+ }
135
+ const highestPage = Math.max(...candidates.map(({ group }) => pageOrder(group)));
136
+ return candidates.filter(({ group }) => pageOrder(group) === highestPage);
137
+ };
138
+ const isCurrentToolbarAction = (source: HTMLElement) => findToolbarGroups().some(({ actions }) => actions.includes(source));
139
+ const restore = (retire?: (surface: HTMLElement) => void) => {
140
+ search.restore(retire);
141
+ if (backProjection) {
142
+ if (retire) retire(backProjection);
143
+ else backProjection.remove();
144
+ }
145
+ backProjection = undefined;
146
+ if (backSource) {
147
+ backSource.removeAttribute(marker);
148
+ if (previousBackAriaHidden === null) backSource.removeAttribute('aria-hidden');
149
+ else backSource.setAttribute('aria-hidden', previousBackAriaHidden);
150
+ backSource.dispatchEvent(new CustomEvent('nativeUIChange'));
151
+ }
152
+ backSource = undefined;
153
+ previousBackAriaHidden = null;
154
+ for (const { projection, actions } of toolbarProjections) {
155
+ if (retire) retire(projection);
156
+ else projection.remove();
157
+ for (const { source, previousAriaHidden } of actions) {
158
+ source.removeAttribute(marker);
159
+ if (previousAriaHidden === null) source.removeAttribute('aria-hidden');
160
+ else source.setAttribute('aria-hidden', previousAriaHidden);
161
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
162
+ }
163
+ }
164
+ toolbarProjections = [];
165
+ sourceObserver?.disconnect();
166
+ sourceObserver = undefined;
167
+ root?.classList.remove(readyClass);
168
+ root = undefined;
169
+ };
170
+ const copyAttributes = (target: HTMLElement, original: HTMLElement) => {
171
+ for (const name of ['id', 'slot', marker, 'aria-hidden']) target.removeAttribute(name);
172
+ for (const attribute of Array.from(target.attributes))
173
+ if (!['class', marker, fadeMarker].includes(attribute.name) && !original.hasAttribute(attribute.name))
174
+ target.removeAttribute(attribute.name);
175
+ for (const attribute of Array.from(original.attributes))
176
+ if (!['id', 'slot', marker, 'aria-hidden'].includes(attribute.name)) target.setAttribute(attribute.name, attribute.value);
177
+ target.classList.remove(prehiddenClass);
178
+ };
179
+ const syncBack = (target: HTMLIonBackButtonElement, original: HTMLIonBackButtonElement) => {
180
+ copyAttributes(target, original);
181
+ target.classList.add(backProjectionClass, 'ion-cloned-element');
182
+ target.disabled = original.disabled;
183
+ target.defaultHref = original.defaultHref;
184
+ target.icon = original.icon;
185
+ target.color = original.color;
186
+ target.mode = original.mode;
187
+ target.text = '';
188
+ if (root) alignVerticalBarClone(target, original, root);
189
+ };
190
+ const syncAction = (target: HTMLElement, original: HTMLElement) => {
191
+ copyAttributes(target, original);
192
+ target.classList.add('ios-theme-vertical-bars-toolbar-action', 'ion-cloned-element');
193
+ const label =
194
+ original.getAttribute('aria-label') ??
195
+ original.shadowRoot?.querySelector('[part~=native]')?.getAttribute('aria-label') ??
196
+ original.textContent?.trim();
197
+ if (label) target.setAttribute('aria-label', label);
198
+ if ('disabled' in original) (target as HTMLIonButtonElement).disabled = (original as HTMLIonButtonElement).disabled;
199
+ if (original.matches('ion-button')) {
200
+ const icon = original.querySelector<HTMLElement>('ion-icon, svg');
201
+ const clonedIcon = icon?.cloneNode(true) as HTMLElement | undefined;
202
+ clonedIcon?.setAttribute('slot', 'icon-only');
203
+ target.replaceChildren(...(clonedIcon ? [clonedIcon] : [document.createTextNode(original.textContent?.trim() || label || '')]));
204
+ }
205
+ };
206
+ const sameSources = (back: HTMLIonBackButtonElement | undefined, groups: ToolbarSource[]) =>
207
+ back === backSource &&
208
+ groups.length === toolbarProjections.length &&
209
+ groups.every(({ group, actions }, index) => {
210
+ const current = toolbarProjections[index];
211
+ return (
212
+ group === current.source &&
213
+ actions.length === current.actions.length &&
214
+ actions.every((action, actionIndex) => action === current.actions[actionIndex].source)
215
+ );
216
+ });
217
+ const syncExisting = () => {
218
+ if (backSource && backProjection) syncBack(backProjection, backSource);
219
+ let topOffset = backSource ? toolbarControlSize + toolbarControlGap : 0;
220
+ for (const { projection, source, actions } of toolbarProjections) {
221
+ if (actions.length === 1 && !projection.matches('ion-buttons')) syncAction(projection, actions[0].source);
222
+ else copyAttributes(projection, source);
223
+ projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
224
+ projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
225
+ if (projection.matches('ion-buttons')) actions.forEach(({ source: action, projection: clone }) => syncAction(clone, action));
226
+ const frames = actions.map(({ source }) => verticalBarFrame(source));
227
+ if (root && frames.every((frame) => !!frame)) {
228
+ if (actions.length === 1) alignVerticalBarClone(projection, actions[0].source, root);
229
+ else {
230
+ const left = Math.min(...frames.map((frame) => frame!.x));
231
+ const top = Math.min(...frames.map((frame) => frame!.y));
232
+ const right = Math.max(...frames.map((frame) => frame!.x + frame!.width));
233
+ const bottom = Math.max(...frames.map((frame) => frame!.y + frame!.height));
234
+ const origin = root.getBoundingClientRect();
235
+ Object.assign(projection.style, {
236
+ left: `${left - origin.left}px`,
237
+ top: `${top - origin.top}px`,
238
+ right: 'auto',
239
+ bottom: 'auto',
240
+ width: `${right - left}px`,
241
+ height: `${bottom - top}px`,
242
+ padding: '0',
243
+ });
244
+ actions.forEach(({ source, projection: clone }) => alignVerticalBarClone(clone, source, projection));
245
+ }
246
+ }
247
+ topOffset += actions.length * toolbarControlSize + toolbarControlGap;
248
+ }
249
+ };
250
+ const project = (nextBack: HTMLIonBackButtonElement | undefined, groups: ToolbarSource[]) => {
251
+ root = topModal(doc) ?? verticalBarsRoot()!;
252
+ let topOffset = 0;
253
+ if (nextBack) {
254
+ backSource = nextBack;
255
+ previousBackAriaHidden = nextBack.getAttribute('aria-hidden');
256
+ backProjection = nextBack.cloneNode(false) as HTMLIonBackButtonElement;
257
+ syncBack(backProjection, nextBack);
258
+ backProjection.addEventListener(
259
+ 'click',
260
+ (event) => {
261
+ event.preventDefault();
262
+ event.stopImmediatePropagation();
263
+ if (backSource && backSource === findBack()) activateProjectedElement(backSource);
264
+ },
265
+ { capture: true },
266
+ );
267
+ root.append(backProjection);
268
+ nextBack.setAttribute(marker, '');
269
+ nextBack.setAttribute('aria-hidden', 'true');
270
+ nextBack.dispatchEvent(new CustomEvent('nativeUIChange'));
271
+ topOffset = toolbarControlSize + toolbarControlGap;
272
+ }
273
+ for (const { group, actions: sources } of groups) {
274
+ const projection = (sources.length === 1 ? sources[0] : group).cloneNode(false) as HTMLElement;
275
+ if (sources.length === 1) syncAction(projection, sources[0]);
276
+ else copyAttributes(projection, group);
277
+ projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
278
+ projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
279
+ const actions = sources.map((source) => {
280
+ const clone = sources.length === 1 ? projection : (source.cloneNode(false) as HTMLElement);
281
+ if (sources.length > 1) syncAction(clone, source);
282
+ clone.addEventListener(
283
+ 'click',
284
+ (event) => {
285
+ event.preventDefault();
286
+ event.stopImmediatePropagation();
287
+ if (isCurrentToolbarAction(source)) activateProjectedElement(source);
288
+ },
289
+ { capture: true },
290
+ );
291
+ if (sources.length > 1) projection.append(clone);
292
+ const previousAriaHidden = source.getAttribute('aria-hidden');
293
+ source.setAttribute(marker, '');
294
+ source.setAttribute('aria-hidden', 'true');
295
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
296
+ return { source, projection: clone, previousAriaHidden };
297
+ });
298
+ root.append(projection);
299
+ toolbarProjections.push({ source: group, projection, actions });
300
+ topOffset += actions.length * toolbarControlSize + toolbarControlGap;
301
+ }
302
+ syncExisting();
303
+ if (nextBack || toolbarProjections.length) root.classList.add(readyClass);
304
+ sourceObserver = new MutationObserver((records) => {
305
+ if (
306
+ records.some(
307
+ (record) =>
308
+ record.type !== 'attributes' || (![marker, 'aria-hidden'].includes(record.attributeName ?? '') && !prehideOnlyMutation(record)),
309
+ )
310
+ )
311
+ schedule();
312
+ });
313
+ if (nextBack?.shadowRoot)
314
+ sourceObserver.observe(nextBack.shadowRoot, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
315
+ for (const { group } of groups)
316
+ sourceObserver.observe(group, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
317
+ };
318
+ const reveal = () => {
319
+ if (incomingInstant === undefined) return;
320
+ for (const surface of [backProjection, ...toolbarProjections.map(({ projection }) => projection), search.surface])
321
+ if (surface) crossfade.play(surface, false, incomingInstant);
322
+ incomingInstant = undefined;
323
+ };
324
+ const performUpdate = () => {
325
+ frame = 0;
326
+ syncToolbarText(doc);
327
+ const currentRoot = verticalBarsRoot();
328
+ if (observingVerticalBars !== !!currentRoot) {
329
+ observingVerticalBars = !!currentRoot;
330
+ observer.disconnect();
331
+ observer.observe(doc.documentElement, {
332
+ subtree: true,
333
+ childList: true,
334
+ characterData: observingVerticalBars,
335
+ attributes: true,
336
+ attributeOldValue: true,
337
+ attributeFilter: observingVerticalBars ? undefined : ['class'],
338
+ });
339
+ }
340
+ if (stopped || suspended || !currentRoot || !enabled()) return restore();
341
+ const nextBack = toolbarEnabled ? findBack() : undefined;
342
+ const groups = toolbarEnabled ? findToolbarGroups() : [];
343
+ const projectionRoot = topModal(doc) ?? currentRoot;
344
+ search.update(projectionRoot);
345
+ if (!nextBack && !groups.length && !search.source) return restore();
346
+ if (root === (topModal(doc) ?? currentRoot) && sameSources(nextBack, groups)) {
347
+ syncExisting();
348
+ reveal();
349
+ return;
350
+ }
351
+ restore();
352
+ project(nextBack, groups);
353
+ search.update(projectionRoot);
354
+ reveal();
355
+ updates++;
356
+ };
357
+ const update = () => {
358
+ try {
359
+ performUpdate();
360
+ } catch {
361
+ restore();
362
+ } finally {
363
+ const pending = waiters;
364
+ waiters = [];
365
+ pending.forEach((resolve) => resolve());
366
+ }
367
+ };
368
+ const schedule = (): Promise<void> => {
369
+ if (stopped) return Promise.resolve();
370
+ // Release Web ownership before the native runtime can acknowledge its next frame.
371
+ if (!enabled()) restore();
372
+ const done = new Promise<void>((resolve) => waiters.push(resolve));
373
+ if (!frame) frame = win.requestAnimationFrame(update);
374
+ return done;
375
+ };
376
+ const observer = new MutationObserver((records) => {
377
+ const insideProjection = (target: Node) => {
378
+ const element = target instanceof Element ? target : target.parentNode instanceof Element ? target.parentNode : undefined;
379
+ const rootNode = target.getRootNode();
380
+ const shadowHost = rootNode instanceof ShadowRoot ? rootNode.host : undefined;
381
+ return !![element, shadowHost].some((candidate) =>
382
+ candidate?.closest(`.${backProjectionClass}, .${toolbarProjectionClass}, .${searchProjectionClass}`),
383
+ );
384
+ };
385
+ const verticalBarsChanged = records.some(
386
+ (record) =>
387
+ (record.type === 'attributes' && (record.target as Element).matches('ion-app')) ||
388
+ (record.type === 'childList' &&
389
+ Array.from(record.addedNodes).some(
390
+ (node) =>
391
+ node instanceof Element &&
392
+ (node.matches('ion-app.ios-theme-vertical-bars') || !!node.querySelector('ion-app.ios-theme-vertical-bars')),
393
+ )),
394
+ );
395
+ if (
396
+ (observingVerticalBars &&
397
+ records.some(
398
+ (record) =>
399
+ record.attributeName !== marker &&
400
+ record.attributeName !== toolbarTextMarker &&
401
+ !prehideOnlyMutation(record) &&
402
+ !insideProjection(record.target),
403
+ )) ||
404
+ (!observingVerticalBars && verticalBarsChanged && records.some((record) => !prehideOnlyMutation(record)))
405
+ )
406
+ schedule();
407
+ });
408
+ observer.observe(doc.documentElement, {
409
+ subtree: true,
410
+ childList: true,
411
+ attributes: true,
412
+ attributeOldValue: true,
413
+ attributeFilter: ['class'],
414
+ });
415
+ const pageLifecycle = (event: Event) => {
416
+ const page = event.target;
417
+ if (!(page instanceof HTMLElement) || !page.matches('.ion-page')) return;
418
+ if (event.type === VERTICAL_BARS_TRANSITION_CANCELED) {
419
+ const entering = (event as CustomEvent<{ entering?: HTMLElement }>).detail?.entering;
420
+ verticalBarsPages.cancel(entering, page);
421
+ } else verticalBarsPages.lifecycle(event);
422
+ if (verticalBarsRoot()) schedule();
423
+ };
424
+ for (const name of ['ionViewWillEnter', 'ionViewWillLeave', 'ionViewDidEnter', 'ionViewDidLeave'])
425
+ doc.addEventListener(name, pageLifecycle, { capture: true, signal: listeners.signal });
426
+ doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, pageLifecycle, { capture: true, signal: listeners.signal });
427
+ for (const name of ['ionModalWillPresent', 'ionModalDidDismiss'])
428
+ doc.addEventListener(name, schedule, { capture: true, signal: listeners.signal });
429
+ win.addEventListener(
430
+ 'nativeUIVerticalBarsAcquire',
431
+ (event) => {
432
+ const instant = (event as CustomEvent<{ instant: boolean }>).detail?.instant ?? true;
433
+ incomingInstant = undefined;
434
+ restore((surface) => {
435
+ // Keep only the outgoing visual at its last rectangle after Web ownership
436
+ // is released. The native acknowledgement transfers input exactly once.
437
+ const box = surface.getBoundingClientRect();
438
+ surface.style.setProperty('position', 'fixed', 'important');
439
+ surface.style.setProperty('display', 'block', 'important');
440
+ surface.style.setProperty('left', `${box.left}px`, 'important');
441
+ surface.style.setProperty('top', `${box.top}px`, 'important');
442
+ surface.style.setProperty('right', 'auto', 'important');
443
+ surface.style.setProperty('bottom', 'auto', 'important');
444
+ surface.style.setProperty('width', `${box.width}px`, 'important');
445
+ surface.style.setProperty('height', `${box.height}px`, 'important');
446
+ crossfade.retire(surface, instant);
447
+ });
448
+ },
449
+ { signal: listeners.signal },
450
+ );
451
+ win.addEventListener(
452
+ 'nativeUIVerticalBarsRelease',
453
+ (event) => {
454
+ incomingInstant = (event as CustomEvent<{ instant: boolean }>).detail.instant;
455
+ void schedule();
456
+ },
457
+ { signal: listeners.signal },
458
+ );
459
+ win.addEventListener('resize', schedule, { signal: listeners.signal });
460
+ win.addEventListener('nativeUIRefresh', schedule, { signal: listeners.signal });
461
+ schedule();
462
+
463
+ return {
464
+ markForCheck: () => {
465
+ void schedule();
466
+ },
467
+ getStatus: (): NativeUIStatus => ({
468
+ state: stopped ? 'stopped' : 'web',
469
+ projected:
470
+ Number(!!backSource) + Number(!!search.source) + toolbarProjections.reduce((count, group) => count + group.actions.length, 0),
471
+ updates,
472
+ }),
473
+ async suspend() {
474
+ suspended++;
475
+ restore();
476
+ let resumed = false;
477
+ return {
478
+ async resume() {
479
+ if (resumed) return;
480
+ resumed = true;
481
+ suspended = Math.max(0, suspended - 1);
482
+ if (!suspended) await schedule();
483
+ },
484
+ };
485
+ },
486
+ async destroy() {
487
+ if (stopped) return;
488
+ stopped = true;
489
+ win.cancelAnimationFrame(frame);
490
+ frame = 0;
491
+ waiters.forEach((resolve) => resolve());
492
+ waiters = [];
493
+ observer.disconnect();
494
+ sourceObserver?.disconnect();
495
+ listeners.abort();
496
+ crossfade.destroy();
497
+ restore();
498
+ },
499
+ };
500
+ };
@@ -0,0 +1,49 @@
1
+ /// <reference lib="es2021.weakref" />
2
+
3
+ export interface NativeUIIntegration {
4
+ suspend(scopes: HTMLElement[]): Promise<(canceled?: boolean) => void>;
5
+ search?(binding: NativeSearchBinding, active: boolean, focus?: boolean): Promise<boolean>;
6
+ }
7
+
8
+ export interface NativeSearchBinding {
9
+ tabBar: HTMLElement;
10
+ trigger: HTMLElement;
11
+ footer: HTMLElement;
12
+ active: boolean;
13
+ focused: boolean;
14
+ }
15
+
16
+ const searches = new WeakMap<Document, WeakRef<NativeSearchBinding>[]>();
17
+
18
+ export const registerNativeSearch = (tabBar: HTMLElement, trigger: HTMLElement, footer: HTMLElement): NativeSearchBinding => {
19
+ const binding = { tabBar, trigger, footer, active: false, focused: false };
20
+ const entries = (searches.get(tabBar.ownerDocument) ?? []).filter((entry) => entry.deref() && entry.deref()?.footer !== footer);
21
+ entries.push(new WeakRef(binding));
22
+ searches.set(tabBar.ownerDocument, entries);
23
+ tabBar.ownerDocument.defaultView?.dispatchEvent(new Event('nativeUIRefresh'));
24
+ return binding;
25
+ };
26
+
27
+ export const getNativeSearchBindings = (doc: Document): NativeSearchBinding[] =>
28
+ (searches.get(doc) ?? []).flatMap((entry) => entry.deref() ?? []);
29
+
30
+ export const requestNativeSearch = async (binding: NativeSearchBinding, active: boolean, focus?: boolean): Promise<boolean> =>
31
+ (await runtimes.get(binding.tabBar.ownerDocument)?.search?.(binding, active, focus)) ?? false;
32
+
33
+ const runtimes = new WeakMap<Document, NativeUIIntegration>();
34
+ export const VERTICAL_BARS_TRANSITION_CANCELED = 'iosThemeVerticalBarsTransitionCanceled';
35
+
36
+ export const setNativeUIIntegration = (doc: Document, runtime?: NativeUIIntegration) => {
37
+ if (runtime) runtimes.set(doc, runtime);
38
+ else runtimes.delete(doc);
39
+ };
40
+
41
+ export const hasNativeUIIntegration = (doc: Document): boolean => runtimes.has(doc);
42
+
43
+ export const isNativeUI = (element: HTMLElement) => element.hasAttribute('data-native-ui-projected');
44
+
45
+ export const suspendNativeUI = async (scopes: HTMLElement[]): Promise<(canceled?: boolean) => void> =>
46
+ (await runtimes.get(scopes[0]?.ownerDocument)?.suspend(scopes)) ?? (() => {});
47
+
48
+ /** Shared geometry used by the theme's transition adapter; no theme import in the plugin. */
49
+ export const nativeWebViewMetrics = { radius: 0 };
@@ -0,0 +1,12 @@
1
+ export { enableVerticalControlArea, setVerticalControlAreaPlacement, NativeUI } from './native';
2
+ export type {
3
+ NativeUIStatus,
4
+ NativeUISuspension,
5
+ VerticalBarEdge,
6
+ VerticalControlAreaHandle,
7
+ VerticalControlAreaOptions,
8
+ ControlProjection,
9
+ NativeUIControls,
10
+ } from './native';
11
+
12
+ export { applyFoldStateClasses } from './native/foldable/fold-state';