@rdlabo/capacitor-native-ui 0.0.0-stage → 1.2.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 (255) hide show
  1. package/LICENSE +21 -0
  2. package/Package.swift +23 -0
  3. package/README.md +53 -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 +255 -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 +188 -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 +28 -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 +788 -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/crossfade.d.ts +12 -0
  129. package/dist/native/shared/crossfade.d.ts.map +1 -0
  130. package/dist/native/shared/crossfade.js +67 -0
  131. package/dist/native/shared/crossfade.js.map +1 -0
  132. package/dist/native/shared/diagnostics.d.ts +4 -0
  133. package/dist/native/shared/diagnostics.d.ts.map +1 -0
  134. package/dist/native/shared/diagnostics.js +18 -0
  135. package/dist/native/shared/diagnostics.js.map +1 -0
  136. package/dist/native/shared/dom.d.ts +46 -0
  137. package/dist/native/shared/dom.d.ts.map +1 -0
  138. package/dist/native/shared/dom.js +180 -0
  139. package/dist/native/shared/dom.js.map +1 -0
  140. package/dist/native/shared/events.d.ts +15 -0
  141. package/dist/native/shared/events.d.ts.map +1 -0
  142. package/dist/native/shared/events.js +17 -0
  143. package/dist/native/shared/events.js.map +1 -0
  144. package/dist/native/shared/icons.d.ts +8 -0
  145. package/dist/native/shared/icons.d.ts.map +1 -0
  146. package/dist/native/shared/icons.js +61 -0
  147. package/dist/native/shared/icons.js.map +1 -0
  148. package/dist/native/shared/modal.d.ts +10 -0
  149. package/dist/native/shared/modal.d.ts.map +1 -0
  150. package/dist/native/shared/modal.js +160 -0
  151. package/dist/native/shared/modal.js.map +1 -0
  152. package/dist/native/shared/projection-id.d.ts +4 -0
  153. package/dist/native/shared/projection-id.d.ts.map +1 -0
  154. package/dist/native/shared/projection-id.js +7 -0
  155. package/dist/native/shared/projection-id.js.map +1 -0
  156. package/dist/native/shared/toolbar-options.d.ts +9 -0
  157. package/dist/native/shared/toolbar-options.d.ts.map +1 -0
  158. package/dist/native/shared/toolbar-options.js +36 -0
  159. package/dist/native/shared/toolbar-options.js.map +1 -0
  160. package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
  161. package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
  162. package/dist/native/shared/vertical-bars-geometry.js +85 -0
  163. package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
  164. package/dist/native/startup.d.ts +8 -0
  165. package/dist/native/startup.d.ts.map +1 -0
  166. package/dist/native/startup.js +86 -0
  167. package/dist/native/startup.js.map +1 -0
  168. package/dist/native/vertical-bars-web-search.d.ts +10 -0
  169. package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
  170. package/dist/native/vertical-bars-web-search.js +90 -0
  171. package/dist/native/vertical-bars-web-search.js.map +1 -0
  172. package/dist/native/vertical-bars-web.d.ts +5 -0
  173. package/dist/native/vertical-bars-web.d.ts.map +1 -0
  174. package/dist/native/vertical-bars-web.js +421 -0
  175. package/dist/native/vertical-bars-web.js.map +1 -0
  176. package/dist/native-integration/index.d.ts +25 -0
  177. package/dist/native-integration/index.d.ts.map +1 -0
  178. package/dist/native-integration/index.js +33 -0
  179. package/dist/native-integration/index.js.map +1 -0
  180. package/dist/vertical-bars.d.ts +6 -0
  181. package/dist/vertical-bars.js +5 -0
  182. package/docs/configuration.md +187 -0
  183. package/docs/framework-independent.md +82 -0
  184. package/docs/migration.md +206 -0
  185. package/docs/native-mini-player.md +47 -0
  186. package/docs/native-ui-extensions.md +365 -0
  187. package/docs/native-ui.md +409 -0
  188. package/docs/setup.md +205 -0
  189. package/docs/vertical-bars.md +379 -0
  190. package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
  191. package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
  192. package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
  193. package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
  194. package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
  195. package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
  196. package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
  197. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
  198. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
  199. package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
  200. package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
  201. package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
  202. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
  203. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
  204. package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
  205. package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
  206. package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
  207. package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
  208. package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
  209. package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
  210. package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
  211. package/package.json +89 -4
  212. package/src/core.ts +15 -0
  213. package/src/native/activation.ts +394 -0
  214. package/src/native/components/index.ts +45 -0
  215. package/src/native/components/ion-back-button.ts +22 -0
  216. package/src/native/components/ion-button.ts +25 -0
  217. package/src/native/components/ion-buttons.ts +37 -0
  218. package/src/native/components/ion-fab.ts +151 -0
  219. package/src/native/components/ion-menu-button.ts +37 -0
  220. package/src/native/components/ion-segment.ts +27 -0
  221. package/src/native/components/ion-tab-bar.ts +50 -0
  222. package/src/native/components/searchable-tabs.ts +499 -0
  223. package/src/native/definitions.ts +324 -0
  224. package/src/native/extensions.ts +176 -0
  225. package/src/native/foldable/fold-state.ts +10 -0
  226. package/src/native/foldable/types.ts +4 -0
  227. package/src/native/html.ts +193 -0
  228. package/src/native/index.ts +41 -0
  229. package/src/native/legacy-layout.ts +21 -0
  230. package/src/native/overlays/content.ts +51 -0
  231. package/src/native/overlays/controller.ts +349 -0
  232. package/src/native/overlays/dialog-animation.ts +53 -0
  233. package/src/native/overlays/dismiss-animation.ts +15 -0
  234. package/src/native/overlays/focus.ts +64 -0
  235. package/src/native/overlays/popover.ts +120 -0
  236. package/src/native/overlays/styles.ts +181 -0
  237. package/src/native/prehide.ts +285 -0
  238. package/src/native/runtime.ts +988 -0
  239. package/src/native/shared/bridge.ts +18 -0
  240. package/src/native/shared/candidate.ts +162 -0
  241. package/src/native/shared/clipping.ts +24 -0
  242. package/src/native/shared/crossfade.ts +67 -0
  243. package/src/native/shared/diagnostics.ts +13 -0
  244. package/src/native/shared/dom.ts +304 -0
  245. package/src/native/shared/events.ts +14 -0
  246. package/src/native/shared/icons.ts +57 -0
  247. package/src/native/shared/modal.ts +157 -0
  248. package/src/native/shared/projection-id.ts +2 -0
  249. package/src/native/shared/toolbar-options.ts +53 -0
  250. package/src/native/shared/vertical-bars-geometry.ts +80 -0
  251. package/src/native/startup.ts +84 -0
  252. package/src/native/vertical-bars-web-search.ts +86 -0
  253. package/src/native/vertical-bars-web.ts +498 -0
  254. package/src/native-integration/index.ts +49 -0
  255. package/src/vertical-bars.ts +11 -0
@@ -0,0 +1,285 @@
1
+ import { localToolbarOption, toolbarOptions } from './shared/toolbar-options';
2
+ import type { VerticalControlAreaOptions } from './definitions';
3
+ import { modalUsesVerticalBars } from './shared/modal';
4
+ import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from './shared/events';
5
+ import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
6
+ import {
7
+ childElements,
8
+ clearVerticalBarsPlacement,
9
+ createVerticalBarsPageState,
10
+ verticalBarsActionCandidate,
11
+ verticalBarsBackCandidate,
12
+ verticalBarsBackWebClass,
13
+ verticalBarsOwned,
14
+ isVerticalBarsToolbarActionShape,
15
+ isPermanentlyExcluded,
16
+ isNativeUIDisabled,
17
+ prehiddenClass,
18
+ prehideRootClass,
19
+ rejectedClass,
20
+ setVerticalBarsPlacement,
21
+ syncToolbarText,
22
+ } from './shared/dom';
23
+
24
+ const overlays = 'ion-menu, ion-popover';
25
+ const sourceSelector = 'ion-back-button, ion-menu-button, ion-buttons, ion-button';
26
+ // Realm- and teardown-safe: globals such as Element/HTMLElement may be gone
27
+ // when a queued mutation microtask or a stale listener still runs.
28
+ const isHtmlElement = (node: unknown): node is HTMLElement => !!node && (node as Node).nodeType === 1;
29
+ const asElement = (node: Node | EventTarget | null): HTMLElement | undefined => (isHtmlElement(node) ? node : undefined);
30
+ const backSupported = (element: HTMLElement): boolean => {
31
+ const back = element as HTMLIonBackButtonElement;
32
+ return back.icon === undefined && back.color === undefined && !!back.shadowRoot;
33
+ };
34
+ const eligibleBack = verticalBarsBackCandidate;
35
+ const eligible = verticalBarsActionCandidate;
36
+ const localPlacement = (element: HTMLElement) => localToolbarOption(element, true);
37
+ const selectedAction = (element: HTMLElement, options: VerticalControlAreaOptions): boolean => {
38
+ if (!element.matches('ion-button, ion-menu-button')) return false;
39
+ const explicit = localPlacement(element);
40
+ return explicit ?? (toolbarOptions(options, true).button && isVerticalBarsToolbarActionShape(element));
41
+ };
42
+
43
+ /** Capture toolbar placement once per routed-page epoch, before Ionic paints its transition. */
44
+ export const prehideVerticalBarsToolbarSources = (
45
+ doc: Document,
46
+ enabled: () => boolean = () => true,
47
+ sourceRoot: HTMLElement = doc.body,
48
+ options: VerticalControlAreaOptions = {},
49
+ ): { suspend: () => () => void; stop: () => void } => {
50
+ doc.documentElement.classList.toggle(prehideRootClass, enabled());
51
+ const scopes = new Map<HTMLElement, Set<HTMLElement>>();
52
+ const owner = new WeakMap<HTMLElement, HTMLElement>();
53
+ const pendingBacks = new Map<HTMLElement, { scope: HTMLElement; timer: ReturnType<typeof setTimeout> }>();
54
+ const verticalBarsPages = createVerticalBarsPageState();
55
+ const listeners = new AbortController();
56
+ const root = () =>
57
+ sourceRoot.closest<HTMLElement>('ion-app.ios-theme-vertical-bars') ??
58
+ sourceRoot.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');
59
+ let suspended = 0;
60
+ let stopped = false;
61
+ const routedPage = (element: HTMLElement) => element.closest<HTMLElement>('.ion-page:not(ion-app, body)');
62
+ const release = (scope: HTMLElement) => {
63
+ for (const [element, pending] of pendingBacks) {
64
+ if (pending.scope !== scope) continue;
65
+ clearTimeout(pending.timer);
66
+ pendingBacks.delete(element);
67
+ }
68
+ const owned = scopes.get(scope);
69
+ if (!owned) return;
70
+ owned.forEach((element) => {
71
+ if (owner.get(element) !== scope) return;
72
+ owner.delete(element);
73
+ element.classList.remove(prehiddenClass);
74
+ element.classList.remove(verticalBarsBackWebClass);
75
+ clearVerticalBarsPlacement(element);
76
+ });
77
+ scopes.delete(scope);
78
+ };
79
+ const capture = (scope: HTMLElement) => {
80
+ if (!root()?.contains(scope) || scope.closest(overlays) || !modalUsesVerticalBars(scope) || verticalBarsPages.isDeparted(scope)) return;
81
+ const owned = scopes.get(scope) ?? new Set<HTMLElement>();
82
+ const place = (element: HTMLElement, rail: boolean) => {
83
+ if (owned.has(element)) return;
84
+ const previousScope = owner.get(element);
85
+ if (previousScope) scopes.get(previousScope)?.delete(element);
86
+ owner.set(element, scope);
87
+ setVerticalBarsPlacement(element, rail);
88
+ if (element.matches('ion-back-button')) element.classList.toggle(verticalBarsBackWebClass, !rail);
89
+ owned.add(element);
90
+ };
91
+ const sources = scope.matches('ion-back-button') ? [scope] : Array.from(scope.querySelectorAll<HTMLElement>(sourceSelector));
92
+ sources.forEach((element) => {
93
+ if (!sourceRoot.contains(element)) return;
94
+ // Shell opt-out is temporary; decide initial placement once it is lifted.
95
+ if (isNativeUIDisabled(element)) return;
96
+ if (!modalUsesVerticalBars(element) || element.closest(overlays) || (scope.matches('.ion-page') && routedPage(element) !== scope))
97
+ return;
98
+ if (element.matches('ion-buttons')) {
99
+ const children = childElements(element);
100
+ const actions = children.filter((child) => eligible(child) && selectedAction(child, options));
101
+ const group =
102
+ (localPlacement(element) ?? toolbarOptions(options, true).buttons) &&
103
+ eligible(element) &&
104
+ actions.length === children.length &&
105
+ (actions.length > 1 || (actions.length === 1 && actions[0].matches('ion-menu-button')));
106
+ const freshGroup = !owned.has(element);
107
+ place(element, group);
108
+ children.forEach((child) => {
109
+ if (!child.matches('ion-back-button')) place(child, freshGroup && actions.includes(child));
110
+ });
111
+ } else if (element.matches('ion-back-button') || !element.parentElement?.matches('ion-buttons')) {
112
+ if (owned.has(element)) return;
113
+ if (
114
+ element.matches('ion-back-button') &&
115
+ eligibleBack(element) &&
116
+ (element as HTMLIonBackButtonElement).icon === undefined &&
117
+ (element as HTMLIonBackButtonElement).color === undefined &&
118
+ !element.shadowRoot &&
119
+ !element.classList.contains('hydrated')
120
+ ) {
121
+ const previousPending = pendingBacks.get(element);
122
+ if (previousPending && previousPending.scope !== scope) {
123
+ clearTimeout(previousPending.timer);
124
+ pendingBacks.delete(element);
125
+ }
126
+ if (!pendingBacks.has(element)) {
127
+ const timer = setTimeout(() => {
128
+ if (pendingBacks.get(element)?.scope !== scope) return;
129
+ pendingBacks.delete(element);
130
+ // A custom element that never hydrates must remain usable on Web.
131
+ place(element, false);
132
+ reconcile();
133
+ }, 1500);
134
+ pendingBacks.set(element, { scope, timer });
135
+ }
136
+ return;
137
+ }
138
+ const pending = pendingBacks.get(element);
139
+ if (pending) {
140
+ clearTimeout(pending.timer);
141
+ pendingBacks.delete(element);
142
+ }
143
+ place(
144
+ element,
145
+ element.matches('ion-back-button')
146
+ ? (localPlacement(element) ?? toolbarOptions(options, true).backButton) && eligibleBack(element) && backSupported(element)
147
+ : eligible(element) && selectedAction(element, options),
148
+ );
149
+ }
150
+ });
151
+ scopes.set(scope, owned);
152
+ };
153
+ const reconcile = () => {
154
+ doc.documentElement.classList.toggle(prehideRootClass, enabled() && !suspended);
155
+ syncToolbarText(doc);
156
+ const verticalBars = root();
157
+ if (!verticalBars) {
158
+ Array.from(scopes.keys()).forEach(release);
159
+ return;
160
+ }
161
+ for (const scope of scopes.keys()) if (!scope.isConnected || !modalUsesVerticalBars(scope)) release(scope);
162
+ for (const [element, pending] of pendingBacks) if (element.shadowRoot || element.classList.contains('hydrated')) capture(pending.scope);
163
+ // Ionic inserts the destination as invisible before WillEnter. Hide its
164
+ // sources in that same mutation microtask; capture new DOM identities on active pages too.
165
+ verticalBars.querySelectorAll<HTMLElement>('.ion-page:not(ion-app, body, .ion-page-hidden)').forEach(capture);
166
+ // Root toolbars can mount after startup; each new DOM identity is captured once.
167
+ verticalBars.querySelectorAll<HTMLElement>(sourceSelector).forEach((element) => {
168
+ if (routedPage(element) || element.closest(overlays)) return;
169
+ const scope = element.closest<HTMLElement>('ion-toolbar') ?? (element.matches('ion-back-button') ? element : null);
170
+ if (scope) capture(scope);
171
+ });
172
+ for (const owned of scopes.values()) {
173
+ for (const element of owned) {
174
+ const groupOwnsChildren =
175
+ element.parentElement?.matches('ion-buttons') &&
176
+ verticalBarsOwned(element.parentElement) &&
177
+ childElements(element.parentElement).every((child) => verticalBarsOwned(child));
178
+ const hide =
179
+ enabled() &&
180
+ !element.closest('[data-native-ui-placement="element"]') &&
181
+ !suspended &&
182
+ verticalBarsOwned(element) &&
183
+ !groupOwnsChildren &&
184
+ element.isConnected &&
185
+ !element.closest(`.${rejectedClass}`) &&
186
+ !isPermanentlyExcluded(element) &&
187
+ !isNativeUIDisabled(element) &&
188
+ (!element.matches('ion-back-button') || backSupported(element) || !element.classList.contains('hydrated')) &&
189
+ (!element.matches('ion-buttons') || childElements(element).every((child) => verticalBarsOwned(child)));
190
+ element.classList.toggle(prehiddenClass, hide);
191
+ if (element.matches('ion-back-button')) element.classList.toggle(verticalBarsBackWebClass, !hide);
192
+ }
193
+ }
194
+ };
195
+ const onWillEnter = (event: Event) => {
196
+ const page = asElement(event.target);
197
+ if (page?.matches('.ion-page')) {
198
+ verticalBarsPages.lifecycle(event);
199
+ release(page); // Cached pages begin a new placement epoch.
200
+ capture(page);
201
+ reconcile();
202
+ }
203
+ };
204
+ const onPageLifecycle: EventListener = (event) => {
205
+ verticalBarsPages.lifecycle(event);
206
+ if (event.type !== LIFECYCLE_DID_LEAVE) return;
207
+ const page = asElement(event.target);
208
+ if (page) release(page);
209
+ };
210
+ // A canceled transition keeps the leaving page active and abandons the entering one.
211
+ const onTransitionCanceled: EventListener = (event) => {
212
+ const leaving = asElement(event.target);
213
+ if (!leaving) return;
214
+ const entering = (event as CustomEvent<{ entering?: HTMLElement }>).detail?.entering;
215
+ verticalBarsPages.cancel(entering, leaving);
216
+ reconcile();
217
+ };
218
+ doc.addEventListener(LIFECYCLE_WILL_ENTER, onWillEnter, { capture: true, signal: listeners.signal });
219
+ for (const name of [LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE])
220
+ doc.addEventListener(name, onPageLifecycle, { capture: true, signal: listeners.signal });
221
+ doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, onTransitionCanceled, { capture: true, signal: listeners.signal });
222
+ doc.defaultView?.addEventListener('nativeUIRefresh', reconcile, { signal: listeners.signal });
223
+ reconcile();
224
+ const mutationRelevant = (record: MutationRecord) => {
225
+ if (record.type === 'characterData') return record.target.parentElement?.matches('ion-toolbar') === true;
226
+ if (record.type === 'childList' && asElement(record.target)?.matches('ion-toolbar')) return true;
227
+ if (record.type === 'childList')
228
+ return [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)].some(
229
+ (node) =>
230
+ isHtmlElement(node) &&
231
+ (node.matches(`${sourceSelector}, .ion-page, ion-toolbar`) || !!node.querySelector(`${sourceSelector}, .ion-page, ion-toolbar`)),
232
+ );
233
+ if (record.attributeName !== 'class') return true;
234
+ const current = (record.target as Element).className;
235
+ const previous = record.oldValue ?? '';
236
+ const withoutOwned = (value: string) =>
237
+ value
238
+ .split(/\s+/)
239
+ .filter((name) => name && ![prehiddenClass, prehideRootClass, verticalBarsBackWebClass].includes(name))
240
+ .join(' ');
241
+ return typeof current !== 'string' || withoutOwned(previous) !== withoutOwned(current);
242
+ };
243
+ let scheduled = false;
244
+ const observer = new (doc.defaultView?.MutationObserver ?? MutationObserver)((records) => {
245
+ if (scheduled || stopped || !records.some(mutationRelevant)) return;
246
+ scheduled = true;
247
+ queueMicrotask(() => {
248
+ scheduled = false;
249
+ if (!stopped) reconcile();
250
+ });
251
+ });
252
+ observer.observe(doc.documentElement, {
253
+ subtree: true,
254
+ childList: true,
255
+ characterData: true,
256
+ attributes: true,
257
+ attributeOldValue: true,
258
+ attributeFilter: ['class', 'data-native-ui', 'hidden', 'inert', 'icon', 'color'],
259
+ });
260
+ return {
261
+ suspend: () => {
262
+ if (stopped) return () => {};
263
+ suspended++;
264
+ doc.documentElement.classList.remove(prehideRootClass);
265
+ reconcile();
266
+ let resumed = false;
267
+ return () => {
268
+ if (resumed || stopped) return;
269
+ resumed = true;
270
+ if (--suspended === 0) {
271
+ doc.documentElement.classList.add(prehideRootClass);
272
+ reconcile();
273
+ }
274
+ };
275
+ },
276
+ stop: () => {
277
+ if (stopped) return;
278
+ stopped = true;
279
+ observer.disconnect();
280
+ listeners.abort();
281
+ doc.documentElement.classList.remove(prehideRootClass);
282
+ Array.from(scopes.keys()).forEach(release);
283
+ },
284
+ };
285
+ };