@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.
- package/dist/native/definitions.d.ts +19 -0
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/index.d.ts +4 -3
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +22 -9
- package/dist/native/lifecycle.d.ts +4 -0
- package/dist/native/lifecycle.d.ts.map +1 -0
- package/dist/native/lifecycle.js +34 -0
- package/dist/native/runtime.d.ts +2 -2
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +43 -4
- package/docs/native-ui-shell.md +113 -0
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +28 -0
- package/package.json +4 -2
- package/src/native/definitions.ts +34 -0
- package/src/native/index.ts +38 -11
- package/src/native/lifecycle.ts +38 -0
- package/src/native/runtime.ts +49 -4
|
@@ -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":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAC3D,YAAY,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,mBAAmB;
|
|
1
|
+
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/native/definitions.ts"],"names":[],"mappings":"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"}
|
package/dist/native/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { NativeUIShellHandle } from './definitions';
|
|
2
|
-
export type { NativeUIShellComponent, NativeUIShellHandle, NativeUIShellStatus } from './definitions';
|
|
3
|
-
export declare const
|
|
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":"
|
|
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"}
|
package/dist/native/index.js
CHANGED
|
@@ -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
|
|
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
|
+
};
|
package/dist/native/runtime.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/native/runtime.js
CHANGED
|
@@ -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 (
|
|
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(
|
|
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(), () =>
|
|
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);
|
package/docs/native-ui-shell.md
CHANGED
|
@@ -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<<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>></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** | () => Promise<void> | 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.
|
|
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
|
}
|
package/src/native/index.ts
CHANGED
|
@@ -1,32 +1,59 @@
|
|
|
1
1
|
import { Capacitor, registerPlugin } from '@capacitor/core';
|
|
2
|
-
import
|
|
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 {
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
};
|
package/src/native/runtime.ts
CHANGED
|
@@ -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 {
|
|
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 (
|
|
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(
|
|
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(), () =>
|
|
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 =
|