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