@rdlabo/ionic-theme-ios27 1.2.1 → 2.0.0-beta.pr303.shaf4f5ca16816f

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 (195) hide show
  1. package/README.md +24 -280
  2. package/dist/css/components/ion-toolbar.css +1 -1
  3. package/dist/css/ionic-theme-ios27.css +1 -1
  4. package/dist/css/vertical-bars.css +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/layout/index.d.ts +18 -0
  10. package/dist/layout/index.d.ts.map +1 -0
  11. package/dist/layout/index.js +26 -0
  12. package/dist/layout/index.js.map +1 -0
  13. package/dist/segment/index.js +3 -3
  14. package/dist/segment/index.js.map +1 -1
  15. package/dist/sheets-of-glass/index.js +7 -7
  16. package/dist/sheets-of-glass/index.js.map +1 -1
  17. package/dist/tab-bar-searchable/dismiss-button.js +53 -3
  18. package/dist/tab-bar-searchable/dismiss-button.js.map +1 -1
  19. package/dist/tab-bar-searchable/index.js +18 -3
  20. package/dist/tab-bar-searchable/index.js.map +1 -1
  21. package/dist/transition/ios.transition.d.ts.map +1 -1
  22. package/dist/transition/ios.transition.js +7 -4
  23. package/dist/transition/ios.transition.js.map +1 -1
  24. package/dist/transition/native-ui.d.ts +6 -0
  25. package/dist/transition/native-ui.d.ts.map +1 -0
  26. package/dist/{native-integration/index.js → transition/native-ui.js} +16 -30
  27. package/dist/transition/native-ui.js.map +1 -0
  28. package/docs/adaptive-themes.md +151 -0
  29. package/docs/add-native-ui.md +53 -0
  30. package/docs/animation.md +1 -1
  31. package/docs/eslint.md +1 -1
  32. package/docs/features.md +3 -3
  33. package/docs/iphone-duo-with-original-theme.md +40 -28
  34. package/docs/iphone-duo.md +29 -32
  35. package/docs/migration.md +12 -24
  36. package/docs/setup.md +82 -0
  37. package/docs/special-markup.md +5 -5
  38. package/docs/using-ion-item-group.md +2 -2
  39. package/docs/vertical-bars-layout.md +47 -0
  40. package/package.json +12 -42
  41. package/src/index.ts +3 -1
  42. package/src/layout/index.ts +29 -0
  43. package/src/segment/index.ts +3 -3
  44. package/src/sheets-of-glass/index.ts +7 -7
  45. package/src/styles/components/ion-toolbar.scss +1 -1
  46. package/src/styles/vertical-bars.scss +15 -18
  47. package/src/tab-bar-searchable/dismiss-button.ts +73 -4
  48. package/src/tab-bar-searchable/index.ts +20 -3
  49. package/src/transition/ios.transition.ts +7 -4
  50. package/src/{native-integration/index.ts → transition/native-ui.ts} +17 -46
  51. package/Package.swift +0 -21
  52. package/dist/_virtual/_rolldown/runtime.js +0 -13
  53. package/dist/native/components/index.d.ts +0 -19
  54. package/dist/native/components/index.d.ts.map +0 -1
  55. package/dist/native/components/index.js +0 -40
  56. package/dist/native/components/index.js.map +0 -1
  57. package/dist/native/components/ion-back-button.d.ts +0 -9
  58. package/dist/native/components/ion-back-button.d.ts.map +0 -1
  59. package/dist/native/components/ion-back-button.js +0 -20
  60. package/dist/native/components/ion-back-button.js.map +0 -1
  61. package/dist/native/components/ion-button.d.ts +0 -10
  62. package/dist/native/components/ion-button.d.ts.map +0 -1
  63. package/dist/native/components/ion-button.js +0 -23
  64. package/dist/native/components/ion-button.js.map +0 -1
  65. package/dist/native/components/ion-buttons.d.ts +0 -11
  66. package/dist/native/components/ion-buttons.d.ts.map +0 -1
  67. package/dist/native/components/ion-buttons.js +0 -30
  68. package/dist/native/components/ion-buttons.js.map +0 -1
  69. package/dist/native/components/ion-fab.d.ts +0 -10
  70. package/dist/native/components/ion-fab.d.ts.map +0 -1
  71. package/dist/native/components/ion-fab.js +0 -132
  72. package/dist/native/components/ion-fab.js.map +0 -1
  73. package/dist/native/components/ion-menu-button.d.ts +0 -13
  74. package/dist/native/components/ion-menu-button.d.ts.map +0 -1
  75. package/dist/native/components/ion-menu-button.js +0 -31
  76. package/dist/native/components/ion-menu-button.js.map +0 -1
  77. package/dist/native/components/ion-segment.d.ts +0 -11
  78. package/dist/native/components/ion-segment.d.ts.map +0 -1
  79. package/dist/native/components/ion-segment.js +0 -30
  80. package/dist/native/components/ion-segment.js.map +0 -1
  81. package/dist/native/components/ion-tab-bar.d.ts +0 -11
  82. package/dist/native/components/ion-tab-bar.d.ts.map +0 -1
  83. package/dist/native/components/ion-tab-bar.js +0 -50
  84. package/dist/native/components/ion-tab-bar.js.map +0 -1
  85. package/dist/native/components/searchable-tabs.d.ts +0 -20
  86. package/dist/native/components/searchable-tabs.d.ts.map +0 -1
  87. package/dist/native/components/searchable-tabs.js +0 -383
  88. package/dist/native/components/searchable-tabs.js.map +0 -1
  89. package/dist/native/definitions.d.ts +0 -152
  90. package/dist/native/definitions.d.ts.map +0 -1
  91. package/dist/native/definitions.js +0 -0
  92. package/dist/native/foldable/fold-state.d.ts +0 -5
  93. package/dist/native/foldable/fold-state.d.ts.map +0 -1
  94. package/dist/native/foldable/fold-state.js +0 -14
  95. package/dist/native/foldable/fold-state.js.map +0 -1
  96. package/dist/native/foldable/types.d.ts +0 -12
  97. package/dist/native/foldable/types.d.ts.map +0 -1
  98. package/dist/native/foldable/types.js +0 -0
  99. package/dist/native/index.d.ts +0 -11
  100. package/dist/native/index.d.ts.map +0 -1
  101. package/dist/native/index.js +0 -242
  102. package/dist/native/index.js.map +0 -1
  103. package/dist/native/prehide.d.ts +0 -7
  104. package/dist/native/prehide.d.ts.map +0 -1
  105. package/dist/native/prehide.js +0 -230
  106. package/dist/native/prehide.js.map +0 -1
  107. package/dist/native/runtime.d.ts +0 -5
  108. package/dist/native/runtime.d.ts.map +0 -1
  109. package/dist/native/runtime.js +0 -640
  110. package/dist/native/runtime.js.map +0 -1
  111. package/dist/native/shared/candidate.d.ts +0 -21
  112. package/dist/native/shared/candidate.d.ts.map +0 -1
  113. package/dist/native/shared/candidate.js +0 -98
  114. package/dist/native/shared/candidate.js.map +0 -1
  115. package/dist/native/shared/crossfade.d.ts +0 -10
  116. package/dist/native/shared/crossfade.d.ts.map +0 -1
  117. package/dist/native/shared/crossfade.js +0 -50
  118. package/dist/native/shared/crossfade.js.map +0 -1
  119. package/dist/native/shared/dom.d.ts +0 -46
  120. package/dist/native/shared/dom.d.ts.map +0 -1
  121. package/dist/native/shared/dom.js +0 -172
  122. package/dist/native/shared/dom.js.map +0 -1
  123. package/dist/native/shared/events.d.ts +0 -11
  124. package/dist/native/shared/events.d.ts.map +0 -1
  125. package/dist/native/shared/events.js +0 -13
  126. package/dist/native/shared/events.js.map +0 -1
  127. package/dist/native/shared/icons.d.ts +0 -8
  128. package/dist/native/shared/icons.d.ts.map +0 -1
  129. package/dist/native/shared/icons.js +0 -61
  130. package/dist/native/shared/icons.js.map +0 -1
  131. package/dist/native/shared/modal.d.ts +0 -10
  132. package/dist/native/shared/modal.d.ts.map +0 -1
  133. package/dist/native/shared/modal.js +0 -160
  134. package/dist/native/shared/modal.js.map +0 -1
  135. package/dist/native/vertical-bars-web-search.d.ts +0 -9
  136. package/dist/native/vertical-bars-web-search.d.ts.map +0 -1
  137. package/dist/native/vertical-bars-web-search.js +0 -84
  138. package/dist/native/vertical-bars-web-search.js.map +0 -1
  139. package/dist/native/vertical-bars-web.d.ts +0 -5
  140. package/dist/native/vertical-bars-web.d.ts.map +0 -1
  141. package/dist/native/vertical-bars-web.js +0 -341
  142. package/dist/native/vertical-bars-web.js.map +0 -1
  143. package/dist/native-integration/index.d.ts +0 -23
  144. package/dist/native-integration/index.d.ts.map +0 -1
  145. package/dist/native-integration/index.js.map +0 -1
  146. package/dist/native-integration/transition.d.ts +0 -5
  147. package/dist/native-integration/transition.d.ts.map +0 -1
  148. package/dist/native-integration/transition.js +0 -16
  149. package/dist/native-integration/transition.js.map +0 -1
  150. package/dist/vertical-bars.d.ts +0 -5
  151. package/dist/vertical-bars.js +0 -4
  152. package/docs/e2e-testing.md +0 -51
  153. package/docs/feedback.md +0 -65
  154. package/docs/native-ui-shell.md +0 -285
  155. package/docs/vertical-bars.md +0 -285
  156. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButton.swift +0 -71
  157. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButtons.swift +0 -24
  158. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellComponents.swift +0 -18
  159. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellFab.swift +0 -83
  160. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +0 -432
  161. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSegment.swift +0 -91
  162. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +0 -221
  163. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +0 -437
  164. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +0 -118
  165. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +0 -396
  166. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellCrossfade.swift +0 -35
  167. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellHost.swift +0 -8
  168. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellRendering.swift +0 -27
  169. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +0 -135
  170. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +0 -650
  171. package/src/native/components/index.ts +0 -53
  172. package/src/native/components/ion-back-button.ts +0 -19
  173. package/src/native/components/ion-button.ts +0 -24
  174. package/src/native/components/ion-buttons.ts +0 -38
  175. package/src/native/components/ion-fab.ts +0 -151
  176. package/src/native/components/ion-menu-button.ts +0 -24
  177. package/src/native/components/ion-segment.ts +0 -27
  178. package/src/native/components/ion-tab-bar.ts +0 -50
  179. package/src/native/components/searchable-tabs.ts +0 -459
  180. package/src/native/definitions.ts +0 -191
  181. package/src/native/foldable/fold-state.ts +0 -9
  182. package/src/native/foldable/types.ts +0 -4
  183. package/src/native/index.ts +0 -281
  184. package/src/native/prehide.ts +0 -265
  185. package/src/native/runtime.ts +0 -781
  186. package/src/native/shared/candidate.ts +0 -150
  187. package/src/native/shared/crossfade.ts +0 -50
  188. package/src/native/shared/dom.ts +0 -286
  189. package/src/native/shared/events.ts +0 -8
  190. package/src/native/shared/icons.ts +0 -57
  191. package/src/native/shared/modal.ts +0 -157
  192. package/src/native/vertical-bars-web-search.ts +0 -80
  193. package/src/native/vertical-bars-web.ts +0 -411
  194. package/src/native-integration/transition.ts +0 -15
  195. package/src/vertical-bars.ts +0 -11
@@ -1,281 +0,0 @@
1
- import { Capacitor, registerPlugin } from '@capacitor/core';
2
- import { setConfig } from '../transition/ios.transition';
3
- import type {
4
- NativeUIShellHandle,
5
- NativeUIShellOptions,
6
- NativeUIShellPlugin,
7
- VerticalBarEdge,
8
- VerticalBarPlacement,
9
- VerticalControlAreaHandle,
10
- VerticalControlAreaOptions,
11
- WebViewMetrics,
12
- } from './definitions';
13
- import { createRuntime } from './runtime';
14
- import { createVerticalBarsWebProjection } from './vertical-bars-web';
15
- import { prehideVerticalBarsToolbarSources } from './prehide';
16
- import { observeVerticalBarsModals } from './shared/modal';
17
- export type {
18
- NativeUIShellComponent,
19
- NativeUIShellControls,
20
- NativeUIShellHandle,
21
- NativeUIShellOptions,
22
- NativeUIShellStatus,
23
- NativeUIShellSuspension,
24
- VerticalBarEdge,
25
- VerticalBarPlacement,
26
- VerticalControlAreaHandle,
27
- VerticalControlAreaOptions,
28
- WebViewMetrics,
29
- } from './definitions';
30
-
31
- const plugin = registerPlugin<NativeUIShellPlugin>('IonicNativeUIShell');
32
- export const IonicNativeUIShell = plugin;
33
- let active: Promise<NativeUIShellHandle> | undefined;
34
- let activeConfiguration: string | undefined;
35
- const web = (reason: string): NativeUIShellHandle => ({
36
- getStatus: () => ({ state: 'web', projected: 0, updates: 0, reason }),
37
- suspend: async () => ({ resume: async () => {} }),
38
- destroy: async () => {},
39
- });
40
-
41
- const combine = (native: NativeUIShellHandle, fallback: NativeUIShellHandle): NativeUIShellHandle => ({
42
- getStatus() {
43
- const nativeStatus = native.getStatus();
44
- const fallbackStatus = fallback.getStatus();
45
- return {
46
- ...nativeStatus,
47
- state:
48
- nativeStatus.state === 'stopped' && fallbackStatus.state === 'stopped' ? 'stopped' : nativeStatus.projected > 0 ? 'native' : 'web',
49
- projected: nativeStatus.projected + fallbackStatus.projected,
50
- updates: nativeStatus.updates + fallbackStatus.updates,
51
- };
52
- },
53
- async suspend() {
54
- const [nativeLease, fallbackLease] = await Promise.all([native.suspend(), fallback.suspend()]);
55
- return { resume: async () => void (await Promise.all([nativeLease.resume(), fallbackLease.resume()])) };
56
- },
57
- async destroy() {
58
- await Promise.all([native.destroy(), fallback.destroy()]);
59
- },
60
- });
61
-
62
- /** Wraps a runtime handle with the shared enable-lifecycle: reason, suspend hooks and idempotent destroy. */
63
- const manage = (
64
- handle: NativeUIShellHandle,
65
- lifecycle: {
66
- reason?: string;
67
- suspend?: () => (() => void) | undefined;
68
- /** Runs after teardown; clears the shared slot only while this activation owns it. */
69
- release?: () => void;
70
- destroy?: () => void | Promise<void>;
71
- } = {},
72
- ): NativeUIShellHandle => {
73
- let destroyed = false;
74
- return {
75
- getStatus: () => (lifecycle.reason ? { ...handle.getStatus(), reason: lifecycle.reason } : handle.getStatus()),
76
- async suspend() {
77
- const lease = await handle.suspend();
78
- const resume = lifecycle.suspend?.();
79
- return {
80
- async resume() {
81
- await lease.resume();
82
- resume?.();
83
- },
84
- };
85
- },
86
- async destroy() {
87
- if (destroyed) return;
88
- destroyed = true;
89
- try {
90
- await handle.destroy();
91
- } finally {
92
- await lifecycle.destroy?.();
93
- lifecycle.release?.();
94
- }
95
- },
96
- };
97
- };
98
-
99
- /** Reads the current native WebView geometry and applies it to page transitions. */
100
- export const configureNativeTransition = async (): Promise<WebViewMetrics> => {
101
- const metrics = typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' ? await plugin.getWebViewMetrics() : { radius: 0 };
102
- setConfig({ radius: metrics.radius });
103
- return metrics;
104
- };
105
-
106
- /** Resolves a logical vertical-bar edge to the physical side for the given direction. */
107
- const physicalVerticalBarEdge = (edge: Exclude<VerticalBarEdge, null>, rtl: boolean): 'left' | 'right' =>
108
- (edge === 'leading') !== rtl ? 'left' : 'right';
109
-
110
- const elementRtl = (element: Element): boolean => element.closest('[dir]')?.getAttribute('dir') === 'rtl';
111
-
112
- // Device facts are supplied by the application; the theme only compares placement.
113
- const nativePlacements = new WeakMap<HTMLElement, { edge: VerticalBarEdge; rtl?: boolean }>();
114
- const horizontalFallbackAttribute = 'data-native-ui-shell-vertical-bars-suspended';
115
-
116
- // Keep the requested rail while ordinary Native UI Shell temporarily uses its
117
- // horizontal layout. Removing the effective class restores normal measurements,
118
- // toolbar ownership and safe areas throughout the existing rendering pipeline.
119
- const observeNativeVerticalBarsLayout = (doc: Document): (() => void) => {
120
- const reconcile = () => {
121
- const app = doc.querySelector<HTMLElement>('ion-app');
122
- if (!app) return;
123
- const requested = app.classList.contains('ios-theme-vertical-bars') || app.hasAttribute(horizontalFallbackAttribute);
124
- const suspended = requested && nativePlacements.get(app)?.edge == null;
125
- if (app.hasAttribute(horizontalFallbackAttribute) !== suspended) app.toggleAttribute(horizontalFallbackAttribute, suspended);
126
- if (app.classList.contains('ios-theme-vertical-bars') !== (requested && !suspended)) {
127
- app.classList.toggle('ios-theme-vertical-bars', requested && !suspended);
128
- }
129
- };
130
- const observer = new MutationObserver(reconcile);
131
- observer.observe(doc.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
132
- doc.defaultView?.addEventListener('nativeUIShellRefresh', reconcile);
133
- reconcile();
134
- return () => {
135
- observer.disconnect();
136
- doc.defaultView?.removeEventListener('nativeUIShellRefresh', reconcile);
137
- const app = doc.querySelector<HTMLElement>(`ion-app[${horizontalFallbackAttribute}]`);
138
- if (app) {
139
- app.removeAttribute(horizontalFallbackAttribute);
140
- app.classList.add('ios-theme-vertical-bars');
141
- }
142
- };
143
- };
144
-
145
- /**
146
- * Applies one placement to the CSS layout and both Web/native projections.
147
- * Pass `rtl` when the document direction is known; otherwise the nearest `dir` attribute is used.
148
- */
149
- export const setVerticalControlAreaPlacement = (placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean): void => {
150
- if (typeof document === 'undefined') return;
151
- const app = document.querySelector<HTMLElement>('ion-app');
152
- if (!app) throw new Error('Vertical Control Area requires ion-app');
153
- const { edge, inset = 0 } = placement && typeof placement === 'object' ? placement : { edge: placement, inset: 0 };
154
- if (placement && typeof placement === 'object' && placement.nativeEdge !== undefined) {
155
- nativePlacements.set(app, { edge: placement.nativeEdge, rtl });
156
- }
157
- app.removeAttribute(horizontalFallbackAttribute);
158
- app.classList.toggle('ios-theme-vertical-bars', edge !== null);
159
- app.classList.toggle('ios-theme-vertical-bars-left', edge !== null && physicalVerticalBarEdge(edge, rtl ?? elementRtl(app)) === 'left');
160
- if (edge && Number.isFinite(inset) && inset > 0) app.style.setProperty('--ios-theme-vertical-bars-native-inset', `${inset}px`);
161
- else app.style.removeProperty('--ios-theme-vertical-bars-native-inset');
162
- document.defaultView?.dispatchEvent(new Event('nativeUIShellRefresh'));
163
- };
164
-
165
- /**
166
- * Call once after ion-app is mounted. Ionic markup remains the source of truth.
167
- * Native vertical buttons default to SwiftUI appearance.
168
- * See VerticalControlAreaOptions for source styling and local overrides.
169
- */
170
- export const enableVerticalControlArea = async (options: VerticalControlAreaOptions = {}): Promise<VerticalControlAreaHandle> => {
171
- const handle = await enableNativeUIShell({
172
- buttonProjection: options.buttonProjection,
173
- buttonDefaultFill: options.buttonDefaultFill,
174
- controls: { tabs: true, toolbar: true },
175
- verticalBarsOnly: true,
176
- });
177
- return {
178
- getStatus: () => handle.getStatus(),
179
- suspend: () => handle.suspend(),
180
- destroy: () => handle.destroy(),
181
- setPlacement: setVerticalControlAreaPlacement,
182
- };
183
- };
184
-
185
- /** Call once at application startup. Ionic markup remains the source of truth. */
186
- export const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise<NativeUIShellHandle> => {
187
- if (options.enabled === false) {
188
- const current = active;
189
- active = undefined;
190
- activeConfiguration = undefined;
191
- return current
192
- ? current.then(async (handle) => {
193
- await handle.destroy();
194
- return web('Disabled');
195
- })
196
- : Promise.resolve(web('Disabled'));
197
- }
198
- if (typeof document === 'undefined') return Promise.resolve(web('Requires a document'));
199
- const controls = options.controls;
200
- const configuration = JSON.stringify([
201
- options.verticalBarsOnly === true,
202
- options.buttonProjection ?? 'system',
203
- // Local source overrides can use this even when the startup projection is system.
204
- options.buttonDefaultFill ?? null,
205
- ...(['tabs', 'toolbar', 'segment', 'fab'] as const).map((component) => !controls || controls[component] === true),
206
- ]);
207
- if (active && activeConfiguration !== configuration)
208
- return Promise.reject(
209
- new Error('Native UI Shell is already running with different controls; destroy it before changing configuration.'),
210
- );
211
- if (active) return active;
212
- activeConfiguration = configuration;
213
- const stopModals = observeVerticalBarsModals(document);
214
- const prehide =
215
- options.controls === undefined || options.controls.toolbar === true ? prehideVerticalBarsToolbarSources(document) : undefined;
216
- // A slow destroy of a superseded activation must not release a newer one.
217
- let start: Promise<NativeUIShellHandle>;
218
- const release = () => {
219
- if (active === start) {
220
- active = undefined;
221
- activeConfiguration = undefined;
222
- }
223
- };
224
- const fallback = (reason: string) =>
225
- manage(createVerticalBarsWebProjection(document, options), {
226
- reason,
227
- suspend: () => prehide?.suspend(),
228
- destroy: () => {
229
- prehide?.stop();
230
- stopModals();
231
- },
232
- release,
233
- });
234
- return (start = active =
235
- (async () => {
236
- if (Capacitor.getPlatform() !== 'ios') return fallback('Requires Capacitor iOS');
237
- let runtime: NativeUIShellHandle | undefined;
238
- let metricsListener: Awaited<ReturnType<typeof plugin.addListener>> | undefined;
239
- let stopVerticalBarsLayout: (() => void) | undefined;
240
- try {
241
- const capabilities = await plugin.configure({ verticalBarsOnly: options.verticalBarsOnly === true });
242
- if (!capabilities.supported) return fallback('Requires iOS 26 or later');
243
- if (!options.verticalBarsOnly) stopVerticalBarsLayout = observeNativeVerticalBarsLayout(document);
244
- // The application owns device state and selects the rail through placement classes.
245
- const nativeVerticalBars = () => {
246
- const app = document.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');
247
- if (!app) return false;
248
- const reported = nativePlacements.get(app);
249
- if (!reported?.edge) return false;
250
- const physicalEdge = app.classList.contains('ios-theme-vertical-bars-left') ? 'left' : 'right';
251
- return physicalEdge === physicalVerticalBarEdge(reported.edge, reported.rtl ?? elementRtl(app));
252
- };
253
- if (!options.verticalBarsOnly) {
254
- metricsListener = await plugin.addListener('webViewMetricsChange', (metrics) => {
255
- setConfig({ radius: metrics.radius });
256
- });
257
- await configureNativeTransition().catch(() => undefined);
258
- }
259
- const native = await createRuntime(document, plugin, options, nativeVerticalBars, options.verticalBarsOnly === true);
260
- runtime = combine(
261
- native,
262
- createVerticalBarsWebProjection(document, options, () => !nativeVerticalBars() || native.getStatus().state === 'stopped'),
263
- );
264
- return manage(runtime, {
265
- suspend: () => prehide?.suspend(),
266
- release,
267
- destroy: async () => {
268
- await metricsListener?.remove().catch(() => {});
269
- stopVerticalBarsLayout?.();
270
- prehide?.stop();
271
- stopModals();
272
- },
273
- });
274
- } catch (error) {
275
- await runtime?.destroy();
276
- await metricsListener?.remove().catch(() => {});
277
- stopVerticalBarsLayout?.();
278
- return fallback(error instanceof Error ? error.message : String(error));
279
- }
280
- })());
281
- };
@@ -1,265 +0,0 @@
1
- import { modalUsesVerticalBars } from './shared/modal';
2
- import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core/components/index.js';
3
- import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
4
- import {
5
- childElements,
6
- clearVerticalBarsPlacement,
7
- createVerticalBarsPageState,
8
- verticalBarsActionCandidate,
9
- verticalBarsBackCandidate,
10
- verticalBarsBackWebClass,
11
- verticalBarsOwned,
12
- isVerticalBarsToolbarActionShape,
13
- isPermanentlyExcluded,
14
- isShellDisabled,
15
- prehiddenClass,
16
- prehideRootClass,
17
- rejectedClass,
18
- setVerticalBarsPlacement,
19
- syncToolbarText,
20
- } from './shared/dom';
21
-
22
- const overlays = 'ion-menu, ion-popover';
23
- const sourceSelector = 'ion-back-button, ion-menu-button, ion-buttons, ion-button';
24
- // Realm- and teardown-safe: globals such as Element/HTMLElement may be gone
25
- // when a queued mutation microtask or a stale listener still runs.
26
- const isHtmlElement = (node: unknown): node is HTMLElement => !!node && (node as Node).nodeType === 1;
27
- const asElement = (node: Node | EventTarget | null): HTMLElement | undefined => (isHtmlElement(node) ? node : undefined);
28
- const backSupported = (element: HTMLElement): boolean => {
29
- const back = element as HTMLIonBackButtonElement;
30
- return back.icon === undefined && back.color === undefined && !!back.shadowRoot;
31
- };
32
- const eligibleBack = verticalBarsBackCandidate;
33
- const eligible = verticalBarsActionCandidate;
34
-
35
- /** Capture toolbar placement once per routed-page epoch, before Ionic paints its transition. */
36
- export const prehideVerticalBarsToolbarSources = (doc: Document): { suspend: () => () => void; stop: () => void } => {
37
- doc.documentElement.classList.add(prehideRootClass);
38
- const scopes = new Map<HTMLElement, Set<HTMLElement>>();
39
- const owner = new WeakMap<HTMLElement, HTMLElement>();
40
- const pendingBacks = new Map<HTMLElement, { scope: HTMLElement; timer: ReturnType<typeof setTimeout> }>();
41
- const verticalBarsPages = createVerticalBarsPageState();
42
- const listeners = new AbortController();
43
- const root = () => doc.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');
44
- let suspended = 0;
45
- let stopped = false;
46
- const routedPage = (element: HTMLElement) => element.closest<HTMLElement>('.ion-page:not(ion-app, body)');
47
- const release = (scope: HTMLElement) => {
48
- for (const [element, pending] of pendingBacks) {
49
- if (pending.scope !== scope) continue;
50
- clearTimeout(pending.timer);
51
- pendingBacks.delete(element);
52
- }
53
- const owned = scopes.get(scope);
54
- if (!owned) return;
55
- owned.forEach((element) => {
56
- if (owner.get(element) !== scope) return;
57
- owner.delete(element);
58
- element.classList.remove(prehiddenClass);
59
- element.classList.remove(verticalBarsBackWebClass);
60
- clearVerticalBarsPlacement(element);
61
- });
62
- scopes.delete(scope);
63
- };
64
- const capture = (scope: HTMLElement) => {
65
- if (!root()?.contains(scope) || scope.closest(overlays) || !modalUsesVerticalBars(scope) || verticalBarsPages.isDeparted(scope)) return;
66
- const owned = scopes.get(scope) ?? new Set<HTMLElement>();
67
- const place = (element: HTMLElement, rail: boolean) => {
68
- if (owned.has(element)) return;
69
- const previousScope = owner.get(element);
70
- if (previousScope) scopes.get(previousScope)?.delete(element);
71
- owner.set(element, scope);
72
- setVerticalBarsPlacement(element, rail);
73
- if (element.matches('ion-back-button')) element.classList.toggle(verticalBarsBackWebClass, !rail);
74
- owned.add(element);
75
- };
76
- const sources = scope.matches('ion-back-button') ? [scope] : Array.from(scope.querySelectorAll<HTMLElement>(sourceSelector));
77
- sources.forEach((element) => {
78
- // Shell opt-out is temporary; decide initial placement once it is lifted.
79
- if (isShellDisabled(element)) return;
80
- if (!modalUsesVerticalBars(element) || element.closest(overlays) || (scope.matches('.ion-page') && routedPage(element) !== scope))
81
- return;
82
- if (element.matches('ion-buttons')) {
83
- const children = childElements(element);
84
- const actions = children.filter((child) => eligible(child) && isVerticalBarsToolbarActionShape(child));
85
- const group =
86
- eligible(element) &&
87
- actions.length === children.length &&
88
- (actions.length > 1 || (actions.length === 1 && actions[0].matches('ion-menu-button')));
89
- const freshGroup = !owned.has(element);
90
- place(element, group);
91
- children.forEach((child) => {
92
- if (!child.matches('ion-back-button')) place(child, freshGroup && actions.includes(child));
93
- });
94
- } else if (element.matches('ion-back-button') || !element.parentElement?.matches('ion-buttons')) {
95
- if (owned.has(element)) return;
96
- if (
97
- element.matches('ion-back-button') &&
98
- eligibleBack(element) &&
99
- (element as HTMLIonBackButtonElement).icon === undefined &&
100
- (element as HTMLIonBackButtonElement).color === undefined &&
101
- !element.shadowRoot &&
102
- !element.classList.contains('hydrated')
103
- ) {
104
- const previousPending = pendingBacks.get(element);
105
- if (previousPending && previousPending.scope !== scope) {
106
- clearTimeout(previousPending.timer);
107
- pendingBacks.delete(element);
108
- }
109
- if (!pendingBacks.has(element)) {
110
- const timer = setTimeout(() => {
111
- if (pendingBacks.get(element)?.scope !== scope) return;
112
- pendingBacks.delete(element);
113
- // A custom element that never hydrates must remain usable on Web.
114
- place(element, false);
115
- reconcile();
116
- }, 1500);
117
- pendingBacks.set(element, { scope, timer });
118
- }
119
- return;
120
- }
121
- const pending = pendingBacks.get(element);
122
- if (pending) {
123
- clearTimeout(pending.timer);
124
- pendingBacks.delete(element);
125
- }
126
- place(
127
- element,
128
- element.matches('ion-back-button')
129
- ? eligibleBack(element) && backSupported(element)
130
- : eligible(element) && isVerticalBarsToolbarActionShape(element),
131
- );
132
- }
133
- });
134
- scopes.set(scope, owned);
135
- };
136
- const reconcile = () => {
137
- syncToolbarText(doc);
138
- const verticalBars = root();
139
- if (!verticalBars) {
140
- Array.from(scopes.keys()).forEach(release);
141
- return;
142
- }
143
- for (const scope of scopes.keys()) if (!scope.isConnected || !modalUsesVerticalBars(scope)) release(scope);
144
- for (const [element, pending] of pendingBacks) if (element.shadowRoot || element.classList.contains('hydrated')) capture(pending.scope);
145
- // Ionic inserts the destination as invisible before WillEnter. Hide its
146
- // sources in that same mutation microtask; capture new DOM identities on active pages too.
147
- verticalBars.querySelectorAll<HTMLElement>('.ion-page:not(ion-app, body, .ion-page-hidden)').forEach(capture);
148
- // Root toolbars can mount after startup; each new DOM identity is captured once.
149
- verticalBars.querySelectorAll<HTMLElement>(sourceSelector).forEach((element) => {
150
- if (routedPage(element) || element.closest(overlays)) return;
151
- const scope = element.closest<HTMLElement>('ion-toolbar') ?? (element.matches('ion-back-button') ? element : null);
152
- if (scope) capture(scope);
153
- });
154
- for (const owned of scopes.values()) {
155
- for (const element of owned) {
156
- const groupOwnsChildren =
157
- element.parentElement?.matches('ion-buttons') &&
158
- verticalBarsOwned(element.parentElement) &&
159
- childElements(element.parentElement).every((child) => verticalBarsOwned(child));
160
- const hide =
161
- !suspended &&
162
- verticalBarsOwned(element) &&
163
- !groupOwnsChildren &&
164
- element.isConnected &&
165
- !element.closest(`.${rejectedClass}`) &&
166
- !isPermanentlyExcluded(element) &&
167
- !isShellDisabled(element) &&
168
- (!element.matches('ion-back-button') || backSupported(element) || !element.classList.contains('hydrated')) &&
169
- (!element.matches('ion-buttons') || childElements(element).every((child) => verticalBarsOwned(child)));
170
- element.classList.toggle(prehiddenClass, hide);
171
- if (element.matches('ion-back-button')) element.classList.toggle(verticalBarsBackWebClass, !hide);
172
- }
173
- }
174
- };
175
- const onWillEnter = (event: Event) => {
176
- const page = asElement(event.target);
177
- if (page?.matches('.ion-page')) {
178
- verticalBarsPages.lifecycle(event);
179
- release(page); // Cached pages begin a new placement epoch.
180
- capture(page);
181
- reconcile();
182
- }
183
- };
184
- const onPageLifecycle: EventListener = (event) => {
185
- verticalBarsPages.lifecycle(event);
186
- if (event.type !== LIFECYCLE_DID_LEAVE) return;
187
- const page = asElement(event.target);
188
- if (page) release(page);
189
- };
190
- // A canceled transition keeps the leaving page active and abandons the entering one.
191
- const onTransitionCanceled: EventListener = (event) => {
192
- const leaving = asElement(event.target);
193
- if (!leaving) return;
194
- const entering = (event as CustomEvent<{ entering?: HTMLElement }>).detail?.entering;
195
- verticalBarsPages.cancel(entering, leaving);
196
- reconcile();
197
- };
198
- doc.addEventListener(LIFECYCLE_WILL_ENTER, onWillEnter, { capture: true, signal: listeners.signal });
199
- for (const name of [LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE])
200
- doc.addEventListener(name, onPageLifecycle, { capture: true, signal: listeners.signal });
201
- doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, onTransitionCanceled, { capture: true, signal: listeners.signal });
202
- doc.defaultView?.addEventListener('nativeUIShellRefresh', reconcile, { signal: listeners.signal });
203
- reconcile();
204
- const mutationRelevant = (record: MutationRecord) => {
205
- if (record.type === 'characterData') return record.target.parentElement?.matches('ion-toolbar') === true;
206
- if (record.type === 'childList' && asElement(record.target)?.matches('ion-toolbar')) return true;
207
- if (record.type === 'childList')
208
- return [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)].some(
209
- (node) =>
210
- isHtmlElement(node) &&
211
- (node.matches(`${sourceSelector}, .ion-page, ion-toolbar`) || !!node.querySelector(`${sourceSelector}, .ion-page, ion-toolbar`)),
212
- );
213
- if (record.attributeName !== 'class') return true;
214
- const current = (record.target as Element).className;
215
- const previous = record.oldValue ?? '';
216
- const withoutOwned = (value: string) =>
217
- value
218
- .split(/\s+/)
219
- .filter((name) => name && ![prehiddenClass, prehideRootClass, verticalBarsBackWebClass].includes(name))
220
- .join(' ');
221
- return typeof current !== 'string' || withoutOwned(previous) !== withoutOwned(current);
222
- };
223
- let scheduled = false;
224
- const observer = new (doc.defaultView?.MutationObserver ?? MutationObserver)((records) => {
225
- if (scheduled || stopped || !records.some(mutationRelevant)) return;
226
- scheduled = true;
227
- queueMicrotask(() => {
228
- scheduled = false;
229
- if (!stopped) reconcile();
230
- });
231
- });
232
- observer.observe(doc.documentElement, {
233
- subtree: true,
234
- childList: true,
235
- characterData: true,
236
- attributes: true,
237
- attributeOldValue: true,
238
- attributeFilter: ['class', 'data-shell', 'hidden', 'inert', 'icon', 'color'],
239
- });
240
- return {
241
- suspend: () => {
242
- if (stopped) return () => {};
243
- suspended++;
244
- doc.documentElement.classList.remove(prehideRootClass);
245
- reconcile();
246
- let resumed = false;
247
- return () => {
248
- if (resumed || stopped) return;
249
- resumed = true;
250
- if (--suspended === 0) {
251
- doc.documentElement.classList.add(prehideRootClass);
252
- reconcile();
253
- }
254
- };
255
- },
256
- stop: () => {
257
- if (stopped) return;
258
- stopped = true;
259
- observer.disconnect();
260
- listeners.abort();
261
- doc.documentElement.classList.remove(prehideRootClass);
262
- Array.from(scopes.keys()).forEach(release);
263
- },
264
- };
265
- };