@rdlabo/ionic-theme-ios27 0.1.0-beta.pr175.sha00bf1efcf4b7 → 0.1.0-beta.pr177.shaea4b7eeec994

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.
@@ -7,10 +7,24 @@ export interface NativeUIShellStatus {
7
7
  updates: number;
8
8
  reason?: string;
9
9
  }
10
+ export interface NativeUIShellOptions {
11
+ enabled?: boolean;
12
+ controls?: NativeUIShellControls;
13
+ }
14
+ export interface NativeUIShellControls {
15
+ tabs?: boolean;
16
+ toolbar?: boolean;
17
+ segment?: boolean;
18
+ fab?: boolean;
19
+ }
10
20
  export interface NativeUIShellHandle {
11
21
  getStatus(): NativeUIShellStatus;
22
+ suspend(): Promise<NativeUIShellSuspension>;
12
23
  destroy(): Promise<void>;
13
24
  }
25
+ export interface NativeUIShellSuspension {
26
+ resume(): Promise<void>;
27
+ }
14
28
  export interface Frame {
15
29
  x: number;
16
30
  y: number;
@@ -89,10 +103,14 @@ export interface ShellActivation {
89
103
  id: string;
90
104
  sequence: number;
91
105
  }
106
+ export interface WebViewMetrics {
107
+ radius: number;
108
+ }
92
109
  export interface NativeUIShellPlugin {
93
110
  configure(): Promise<{
94
111
  supported: boolean;
95
112
  }>;
113
+ getWebViewMetrics(): Promise<WebViewMetrics>;
96
114
  update(snapshot: ShellSnapshot): Promise<{
97
115
  revision: number;
98
116
  rejectedSearches?: string[];
@@ -103,5 +121,6 @@ export interface NativeUIShellPlugin {
103
121
  }): Promise<void>;
104
122
  addListener(name: 'activate', listener: (event: ShellActivation) => void): Promise<PluginListenerHandle>;
105
123
  addListener(name: 'search', listener: (event: ShellSearchEvent) => void): Promise<PluginListenerHandle>;
124
+ addListener(name: 'webViewMetricsChange', listener: (event: WebViewMetrics) => void): Promise<PluginListenerHandle>;
106
125
  }
107
126
  //# sourceMappingURL=definitions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/native/definitions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/native/definitions.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import type { NativeUIShellHandle } from './definitions';
2
- export type { NativeUIShellComponent, NativeUIShellHandle, NativeUIShellStatus } from './definitions';
3
- export declare const enableNativeUIShell: () => Promise<NativeUIShellHandle>;
1
+ import type { NativeUIShellHandle, NativeUIShellOptions, WebViewMetrics } from './definitions';
2
+ export type { NativeUIShellComponent, NativeUIShellControls, NativeUIShellHandle, NativeUIShellOptions, NativeUIShellStatus, NativeUIShellSuspension, WebViewMetrics, } from './definitions';
3
+ export declare const configureNativeTransition: () => Promise<WebViewMetrics>;
4
+ export declare const enableNativeUIShell: (options?: NativeUIShellOptions) => Promise<NativeUIShellHandle>;
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/native/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAuB,MAAM,eAAe,CAAC;AAE9E,YAAY,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAUtG,eAAO,MAAM,mBAAmB,QAAO,OAAO,CAAC,mBAAmB,CAqBjE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/native/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAuB,cAAc,EAAE,MAAM,eAAe,CAAC;AAGpH,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,GACf,MAAM,eAAe,CAAC;AAWvB,eAAO,MAAM,yBAAyB,QAAa,OAAO,CAAC,cAAc,CAIxE,CAAC;AAGF,eAAO,MAAM,mBAAmB,GAAI,UAAS,oBAAyB,KAAG,OAAO,CAAC,mBAAmB,CA8BnG,CAAC"}
@@ -1,28 +1,41 @@
1
1
  import { Capacitor, registerPlugin } from '@capacitor/core';
2
+ import { setConfig } from '../transition/ios.transition';
3
+ import { bindMetricsLifecycle } from './lifecycle';
2
4
  import { createRuntime } from './runtime';
3
5
  const plugin = registerPlugin('IonicNativeUIShell');
4
6
  let active;
5
7
  const web = (reason) => ({
6
8
  getStatus: () => ({ state: 'web', projected: 0, updates: 0, reason }),
9
+ suspend: async () => ({ resume: async () => { } }),
7
10
  destroy: async () => { },
8
11
  });
9
- export const enableNativeUIShell = () => {
12
+ export const configureNativeTransition = async () => {
13
+ const metrics = typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' ? await plugin.getWebViewMetrics() : { radius: 0 };
14
+ setConfig({ radius: metrics.radius });
15
+ return metrics;
16
+ };
17
+ export const enableNativeUIShell = (options = {}) => {
18
+ if (options.enabled === false) {
19
+ const current = active;
20
+ active = undefined;
21
+ return current
22
+ ? current.then(async (handle) => {
23
+ await handle.destroy();
24
+ return web('Disabled');
25
+ })
26
+ : Promise.resolve(web('Disabled'));
27
+ }
10
28
  if (typeof document === 'undefined' || Capacitor.getPlatform() !== 'ios')
11
29
  return Promise.resolve(web('Requires Capacitor iOS'));
12
30
  return (active ?? (active = (async () => {
13
31
  try {
32
+ await configureNativeTransition().catch(() => undefined);
14
33
  if (!(await plugin.configure()).supported) {
15
34
  active = undefined;
16
35
  return web('Requires iOS 26 or later');
17
36
  }
18
- const runtime = await createRuntime(document, plugin);
19
- return {
20
- getStatus: runtime.getStatus,
21
- async destroy() {
22
- await runtime.destroy();
23
- active = undefined;
24
- },
25
- };
37
+ const runtime = await createRuntime(document, plugin, options);
38
+ return await bindMetricsLifecycle(runtime, () => plugin.addListener('webViewMetricsChange', (metrics) => setConfig({ radius: metrics.radius })), () => (active = undefined));
26
39
  }
27
40
  catch (error) {
28
41
  active = undefined;
@@ -0,0 +1,4 @@
1
+ import type { PluginListenerHandle } from '@capacitor/core';
2
+ import type { NativeUIShellHandle } from './definitions';
3
+ export declare const bindMetricsLifecycle: (runtime: NativeUIShellHandle, listen: () => Promise<PluginListenerHandle>, deactivate: () => void) => Promise<NativeUIShellHandle>;
4
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/native/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,eAAO,MAAM,oBAAoB,GAC/B,SAAS,mBAAmB,EAC5B,QAAQ,MAAM,OAAO,CAAC,oBAAoB,CAAC,EAC3C,YAAY,MAAM,IAAI,KACrB,OAAO,CAAC,mBAAmB,CA8B7B,CAAC"}
@@ -0,0 +1,34 @@
1
+ export const bindMetricsLifecycle = async (runtime, listen, deactivate) => {
2
+ let listener;
3
+ try {
4
+ listener = await listen();
5
+ }
6
+ catch (error) {
7
+ await runtime.destroy();
8
+ throw error;
9
+ }
10
+ let destroying;
11
+ return {
12
+ getStatus: runtime.getStatus,
13
+ suspend: runtime.suspend,
14
+ destroy() {
15
+ return (destroying ?? (destroying = (async () => {
16
+ let removalError;
17
+ try {
18
+ await listener.remove();
19
+ }
20
+ catch (error) {
21
+ removalError = error;
22
+ }
23
+ try {
24
+ await runtime.destroy();
25
+ }
26
+ finally {
27
+ deactivate();
28
+ }
29
+ if (removalError)
30
+ throw removalError;
31
+ })()));
32
+ },
33
+ };
34
+ };
@@ -1,3 +1,3 @@
1
- import type { NativeUIShellHandle, NativeUIShellPlugin } from './definitions';
2
- export declare const createRuntime: (doc: Document, plugin: NativeUIShellPlugin) => Promise<NativeUIShellHandle>;
1
+ import type { NativeUIShellHandle, NativeUIShellOptions, NativeUIShellPlugin } from './definitions';
2
+ export declare const createRuntime: (doc: Document, plugin: NativeUIShellPlugin, options?: NativeUIShellOptions) => Promise<NativeUIShellHandle>;
3
3
  //# sourceMappingURL=runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/native/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAkC,mBAAmB,EAAE,mBAAmB,EAAuB,MAAM,eAAe,CAAC;AAkBnI,eAAO,MAAM,aAAa,GAAU,KAAK,QAAQ,EAAE,QAAQ,mBAAmB,KAAG,OAAO,CAAC,mBAAmB,CA6c3G,CAAC"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/native/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EAEpB,MAAM,eAAe,CAAC;AAkBvB,eAAO,MAAM,aAAa,GACxB,KAAK,QAAQ,EACb,QAAQ,mBAAmB,EAC3B,UAAS,oBAAyB,KACjC,OAAO,CAAC,mBAAmB,CA+e7B,CAAC"}
@@ -12,7 +12,7 @@ const bounded = (promise) => new Promise((resolve, reject) => {
12
12
  const timer = setTimeout(() => reject(new Error('Native UI Shell bridge timed out')), 5000);
13
13
  promise.then(resolve, reject).finally(() => clearTimeout(timer));
14
14
  });
15
- export const createRuntime = async (doc, plugin) => {
15
+ export const createRuntime = async (doc, plugin, options = {}) => {
16
16
  const win = doc.defaultView;
17
17
  const icons = createIconRenderer();
18
18
  const crossfade = createCrossfade(win);
@@ -22,6 +22,7 @@ export const createRuntime = async (doc, plugin) => {
22
22
  const suspended = new Set();
23
23
  const pages = new Set();
24
24
  const presented = new Set();
25
+ const manualSuspensions = new Set();
25
26
  const moving = new Map();
26
27
  const observed = new Set();
27
28
  const listeners = new AbortController();
@@ -44,6 +45,19 @@ export const createRuntime = async (doc, plugin) => {
44
45
  let listener;
45
46
  let searchListener;
46
47
  let waiters = [];
48
+ const control = (kind) => {
49
+ if (kind === 'ion-tab-bar')
50
+ return 'tabs';
51
+ if (kind === 'ion-segment')
52
+ return 'segment';
53
+ if (kind === 'ion-fab')
54
+ return 'fab';
55
+ return 'toolbar';
56
+ };
57
+ const controlEnabled = (candidate) => {
58
+ const controls = options.controls;
59
+ return controls === undefined || controls[control(candidate.control.kind)] === true;
60
+ };
47
61
  const id = (element) => {
48
62
  let value = ids.get(element);
49
63
  if (!value) {
@@ -87,6 +101,10 @@ export const createRuntime = async (doc, plugin) => {
87
101
  };
88
102
  const search = createSearchSupport(doc, id, schedule);
89
103
  const candidateSources = (candidate) => candidate.sources ?? [candidate.element];
104
+ const readEnabledCandidate = (element) => {
105
+ const candidate = readCandidate(element, id);
106
+ return candidate && controlEnabled(candidate) ? candidate : undefined;
107
+ };
90
108
  const flush = async () => {
91
109
  if (stopped)
92
110
  return;
@@ -106,7 +124,8 @@ export const createRuntime = async (doc, plugin) => {
106
124
  for (const element of presented)
107
125
  if (!element.isConnected)
108
126
  presented.delete(element);
109
- return (presented.size > 0 ||
127
+ return (manualSuspensions.size > 0 ||
128
+ presented.size > 0 ||
110
129
  Array.from(doc.querySelectorAll(overlays)).some((element) => element.presented || element.classList.contains('show-menu')));
111
130
  };
112
131
  const signature = (candidate) => JSON.stringify({ control: candidate.control, icons: candidate.icons.map(({ source }) => source) });
@@ -123,7 +142,7 @@ export const createRuntime = async (doc, plugin) => {
123
142
  return unprojected(sources.keys(), () => search
124
143
  .decorate(Array.from(doc.querySelectorAll(selector))
125
144
  .filter((element) => !blocked(element))
126
- .map((element) => readCandidate(element, id))
145
+ .map(readEnabledCandidate)
127
146
  .filter((candidate) => !!candidate), blocked)
128
147
  .filter((candidate) => !rejected.has(candidate.element) || rejected.get(candidate.element) !== signature(candidate)));
129
148
  };
@@ -366,6 +385,25 @@ export const createRuntime = async (doc, plugin) => {
366
385
  updates,
367
386
  reason,
368
387
  }),
388
+ async suspend() {
389
+ const token = Symbol();
390
+ let resumed = false;
391
+ if (!stopped) {
392
+ manualSuspensions.add(token);
393
+ getNativeSearchBindings(doc).forEach((binding) => search.retire(binding));
394
+ await flush();
395
+ }
396
+ return {
397
+ async resume() {
398
+ if (resumed)
399
+ return;
400
+ resumed = true;
401
+ if (stopped || !manualSuspensions.delete(token))
402
+ return;
403
+ await flush();
404
+ },
405
+ };
406
+ },
369
407
  async destroy() {
370
408
  if (stopped)
371
409
  return;
@@ -392,6 +430,7 @@ export const createRuntime = async (doc, plugin) => {
392
430
  icons.clear();
393
431
  pages.clear();
394
432
  presented.clear();
433
+ manualSuspensions.clear();
395
434
  moving.clear();
396
435
  suspended.clear();
397
436
  await listener?.remove().catch(() => { });
@@ -418,7 +457,7 @@ export const createRuntime = async (doc, plugin) => {
418
457
  const owner = Array.from(sources.keys()).find((source) => source === element || source.contains(element));
419
458
  if (!owner)
420
459
  return;
421
- const direct = !blocked(owner) && unprojected(sources.keys(), () => readCandidate(owner, id));
460
+ const direct = !blocked(owner) && unprojected(sources.keys(), () => readEnabledCandidate(owner));
422
461
  const candidate = direct || read().find((candidate) => candidate.actions.has(event.id));
423
462
  const item = candidate?.control.items.find((item) => item.id === event.id);
424
463
  const searchAction = candidate?.control.search && [candidate.control.search.trigger.id, candidate.control.search.closeId].includes(event.id);
@@ -24,6 +24,14 @@ import { enableNativeUIShell } from '@rdlabo/ionic-theme-ios27/native';
24
24
  void enableNativeUIShell();
25
25
  ```
26
26
 
27
+ `enableNativeUIShell()` also reads the WebView's effective top-left corner radius and applies it to page transitions. To configure only the transition without enabling native controls, call:
28
+
29
+ ```ts
30
+ import { configureNativeTransition } from '@rdlabo/ionic-theme-ios27/native';
31
+
32
+ await configureNativeTransition();
33
+ ```
34
+
27
35
  Keep the existing `navAnimation: iosTransitionAnimation` setting. No per-page registration, component list, native callback, or Swift view controller is required. Run `npx cap sync ios` after installing or updating the package. The native plugin uses Swift Package Manager (SPM). For an existing CocoaPods app, run `npx cap spm-migration-assistant` and link the generated `CapApp-SPM` package to the app target in Xcode. Build with Xcode 26 or later and Capacitor 8; native glass requires iOS 26 or later. Web, Android, SSR and older iOS keep the Web implementation.
28
36
 
29
37
  This is an opt-in feature. The ordinary package entry point does not import Capacitor, and `@capacitor/core` is an optional peer dependency. Native sources are still detected and built by Capacitor's sync when this package is installed in a Capacitor project, even if the application does not call `enableNativeUIShell()`.
@@ -122,8 +130,113 @@ console.log(shell.getStatus()); // state, projected control count, update count,
122
130
  await shell.destroy(); // restore DOM, remove native controls and release listeners/cache
123
131
  ```
124
132
 
133
+ Projection is globally enabled by default. Limit it to selected Ionic components when an application only wants part of the native shell, or disable it globally while retaining the same configuration path:
134
+
135
+ ```ts
136
+ const shell = await enableNativeUIShell({
137
+ enabled: true,
138
+ controls: {
139
+ tabs: true,
140
+ },
141
+ });
142
+
143
+ // Equivalent to leaving Native UI Shell off; all controls remain on the Web.
144
+ const disabledShell = await enableNativeUIShell({ enabled: false });
145
+ ```
146
+
147
+ Omitting `controls` enables every supported control for backward compatibility. When `controls` is present, only entries set to `true` are native-eligible. Available entries are `tabs`, `toolbar`, `segment`, and `fab`.
148
+
149
+ For a custom modal or overlay that Native UI Shell cannot detect, acquire a suspension before presenting it. The resolved suspension means projected controls have returned to Web rendering. Always release it after dismissal:
150
+
151
+ ```ts
152
+ const suspension = await shell.suspend();
153
+
154
+ try {
155
+ await modal.present();
156
+ await modal.onDidDismiss();
157
+ } finally {
158
+ await suspension.resume();
159
+ }
160
+ ```
161
+
162
+ Suspensions are nestable and `resume()` is idempotent. Native projection resumes only after every active suspension has been released, using the current DOM rather than a stale snapshot.
163
+
125
164
  The native material and control appearance follow the running iOS version; an iOS 26 device does not acquire iOS 27's appearance merely by installing this theme.
126
165
 
166
+ ## Native UI Shell API
167
+
168
+ The generated reference below documents the handle returned by `enableNativeUIShell()`. The underlying Capacitor bridge and its control-snapshot protocol are implementation details.
169
+
170
+ <docgen-index>
171
+
172
+ * [`getStatus()`](#getstatus)
173
+ * [`suspend()`](#suspend)
174
+ * [`destroy()`](#destroy)
175
+ * [Interfaces](#interfaces)
176
+
177
+ </docgen-index>
178
+
179
+ <docgen-api>
180
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
181
+
182
+ ### getStatus()
183
+
184
+ ```typescript
185
+ getStatus() => NativeUIShellStatus
186
+ ```
187
+
188
+ Returns the current Web/native projection state.
189
+
190
+ **Returns:** <code><a href="#nativeuishellstatus">NativeUIShellStatus</a></code>
191
+
192
+ --------------------
193
+
194
+
195
+ ### suspend()
196
+
197
+ ```typescript
198
+ suspend() => Promise<NativeUIShellSuspension>
199
+ ```
200
+
201
+ Restores projected controls to the Web until the returned lease is resumed.
202
+
203
+ **Returns:** <code>Promise&lt;<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>&gt;</code>
204
+
205
+ --------------------
206
+
207
+
208
+ ### destroy()
209
+
210
+ ```typescript
211
+ destroy() => Promise<void>
212
+ ```
213
+
214
+ Stops synchronization, restores Web controls and releases native resources.
215
+
216
+ --------------------
217
+
218
+
219
+ ### Interfaces
220
+
221
+
222
+ #### NativeUIShellStatus
223
+
224
+ | Prop | Type |
225
+ | --------------- | ------------------------------------------- |
226
+ | **`state`** | <code>'native' \| 'stopped' \| 'web'</code> |
227
+ | **`projected`** | <code>number</code> |
228
+ | **`updates`** | <code>number</code> |
229
+ | **`reason`** | <code>string</code> |
230
+
231
+
232
+ #### NativeUIShellSuspension
233
+
234
+ | Method | Signature | Description |
235
+ | ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
236
+ | **resume** | () =&gt; Promise&lt;void&gt; | Releases this suspension. Native projection resumes after all active suspensions are released. |
237
+
238
+ </docgen-api>
239
+
127
240
  ## Source layout
128
241
 
129
242
  Each TypeScript module in [`src/native/components`](../src/native/components) declares its Ionic tag and DOM reader. `components/index.ts` combines those exports into discovery selectors and the component type. Shared DOM measurements, item data and SVG rendering live in `src/native/shared`; `runtime.ts` owns synchronization, visibility handoffs and lifecycle events.
@@ -8,6 +8,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
8
8
  public let jsName = "IonicNativeUIShell"
9
9
  public let pluginMethods: [CAPPluginMethod] = [
10
10
  CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "getWebViewMetrics", returnType: CAPPluginReturnPromise),
11
12
  CAPPluginMethod(name: "update", returnType: CAPPluginReturnPromise),
12
13
  CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise)
13
14
  ]
@@ -45,6 +46,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
45
46
  }
46
47
  }
47
48
  if keyboard { self.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh") }
49
+ else if name == UIDevice.orientationDidChangeNotification { self.notifyWebViewMetricsChange() }
48
50
  })
49
51
  }
50
52
  for name in [UIApplication.didBecomeActiveNotification, UIResponder.keyboardDidHideNotification,
@@ -52,6 +54,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
52
54
  observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
53
55
  if name == UIResponder.keyboardDidHideNotification { self?.keyboardVisible = false }
54
56
  self?.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh", data: name == UIApplication.didBecomeActiveNotification ? "{\"retireSearch\":true}" : "{}")
57
+ if name == UIApplication.didBecomeActiveNotification { self?.notifyWebViewMetricsChange() }
55
58
  })
56
59
  }
57
60
  }
@@ -60,6 +63,31 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
60
63
  observers.forEach(NotificationCenter.default.removeObserver)
61
64
  }
62
65
 
66
+ private func webViewMetrics() -> JSObject? {
67
+ guard #available(iOS 26.0, *), let webView = bridge?.webView else { return nil }
68
+ webView.layoutIfNeeded()
69
+ return ["radius": Double(webView.effectiveRadius(corner: .topLeft))]
70
+ }
71
+
72
+ private func notifyWebViewMetricsChange() {
73
+ guard let metrics = webViewMetrics() else { return }
74
+ notifyListeners("webViewMetricsChange", data: metrics)
75
+ }
76
+
77
+ @objc func getWebViewMetrics(_ call: CAPPluginCall) {
78
+ DispatchQueue.main.async { [weak self] in
79
+ guard #available(iOS 26.0, *) else {
80
+ call.resolve(["radius": 0])
81
+ return
82
+ }
83
+ guard let metrics = self?.webViewMetrics() else {
84
+ call.reject("WebView unavailable")
85
+ return
86
+ }
87
+ call.resolve(metrics)
88
+ }
89
+ }
90
+
63
91
  @objc func configure(_ call: CAPPluginCall) {
64
92
  DispatchQueue.main.async { [weak self] in
65
93
  // A new JS context starts revision numbering again (live reload / navigation).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rdlabo/ionic-theme-ios27",
3
3
  "private": false,
4
- "version": "0.1.0-beta.pr175.sha00bf1efcf4b7",
4
+ "version": "0.1.0-beta.pr177.shaea4b7eeec994",
5
5
  "description": "iOS27 Theme for Ionic Framework",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -27,9 +27,10 @@
27
27
  "Package.swift"
28
28
  ],
29
29
  "scripts": {
30
- "build": "npm run build:css && npm run build:ts",
30
+ "build": "npm run docgen && npm run build:css && npm run build:ts",
31
31
  "build:css": "rm -rf dist/css && sass src/styles:dist/css --style=compressed --no-source-map --pkg-importer=node",
32
32
  "build:ts": "tsc",
33
+ "docgen": "docgen --project tsconfig.docgen.json --api NativeUIShellHandle --output-readme docs/native-ui-shell.md",
33
34
  "build:demo": "npm run build && cd demo && npm install && npm run build -- --configuration=production",
34
35
  "lint": "prettier --check \"./**/*.{scss,ts}\" && prettier --parser angular --check \"./**/*.html\"",
35
36
  "fmt": "prettier --write \"./**/*.{scss,ts}\" && prettier --parser angular --write \"./**/*.html\"",
@@ -59,6 +60,7 @@
59
60
  "devDependencies": {
60
61
  "@capacitor/core": "^8.5.2",
61
62
  "@ionic/angular": "^9.0.0",
63
+ "@rdlabo/capacitor-docgen": "^0.4.1",
62
64
  "husky": "^9.1.7",
63
65
  "lint-staged": "^16.2.1",
64
66
  "np": "^10.2.0",
@@ -10,11 +10,38 @@ export interface NativeUIShellStatus {
10
10
  reason?: string;
11
11
  }
12
12
 
13
+ export interface NativeUIShellOptions {
14
+ /** Enables Native UI Shell globally. Defaults to `true`. */
15
+ enabled?: boolean;
16
+ /** Controls eligible for native projection. Omit to enable every control; when present, only `true` controls are enabled. */
17
+ controls?: NativeUIShellControls;
18
+ }
19
+
20
+ export interface NativeUIShellControls {
21
+ /** Projects tab bars and their native search presentation. */
22
+ tabs?: boolean;
23
+ /** Projects toolbar buttons, including back and menu buttons. */
24
+ toolbar?: boolean;
25
+ /** Projects segments. */
26
+ segment?: boolean;
27
+ /** Projects floating action buttons. */
28
+ fab?: boolean;
29
+ }
30
+
13
31
  export interface NativeUIShellHandle {
32
+ /** Returns the current Web/native projection state. */
14
33
  getStatus(): NativeUIShellStatus;
34
+ /** Restores projected controls to the Web until the returned lease is resumed. */
35
+ suspend(): Promise<NativeUIShellSuspension>;
36
+ /** Stops synchronization, restores Web controls and releases native resources. */
15
37
  destroy(): Promise<void>;
16
38
  }
17
39
 
40
+ export interface NativeUIShellSuspension {
41
+ /** Releases this suspension. Native projection resumes after all active suspensions are released. */
42
+ resume(): Promise<void>;
43
+ }
44
+
18
45
  export interface Frame {
19
46
  x: number;
20
47
  y: number;
@@ -98,10 +125,17 @@ export interface ShellActivation {
98
125
  sequence: number;
99
126
  }
100
127
 
128
+ export interface WebViewMetrics {
129
+ /** The WebView's effective top-left corner radius in points, or `0` when unavailable. */
130
+ radius: number;
131
+ }
132
+
101
133
  export interface NativeUIShellPlugin {
102
134
  configure(): Promise<{ supported: boolean }>;
135
+ getWebViewMetrics(): Promise<WebViewMetrics>;
103
136
  update(snapshot: ShellSnapshot): Promise<{ revision: number; rejectedSearches?: string[]; rejectedControls?: string[] }>;
104
137
  clear(options: { revision: number }): Promise<void>;
105
138
  addListener(name: 'activate', listener: (event: ShellActivation) => void): Promise<PluginListenerHandle>;
106
139
  addListener(name: 'search', listener: (event: ShellSearchEvent) => void): Promise<PluginListenerHandle>;
140
+ addListener(name: 'webViewMetricsChange', listener: (event: WebViewMetrics) => void): Promise<PluginListenerHandle>;
107
141
  }
@@ -1,32 +1,59 @@
1
1
  import { Capacitor, registerPlugin } from '@capacitor/core';
2
- import type { NativeUIShellHandle, NativeUIShellPlugin } from './definitions';
2
+ import { setConfig } from '../transition/ios.transition';
3
+ import type { NativeUIShellHandle, NativeUIShellOptions, NativeUIShellPlugin, WebViewMetrics } from './definitions';
4
+ import { bindMetricsLifecycle } from './lifecycle';
3
5
  import { createRuntime } from './runtime';
4
- export type { NativeUIShellComponent, NativeUIShellHandle, NativeUIShellStatus } from './definitions';
6
+ export type {
7
+ NativeUIShellComponent,
8
+ NativeUIShellControls,
9
+ NativeUIShellHandle,
10
+ NativeUIShellOptions,
11
+ NativeUIShellStatus,
12
+ NativeUIShellSuspension,
13
+ WebViewMetrics,
14
+ } from './definitions';
5
15
 
6
16
  const plugin = registerPlugin<NativeUIShellPlugin>('IonicNativeUIShell');
7
17
  let active: Promise<NativeUIShellHandle> | undefined;
8
18
  const web = (reason: string): NativeUIShellHandle => ({
9
19
  getStatus: () => ({ state: 'web', projected: 0, updates: 0, reason }),
20
+ suspend: async () => ({ resume: async () => {} }),
10
21
  destroy: async () => {},
11
22
  });
12
23
 
24
+ /** Reads the current native WebView geometry and applies it to page transitions. */
25
+ export const configureNativeTransition = async (): Promise<WebViewMetrics> => {
26
+ const metrics = typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' ? await plugin.getWebViewMetrics() : { radius: 0 };
27
+ setConfig({ radius: metrics.radius });
28
+ return metrics;
29
+ };
30
+
13
31
  /** Call once at application startup. Ionic markup remains the source of truth. */
14
- export const enableNativeUIShell = (): Promise<NativeUIShellHandle> => {
32
+ export const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise<NativeUIShellHandle> => {
33
+ if (options.enabled === false) {
34
+ const current = active;
35
+ active = undefined;
36
+ return current
37
+ ? current.then(async (handle) => {
38
+ await handle.destroy();
39
+ return web('Disabled');
40
+ })
41
+ : Promise.resolve(web('Disabled'));
42
+ }
15
43
  if (typeof document === 'undefined' || Capacitor.getPlatform() !== 'ios') return Promise.resolve(web('Requires Capacitor iOS'));
16
44
  return (active ??= (async () => {
17
45
  try {
46
+ await configureNativeTransition().catch(() => undefined);
18
47
  if (!(await plugin.configure()).supported) {
19
48
  active = undefined;
20
49
  return web('Requires iOS 26 or later');
21
50
  }
22
- const runtime = await createRuntime(document, plugin);
23
- return {
24
- getStatus: runtime.getStatus,
25
- async destroy() {
26
- await runtime.destroy();
27
- active = undefined;
28
- },
29
- };
51
+ const runtime = await createRuntime(document, plugin, options);
52
+ return await bindMetricsLifecycle(
53
+ runtime,
54
+ () => plugin.addListener('webViewMetricsChange', (metrics) => setConfig({ radius: metrics.radius })),
55
+ () => (active = undefined),
56
+ );
30
57
  } catch (error) {
31
58
  active = undefined;
32
59
  return web(error instanceof Error ? error.message : String(error));
@@ -0,0 +1,38 @@
1
+ import type { PluginListenerHandle } from '@capacitor/core';
2
+ import type { NativeUIShellHandle } from './definitions';
3
+
4
+ export const bindMetricsLifecycle = async (
5
+ runtime: NativeUIShellHandle,
6
+ listen: () => Promise<PluginListenerHandle>,
7
+ deactivate: () => void,
8
+ ): Promise<NativeUIShellHandle> => {
9
+ let listener: PluginListenerHandle;
10
+ try {
11
+ listener = await listen();
12
+ } catch (error) {
13
+ await runtime.destroy();
14
+ throw error;
15
+ }
16
+
17
+ let destroying: Promise<void> | undefined;
18
+ return {
19
+ getStatus: runtime.getStatus,
20
+ suspend: runtime.suspend,
21
+ destroy() {
22
+ return (destroying ??= (async () => {
23
+ let removalError: unknown;
24
+ try {
25
+ await listener.remove();
26
+ } catch (error) {
27
+ removalError = error;
28
+ }
29
+ try {
30
+ await runtime.destroy();
31
+ } finally {
32
+ deactivate();
33
+ }
34
+ if (removalError) throw removalError;
35
+ })());
36
+ },
37
+ };
38
+ };
@@ -2,7 +2,14 @@ import type { PluginListenerHandle } from '@capacitor/core';
2
2
  import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from '@ionic/core';
3
3
  import { getNativeSearchBindings, setNativeUIShellIntegration } from '../native-integration';
4
4
  import { createSearchSupport } from './components/searchable-tabs';
5
- import type { ShellActivation, ShellSnapshot, NativeUIShellHandle, NativeUIShellPlugin, NativeUIShellStatus } from './definitions';
5
+ import type {
6
+ ShellActivation,
7
+ ShellSnapshot,
8
+ NativeUIShellHandle,
9
+ NativeUIShellOptions,
10
+ NativeUIShellPlugin,
11
+ NativeUIShellStatus,
12
+ } from './definitions';
6
13
  import { readCandidate, selector, shadowSelector, motionSelector } from './components';
7
14
  import { marker, unprojected } from './shared/dom';
8
15
  import { createIconRenderer } from './shared/icons';
@@ -20,7 +27,11 @@ const bounded = <T>(promise: Promise<T>): Promise<T> =>
20
27
  promise.then(resolve, reject).finally(() => clearTimeout(timer));
21
28
  });
22
29
 
23
- export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin): Promise<NativeUIShellHandle> => {
30
+ export const createRuntime = async (
31
+ doc: Document,
32
+ plugin: NativeUIShellPlugin,
33
+ options: NativeUIShellOptions = {},
34
+ ): Promise<NativeUIShellHandle> => {
24
35
  const win = doc.defaultView!;
25
36
  const icons = createIconRenderer();
26
37
  const crossfade = createCrossfade(win);
@@ -30,6 +41,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
30
41
  const suspended = new Set<HTMLElement[]>();
31
42
  const pages = new Set<HTMLElement>();
32
43
  const presented = new Set<HTMLElement>();
44
+ const manualSuspensions = new Set<symbol>();
33
45
  const moving = new Map<HTMLElement, Set<string>>();
34
46
  const observed = new Set<Element | ShadowRoot>();
35
47
  const listeners = new AbortController();
@@ -52,6 +64,16 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
52
64
  let listener: PluginListenerHandle | undefined;
53
65
  let searchListener: PluginListenerHandle | undefined;
54
66
  let waiters: (() => void)[] = [];
67
+ const control = (kind: Candidate['control']['kind']): keyof NonNullable<NativeUIShellOptions['controls']> => {
68
+ if (kind === 'ion-tab-bar') return 'tabs';
69
+ if (kind === 'ion-segment') return 'segment';
70
+ if (kind === 'ion-fab') return 'fab';
71
+ return 'toolbar';
72
+ };
73
+ const controlEnabled = (candidate: Candidate) => {
74
+ const controls = options.controls;
75
+ return controls === undefined || controls[control(candidate.control.kind)] === true;
76
+ };
55
77
  const id = (element: Element) => {
56
78
  let value = ids.get(element);
57
79
  if (!value) {
@@ -91,6 +113,10 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
91
113
  };
92
114
  const search = createSearchSupport(doc, id, schedule);
93
115
  const candidateSources = (candidate: Candidate) => candidate.sources ?? [candidate.element];
116
+ const readEnabledCandidate = (element: HTMLElement): Candidate | undefined => {
117
+ const candidate = readCandidate(element, id);
118
+ return candidate && controlEnabled(candidate) ? candidate : undefined;
119
+ };
94
120
  const flush = async () => {
95
121
  if (stopped) return;
96
122
  schedule();
@@ -108,6 +134,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
108
134
  const overlayOpen = () => {
109
135
  for (const element of presented) if (!element.isConnected) presented.delete(element);
110
136
  return (
137
+ manualSuspensions.size > 0 ||
111
138
  presented.size > 0 ||
112
139
  Array.from(doc.querySelectorAll(overlays)).some(
113
140
  (element) => (element as Element & { presented?: boolean }).presented || element.classList.contains('show-menu'),
@@ -128,7 +155,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
128
155
  .decorate(
129
156
  Array.from(doc.querySelectorAll<HTMLElement>(selector))
130
157
  .filter((element) => !blocked(element))
131
- .map((element) => readCandidate(element, id))
158
+ .map(readEnabledCandidate)
132
159
  .filter((candidate): candidate is Candidate => !!candidate),
133
160
  blocked,
134
161
  )
@@ -375,6 +402,23 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
375
402
  updates,
376
403
  reason,
377
404
  }),
405
+ async suspend() {
406
+ const token = Symbol();
407
+ let resumed = false;
408
+ if (!stopped) {
409
+ manualSuspensions.add(token);
410
+ getNativeSearchBindings(doc).forEach((binding) => search.retire(binding));
411
+ await flush();
412
+ }
413
+ return {
414
+ async resume() {
415
+ if (resumed) return;
416
+ resumed = true;
417
+ if (stopped || !manualSuspensions.delete(token)) return;
418
+ await flush();
419
+ },
420
+ };
421
+ },
378
422
  async destroy() {
379
423
  if (stopped) return;
380
424
  stopped = true;
@@ -399,6 +443,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
399
443
  icons.clear();
400
444
  pages.clear();
401
445
  presented.clear();
446
+ manualSuspensions.clear();
402
447
  moving.clear();
403
448
  suspended.clear();
404
449
  await listener?.remove().catch(() => {});
@@ -427,7 +472,7 @@ export const createRuntime = async (doc: Document, plugin: NativeUIShellPlugin):
427
472
  if (!element) return;
428
473
  const owner = Array.from(sources.keys()).find((source) => source === element || source.contains(element));
429
474
  if (!owner) return;
430
- const direct = !blocked(owner) && unprojected(sources.keys(), () => readCandidate(owner, id));
475
+ const direct = !blocked(owner) && unprojected(sources.keys(), () => readEnabledCandidate(owner));
431
476
  const candidate = direct || read().find((candidate) => candidate.actions.has(event.id));
432
477
  const item = candidate?.control.items.find((item) => item.id === event.id);
433
478
  const searchAction =