@webview-native-ui/engine 1.0.1 → 1.0.2

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 (230) hide show
  1. package/README.md +14 -0
  2. package/WebViewNativeUIControls.podspec +17 -0
  3. package/dist/controls/activation.d.ts +15 -0
  4. package/dist/controls/activation.js +370 -0
  5. package/dist/controls/components/index.d.ts +16 -0
  6. package/dist/controls/components/index.js +38 -0
  7. package/dist/controls/components/ion-back-button.d.ts +4 -0
  8. package/dist/controls/components/ion-back-button.js +16 -0
  9. package/dist/controls/components/ion-button.d.ts +4 -0
  10. package/dist/controls/components/ion-button.js +26 -0
  11. package/dist/controls/components/ion-buttons.d.ts +5 -0
  12. package/dist/controls/components/ion-buttons.js +34 -0
  13. package/dist/controls/components/ion-fab.d.ts +4 -0
  14. package/dist/controls/components/ion-fab.js +164 -0
  15. package/dist/controls/components/ion-menu-button.d.ts +7 -0
  16. package/dist/controls/components/ion-menu-button.js +41 -0
  17. package/dist/controls/components/ion-segment.d.ts +5 -0
  18. package/dist/controls/components/ion-segment.js +24 -0
  19. package/dist/controls/components/ion-tab-bar.d.ts +5 -0
  20. package/dist/controls/components/ion-tab-bar.js +49 -0
  21. package/dist/controls/components/searchable-tabs.d.ts +20 -0
  22. package/dist/controls/components/searchable-tabs.js +525 -0
  23. package/dist/controls/definitions.d.ts +360 -0
  24. package/dist/controls/definitions.js +1 -0
  25. package/dist/controls/extensions.d.ts +44 -0
  26. package/dist/controls/extensions.js +164 -0
  27. package/dist/controls/foldable/fold-state.d.ts +4 -0
  28. package/dist/controls/foldable/fold-state.js +8 -0
  29. package/dist/controls/foldable/types.d.ts +9 -0
  30. package/dist/controls/foldable/types.js +1 -0
  31. package/dist/controls/html.d.ts +8 -0
  32. package/dist/controls/html.js +260 -0
  33. package/dist/controls/index.d.ts +7 -0
  34. package/dist/controls/index.js +4 -0
  35. package/dist/controls/ionic.d.ts +1 -0
  36. package/dist/controls/ionic.js +1 -0
  37. package/dist/controls/legacy-layout.d.ts +7 -0
  38. package/dist/controls/legacy-layout.js +23 -0
  39. package/dist/controls/overlays/content.d.ts +2 -0
  40. package/dist/controls/overlays/content.js +55 -0
  41. package/dist/controls/overlays/controller.d.ts +7 -0
  42. package/dist/controls/overlays/controller.js +369 -0
  43. package/dist/controls/overlays/covered-overlay.d.ts +4 -0
  44. package/dist/controls/overlays/covered-overlay.js +9 -0
  45. package/dist/controls/overlays/dialog-animation.d.ts +7 -0
  46. package/dist/controls/overlays/dialog-animation.js +49 -0
  47. package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
  48. package/dist/controls/overlays/dismiss-animation.js +14 -0
  49. package/dist/controls/overlays/focus.d.ts +2 -0
  50. package/dist/controls/overlays/focus.js +67 -0
  51. package/dist/controls/overlays/popover.d.ts +10 -0
  52. package/dist/controls/overlays/popover.js +117 -0
  53. package/dist/controls/overlays/styles.d.ts +9 -0
  54. package/dist/controls/overlays/styles.js +194 -0
  55. package/dist/controls/prehide.d.ts +6 -0
  56. package/dist/controls/prehide.js +280 -0
  57. package/dist/controls/runtime.d.ts +5 -0
  58. package/dist/controls/runtime.js +1685 -0
  59. package/dist/controls/shared/bridge.d.ts +4 -0
  60. package/dist/controls/shared/bridge.js +15 -0
  61. package/dist/controls/shared/candidate.d.ts +23 -0
  62. package/dist/controls/shared/candidate.js +128 -0
  63. package/dist/controls/shared/clipping.d.ts +1 -0
  64. package/dist/controls/shared/clipping.js +1 -0
  65. package/dist/controls/shared/connection.d.ts +21 -0
  66. package/dist/controls/shared/connection.js +112 -0
  67. package/dist/controls/shared/control-options.d.ts +4 -0
  68. package/dist/controls/shared/control-options.js +14 -0
  69. package/dist/controls/shared/covered-snapshot.d.ts +35 -0
  70. package/dist/controls/shared/covered-snapshot.js +41 -0
  71. package/dist/controls/shared/covered-source.d.ts +22 -0
  72. package/dist/controls/shared/covered-source.js +72 -0
  73. package/dist/controls/shared/crossfade.d.ts +11 -0
  74. package/dist/controls/shared/crossfade.js +71 -0
  75. package/dist/controls/shared/diagnostics.d.ts +2 -0
  76. package/dist/controls/shared/diagnostics.js +16 -0
  77. package/dist/controls/shared/dom.d.ts +45 -0
  78. package/dist/controls/shared/dom.js +258 -0
  79. package/dist/controls/shared/events.d.ts +12 -0
  80. package/dist/controls/shared/events.js +13 -0
  81. package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
  82. package/dist/controls/shared/horizontal-navigation.js +90 -0
  83. package/dist/controls/shared/icons.d.ts +5 -0
  84. package/dist/controls/shared/icons.js +66 -0
  85. package/dist/controls/shared/modal-animation.d.ts +21 -0
  86. package/dist/controls/shared/modal-animation.js +97 -0
  87. package/dist/controls/shared/modal.d.ts +26 -0
  88. package/dist/controls/shared/modal.js +261 -0
  89. package/dist/controls/shared/projection-id.d.ts +2 -0
  90. package/dist/controls/shared/projection-id.js +2 -0
  91. package/dist/controls/shared/scroll-placement.d.ts +1 -0
  92. package/dist/controls/shared/scroll-placement.js +1 -0
  93. package/dist/controls/shared/tab-bar-width.d.ts +7 -0
  94. package/dist/controls/shared/tab-bar-width.js +72 -0
  95. package/dist/controls/shared/toolbar-options.d.ts +10 -0
  96. package/dist/controls/shared/toolbar-options.js +38 -0
  97. package/dist/controls/shared/transition-identity.d.ts +3 -0
  98. package/dist/controls/shared/transition-identity.js +64 -0
  99. package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
  100. package/dist/controls/shared/vertical-bars-geometry.js +82 -0
  101. package/dist/controls/shared/vertical-navigation.d.ts +5 -0
  102. package/dist/controls/shared/vertical-navigation.js +39 -0
  103. package/dist/controls/startup.d.ts +7 -0
  104. package/dist/controls/startup.js +72 -0
  105. package/dist/controls/transport.d.ts +10 -0
  106. package/dist/controls/transport.js +1 -0
  107. package/dist/controls/vertical-bars-web-search.d.ts +8 -0
  108. package/dist/controls/vertical-bars-web-search.js +83 -0
  109. package/dist/controls/vertical-bars-web.d.ts +2 -0
  110. package/dist/controls/vertical-bars-web.js +609 -0
  111. package/dist/index.d.ts +1 -0
  112. package/dist/index.js +1 -0
  113. package/dist/integration/index.d.ts +36 -0
  114. package/dist/integration/index.js +25 -0
  115. package/dist/native/session.js +14 -8
  116. package/dist/projection/pause.d.ts +10 -0
  117. package/dist/projection/pause.js +58 -0
  118. package/dist/projection/styles.d.ts +1 -1
  119. package/dist/projection/styles.js +0 -1
  120. package/dist/projection/surfaces.js +2 -1
  121. package/dist/projection/web.js +39 -5
  122. package/dist/types.d.ts +5 -1
  123. package/docs/README.md +1 -0
  124. package/docs/configuration/README.md +3 -1
  125. package/docs/configuration/ionic-overlays.md +36 -2
  126. package/docs/configuration/pause-resume.md +51 -0
  127. package/docs/controls.md +39 -0
  128. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
  129. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
  130. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
  131. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
  132. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
  133. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
  134. package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
  135. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
  136. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
  137. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
  138. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
  139. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
  140. package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
  141. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
  142. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
  143. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
  144. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
  145. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
  146. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
  147. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
  148. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
  149. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
  150. package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
  151. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +1 -1
  152. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
  153. package/package.json +30 -5
  154. package/src/controls/activation.ts +427 -0
  155. package/src/controls/components/index.ts +45 -0
  156. package/src/controls/components/ion-back-button.ts +22 -0
  157. package/src/controls/components/ion-button.ts +28 -0
  158. package/src/controls/components/ion-buttons.ts +37 -0
  159. package/src/controls/components/ion-fab.ts +151 -0
  160. package/src/controls/components/ion-menu-button.ts +37 -0
  161. package/src/controls/components/ion-segment.ts +27 -0
  162. package/src/controls/components/ion-tab-bar.ts +55 -0
  163. package/src/controls/components/searchable-tabs.ts +534 -0
  164. package/src/controls/definitions.ts +338 -0
  165. package/src/controls/extensions.ts +198 -0
  166. package/src/controls/foldable/fold-state.ts +10 -0
  167. package/src/controls/foldable/types.ts +4 -0
  168. package/src/controls/html.ts +239 -0
  169. package/src/controls/index.ts +7 -0
  170. package/src/controls/ionic.ts +1 -0
  171. package/src/controls/legacy-layout.ts +21 -0
  172. package/src/controls/overlays/content.ts +51 -0
  173. package/src/controls/overlays/controller.ts +394 -0
  174. package/src/controls/overlays/covered-overlay.ts +11 -0
  175. package/src/controls/overlays/dialog-animation.ts +53 -0
  176. package/src/controls/overlays/dismiss-animation.ts +15 -0
  177. package/src/controls/overlays/focus.ts +64 -0
  178. package/src/controls/overlays/popover.ts +116 -0
  179. package/src/controls/overlays/styles.ts +195 -0
  180. package/src/controls/prehide.ts +285 -0
  181. package/src/controls/runtime.ts +1731 -0
  182. package/src/controls/shared/bridge.ts +18 -0
  183. package/src/controls/shared/candidate.ts +163 -0
  184. package/src/controls/shared/clipping.ts +1 -0
  185. package/src/controls/shared/connection.ts +105 -0
  186. package/src/controls/shared/control-options.ts +20 -0
  187. package/src/controls/shared/covered-snapshot.ts +52 -0
  188. package/src/controls/shared/covered-source.ts +78 -0
  189. package/src/controls/shared/crossfade.ts +70 -0
  190. package/src/controls/shared/diagnostics.ts +13 -0
  191. package/src/controls/shared/dom.ts +310 -0
  192. package/src/controls/shared/events.ts +14 -0
  193. package/src/controls/shared/horizontal-navigation.ts +106 -0
  194. package/src/controls/shared/icons.ts +64 -0
  195. package/src/controls/shared/modal-animation.ts +109 -0
  196. package/src/controls/shared/modal.ts +286 -0
  197. package/src/controls/shared/projection-id.ts +2 -0
  198. package/src/controls/shared/scroll-placement.ts +1 -0
  199. package/src/controls/shared/tab-bar-width.ts +66 -0
  200. package/src/controls/shared/toolbar-options.ts +51 -0
  201. package/src/controls/shared/transition-identity.ts +67 -0
  202. package/src/controls/shared/vertical-bars-geometry.ts +92 -0
  203. package/src/controls/shared/vertical-navigation.ts +34 -0
  204. package/src/controls/startup.ts +74 -0
  205. package/src/controls/transport.ts +11 -0
  206. package/src/controls/vertical-bars-web-search.ts +86 -0
  207. package/src/controls/vertical-bars-web.ts +674 -0
  208. package/src/dom/geometry.ts +24 -0
  209. package/src/dom/overlays.ts +196 -0
  210. package/src/dom/scroll-bindings.ts +66 -0
  211. package/src/dom/scroll-placement.ts +92 -0
  212. package/src/dom/visibility.ts +29 -0
  213. package/src/index.ts +19 -0
  214. package/src/integration/index.ts +64 -0
  215. package/src/ionic.ts +34 -0
  216. package/src/native/bootstrap.ts +132 -0
  217. package/src/native/bridge.ts +18 -0
  218. package/src/native/resource-url.ts +11 -0
  219. package/src/native/session.ts +708 -0
  220. package/src/native/updates.ts +103 -0
  221. package/src/native/values.ts +29 -0
  222. package/src/projection/events.ts +58 -0
  223. package/src/projection/pause.ts +58 -0
  224. package/src/projection/placement.ts +34 -0
  225. package/src/projection/scope.ts +13 -0
  226. package/src/projection/styles.ts +26 -0
  227. package/src/projection/surface.ts +88 -0
  228. package/src/projection/surfaces.ts +119 -0
  229. package/src/projection/web.ts +1013 -0
  230. package/src/types.ts +58 -0
@@ -0,0 +1,674 @@
1
+ import { getUnprojectedVisibility } from '../index.js';
2
+
3
+ import { enableSupportIonicOverlays } from '../ionic.js';
4
+ import { getProjectionState } from './shared/covered-source.js';
5
+ import { controlSelected } from './shared/control-options.js';
6
+ import { createCrossfade, fadeMarker } from './shared/crossfade.js';
7
+ import { alignVerticalBarClone, verticalBarFrame } from './shared/vertical-bars-geometry.js';
8
+ import { createVerticalBarsWebSearch, searchProjectionClass } from './vertical-bars-web-search.js';
9
+ import { inVerticalBarsSurface, topModal, movingModal, modalPreparing, modalPresentation } from './shared/modal.js';
10
+ import type { NativeUIRuntimeHandle, NativeUIOptions, NativeUIStatus } from './definitions.js';
11
+ import { VERTICAL_BARS_TRANSITION_CANCELED } from '../integration/index.js';
12
+ import {
13
+ activateProjectedElement,
14
+ createVerticalBarsPageState,
15
+ verticalBarsActionCandidate,
16
+ verticalBarsBackCandidate,
17
+ verticalBarsEnteringPage,
18
+ verticalBarsToolbarActions,
19
+ isExcluded,
20
+ isNativeUIDisabled,
21
+ isVerticalBarsToolbarGroup,
22
+ preferredVerticalBarsBack,
23
+ verticalBarsOwned,
24
+ marker,
25
+ syncToolbarText,
26
+ toolbarTextMarker,
27
+ prehideOnlyMutation,
28
+ prehiddenClass,
29
+ unprojected,
30
+ withoutPrehide,
31
+ } from './shared/dom.js';
32
+
33
+ const backProjectionClass = 'ios-theme-vertical-bars-back-button-projection';
34
+ const toolbarProjectionClass = 'ios-theme-vertical-bars-toolbar-projection';
35
+ const readyClass = 'ios-theme-vertical-bars-toolbar-ready';
36
+ const toolbarControlSize = 48;
37
+ const toolbarItemGap = 4;
38
+ const toolbarControlGap = 12;
39
+ const toolbarGroupHeight = (count: number) => count * toolbarControlSize + Math.max(0, count - 1) * toolbarItemGap;
40
+
41
+ interface ToolbarProjection {
42
+ source: HTMLElement;
43
+ root: HTMLElement;
44
+ projection: HTMLElement;
45
+ actions: { source: HTMLElement; projection: HTMLElement; previousAriaHidden: string | null | undefined }[];
46
+ }
47
+
48
+ interface BackProjection {
49
+ source: HTMLIonBackButtonElement;
50
+ projection: HTMLIonBackButtonElement;
51
+ root: HTMLElement;
52
+ previousAriaHidden: string | null | undefined;
53
+ }
54
+
55
+ interface ToolbarSource {
56
+ group: HTMLElement;
57
+ actions: HTMLElement[];
58
+ }
59
+
60
+ export const createVerticalBarsWebProjection = (
61
+ doc: Document,
62
+ options: NativeUIOptions,
63
+ enabled: () => boolean = () => true,
64
+ ): NativeUIRuntimeHandle => {
65
+ const projectionState = getProjectionState(doc);
66
+ const { isProjectionMutation, sourcesChangedEvent, targetsChangedEvent } = projectionState;
67
+ const win = doc.defaultView!;
68
+ const crossfade = createCrossfade(win);
69
+ let incomingInstant: boolean | undefined;
70
+ const toolbarEnabled = options.ionic?.auto !== false;
71
+ let root: HTMLElement | undefined;
72
+ let backProjections: BackProjection[] = [];
73
+ let toolbarProjections: ToolbarProjection[] = [];
74
+ let suspended = 0;
75
+ let stopped = false;
76
+ let frame = 0;
77
+ let updates = 0;
78
+ let observingVerticalBars = false;
79
+ const verticalBarsPages = createVerticalBarsPageState();
80
+ let sourceObserver: MutationObserver | undefined;
81
+ let waiters: (() => void)[] = [];
82
+ const listeners = new AbortController();
83
+ const verticalBarsRoot = () => doc.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');
84
+ const surfaceOf = (element: Element) => element.closest<HTMLElement>('ion-modal') ?? verticalBarsRoot()!;
85
+ const hasNativeSources = () =>
86
+ projectionState.snapshotsEnabled && !!doc.querySelector('[data-native-ui-vertical-bars], [data-native-ui-vertical-bars-member]');
87
+ const isPrepared = (element: Element) => {
88
+ const modal = element.closest('ion-modal');
89
+ // Preparing hidden proxy DOM inside an animating Ionic sheet can rewind its
90
+ // compositor animation. Native owns entry; prepare the image target at DidPresent.
91
+ if (modal && (modalPreparing(modal) || modalPresentation(modal))) return false;
92
+ return (
93
+ (projectionState.snapshotsEnabled &&
94
+ (element.matches('[data-native-ui-vertical-bars], [data-native-ui-vertical-bars-member]') ||
95
+ !!element.querySelector('[data-native-ui-vertical-bars], [data-native-ui-vertical-bars-member]'))) ||
96
+ [...projectionState.preparedOwners].some((owner) => owner === element || owner.contains(element) || element.contains(owner))
97
+ );
98
+ };
99
+ // Covered toolbar controls keep a Web visual on their own surface, below the modal.
100
+ const isCovered = (element: Element) =>
101
+ (!!movingModal(doc) && element.closest('ion-modal') === movingModal(doc)) ||
102
+ projectionState.owns(element as HTMLElement) ||
103
+ projectionState.isVisible(element as HTMLElement) ||
104
+ isPrepared(element) ||
105
+ (!!topModal(doc) && surfaceOf(element) !== topModal(doc));
106
+ const projectedSources = () =>
107
+ [
108
+ ...Array.from(doc.querySelectorAll<HTMLElement>('[data-native-ui-vertical-bars], [data-native-ui-vertical-bars-member]')),
109
+ ...projectionState.owners,
110
+ ...projectionState.preparedOwners,
111
+ ...backProjections.map(({ source }) => source),
112
+ search.source,
113
+ ...toolbarProjections.flatMap(({ actions }) => actions.map(({ source }) => source)),
114
+ ].filter((source): source is HTMLElement => !!source);
115
+ const isRendered = (element: HTMLElement) =>
116
+ withoutPrehide(element, () => {
117
+ const style = getComputedStyle(element);
118
+ const rect = element.getBoundingClientRect();
119
+ return (
120
+ element.isConnected &&
121
+ style.display !== 'none' &&
122
+ (style.visibility === 'visible' || getUnprojectedVisibility(element) === 'visible') &&
123
+ rect.width > 0 &&
124
+ rect.height > 0
125
+ );
126
+ });
127
+ const search = createVerticalBarsWebSearch(
128
+ doc,
129
+ (element) =>
130
+ options.ionic?.auto !== false &&
131
+ controlSelected(element, options, 'tabs', true) &&
132
+ !!verticalBarsRoot()?.contains(element) &&
133
+ (!options.root || options.root.contains(element)) &&
134
+ !element.closest('[data-native-ui-placement="element"]') &&
135
+ inVerticalBarsSurface(element) &&
136
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
137
+ !isNativeUIDisabled(element) &&
138
+ !verticalBarsPages.isDeparted(element) &&
139
+ isRendered(element),
140
+ () => {
141
+ void schedule();
142
+ },
143
+ );
144
+ const inEligibleToolbar = (element: HTMLElement) =>
145
+ !!verticalBarsRoot()?.contains(element) &&
146
+ (!options.root || options.root.contains(element)) &&
147
+ !element.closest('[data-native-ui-placement="element"]') &&
148
+ (inVerticalBarsSurface(element) || isCovered(element)) &&
149
+ verticalBarsActionCandidate(element) &&
150
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
151
+ (!verticalBarsPages.isDeparted(element) || projectionState.isVisible(element));
152
+ const isEligibleBack = (element: HTMLIonBackButtonElement) =>
153
+ !!verticalBarsRoot()?.contains(element) &&
154
+ (!options.root || options.root.contains(element)) &&
155
+ !element.closest('[data-native-ui-placement="element"]') &&
156
+ verticalBarsOwned(element) &&
157
+ verticalBarsBackCandidate(element) &&
158
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
159
+ (!verticalBarsPages.isDeparted(element) || projectionState.isVisible(element)) &&
160
+ unprojected(projectedSources(), () => isRendered(element));
161
+ const pageOrder = (element: Element) => Array.from(doc.querySelectorAll('.ion-page')).indexOf(element.closest('.ion-page')!);
162
+ const findBack = (surface = topModal(doc) ?? verticalBarsRoot()) => {
163
+ const candidates = Array.from(doc.querySelectorAll<HTMLIonBackButtonElement>(`ion-back-button:not(.${backProjectionClass})`)).filter(
164
+ (element) => surfaceOf(element) === surface && isEligibleBack(element),
165
+ );
166
+ return preferredVerticalBarsBack(candidates, doc);
167
+ };
168
+ const findToolbarGroups = () => {
169
+ const candidates = Array.from(doc.querySelectorAll<HTMLIonButtonsElement>(`ion-buttons:not(.${toolbarProjectionClass})`)).flatMap(
170
+ (group): ToolbarSource[] => {
171
+ const actions = verticalBarsToolbarActions(group).filter((action) => unprojected(projectedSources(), () => isRendered(action)));
172
+ if (!actions.length) return [];
173
+ if (!isVerticalBarsToolbarGroup(group)) return actions.filter(inEligibleToolbar).map((action) => ({ group, actions: [action] }));
174
+ return inEligibleToolbar(group) && unprojected(projectedSources(), () => isRendered(group)) ? [{ group, actions }] : [];
175
+ },
176
+ );
177
+ for (const action of Array.from(doc.querySelectorAll<HTMLElement>('ion-button, ion-menu-button'))) {
178
+ if (action.closest('ion-buttons') || !verticalBarsOwned(action) || !inEligibleToolbar(action)) continue;
179
+ if (unprojected(projectedSources(), () => isRendered(action))) candidates.push({ group: action, actions: [action] });
180
+ }
181
+ const highestPages = new Map<HTMLElement, number>();
182
+ for (const { group } of candidates) {
183
+ const surface = surfaceOf(group);
184
+ highestPages.set(surface, Math.max(highestPages.get(surface) ?? -1, pageOrder(group)));
185
+ }
186
+ return candidates.filter(({ group }) => (enabled() || isCovered(group)) && pageOrder(group) === highestPages.get(surfaceOf(group)));
187
+ };
188
+ const isCurrentToolbarAction = (source: HTMLElement) =>
189
+ inVerticalBarsSurface(source) && findToolbarGroups().some(({ actions }) => actions.includes(source));
190
+ const nativeOwned = projectionState.nativeOwns;
191
+ const restore = (retire?: (surface: HTMLElement) => void, retained = new Set<BackProjection | ToolbarProjection>()) => {
192
+ search.restore(retire);
193
+ for (const current of backProjections) {
194
+ if (retained.has(current)) continue;
195
+ const { source, projection, root: surface, previousAriaHidden } = current;
196
+ surface.classList.remove(readyClass);
197
+ if (retire) retire(projection);
198
+ else projection.remove();
199
+ if (!nativeOwned(source)) {
200
+ source.removeAttribute(marker);
201
+ if (previousAriaHidden === null) source.removeAttribute('aria-hidden');
202
+ else if (previousAriaHidden !== undefined) source.setAttribute('aria-hidden', previousAriaHidden);
203
+ }
204
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
205
+ }
206
+ backProjections = backProjections.filter((current) => retained.has(current));
207
+ for (const current of toolbarProjections) {
208
+ if (retained.has(current)) continue;
209
+ const { projection, actions, root: surface } = current;
210
+ surface.classList.remove(readyClass);
211
+ if (retire) retire(projection);
212
+ else projection.remove();
213
+ for (const { source, previousAriaHidden } of actions) {
214
+ if (!nativeOwned(source)) {
215
+ source.removeAttribute(marker);
216
+ if (previousAriaHidden === null) source.removeAttribute('aria-hidden');
217
+ else if (previousAriaHidden !== undefined) source.setAttribute('aria-hidden', previousAriaHidden);
218
+ }
219
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
220
+ }
221
+ }
222
+ toolbarProjections = toolbarProjections.filter((current) => retained.has(current));
223
+ sourceObserver?.disconnect();
224
+ sourceObserver = undefined;
225
+ root?.classList.remove(readyClass);
226
+ root = undefined;
227
+ };
228
+ const copyAttributes = (target: HTMLElement, original: HTMLElement) => {
229
+ for (const name of ['id', 'slot', marker, 'aria-hidden']) target.removeAttribute(name);
230
+ for (const attribute of Array.from(target.attributes))
231
+ if (
232
+ !['class', marker, fadeMarker, 'data-native-ui-proxy-prepared'].includes(attribute.name) &&
233
+ !attribute.name.startsWith('data-native-ui-snapshot') &&
234
+ !original.hasAttribute(attribute.name)
235
+ )
236
+ target.removeAttribute(attribute.name);
237
+ for (const attribute of Array.from(original.attributes))
238
+ if (!['id', 'slot', marker, 'aria-hidden'].includes(attribute.name) && !attribute.name.startsWith('data-native-ui-snapshot'))
239
+ target.setAttribute(attribute.name, attribute.value);
240
+ target.classList.remove(prehiddenClass);
241
+ // Establish ownership before insertion/layout: glass can sample even a single Web paint.
242
+ target.toggleAttribute('data-native-ui-proxy-prepared', isPrepared(original));
243
+ };
244
+ const syncBack = (target: HTMLIonBackButtonElement, original: HTMLIonBackButtonElement, surface: HTMLElement) => {
245
+ copyAttributes(target, original);
246
+ target.classList.add(backProjectionClass, 'ion-cloned-element');
247
+ target.disabled = original.disabled;
248
+ target.defaultHref = original.defaultHref;
249
+ target.icon = original.icon;
250
+ target.color = original.color;
251
+ target.mode = original.mode;
252
+ target.text = '';
253
+ alignVerticalBarClone(target, original, surface);
254
+ };
255
+ const syncAction = (target: HTMLElement, original: HTMLElement) => {
256
+ copyAttributes(target, original);
257
+ target.classList.add('ios-theme-vertical-bars-toolbar-action', 'ion-cloned-element');
258
+ const label =
259
+ original.getAttribute('aria-label') ??
260
+ original.shadowRoot?.querySelector('[part~=native]')?.getAttribute('aria-label') ??
261
+ original.textContent?.trim();
262
+ if (label) target.setAttribute('aria-label', label);
263
+ if ('disabled' in original) (target as HTMLIonButtonElement).disabled = (original as HTMLIonButtonElement).disabled;
264
+ if (original.matches('ion-button')) {
265
+ const icon = original.querySelector<HTMLElement>('ion-icon, svg');
266
+ const clonedIcon = icon?.cloneNode(true) as HTMLElement | undefined;
267
+ clonedIcon?.setAttribute('slot', 'icon-only');
268
+ target.replaceChildren(...(clonedIcon ? [clonedIcon] : [document.createTextNode(original.textContent?.trim() || label || '')]));
269
+ }
270
+ };
271
+ const sameToolbar = (current: ToolbarProjection, { group, actions }: ToolbarSource) =>
272
+ group === current.source &&
273
+ surfaceOf(group) === current.root &&
274
+ actions.length === current.actions.length &&
275
+ actions.every((action, index) => action === current.actions[index].source);
276
+ const sameSources = (backs: HTMLIonBackButtonElement[], groups: ToolbarSource[]) =>
277
+ backs.length === backProjections.length &&
278
+ backs.every((source, index) => source === backProjections[index].source && surfaceOf(source) === backProjections[index].root) &&
279
+ groups.length === toolbarProjections.length &&
280
+ groups.every((group, index) => sameToolbar(toolbarProjections[index], group));
281
+ const syncExisting = () => {
282
+ for (const { source, projection, root: surface } of backProjections) {
283
+ if (!surface.classList.contains(readyClass)) surface.classList.add(readyClass);
284
+ if (projectionState.isVisible(source)) {
285
+ projection.inert = true;
286
+ continue;
287
+ }
288
+ syncBack(projection, source, surface);
289
+ projection.inert = isCovered(source);
290
+ if (projection.inert) projection.setAttribute('aria-hidden', 'true');
291
+ }
292
+ const offsets = new Map<HTMLElement, number>();
293
+ for (const { projection, source, actions, root: surface } of toolbarProjections) {
294
+ if (!surface.classList.contains(readyClass)) surface.classList.add(readyClass);
295
+ const topOffset = offsets.get(surface) ?? (findBack(surface) ? toolbarControlSize + toolbarControlGap : 0);
296
+ if (projectionState.isVisible(source) || actions.some((action) => projectionState.isVisible(action.source))) {
297
+ projection.inert = true;
298
+ offsets.set(surface, topOffset + toolbarGroupHeight(actions.length) + toolbarControlGap);
299
+ continue;
300
+ }
301
+ if (actions.length === 1 && !projection.matches('ion-buttons')) syncAction(projection, actions[0].source);
302
+ else copyAttributes(projection, source);
303
+ projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
304
+ projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
305
+ // Preserve appearance without bypassing the modal's input/focus boundary.
306
+ projection.inert = isCovered(source);
307
+ if (projection.inert) projection.setAttribute('aria-hidden', 'true');
308
+ if (projection.matches('ion-buttons')) actions.forEach(({ source: action, projection: clone }) => syncAction(clone, action));
309
+ const frames = actions.map(({ source }) => verticalBarFrame(source));
310
+ if (frames.every((frame) => !!frame)) {
311
+ if (actions.length === 1) alignVerticalBarClone(projection, actions[0].source, surface);
312
+ else {
313
+ const left = Math.min(...frames.map((frame) => frame!.x));
314
+ const top = Math.min(...frames.map((frame) => frame!.y));
315
+ const right = Math.max(...frames.map((frame) => frame!.x + frame!.width));
316
+ const bottom = Math.max(...frames.map((frame) => frame!.y + frame!.height));
317
+ const origin = surface.getBoundingClientRect();
318
+ Object.assign(projection.style, {
319
+ left: `${left - origin.left}px`,
320
+ top: `${top - origin.top}px`,
321
+ right: 'auto',
322
+ bottom: 'auto',
323
+ width: `${right - left}px`,
324
+ height: `${bottom - top}px`,
325
+ padding: '0',
326
+ boxSizing: 'border-box',
327
+ });
328
+ actions.forEach(({ source, projection: clone }) => alignVerticalBarClone(clone, source, projection));
329
+ }
330
+ }
331
+ offsets.set(surface, topOffset + toolbarGroupHeight(actions.length) + toolbarControlGap);
332
+ }
333
+ };
334
+ const project = (nextBacks: HTMLIonBackButtonElement[], groups: ToolbarSource[]) => {
335
+ root = topModal(doc) ?? verticalBarsRoot()!;
336
+ let topOffset = 0;
337
+ for (const source of nextBacks) {
338
+ const surface = surfaceOf(source);
339
+ if (surface === root) topOffset = toolbarControlSize + toolbarControlGap;
340
+ if (backProjections.some((current) => current.source === source)) continue;
341
+ const previousAriaHidden = nativeOwned(source) && source.hasAttribute(marker) ? undefined : source.getAttribute('aria-hidden');
342
+ const projection = source.cloneNode(false) as HTMLIonBackButtonElement;
343
+ syncBack(projection, source, surface);
344
+ projection.addEventListener(
345
+ 'click',
346
+ (event) => {
347
+ event.preventDefault();
348
+ event.stopImmediatePropagation();
349
+ if (inVerticalBarsSurface(source) && source === findBack()) activateProjectedElement(source);
350
+ },
351
+ { capture: true },
352
+ );
353
+ projectionState.setTarget(source, projection);
354
+ surface.append(projection);
355
+ backProjections.push({ source, projection, root: surface, previousAriaHidden });
356
+ source.setAttribute(marker, '');
357
+ source.setAttribute('aria-hidden', 'true');
358
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
359
+ if (surface === root) topOffset = toolbarControlSize + toolbarControlGap;
360
+ }
361
+ for (const { group, actions: sources } of groups) {
362
+ if (toolbarProjections.some((current) => sameToolbar(current, { group, actions: sources }))) {
363
+ topOffset += toolbarGroupHeight(sources.length) + toolbarControlGap;
364
+ continue;
365
+ }
366
+ const projection = (sources.length === 1 ? sources[0] : group).cloneNode(false) as HTMLElement;
367
+ if (sources.length === 1) syncAction(projection, sources[0]);
368
+ else copyAttributes(projection, group);
369
+ projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
370
+ projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
371
+ const actions = sources.map((source) => {
372
+ const clone = sources.length === 1 ? projection : (source.cloneNode(false) as HTMLElement);
373
+ if (sources.length > 1) syncAction(clone, source);
374
+ clone.addEventListener(
375
+ 'click',
376
+ (event) => {
377
+ event.preventDefault();
378
+ event.stopImmediatePropagation();
379
+ if (isCurrentToolbarAction(source)) activateProjectedElement(source);
380
+ },
381
+ { capture: true },
382
+ );
383
+ if (sources.length > 1) projection.append(clone);
384
+ projectionState.setTarget(source, clone);
385
+ const previousAriaHidden = nativeOwned(source) && source.hasAttribute(marker) ? undefined : source.getAttribute('aria-hidden');
386
+ source.setAttribute(marker, '');
387
+ source.setAttribute('aria-hidden', 'true');
388
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
389
+ return { source, projection: clone, previousAriaHidden };
390
+ });
391
+ const surface = surfaceOf(group);
392
+ projectionState.setTarget(group, projection);
393
+ surface.append(projection);
394
+ toolbarProjections.push({ source: group, root: surface, projection, actions });
395
+ topOffset += toolbarGroupHeight(actions.length) + toolbarControlGap;
396
+ }
397
+ backProjections.sort((a, b) => nextBacks.indexOf(a.source) - nextBacks.indexOf(b.source));
398
+ toolbarProjections.sort(
399
+ (a, b) => groups.findIndex((group) => sameToolbar(a, group)) - groups.findIndex((group) => sameToolbar(b, group)),
400
+ );
401
+ syncExisting();
402
+ if (nextBacks.length || toolbarProjections.length) root.classList.add(readyClass);
403
+ sourceObserver = new MutationObserver((records) => {
404
+ if (
405
+ records.some(
406
+ (record) =>
407
+ record.type !== 'attributes' || (![marker, 'aria-hidden'].includes(record.attributeName ?? '') && !prehideOnlyMutation(record)),
408
+ )
409
+ )
410
+ void schedule();
411
+ });
412
+ for (const back of nextBacks)
413
+ if (back.shadowRoot)
414
+ sourceObserver.observe(back.shadowRoot, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
415
+ for (const { group } of groups)
416
+ sourceObserver.observe(group, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
417
+ };
418
+ const reveal = () => {
419
+ if (incomingInstant === undefined) return;
420
+ for (const surface of [
421
+ ...backProjections.map(({ projection }) => projection),
422
+ ...toolbarProjections.map(({ projection }) => projection),
423
+ search.surface,
424
+ ])
425
+ if (surface) crossfade.play(surface, false, incomingInstant);
426
+ incomingInstant = undefined;
427
+ };
428
+ const performUpdate = () => {
429
+ frame = 0;
430
+ syncToolbarText(doc);
431
+ const currentRoot = verticalBarsRoot();
432
+ if (observingVerticalBars !== !!currentRoot) {
433
+ observingVerticalBars = !!currentRoot;
434
+ observer.disconnect();
435
+ observer.observe(doc.documentElement, {
436
+ subtree: true,
437
+ childList: true,
438
+ characterData: observingVerticalBars,
439
+ attributes: true,
440
+ attributeOldValue: true,
441
+ attributeFilter: observingVerticalBars ? undefined : ['class'],
442
+ });
443
+ }
444
+ if (
445
+ stopped ||
446
+ suspended ||
447
+ options.verticalBars?.enabled === false ||
448
+ !currentRoot ||
449
+ (!enabled() && !topModal(doc) && !projectionState.owners.length && !projectionState.preparedOwners.length && !hasNativeSources())
450
+ )
451
+ return restore();
452
+ const surfaces = new Set(
453
+ Array.from(doc.querySelectorAll<HTMLIonBackButtonElement>(`ion-back-button:not(.${backProjectionClass})`)).map(surfaceOf),
454
+ );
455
+ const nextBacks = toolbarEnabled
456
+ ? [...surfaces]
457
+ .map((surface) => findBack(surface))
458
+ .filter((back): back is HTMLIonBackButtonElement => !!back && (enabled() || isCovered(back)))
459
+ : [];
460
+ const groups = toolbarEnabled ? findToolbarGroups() : [];
461
+ const projectionRoot = topModal(doc) ?? currentRoot;
462
+ if (enabled()) search.update(projectionRoot);
463
+ else search.restore();
464
+ if (!nextBacks.length && !groups.length && !search.source) return restore();
465
+ if (root && sameSources(nextBacks, groups)) {
466
+ syncExisting();
467
+ reveal();
468
+ return;
469
+ }
470
+ // Image scopes retain their DOM identity while another surface adds/removes controls.
471
+ const retained = new Set<BackProjection | ToolbarProjection>([
472
+ ...backProjections.filter((current) => nextBacks.includes(current.source) && current.root === surfaceOf(current.source)),
473
+ ...toolbarProjections.filter((current) => groups.some((group) => sameToolbar(current, group))),
474
+ ]);
475
+ restore(undefined, retained);
476
+ project(nextBacks, groups);
477
+ if (enabled()) search.update(projectionRoot);
478
+ else search.restore();
479
+ reveal();
480
+ updates++;
481
+ };
482
+ const update = () => {
483
+ try {
484
+ performUpdate();
485
+ } catch {
486
+ restore();
487
+ } finally {
488
+ win.dispatchEvent(new Event(targetsChangedEvent));
489
+ const pending = waiters;
490
+ waiters = [];
491
+ pending.forEach((resolve) => resolve());
492
+ }
493
+ };
494
+ const schedule = (): Promise<void> => {
495
+ if (stopped) return Promise.resolve();
496
+ // Release Web ownership before the native runtime can acknowledge its next frame.
497
+ if (!enabled() && !topModal(doc) && !projectionState.owners.length && !projectionState.preparedOwners.length && !hasNativeSources())
498
+ restore();
499
+ const done = new Promise<void>((resolve) => waiters.push(resolve));
500
+ if (!frame) frame = win.requestAnimationFrame(update);
501
+ return done;
502
+ };
503
+ const observer = new MutationObserver((records) => {
504
+ const insideProjection = (target: Node) => {
505
+ const element = target instanceof Element ? target : target.parentNode instanceof Element ? target.parentNode : undefined;
506
+ const rootNode = target.getRootNode();
507
+ const shadowHost = rootNode instanceof ShadowRoot ? rootNode.host : undefined;
508
+ return !![element, shadowHost].some((candidate) =>
509
+ candidate?.closest(`.${backProjectionClass}, .${toolbarProjectionClass}, .${searchProjectionClass}`),
510
+ );
511
+ };
512
+ const verticalBarsChanged = records.some(
513
+ (record) =>
514
+ (record.type === 'attributes' && (record.target as Element).matches('ion-app')) ||
515
+ (record.type === 'childList' &&
516
+ Array.from(record.addedNodes).some(
517
+ (node) =>
518
+ node instanceof Element &&
519
+ (node.matches('ion-app.ios-theme-vertical-bars') || !!node.querySelector('ion-app.ios-theme-vertical-bars')),
520
+ )),
521
+ );
522
+ if (
523
+ (observingVerticalBars &&
524
+ records.some(
525
+ (record) =>
526
+ !isProjectionMutation(record) &&
527
+ record.attributeName !== marker &&
528
+ record.attributeName !== toolbarTextMarker &&
529
+ !prehideOnlyMutation(record) &&
530
+ !insideProjection(record.target),
531
+ )) ||
532
+ (!observingVerticalBars && verticalBarsChanged && records.some((record) => !prehideOnlyMutation(record)))
533
+ )
534
+ void schedule();
535
+ });
536
+ observer.observe(doc.documentElement, {
537
+ subtree: true,
538
+ childList: true,
539
+ attributes: true,
540
+ attributeOldValue: true,
541
+ attributeFilter: ['class'],
542
+ });
543
+ const pageLifecycle = (event: Event) => {
544
+ const page = event.target;
545
+ if (!(page instanceof HTMLElement) || !page.matches('.ion-page')) return;
546
+ if (event.type === VERTICAL_BARS_TRANSITION_CANCELED) {
547
+ const entering = (event as CustomEvent<{ entering?: HTMLElement }>).detail?.entering;
548
+ verticalBarsPages.cancel(entering, page);
549
+ } else verticalBarsPages.lifecycle(event);
550
+ if (verticalBarsRoot()) void schedule();
551
+ };
552
+ for (const name of ['ionViewWillEnter', 'ionViewWillLeave', 'ionViewDidEnter', 'ionViewDidLeave'])
553
+ doc.addEventListener(name, pageLifecycle, { capture: true, signal: listeners.signal });
554
+ doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, pageLifecycle, { capture: true, signal: listeners.signal });
555
+ const stopOverlays = enableSupportIonicOverlays(undefined, doc)!.subscribe(({ element, phase }) => {
556
+ if (!element?.matches('ion-modal')) return;
557
+ if (phase === 'willPresent' || phase === 'didPresent') void schedule();
558
+ // Retire modal clones before they can paint at the background rail offset.
559
+ if (phase === 'didDismiss') update();
560
+ });
561
+ listeners.signal.addEventListener('abort', stopOverlays, { once: true });
562
+ win.addEventListener(
563
+ 'nativeUIVerticalBarsAcquire',
564
+ (event) => {
565
+ const instant = (event as CustomEvent<{ instant: boolean }>).detail?.instant ?? true;
566
+ incomingInstant = undefined;
567
+ // Acquisition is dispatched before Native records the source's original ARIA.
568
+ // Transfer Web-owned visibility without destroying the prepared image target.
569
+ for (const current of [...backProjections, ...toolbarProjections.flatMap(({ actions }) => actions)]) {
570
+ if (!nativeOwned(current.source) || current.previousAriaHidden === undefined) continue;
571
+ if (current.previousAriaHidden === null) current.source.removeAttribute('aria-hidden');
572
+ else current.source.setAttribute('aria-hidden', current.previousAriaHidden);
573
+ current.previousAriaHidden = undefined;
574
+ }
575
+ if ([...backProjections, ...toolbarProjections].some(({ source }) => isCovered(source))) {
576
+ // Conceal existing Web surfaces immediately; leave discovery and geometry
577
+ // to the scheduled update so acquisition does not create new controls.
578
+ const retained = [
579
+ ...backProjections,
580
+ ...toolbarProjections.filter(({ actions }) => actions.length > 1),
581
+ ...toolbarProjections.flatMap(({ actions }) => actions),
582
+ ];
583
+ for (const { source, projection } of retained)
584
+ for (const attribute of ['data-native-ui-vertical-bars', 'data-native-ui-vertical-bars-member'])
585
+ projection.toggleAttribute(attribute, source.hasAttribute(attribute));
586
+ void schedule();
587
+ return;
588
+ }
589
+ restore((surface) => {
590
+ // Keep only the outgoing visual at its last rectangle after Web ownership
591
+ // is released. The native acknowledgement transfers input exactly once.
592
+ const box = surface.getBoundingClientRect();
593
+ surface.style.setProperty('position', 'fixed', 'important');
594
+ surface.style.setProperty('display', 'block', 'important');
595
+ surface.style.setProperty('left', `${box.left}px`, 'important');
596
+ surface.style.setProperty('top', `${box.top}px`, 'important');
597
+ surface.style.setProperty('right', 'auto', 'important');
598
+ surface.style.setProperty('bottom', 'auto', 'important');
599
+ surface.style.setProperty('width', `${box.width}px`, 'important');
600
+ surface.style.setProperty('height', `${box.height}px`, 'important');
601
+ crossfade.retire(surface, instant);
602
+ });
603
+ },
604
+ { signal: listeners.signal },
605
+ );
606
+ win.addEventListener(
607
+ 'nativeUIVerticalBarsRelease',
608
+ (event) => {
609
+ incomingInstant = (event as CustomEvent<{ instant: boolean }>).detail.instant;
610
+ void schedule();
611
+ },
612
+ { signal: listeners.signal },
613
+ );
614
+ win.addEventListener(
615
+ 'resize',
616
+ () => {
617
+ void schedule();
618
+ },
619
+ { signal: listeners.signal },
620
+ );
621
+ win.addEventListener(
622
+ 'nativeUIRefresh',
623
+ () => {
624
+ void schedule();
625
+ },
626
+ { signal: listeners.signal },
627
+ );
628
+ win.addEventListener(
629
+ sourcesChangedEvent,
630
+ () => {
631
+ void schedule();
632
+ },
633
+ { signal: listeners.signal },
634
+ );
635
+ void schedule();
636
+
637
+ return {
638
+ markForCheck: () => {
639
+ void schedule();
640
+ },
641
+ getStatus: (): NativeUIStatus => ({
642
+ state: stopped ? 'stopped' : 'web',
643
+ projected:
644
+ backProjections.length + Number(!!search.source) + toolbarProjections.reduce((count, group) => count + group.actions.length, 0),
645
+ updates,
646
+ }),
647
+ async suspend() {
648
+ suspended++;
649
+ restore();
650
+ let resumed = false;
651
+ return {
652
+ async resume() {
653
+ if (resumed) return;
654
+ resumed = true;
655
+ suspended = Math.max(0, suspended - 1);
656
+ if (!suspended) await void schedule();
657
+ },
658
+ };
659
+ },
660
+ async destroy() {
661
+ if (stopped) return;
662
+ stopped = true;
663
+ win.cancelAnimationFrame(frame);
664
+ frame = 0;
665
+ waiters.forEach((resolve) => resolve());
666
+ waiters = [];
667
+ observer.disconnect();
668
+ sourceObserver?.disconnect();
669
+ listeners.abort();
670
+ crossfade.destroy();
671
+ restore();
672
+ },
673
+ };
674
+ };