@rdlabo/ionic-theme-ios27 1.1.0 → 1.2.0-0
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/README.md +8 -1
- package/dist/css/components/ion-list.css +1 -1
- package/dist/css/components/ion-menu.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/vertical-bars.css +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -8
- package/dist/native/components/index.d.ts +1 -0
- package/dist/native/components/index.d.ts.map +1 -1
- package/dist/native/components/index.js +8 -3
- package/dist/native/components/ion-back-button.d.ts.map +1 -1
- package/dist/native/components/ion-back-button.js +5 -2
- package/dist/native/components/ion-button.d.ts.map +1 -1
- package/dist/native/components/ion-button.js +10 -2
- package/dist/native/components/ion-buttons.d.ts.map +1 -1
- package/dist/native/components/ion-buttons.js +12 -4
- package/dist/native/components/ion-fab.js +4 -4
- package/dist/native/components/ion-menu-button.d.ts.map +1 -1
- package/dist/native/components/ion-menu-button.js +5 -4
- package/dist/native/components/searchable-tabs.d.ts.map +1 -1
- package/dist/native/components/searchable-tabs.js +4 -1
- package/dist/native/definitions.d.ts +29 -3
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/definitions.js +6 -1
- package/dist/native/index.d.ts +6 -2
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +154 -17
- package/dist/native/prehide.d.ts +5 -0
- package/dist/native/prehide.d.ts.map +1 -0
- package/dist/native/prehide.js +246 -0
- package/dist/native/runtime.d.ts +1 -1
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +162 -28
- package/dist/native/shared/candidate.d.ts.map +1 -1
- package/dist/native/shared/candidate.js +7 -3
- package/dist/native/shared/dom.d.ts +33 -2
- package/dist/native/shared/dom.d.ts.map +1 -1
- package/dist/native/shared/dom.js +171 -10
- package/dist/native/vertical-bars-web.d.ts +3 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -0
- package/dist/native/vertical-bars-web.js +341 -0
- package/dist/native-integration/index.d.ts +3 -2
- package/dist/native-integration/index.d.ts.map +1 -1
- package/dist/native-integration/index.js +11 -5
- package/dist/transition/index.d.ts +2 -2
- package/dist/transition/index.d.ts.map +1 -1
- package/dist/transition/index.js +1 -1
- package/dist/transition/ios.transition.d.ts.map +1 -1
- package/dist/transition/ios.transition.js +1 -0
- package/dist/utils.d.ts +3 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/vertical-bars.d.ts +4 -0
- package/dist/vertical-bars.d.ts.map +1 -0
- package/dist/vertical-bars.js +2 -0
- package/docs/iphone-duo.md +255 -0
- package/docs/migration.md +2 -2
- package/docs/native-ui-shell.md +15 -3
- package/docs/special-markup.md +6 -0
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +15 -1
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +358 -0
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +215 -35
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +10 -1
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +29 -0
- package/package.json +6 -2
- package/src/index.ts +15 -8
- package/src/native/components/index.ts +12 -3
- package/src/native/components/ion-back-button.ts +8 -2
- package/src/native/components/ion-button.ts +12 -2
- package/src/native/components/ion-buttons.ts +9 -4
- package/src/native/components/ion-fab.ts +4 -4
- package/src/native/components/ion-menu-button.ts +5 -4
- package/src/native/components/searchable-tabs.ts +9 -1
- package/src/native/definitions.ts +40 -3
- package/src/native/index.ts +188 -20
- package/src/native/prehide.ts +255 -0
- package/src/native/runtime.ts +173 -27
- package/src/native/shared/candidate.ts +11 -3
- package/src/native/shared/dom.ts +212 -11
- package/src/native/vertical-bars-web.ts +374 -0
- package/src/native-integration/index.ts +13 -8
- package/src/styles/components/ion-list.scss +2 -2
- package/src/styles/components/ion-menu.scss +2 -2
- package/src/styles/ionic-theme-ios27.scss +1 -0
- package/src/styles/vertical-bars.scss +342 -0
- package/src/transition/index.ts +11 -6
- package/src/transition/ios.transition.ts +1 -0
- package/src/utils.ts +8 -8
- package/src/vertical-bars.ts +3 -0
- package/dist/native/lifecycle.d.ts +0 -4
- package/dist/native/lifecycle.d.ts.map +0 -1
- package/dist/native/lifecycle.js +0 -34
- package/src/native/lifecycle.ts +0 -38
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Animation } from '@ionic/core';
|
|
2
2
|
export interface NativeUIShellIntegration {
|
|
3
|
-
suspend(scopes: HTMLElement[]): Promise<() => void>;
|
|
3
|
+
suspend(scopes: HTMLElement[]): Promise<(canceled?: boolean) => void>;
|
|
4
4
|
search?(binding: NativeSearchBinding, active: boolean, focus?: boolean): Promise<boolean>;
|
|
5
5
|
}
|
|
6
6
|
export interface NativeSearchBinding {
|
|
@@ -13,8 +13,9 @@ export interface NativeSearchBinding {
|
|
|
13
13
|
export declare const registerNativeSearch: (tabBar: HTMLElement, trigger: HTMLElement, footer: HTMLElement) => NativeSearchBinding;
|
|
14
14
|
export declare const getNativeSearchBindings: (doc: Document) => NativeSearchBinding[];
|
|
15
15
|
export declare const requestNativeSearch: (binding: NativeSearchBinding, active: boolean, focus?: boolean) => Promise<boolean>;
|
|
16
|
+
export declare const VERTICAL_BARS_TRANSITION_CANCELED = "iosThemeVerticalBarsTransitionCanceled";
|
|
16
17
|
export declare const setNativeUIShellIntegration: (doc: Document, runtime?: NativeUIShellIntegration) => void;
|
|
17
18
|
export declare const isNativeUIShell: (element: HTMLElement) => boolean;
|
|
18
|
-
export declare const suspendNativeUIShell: (scopes: HTMLElement[]) => Promise<() => void>;
|
|
19
|
+
export declare const suspendNativeUIShell: (scopes: HTMLElement[]) => Promise<(canceled?: boolean) => void>;
|
|
19
20
|
export declare const connectNativeUIShellTransition: (animation: Animation, entering: HTMLElement, leaving?: HTMLElement) => void;
|
|
20
21
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/native-integration/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/native-integration/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC,CAAC;IACtE,MAAM,CAAC,CAAC,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3F;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,WAAW,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAID,eAAO,MAAM,oBAAoB,GAAI,QAAQ,WAAW,EAAE,SAAS,WAAW,EAAE,QAAQ,WAAW,KAAG,mBAOrG,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,KAAK,QAAQ,KAAG,mBAAmB,EACR,CAAC;AAEpE,eAAO,MAAM,mBAAmB,GAAU,SAAS,mBAAmB,EAAE,QAAQ,OAAO,EAAE,QAAQ,OAAO,KAAG,OAAO,CAAC,OAAO,CAC3B,CAAC;AAGhG,eAAO,MAAM,iCAAiC,2CAA2C,CAAC;AAE1F,eAAO,MAAM,2BAA2B,GAAI,KAAK,QAAQ,EAAE,UAAU,wBAAwB,SAG5F,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,WAAW,YAAiD,CAAC;AAEtG,eAAO,MAAM,oBAAoB,GAAU,QAAQ,WAAW,EAAE,KAAG,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CACxB,CAAC;AAGhF,eAAO,MAAM,8BAA8B,GAAI,WAAW,SAAS,EAAE,UAAU,WAAW,EAAE,UAAU,WAAW,SAkEhH,CAAC"}
|
|
@@ -10,6 +10,7 @@ export const registerNativeSearch = (tabBar, trigger, footer) => {
|
|
|
10
10
|
export const getNativeSearchBindings = (doc) => (searches.get(doc) ?? []).flatMap((entry) => entry.deref() ?? []);
|
|
11
11
|
export const requestNativeSearch = async (binding, active, focus) => (await runtimes.get(binding.tabBar.ownerDocument)?.search?.(binding, active, focus)) ?? false;
|
|
12
12
|
const runtimes = new WeakMap();
|
|
13
|
+
export const VERTICAL_BARS_TRANSITION_CANCELED = 'iosThemeVerticalBarsTransitionCanceled';
|
|
13
14
|
export const setNativeUIShellIntegration = (doc, runtime) => {
|
|
14
15
|
if (runtime)
|
|
15
16
|
runtimes.set(doc, runtime);
|
|
@@ -19,6 +20,11 @@ export const setNativeUIShellIntegration = (doc, runtime) => {
|
|
|
19
20
|
export const isNativeUIShell = (element) => element.hasAttribute('data-native-ui-shell');
|
|
20
21
|
export const suspendNativeUIShell = async (scopes) => (await runtimes.get(scopes[0]?.ownerDocument)?.suspend(scopes)) ?? (() => { });
|
|
21
22
|
export const connectNativeUIShellTransition = (animation, entering, leaving) => {
|
|
23
|
+
if (leaving)
|
|
24
|
+
animation.onFinish((step) => {
|
|
25
|
+
if (step === 0)
|
|
26
|
+
leaving.dispatchEvent(new CustomEvent(VERTICAL_BARS_TRANSITION_CANCELED, { bubbles: true, detail: { entering } }));
|
|
27
|
+
});
|
|
22
28
|
if (!runtimes.has(entering.ownerDocument))
|
|
23
29
|
return;
|
|
24
30
|
const scopes = leaving ? [entering, leaving] : [entering];
|
|
@@ -35,16 +41,16 @@ export const connectNativeUIShellTransition = (animation, entering, leaving) =>
|
|
|
35
41
|
let end;
|
|
36
42
|
const prepare = () => (preparation ?? (preparation = suspendNativeUIShell(scopes).then((resume) => {
|
|
37
43
|
if (disposed)
|
|
38
|
-
resume();
|
|
44
|
+
resume(true);
|
|
39
45
|
else
|
|
40
46
|
release = resume;
|
|
41
47
|
})));
|
|
42
|
-
const finish = () => {
|
|
43
|
-
release?.();
|
|
48
|
+
const finish = (canceled = false) => {
|
|
49
|
+
release?.(canceled);
|
|
44
50
|
release = undefined;
|
|
45
51
|
preparation = undefined;
|
|
46
52
|
};
|
|
47
|
-
animation.onFinish(finish);
|
|
53
|
+
animation.onFinish((step) => finish(step === 0));
|
|
48
54
|
animation.play = async (options) => {
|
|
49
55
|
await prepare();
|
|
50
56
|
if (disposed)
|
|
@@ -86,7 +92,7 @@ export const connectNativeUIShellTransition = (animation, entering, leaving) =>
|
|
|
86
92
|
};
|
|
87
93
|
animation.destroy = (...args) => {
|
|
88
94
|
disposed = true;
|
|
89
|
-
finish();
|
|
95
|
+
finish(true);
|
|
90
96
|
return destroy(...args);
|
|
91
97
|
};
|
|
92
98
|
};
|
|
@@ -3,12 +3,12 @@ import type { Animation } from '@ionic/core';
|
|
|
3
3
|
export declare const transition: (opts: TransitionOptions) => Promise<TransitionResult>;
|
|
4
4
|
export declare const lifecycle: (el: HTMLElement | undefined, eventName: string) => void;
|
|
5
5
|
export declare const waitForMount: () => Promise<void>;
|
|
6
|
-
export declare const deepReady: (el:
|
|
6
|
+
export declare const deepReady: (el: Element | undefined) => Promise<void>;
|
|
7
7
|
export declare const setPageHidden: (el: HTMLElement, hidden: boolean) => void;
|
|
8
8
|
export declare const getIonPageElement: (element: HTMLElement) => Element;
|
|
9
9
|
export interface TransitionOptions extends NavOptions {
|
|
10
10
|
progressCallback?: (ani: Animation | undefined) => void;
|
|
11
|
-
baseEl:
|
|
11
|
+
baseEl: HTMLElement;
|
|
12
12
|
enteringEl: HTMLElement;
|
|
13
13
|
leavingEl: HTMLElement | undefined;
|
|
14
14
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transition/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,aAAa,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAoB,MAAM,aAAa,CAAC;AAmB/D,eAAO,MAAM,UAAU,GAAI,MAAM,iBAAiB,KAAG,OAAO,CAAC,gBAAgB,CAyB5E,CAAC;AAkKF,eAAO,MAAM,SAAS,GAAI,IAAI,WAAW,GAAG,SAAS,EAAE,WAAW,MAAM,SAQvE,CAAC;AAeF,eAAO,MAAM,YAAY,QAAO,OAAO,CAAC,IAAI,CAE3C,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transition/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAgB,MAAM,aAAa,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAoB,MAAM,aAAa,CAAC;AAmB/D,eAAO,MAAM,UAAU,GAAI,MAAM,iBAAiB,KAAG,OAAO,CAAC,gBAAgB,CAyB5E,CAAC;AAkKF,eAAO,MAAM,SAAS,GAAI,IAAI,WAAW,GAAG,SAAS,EAAE,WAAW,MAAM,SAQvE,CAAC;AAeF,eAAO,MAAM,YAAY,QAAO,OAAO,CAAC,IAAI,CAE3C,CAAC;AAOF,eAAO,MAAM,SAAS,GAAU,IAAI,OAAO,GAAG,SAAS,KAAG,OAAO,CAAC,IAAI,CAyBrE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,IAAI,WAAW,EAAE,QAAQ,OAAO,SAS7D,CAAC;AA6CF,eAAO,MAAM,iBAAiB,GAAI,SAAS,WAAW,YAWrD,CAAC;AA4BF,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,GAAG,SAAS,KAAK,IAAI,CAAC;IACxD,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,WAAW,CAAC;IACxB,SAAS,EAAE,WAAW,GAAG,SAAS,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB"}
|
package/dist/transition/index.js
CHANGED
|
@@ -159,7 +159,7 @@ export const deepReady = async (el) => {
|
|
|
159
159
|
await waitForCustomElement;
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
|
-
await Promise.all(Array.from(element.children).map(deepReady));
|
|
162
|
+
await Promise.all(Array.from(element.children).map((child) => deepReady(child)));
|
|
163
163
|
}
|
|
164
164
|
};
|
|
165
165
|
export const setPageHidden = (el, hidden) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios.transition.d.ts","sourceRoot":"","sources":["../../src/transition/ios.transition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,MAAM,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAIjD,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"ios.transition.d.ts","sourceRoot":"","sources":["../../src/transition/ios.transition.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgC,MAAM,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAIjD,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;CAChB;AAUD,eAAO,MAAM,SAAS,GAAI,QAAQ,OAAO,CAAC,mBAAmB,CAAC,KAAG,IAEhE,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,iBAAiB,KAAK,SACnB,CAAC"}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { AnimationPosition } from './sheets-of-glass/interfaces';
|
|
2
2
|
import { IonicConfig } from '@ionic/core';
|
|
3
3
|
export declare const getElementRoot: (el: HTMLElement, fallback?: HTMLElement) => HTMLElement | ShadowRoot;
|
|
4
|
-
export declare const raf: (h: FrameRequestCallback) =>
|
|
4
|
+
export declare const raf: (h: FrameRequestCallback) => number;
|
|
5
5
|
export declare const cloneElement: (tagName: string, useCache?: boolean) => HTMLElement;
|
|
6
6
|
export declare const getStep: (targetX: number, animationPosition: AnimationPosition) => number;
|
|
7
7
|
export declare const changeSelectedElement: (targetElement: HTMLElement, selectedElement: HTMLElement, effectTagName: string, selectedClassName: string) => void;
|
|
8
8
|
export declare class Config {
|
|
9
9
|
private m;
|
|
10
10
|
reset(configObj: IonicConfig): void;
|
|
11
|
-
get(key: keyof IonicConfig, fallback?:
|
|
11
|
+
get<T>(key: keyof IonicConfig, fallback?: T): T;
|
|
12
12
|
getBoolean(key: keyof IonicConfig, fallback?: boolean): boolean;
|
|
13
13
|
getNumber(key: keyof IonicConfig, fallback?: number): number;
|
|
14
|
-
set(key: keyof IonicConfig, value:
|
|
14
|
+
set(key: keyof IonicConfig, value: unknown): void;
|
|
15
15
|
}
|
|
16
16
|
export declare const config: Config;
|
|
17
17
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,eAAO,MAAM,cAAc,GAAI,IAAI,WAAW,EAAE,WAAU,WAAgB,6BAEzE,CAAC;AAEF,eAAO,MAAM,GAAG,GAAI,GAAG,oBAAoB,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,eAAO,MAAM,cAAc,GAAI,IAAI,WAAW,EAAE,WAAU,WAAgB,6BAEzE,CAAC;AAEF,eAAO,MAAM,GAAG,GAAI,GAAG,oBAAoB,WAQ1C,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,EAAE,WAAU,OAAc,KAAG,WAcxE,CAAC;AAEF,eAAO,MAAM,OAAO,GAAI,SAAS,MAAM,EAAE,mBAAmB,iBAAiB,WAQ5E,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,eAAe,WAAW,EAC1B,iBAAiB,WAAW,EAC5B,eAAe,MAAM,EACrB,mBAAmB,MAAM,KACxB,IAMF,CAAC;AAEF,qBAAa,MAAM;IACjB,OAAO,CAAC,CAAC,CAAyC;IAElD,KAAK,CAAC,SAAS,EAAE,WAAW;IAI5B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,WAAW,EAAE,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC;IAK/C,UAAU,CAAC,GAAG,EAAE,MAAM,WAAW,EAAE,QAAQ,UAAQ,GAAG,OAAO;IAW7D,SAAS,CAAC,GAAG,EAAE,MAAM,WAAW,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM;IAK5D,GAAG,CAAC,GAAG,EAAE,MAAM,WAAW,EAAE,KAAK,EAAE,OAAO;CAG3C;AAED,eAAO,MAAM,MAAM,QAA6B,CAAC"}
|
package/dist/utils.js
CHANGED
|
@@ -61,7 +61,7 @@ export class Config {
|
|
|
61
61
|
return !!val;
|
|
62
62
|
}
|
|
63
63
|
getNumber(key, fallback) {
|
|
64
|
-
const val = parseFloat(this.m.get(key));
|
|
64
|
+
const val = parseFloat(String(this.m.get(key)));
|
|
65
65
|
return isNaN(val) ? (fallback !== undefined ? fallback : NaN) : val;
|
|
66
66
|
}
|
|
67
67
|
set(key, value) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { enableVerticalControlArea, setVerticalControlAreaPlacement, IonicNativeUIShell } from './native';
|
|
2
|
+
export { HingeStatus } from './native';
|
|
3
|
+
export type { DeviceLayout, NativeUIShellStatus, NativeUIShellSuspension, VerticalBarEdge, VerticalControlAreaHandle } from './native';
|
|
4
|
+
//# sourceMappingURL=vertical-bars.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vertical-bars.d.ts","sourceRoot":"","sources":["../src/vertical-bars.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,+BAA+B,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAC1G,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: iPhone Duo support (experimental)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# iPhone Duo support (experimental)
|
|
6
|
+
|
|
7
|
+
iPhone Duo support is experimental, including the Vertical Control Area, device-layout reporting, and posture-driven split-pane layout. It is planned for release alongside [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell). Its APIs and supported behavior may change.
|
|
8
|
+
|
|
9
|
+
iPhone Duo folds along a hinge and reserves a physical system rail on one side of the display. On iOS 27.1 and later, the system reports both facts to the app: the rail's edge with its safe-area inset, and the hinge posture while the device opens and closes.
|
|
10
|
+
|
|
11
|
+
This package provides three independent pieces for that hardware. Each works **without the iOS 27 theme stylesheets** and **without the full Native UI Shell**:
|
|
12
|
+
|
|
13
|
+
- `dist/css/vertical-bars.css` — opt-in classes that reserve the rail's safe area, plus a registered custom property for a posture-driven split-pane width.
|
|
14
|
+
- `enableVerticalControlArea()` — moves eligible tabs and toolbar controls into the reserved area. On Capacitor iOS they are rendered by a native SwiftUI `TabView` and toolbar; everywhere else the same controls appear as Web clones.
|
|
15
|
+
- Device-layout reporting — the bundled Capacitor plugin reports rail placement, hinge status and the WebView corner radius through `getDeviceLayout()` and the `deviceLayoutChange` event.
|
|
16
|
+
|
|
17
|
+
The pieces map to distinct responsibilities. The plugin reports **device facts** and never touches the DOM. The **application** decides what each value means for its layout. The **stylesheet and runtime** apply that decision — reserving space, projecting controls and adapting the split pane. Keeping these boundaries separate makes the native values easy to mock in tests and keeps the theme's own responsibility small.
|
|
18
|
+
|
|
19
|
+
## Choose what to adopt
|
|
20
|
+
|
|
21
|
+
| Goal | Stylesheet | Runtime |
|
|
22
|
+
| ------------------------------------------------ | ------------------- | ------------------------------------------------------------------ |
|
|
23
|
+
| Hinge posture only (split pane, layout switches) | `vertical-bars.css` | none — subscribe to the plugin directly |
|
|
24
|
+
| Vertical rail for tabs and toolbar actions | `vertical-bars.css` | `enableVerticalControlArea()` |
|
|
25
|
+
| Native shell plus the rail | `vertical-bars.css` | `enableNativeUIShell()` — already includes rail and posture support |
|
|
26
|
+
|
|
27
|
+
```scss
|
|
28
|
+
@use '@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The stylesheet never changes ordinary Ionic UI by itself; every rule requires an opt-in class. Load it unconditionally — these values are simulation and layout inputs, independent from Ionic's normal safe-area variables.
|
|
32
|
+
|
|
33
|
+
The `/vertical-bars` entry point imports `@capacitor/core` at module load, so install it even for Web-only use (it is an optional peer dependency). Apps that want only the stylesheet and its opt-in classes need nothing else.
|
|
34
|
+
|
|
35
|
+
## Read the device layout
|
|
36
|
+
|
|
37
|
+
`npx cap sync ios` registers the plugin automatically; no `configure` call is needed for device layout. An app that only wants the hinge posture — for example to drive a split pane — uses this API alone, with no projection runtime:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { Capacitor } from '@capacitor/core';
|
|
41
|
+
import { HingeStatus, IonicNativeUIShell } from '@rdlabo/ionic-theme-ios27/vertical-bars';
|
|
42
|
+
|
|
43
|
+
// The plugin has no Web implementation; guard the subscription.
|
|
44
|
+
if (Capacitor.getPlatform() === 'ios') {
|
|
45
|
+
await IonicNativeUIShell.startDeviceLayoutMonitoring();
|
|
46
|
+
const listener = await IonicNativeUIShell.addListener('deviceLayoutChange', ({ hingeStatus }) => {
|
|
47
|
+
// apply the posture
|
|
48
|
+
});
|
|
49
|
+
const { hingeStatus } = await IonicNativeUIShell.getDeviceLayout(); // initial value
|
|
50
|
+
|
|
51
|
+
// When the consumer goes away:
|
|
52
|
+
// await listener.remove();
|
|
53
|
+
// await IonicNativeUIShell.stopDeviceLayoutMonitoring();
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`DeviceLayout` carries:
|
|
58
|
+
|
|
59
|
+
| Field | Meaning |
|
|
60
|
+
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| `placement` | `{ edge: 'leading' \| 'trailing' \| null, inset }` — the rail's logical edge in the reading direction and its UIKit safe-area inset in points; `edge` is `null` on devices without a rail |
|
|
62
|
+
| `hingeStatus` | `HingeStatus.Closed`, `PartiallyOpen`, or `FullyOpen`; `null` when the device reports no hinge |
|
|
63
|
+
| `webViewMetrics.radius` | the WebView's effective top-left corner radius in points |
|
|
64
|
+
|
|
65
|
+
`edge` is a **logical** direction: `'leading'` is where a reader starts a line — the physical left in LTR and the physical right in RTL. This is the same vocabulary as UIKit's `verticalBarEdge` trait and `@erkamyaman/capacitor-foldable`'s `getBarPlacement()`, so values from that plugin can be applied as-is without conversion.
|
|
66
|
+
|
|
67
|
+
Monitoring is reference-counted: each consumer pairs `startDeviceLayoutMonitoring()` with `stopDeviceLayoutMonitoring()`, and events stop when the last consumer releases it. `getDeviceLayout()` also works without monitoring for a one-shot read. While `enableVerticalControlArea()` or `enableNativeUIShell()` has native projection active it already holds a monitoring reference, so those users only add a listener and read the initial value — no extra start/stop pair.
|
|
68
|
+
|
|
69
|
+
**Build requirement:** iOS only enables the vertical bar for apps linked against the iOS 27.1 SDK or later — build with Xcode 27.1 or newer. Apps built with an older SDK run in backward-compatibility mode on iPhone Duo: the system reserves no rail, `placement.edge` stays `null`, `inset` stays `0`, and `hingeStatus` stays `null`. Everything else still works in that state — the opt-in classes reserve the DOM strip and the rail follows whatever placement the application applies — so the compat build remains usable and testable; only the real system rail, its measured inset and hinge posture require the newer toolchain.
|
|
70
|
+
|
|
71
|
+
## Reserve the vertical rail
|
|
72
|
+
|
|
73
|
+
Add `.ios-theme-vertical-bars` to `ion-app` to reserve the rail region on the physical right, or add `.ios-theme-vertical-bars-left` as well to use the physical left:
|
|
74
|
+
|
|
75
|
+
```html
|
|
76
|
+
<ion-app class="ios-theme-vertical-bars">...</ion-app>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The classes are physical — `-left` always means the physical left edge — because CSS and the native renderer work in physical coordinates. `setPlacement` (below) is the usual way to apply them: it accepts the logical `placement.edge` reported by the plugin and resolves it through the document's direction, so an RTL app does not need its own conversion.
|
|
80
|
+
|
|
81
|
+
For Chrome development, no native plugin is needed — the class alone reserves `80px` to simulate iPhone Duo. When `setPlacement` receives a native placement, the measured UIKit inset replaces the simulated width, even when that inset is less than `80px`. Override `--ios-theme-vertical-bars-safe-area-left` or `--ios-theme-vertical-bars-safe-area-right` when simulating a different layout.
|
|
82
|
+
|
|
83
|
+
This keeps routers and component backgrounds full-viewport. `ion-content` moves its scroll foreground, `ion-toolbar` moves its container foreground, and `ion-fab` adjusts only when placed beside the system UI. The corresponding Ionic safe-area variable is reset inside those foreground components so descendants do not add the inset again.
|
|
84
|
+
|
|
85
|
+
`ion-menu`, `ion-modal`, and `ion-popover` are handled as separate surfaces: their internal foreground components do not receive the main-page conversion and retain Ionic's standard safe-area handling. A menu presented beside the system UI keeps Ionic's full-viewport animation host and offsets only its visible container by the corresponding inset; a menu from the opposite side is unchanged. Left and right remain physical coordinates in RTL, while Ionic's `side="start"` and `side="end"` values remain logical.
|
|
86
|
+
|
|
87
|
+
The mode is component-mode independent: an app can keep Ionic `mode: 'md'` on iOS and still enable Vertical Bars. No component needs `mode="ios"`.
|
|
88
|
+
|
|
89
|
+
## Project controls into the rail
|
|
90
|
+
|
|
91
|
+
Start the standalone runtime once at application startup:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { Capacitor } from '@capacitor/core';
|
|
95
|
+
import { enableVerticalControlArea, IonicNativeUIShell } from '@rdlabo/ionic-theme-ios27/vertical-bars';
|
|
96
|
+
|
|
97
|
+
// Start on Chrome too; the Web projection stays idle until the class is present.
|
|
98
|
+
const rail = await enableVerticalControlArea();
|
|
99
|
+
|
|
100
|
+
if (Capacitor.getPlatform() === 'ios') {
|
|
101
|
+
// The runtime already monitors device layout; only subscribe.
|
|
102
|
+
await IonicNativeUIShell.addListener('deviceLayoutChange', ({ placement }) => rail.setPlacement(placement));
|
|
103
|
+
rail.setPlacement((await IonicNativeUIShell.getDeviceLayout()).placement);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`setPlacement` on the handle and the exported `setVerticalControlAreaPlacement` are the same function; either applies the application's chosen placement to the CSS layout and both projections. It requires a mounted `ion-app` — call it after the app root exists.
|
|
108
|
+
|
|
109
|
+
- Pass the `placement` object from `getDeviceLayout()`/`deviceLayoutChange`, or just a logical edge: `'leading'` or `'trailing'`. The logical edge resolves to a physical side through the nearest `dir` attribute, or through an explicit `rtl` second argument when the app already knows its direction.
|
|
110
|
+
- Pass `null` to restore the ordinary layout.
|
|
111
|
+
- The device-layout listener reports what iOS chose; the application decides whether to apply it. An app that wants a fixed edge regardless of the report can simply pass its own `'leading'` or `'trailing'`.
|
|
112
|
+
|
|
113
|
+
Start either `enableVerticalControlArea()` or the full `enableNativeUIShell()` — not both. Repeating the same configuration returns the shared runtime; starting a different configuration while it is active throws an error. The application should have one owner responsible for destroying that runtime. If the app already uses `enableNativeUIShell()`, keep that single runtime and call `setVerticalControlAreaPlacement(placement)` from its listener.
|
|
114
|
+
|
|
115
|
+
On supported iOS versions the runtime hands eligible tabs, back navigation, menu buttons, and fixed-toolbar actions to a native SwiftUI `TabView` and toolbar; on Web, Android, or when native projection is unavailable, Web clones remain the fallback. Back navigation can come from outside a fixed toolbar; other actions still require one. Fixed-toolbar actions need an icon or SVG, no direct text node, and standard `fill="default"` or `fill="clear"` — text-only actions stay in the original Web toolbar. Add `.ios-theme-horizontal-only` to an `ion-buttons` group or individual `ion-button` to keep it in the horizontal toolbar. Placement is chosen when a routed page enters; changing an existing button's content does not move it between the toolbar and rail until the page leaves and re-enters. Menus, modals, and popovers retain their own toolbar layout.
|
|
116
|
+
|
|
117
|
+
When the app contains `ion-tabs`, its tab bar moves into the reserved region and aligns above the bottom safe area; the Ionic `slot` value does not select a different position. Without native projection, the stable Web rail is icon-only, matching a four-tab SwiftUI `TabView`. While the user presses and drags across that rail, every icon-and-label tab reveals its label so the pending destination stays identifiable. The Web tab bar receives pointer input in the simulated system region. Use `ion-menu` when navigation should become a sidebar; this mode does not convert tabs into a menu. Web clones also work when no `ion-tabs` exists. Disabling the mode or leaving the page removes the native ownership or Web clones and restores their sources. Override `--ios-theme-vertical-bars-toolbar-top` when the simulated system controls use a different vertical layout.
|
|
118
|
+
|
|
119
|
+
## Adapt the split pane
|
|
120
|
+
|
|
121
|
+
For a side-by-side menu on iPhone Duo, opt the `ion-split-pane` into the separately measured Settings layout. The sidebar is 320pt when fully unfolded and reaches the display midpoint when half-opened (50vw). The application supplies the posture; both states have the same viewport width, so a width media query cannot distinguish them:
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<ion-split-pane
|
|
125
|
+
[class.ios-theme-split-pane-half-open]="halfOpened"
|
|
126
|
+
contentId="main-content"
|
|
127
|
+
when="(min-width: 900px)"
|
|
128
|
+
>
|
|
129
|
+
<ion-menu contentId="main-content">...</ion-menu>
|
|
130
|
+
<div id="main-content">...</div>
|
|
131
|
+
</ion-split-pane>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Set the ordinary split-pane width to 320pt in the application's stylesheet, and let the half-open class change only the width value:
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
ion-split-pane {
|
|
138
|
+
--ios-theme-menu-width: var(--ios-theme-split-pane-width);
|
|
139
|
+
--side-width: var(--ios-theme-menu-width);
|
|
140
|
+
--side-max-width: var(--ios-theme-menu-width);
|
|
141
|
+
transition: --ios-theme-split-pane-width 300ms ease;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The registered `--ios-theme-split-pane-width` defaults to `320px`; `.ios-theme-split-pane-half-open` sets it to `50vw`. Set `halfOpened` when `deviceLayoutChange` reports `HingeStatus.PartiallyOpen` (and read the initial value with `getDeviceLayout`). Ionic's `when` decides whether the menu is a persistent side pane; choose its breakpoint so the pane is hidden when closed — `null` means the device has no hinge, so restore the ordinary breakpoint for it. The application chooses where to apply this width rule; an ordinary split pane elsewhere is unchanged. This layout does not enable Vertical Bars or move an overlay menu.
|
|
146
|
+
|
|
147
|
+
## Vertical Control Area API
|
|
148
|
+
|
|
149
|
+
The generated reference below documents the handle returned by `enableVerticalControlArea()`.
|
|
150
|
+
|
|
151
|
+
<docgen-index>
|
|
152
|
+
|
|
153
|
+
* [`setPlacement(...)`](#setplacement)
|
|
154
|
+
* [`getStatus()`](#getstatus)
|
|
155
|
+
* [`suspend()`](#suspend)
|
|
156
|
+
* [`destroy()`](#destroy)
|
|
157
|
+
* [Interfaces](#interfaces)
|
|
158
|
+
* [Type Aliases](#type-aliases)
|
|
159
|
+
|
|
160
|
+
</docgen-index>
|
|
161
|
+
|
|
162
|
+
<docgen-api>
|
|
163
|
+
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
|
|
164
|
+
|
|
165
|
+
### setPlacement(...)
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
setPlacement(placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean | undefined) => void
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Applies the application's chosen placement to both Web and native controls.
|
|
172
|
+
|
|
173
|
+
| Param | Type |
|
|
174
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
175
|
+
| **`placement`** | <code><a href="#verticalbaredge">VerticalBarEdge</a> \| <a href="#verticalbarplacement">VerticalBarPlacement</a></code> |
|
|
176
|
+
| **`rtl`** | <code>boolean</code> |
|
|
177
|
+
|
|
178
|
+
--------------------
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
### getStatus()
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
getStatus() => NativeUIShellStatus
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Returns the current Web/native projection state.
|
|
188
|
+
|
|
189
|
+
**Returns:** <code><a href="#nativeuishellstatus">NativeUIShellStatus</a></code>
|
|
190
|
+
|
|
191
|
+
--------------------
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
### suspend()
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
suspend() => Promise<NativeUIShellSuspension>
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Restores projected controls to the Web until the returned lease is resumed.
|
|
201
|
+
|
|
202
|
+
**Returns:** <code>Promise<<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>></code>
|
|
203
|
+
|
|
204
|
+
--------------------
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
### destroy()
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
destroy() => Promise<void>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Stops synchronization, restores Web controls and releases native resources.
|
|
214
|
+
|
|
215
|
+
--------------------
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
### Interfaces
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
#### VerticalBarPlacement
|
|
222
|
+
|
|
223
|
+
| Prop | Type | Description |
|
|
224
|
+
| ----------- | ----------------------------------------------------------- | ---------------------------------------------------------- |
|
|
225
|
+
| **`edge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | |
|
|
226
|
+
| **`inset`** | <code>number</code> | UIKit safe-area inset on the vertical-bar edge, in points. |
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
#### NativeUIShellStatus
|
|
230
|
+
|
|
231
|
+
| Prop | Type |
|
|
232
|
+
| --------------- | ------------------------------------------- |
|
|
233
|
+
| **`state`** | <code>'native' \| 'stopped' \| 'web'</code> |
|
|
234
|
+
| **`projected`** | <code>number</code> |
|
|
235
|
+
| **`updates`** | <code>number</code> |
|
|
236
|
+
| **`reason`** | <code>string</code> |
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
#### NativeUIShellSuspension
|
|
240
|
+
|
|
241
|
+
| Method | Signature | Description |
|
|
242
|
+
| ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
243
|
+
| **resume** | () => Promise<void> | Releases this suspension. Native projection resumes after all active suspensions are released. |
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
### Type Aliases
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
#### VerticalBarEdge
|
|
250
|
+
|
|
251
|
+
Logical edge in the reading direction, matching UIVerticalBarEdge and capacitor-foldable.
|
|
252
|
+
|
|
253
|
+
<code>'leading' | 'trailing' | null</code>
|
|
254
|
+
|
|
255
|
+
</docgen-api>
|
package/docs/migration.md
CHANGED
|
@@ -6,7 +6,7 @@ title: Migration
|
|
|
6
6
|
|
|
7
7
|
## From the iOS 26 theme
|
|
8
8
|
|
|
9
|
-
For an app using `@rdlabo/ionic-theme-ios26`, the recommended migration keeps that package and adds `@rdlabo/ionic-theme-ios27`. The [README setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27#get-started) selects iOS 27 or iOS 26 styles by browser capability and leaves Ionic's default iOS appearance on older browsers.
|
|
9
|
+
For an app using `@rdlabo/ionic-theme-ios26`, the recommended migration keeps that package and adds `@rdlabo/ionic-theme-ios27`. The [README setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/readme#get-started) selects iOS 27 or iOS 26 styles by browser capability and leaves Ionic's default iOS appearance on older browsers.
|
|
10
10
|
|
|
11
11
|
### 1. Add the new package
|
|
12
12
|
|
|
@@ -104,7 +104,7 @@ To switch entirely to iOS 27, remove the iOS 26 package and replace its styleshe
|
|
|
104
104
|
+ @use '@rdlabo/ionic-theme-ios27/src/styles/md-remove-ios-class-effect.scss';
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
Change the animation import from `@rdlabo/ionic-theme-ios26` to `@rdlabo/ionic-theme-ios27`; the existing `isPlatform('ios')` configuration can stay. See the README's [iOS 27-only setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27#use-only-the-ios-27-theme). Unconditional imports apply the new styles on every browser using Ionic iOS mode.
|
|
107
|
+
Change the animation import from `@rdlabo/ionic-theme-ios26` to `@rdlabo/ionic-theme-ios27`; the existing `isPlatform('ios')` configuration can stay. See the README's [iOS 27-only setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/readme#use-only-the-ios-27-theme). Unconditional imports apply the new styles on every browser using Ionic iOS mode.
|
|
108
108
|
|
|
109
109
|
## iOS 27 naming
|
|
110
110
|
|
package/docs/native-ui-shell.md
CHANGED
|
@@ -6,7 +6,7 @@ title: Native UI Shell (Experimental)
|
|
|
6
6
|
|
|
7
7
|
Native UI Shell is experimental. Its API and supported controls may change.
|
|
8
8
|
|
|
9
|
-
Native UI Shell gives an Ionic app native navigation and action controls around its Web content. The optional Capacitor iOS plugin renders supported fixed Ionic controls with UIKit and the system's Liquid Glass material. Page content, scrolling, application state and routing remain in Ionic's WebView.
|
|
9
|
+
Native UI Shell gives an Ionic app native navigation and action controls around its Web content. The optional Capacitor iOS plugin renders supported fixed Ionic controls with UIKit or SwiftUI and the system's Liquid Glass material. Page content, scrolling, application state and routing remain in Ionic's WebView.
|
|
10
10
|
|
|
11
11
|
## Background
|
|
12
12
|
|
|
@@ -50,7 +50,7 @@ Native appearance follows the applied class, system or always-dark theme CSS. Sy
|
|
|
50
50
|
| `ion-segment` | Fixed toolbar, non-scrollable, text **or** one icon per item | `UISegmentedControl` |
|
|
51
51
|
| `ion-fab` / `ion-fab-button` / `ion-fab-list` | Glass FAB in an `ion-content` fixed slot; one main button and optional directional lists | Persistent glass `UIButton` per button; one FAB synchronization group |
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
For the ordinary Native UI Shell, only iOS-mode components with the theme variables installed are eligible. Explicitly enabled Vertical Bars is mode-independent as described below. `ionic-theme-disabled`, `ios-theme-disabled`, and the legacy `ios26-disabled` on an element or ancestor always exclude it. A disabled theme on one tab/segment item keeps its whole group on the Web.
|
|
54
54
|
|
|
55
55
|
Use `ios-theme-shell-disabled` to disable only the iOS Native UI Shell while keeping the Web theme. It excludes the element and all its descendants. Adding or removing the class at runtime automatically restores Web rendering or re-evaluates native eligibility.
|
|
56
56
|
|
|
@@ -62,7 +62,7 @@ Use `ios-theme-shell-disabled` to disable only the iOS Native UI Shell while kee
|
|
|
62
62
|
|
|
63
63
|
If a child inside a shared native surface opts out, the entire surface stays on the Web: this includes button groups, tab bars, segments and FAB lists. Opting out of the search FAB or any part of the search footer disables native search integration; the tab bar can still render natively if it remains eligible.
|
|
64
64
|
|
|
65
|
-
Placement is required even when the appearance is glass.
|
|
65
|
+
Placement is required even when the appearance is glass. In the ordinary Native UI Shell, buttons, back buttons, menu-button groups and segments need a toolbar directly inside `ion-header` or `ion-footer`, with no `ion-content` ancestor around the control. Buttons directly inside a header/footer, standalone toolbars, and toolbars or headers nested in scrolling content stay on the Web. FABs without `slot="fixed"` also stay on the Web. Moving a projected control to an excluded location restores its Web rendering; moving it back re-evaluates eligibility. When `.ios-theme-vertical-bars` is enabled, a standard `ion-back-button` can instead be projected to the Vertical Control Area from outside a fixed toolbar, including routed content or a persistent app shell. The application chooses where to enable this mode and which Ionic component mode to use; Vertical Bars projection does not require `ios` mode classes. Overlays, collapsed headers, opted-out controls and departed pages are excluded.
|
|
66
66
|
|
|
67
67
|
Native tabs accept equal-width items with Ionic's default `layout="icon-top"`. The native bar uses a local compact horizontal and regular vertical size class to preserve the Web's stacked icon/label arrangement on iPad and in landscape. This does not change the app's size class. Label size and weight follow the Web snapshot. Other explicit Ionic layouts (`icon-start`, `icon-end`, `icon-bottom`, `icon-hide`, `label-hide`) and unequal item widths keep the entire tab bar on the Web. Start, center and end placement follow the original `ion-tab-bar`, including RTL. Directional `ion-icon` artwork preserves its rendered RTL flip.
|
|
68
68
|
|
|
@@ -167,6 +167,18 @@ Suspensions are nestable and `resume()` is idempotent. Native projection resumes
|
|
|
167
167
|
|
|
168
168
|
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.
|
|
169
169
|
|
|
170
|
+
## Support iPhone Duo (experimental)
|
|
171
|
+
|
|
172
|
+
iPhone Duo support is experimental, including standalone use without Native UI Shell. It is planned for release alongside Native UI Shell. Its APIs and supported behavior may change.
|
|
173
|
+
|
|
174
|
+
The standalone Vertical Control Area entry point (`@rdlabo/ionic-theme-ios27/vertical-bars`) and `dist/css/vertical-bars.css` work without loading the iOS 27 theme. Call `enableVerticalControlArea()` for this use case; it projects only controls placed in the vertical area. Apps already calling `enableNativeUIShell()` should keep that single runtime rather than starting both. See [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo) for the complete setup, including hinge posture and the split-pane layout for apps that do not use this shell at all.
|
|
175
|
+
|
|
176
|
+
On supported iOS versions, adding `.ios-theme-vertical-bars` changes only controls that the system relocates into the physical side rail. Native UI Shell presents eligible tabs, back navigation, menu buttons, and toolbar actions through a SwiftUI `TabView` and toolbar once the class is applied. When the OS reports a rail edge — on iPhone Duo linked against iOS 27.1 or later — it must agree with the applied placement; a disagreeing report keeps the rail on the Web. Older toolchains that cannot report an edge trust the DOM placement directly. SwiftUI owns their adaptive placement and Liquid Glass appearance; Ionic remains the source of labels, icons, selected/disabled state, routing, form submission, and click handlers.
|
|
177
|
+
|
|
178
|
+
The SwiftUI surface is clipped and hit-tested to the system rail. Web content remains visible and interactive outside that physical region. The runtime optimistically updates tab selection before forwarding the action to the original `ion-tab-button`, using the same event and stale-revision protection as the other native controls. Menus, modals, and popovers remain independent surfaces and are not moved into the main-page rail.
|
|
179
|
+
|
|
180
|
+
This mode accepts standard `fill="default"` and `fill="clear"` fixed-toolbar buttons because SwiftUI determines their compact representation. Solid, outline, custom-color, scrolling, and explicitly opted-out controls remain on the Web. Add `.ios-theme-horizontal-only` to an `ion-buttons` group or individual `ion-button` to keep it in the horizontal Web toolbar. On Web, Android, older iOS, or when native projection is unavailable during setup, the Web projection remains the fallback.
|
|
181
|
+
|
|
170
182
|
## Native UI Shell API
|
|
171
183
|
|
|
172
184
|
The generated reference below documents the handle returned by `enableNativeUIShell()`. The underlying Capacitor bridge and its control-snapshot protocol are implementation details.
|
package/docs/special-markup.md
CHANGED
|
@@ -47,6 +47,12 @@ Add one of `tab-bar-position-start`, `tab-bar-position-center`, or `tab-bar-posi
|
|
|
47
47
|
|
|
48
48
|
These classes do not reposition a separate `ion-fab`; leave room for it when choosing the bar's position.
|
|
49
49
|
|
|
50
|
+
## Support iPhone Duo (experimental)
|
|
51
|
+
|
|
52
|
+
iPhone Duo support is experimental. It is planned for release alongside Native UI Shell. Its APIs and supported behavior may change.
|
|
53
|
+
|
|
54
|
+
iPhone Duo support — the vertical system rail, hinge posture, and the posture-driven split-pane layout — is independent of the iOS 27 theme and the full Native UI Shell. See [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo) for the complete setup, including device-layout monitoring without a projection runtime.
|
|
55
|
+
|
|
50
56
|
## Two-line inset list items
|
|
51
57
|
|
|
52
58
|
Place an unslotted `ion-label` immediately alongside an unslotted `ion-note` to render a two-line item. When using the iOS-style inset-list background, wrap the items in `ion-item-group`; keep `ion-list-header` outside the group.
|
|
@@ -45,8 +45,22 @@ private final class ShellSearchInputDelegate: NSObject, UITextFieldDelegate {
|
|
|
45
45
|
func textFieldShouldClear(_ textField: UITextField) -> Bool { clear?(); return false }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Lets the plugin registry retain iOS 26-gated search controllers without
|
|
49
|
+
// availability-gated stored properties, mirroring ShellVerticalBarsControlling.
|
|
50
|
+
protocol ShellSearchControlling: AnyObject {
|
|
51
|
+
var surface: ShellSearchHost { get }
|
|
52
|
+
var ownsKeyboard: Bool { get }
|
|
53
|
+
var ownsKeyboardChrome: Bool { get }
|
|
54
|
+
var transitionCoordinator: UIViewControllerTransitionCoordinator? { get }
|
|
55
|
+
var activate: ((String) -> Void)? { get set }
|
|
56
|
+
var changed: ((String, ShellSearchPhase, String, Bool, Int) -> Int)? { get set }
|
|
57
|
+
func attach(to parent: UIViewController, in container: UIView)
|
|
58
|
+
func detach()
|
|
59
|
+
func apply(_ snapshot: ShellControl, webFrame: CGRect, barFrame: CGRect, triggerFrame: CGRect, rendering: ShellRendering) -> Bool
|
|
60
|
+
}
|
|
61
|
+
|
|
48
62
|
@available(iOS 26.0, *)
|
|
49
|
-
final class ShellSearchController: UITabBarController, UITabBarControllerDelegate, UISearchBarDelegate {
|
|
63
|
+
final class ShellSearchController: UITabBarController, UITabBarControllerDelegate, UISearchBarDelegate, ShellSearchControlling {
|
|
50
64
|
// Wire stays active+focused; local session drives chrome (idle / presented / focused).
|
|
51
65
|
private enum Session: Equatable { case idle, presented, focused }
|
|
52
66
|
|