@rdlabo/ionic-theme-ios27 1.2.0-0 → 1.2.0-beta.pr225.sha520dc52f7232

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 CHANGED
@@ -123,9 +123,9 @@ Use this markup to preview the inset grouped list look. For the list structure t
123
123
 
124
124
  ### Support iPhone Duo without the iOS 27 theme (experimental)
125
125
 
126
- iPhone Duo support is experimental. It is planned for release alongside Native UI Shell. Its APIs and supported behavior may change.
126
+ Keep your existing Ionic theme and move tabs and supported toolbar actions into a vertical side area. **Start in Chrome** with one stylesheet, an app class, and `enableVerticalControlArea()`; then connect the layout to iPhone Duo device events for the system rail and hinge posture.
127
127
 
128
- Import only `@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css`, add `ios-theme-vertical-bars` to `ion-app`, and call `enableVerticalControlArea()` from `@rdlabo/ionic-theme-ios27/vertical-bars` at startup. This uses Ionic's standard appearance outside the Vertical Control Area; on supported iOS, only controls moved into that area are projected natively. To read only the hinge posture — for example to adapt an `ion-split-pane` — subscribe to the plugin's `deviceLayoutChange` event without starting any runtime. iOS enables the real system rail only for apps built with Xcode 27.1 or later; older toolchains run in compatibility mode and keep the DOM-driven rail instead. See [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo) for the complete setup.
128
+ Follow [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme) for the browser preview and iOS setup. For shared layout rules and APIs, see [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo). Available in `1.2.0-0` as an experimental feature; APIs and supported behavior may change.
129
129
 
130
130
  ### Use only the iOS 27 theme
131
131
 
@@ -214,6 +214,7 @@ For Ionic 9 Angular, import `isPlatform` and `provideIonicAngular` from `@ionic/
214
214
  - [Features](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/features) — CSS variables, Liquid Glass, selective imports, and dark mode.
215
215
  - [Native UI Shell (Experimental)](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell) — project supported Ionic controls, text, and icons into UIKit.
216
216
  - [iPhone Duo support (experimental)](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo) — vertical system rail, hinge posture, and split-pane layout; usable without the theme or the shell.
217
+ - [iPhone Duo with your existing theme (experimental)](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme) — standalone setup that keeps your existing Web theme.
217
218
  - [Animation](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/animation) — tab, segment, and searchable effects.
218
219
  - [Migration from iOS 26](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/migration) — upgrade an existing app, including stylesheet, class, and CSS variable changes.
219
220
  - [iOS 26 migration history](https://docs.rdlabo.dev/projects/ionic-theme-ios26/docs/migration) — earlier major-version changes for the previous package.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { iosPopoverEnterAnimation as popoverEnterAnimation, iosPopoverLeaveAnima
4
4
  export * from './tab-bar-searchable';
5
5
  export * from './searchbar';
6
6
  export * from './transition/ios.transition';
7
+ export { withNativeUIShellTransition } from './native-integration/transition';
7
8
  export { registerSegmentEffect } from './segment';
8
9
  export declare const registerTabBarEffect: (targetElement: HTMLElement) => registeredEffect | undefined;
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEhE,cAAc,8BAA8B,CAAC;AAC7C,OAAO,EACL,wBAAwB,IAAI,qBAAqB,EACjD,wBAAwB,IAAI,qBAAqB,GAClD,MAAM,2BAA2B,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,aAAa,CAAC;AAC5B,cAAc,6BAA6B,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAElD,eAAO,MAAM,oBAAoB,GAAI,eAAe,WAAW,KAAG,gBAAgB,GAAG,SAgCpF,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEhE,cAAc,8BAA8B,CAAC;AAC7C,OAAO,EACL,wBAAwB,IAAI,qBAAqB,EACjD,wBAAwB,IAAI,qBAAqB,GAClD,MAAM,2BAA2B,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,aAAa,CAAC;AAC5B,cAAc,6BAA6B,CAAC;AAC5C,OAAO,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAElD,eAAO,MAAM,oBAAoB,GAAI,eAAe,WAAW,KAAG,gBAAgB,GAAG,SAgCpF,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { iosPopoverEnterAnimation as popoverEnterAnimation, iosPopoverLeaveAnima
4
4
  export * from './tab-bar-searchable';
5
5
  export * from './searchbar';
6
6
  export * from './transition/ios.transition';
7
+ export { withNativeUIShellTransition } from './native-integration/transition';
7
8
  export { registerSegmentEffect } from './segment';
8
9
  export const registerTabBarEffect = (targetElement) => {
9
10
  const win = targetElement.ownerDocument.defaultView;
@@ -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,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"}
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;AAMhF,eAAO,MAAM,8BAA8B,GAAI,WAAW,SAAS,EAAE,UAAU,WAAW,EAAE,UAAU,WAAW,SAqEhH,CAAC"}
@@ -19,14 +19,19 @@ export const setNativeUIShellIntegration = (doc, runtime) => {
19
19
  };
20
20
  export const isNativeUIShell = (element) => element.hasAttribute('data-native-ui-shell');
21
21
  export const suspendNativeUIShell = async (scopes) => (await runtimes.get(scopes[0]?.ownerDocument)?.suspend(scopes)) ?? (() => { });
22
+ const connectedTransitions = new WeakSet();
23
+ const cancellationTransitions = new WeakSet();
22
24
  export const connectNativeUIShellTransition = (animation, entering, leaving) => {
23
- if (leaving)
25
+ if (leaving && !cancellationTransitions.has(animation)) {
26
+ cancellationTransitions.add(animation);
24
27
  animation.onFinish((step) => {
25
28
  if (step === 0)
26
29
  leaving.dispatchEvent(new CustomEvent(VERTICAL_BARS_TRANSITION_CANCELED, { bubbles: true, detail: { entering } }));
27
30
  });
28
- if (!runtimes.has(entering.ownerDocument))
31
+ }
32
+ if (connectedTransitions.has(animation) || !runtimes.has(entering.ownerDocument))
29
33
  return;
34
+ connectedTransitions.add(animation);
30
35
  const scopes = leaving ? [entering, leaving] : [entering];
31
36
  const play = animation.play.bind(animation);
32
37
  const progressStart = animation.progressStart.bind(animation);
@@ -0,0 +1,3 @@
1
+ import type { AnimationBuilder } from '@ionic/core';
2
+ export declare const withNativeUIShellTransition: (builder: AnimationBuilder) => AnimationBuilder;
3
+ //# sourceMappingURL=transition.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transition.d.ts","sourceRoot":"","sources":["../../src/native-integration/transition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAQpD,eAAO,MAAM,2BAA2B,GACrC,SAAS,gBAAgB,KAAG,gBAK5B,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { connectNativeUIShellTransition } from './index';
2
+ export const withNativeUIShellTransition = (builder) => (baseEl, opts) => {
3
+ const animation = builder(baseEl, opts);
4
+ if (opts?.enteringEl)
5
+ connectNativeUIShellTransition(animation, opts.enteringEl, opts.leavingEl);
6
+ return animation;
7
+ };
@@ -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;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"}
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;AASD,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,SAErF,CAAC"}
@@ -1,15 +1,14 @@
1
1
  import { createIosTransitionAnimation, shadow } from '@rdlabo/ionic-theme-utils';
2
2
  import { getIonPageElement } from './index';
3
- import { connectNativeUIShellTransition } from '../native-integration';
3
+ import { withNativeUIShellTransition } from '../native-integration/transition';
4
4
  export { shadow };
5
5
  const transitionConfig = {
6
6
  offLeftPercent: 30,
7
7
  getIonPageElement,
8
- connectNativeUIShellTransition,
9
8
  shouldAnimateFixedBackButton: (navEl) => !navEl.closest('ion-app.ios-theme-vertical-bars'),
10
9
  radius: 0,
11
10
  };
12
11
  export const setConfig = (config) => {
13
12
  Object.assign(transitionConfig, config);
14
13
  };
15
- export const iosTransitionAnimation = createIosTransitionAnimation(transitionConfig);
14
+ export const iosTransitionAnimation = withNativeUIShellTransition(createIosTransitionAnimation(transitionConfig));
@@ -1,4 +1,5 @@
1
1
  export { enableVerticalControlArea, setVerticalControlAreaPlacement, IonicNativeUIShell } from './native';
2
2
  export { HingeStatus } from './native';
3
+ export { withNativeUIShellTransition } from './native-integration/transition';
3
4
  export type { DeviceLayout, NativeUIShellStatus, NativeUIShellSuspension, VerticalBarEdge, VerticalControlAreaHandle } from './native';
4
5
  //# sourceMappingURL=vertical-bars.d.ts.map
@@ -1 +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"}
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,OAAO,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,eAAe,EAAE,yBAAyB,EAAE,MAAM,UAAU,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export { enableVerticalControlArea, setVerticalControlAreaPlacement, IonicNativeUIShell } from './native';
2
2
  export { HingeStatus } from './native';
3
+ export { withNativeUIShellTransition } from './native-integration/transition';
@@ -0,0 +1,149 @@
1
+ ---
2
+ title: iPhone Duo with your existing theme (experimental)
3
+ ---
4
+
5
+ # iPhone Duo with your existing theme (experimental)
6
+
7
+ Add a vertical navigation area to your Ionic app while keeping its existing theme. Tabs and supported toolbar actions move to the side of the screen; your content and horizontal controls keep their current appearance. Both Ionic `ios` and `md` modes are supported.
8
+
9
+ **Try it in Chrome first.** You can preview the layout with Web controls before setting up an iPhone Duo or an iOS build. On supported Capacitor iOS, the same Ionic markup supplies native SwiftUI controls in the system rail.
10
+
11
+ Available in `1.2.0-0` as an **experimental** feature. APIs and supported behavior may change.
12
+
13
+ ## Try it in your existing Ionic app
14
+
15
+ ### 1. Install and load the standalone stylesheet
16
+
17
+ This guide assumes an existing Ionic app with Ionic `>=8.8.1 <10` and Capacitor Core `>=8 <9`. Keep your existing Capacitor 8 installation. If your app uses another Capacitor major, migrate its Core, CLI, and platform packages together before following this guide. For a Web-only app without Capacitor, also install `@capacitor/core@^8`; the JavaScript entry point needs it even in Chrome.
18
+
19
+ ```bash
20
+ npm install @rdlabo/ionic-theme-ios27@1.2.0-0
21
+ ```
22
+
23
+ Keep your existing theme imports. Add this to your global Sass file:
24
+
25
+ ```scss
26
+ @use '@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css';
27
+ ```
28
+
29
+ The standalone JavaScript entry point needs `@capacitor/core` even in Chrome. The iOS 27 theme stylesheets are not required.
30
+
31
+ ### 2. Opt your app into the side layout
32
+
33
+ Add the class to your existing app root and keep the content inside it:
34
+
35
+ ```html
36
+ <ion-app class="ios-theme-vertical-bars">
37
+ <!-- Keep your existing pages, tabs, and toolbar controls here. -->
38
+ </ion-app>
39
+ ```
40
+
41
+ The preview reserves `80px` on the physical right. To preview the left side, also add `ios-theme-vertical-bars-left`.
42
+
43
+ ### 3. Connect your navigation animation
44
+
45
+ Configure `navAnimation` before Ionic initializes. Starting the rail runtime does not register this option. The adapter waits for native control retirement and coordinates swipe progress and cancellation while keeping your existing animation.
46
+
47
+ #### Keep Ionic's default animation
48
+
49
+ If you have not configured `navAnimation`, wrap Ionic's standard builders. Select the builder from Ionic's transition `mode` so both `ios` and `md` keep their usual animation:
50
+
51
+ ```ts
52
+ import { iosTransitionAnimation, mdTransitionAnimation, type AnimationBuilder } from '@ionic/core';
53
+ import { withNativeUIShellTransition } from '@rdlabo/ionic-theme-ios27/vertical-bars';
54
+
55
+ const defaultTransition: AnimationBuilder = (baseEl, opts) =>
56
+ (opts.mode === 'ios' ? iosTransitionAnimation : mdTransitionAnimation)(baseEl, opts);
57
+
58
+ const ionicConfig = {
59
+ navAnimation: withNativeUIShellTransition(defaultTransition),
60
+ };
61
+ ```
62
+
63
+ Merge this option into your existing Ionic configuration before initialization: pass it to Angular's `provideIonicAngular()`, React's `setupIonicReact()`, or Vue's `IonicVue` plugin options. Keep your existing theme stylesheet imports. No iOS 27 theme stylesheet is required.
64
+
65
+ #### Keep your custom animation
66
+
67
+ If your app already configures `navAnimation`, wrap that builder instead:
68
+
69
+ ```ts
70
+ import type { AnimationBuilder } from '@ionic/core';
71
+ import { withNativeUIShellTransition } from '@rdlabo/ionic-theme-ios27/vertical-bars';
72
+
73
+ // Pass the animation builder your app already uses.
74
+ const configureNavigation = (existingTransition: AnimationBuilder) => ({
75
+ navAnimation: withNativeUIShellTransition(existingTransition),
76
+ });
77
+ ```
78
+
79
+ The adapter returns the original `Animation`, preserving its effects, duration, and easing. Use it only for navigation, not modal or popover animations. The builder must return a fresh `Animation` for each navigation; Ionic destroys it after the transition. Keep lifecycle events for control registration and transitions without animation.
80
+
81
+ The adapter keeps the builder's animation targets, including any horizontal back-button effect. If you need the iOS 27 transition with that effect excluded in vertical layouts, use `iosTransitionAnimation` from `@rdlabo/ionic-theme-ios27` as your `navAnimation` instead. It already includes the adapter, so no wrapper is needed.
82
+
83
+ The adapter is available after `1.2.0-0`. With `1.2.0-0`, use the package's `iosTransitionAnimation` for iOS mode and keep your existing MD configuration.
84
+
85
+ ### 4. Start the controls after the app root is mounted
86
+
87
+ Call this once from your application startup after `ion-app` exists in the DOM:
88
+
89
+ ```ts
90
+ import { enableVerticalControlArea } from '@rdlabo/ionic-theme-ios27/vertical-bars';
91
+
92
+ const rail = await enableVerticalControlArea();
93
+ ```
94
+
95
+ **What you should see:** your existing tab bar moves to the side, and supported icon-based fixed-toolbar actions appear there too. Content keeps its existing theme and leaves room for the controls. The Web tab rail displays icons; pressing and dragging reveals tab labels.
96
+
97
+ Use your existing Ionic click handlers and routing. Text-only toolbar actions remain horizontal. Add `.ios-theme-horizontal-only` to an `ion-buttons` group or individual `ion-button` to keep an action in the horizontal toolbar.
98
+
99
+ When the application owner is disposed, call `await rail.destroy()` to restore the original controls and release the runtime. If you already use `enableNativeUIShell()`, keep that runtime and follow the [shared placement guide](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#project-controls-into-the-rail).
100
+
101
+ ### If the preview does not appear
102
+
103
+ | What you see | What to check |
104
+ | --- | --- |
105
+ | No space at the side | Load `vertical-bars.css` and put the class on `ion-app`. |
106
+ | Space appears, but controls stay horizontal | Start `enableVerticalControlArea()` after mounting the app root. Use existing tabs or supported icon-based actions in a fixed toolbar. |
107
+ | One action stays horizontal | Text-only actions, custom fills, and explicitly excluded controls keep their original presentation. See [control requirements](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#project-controls-into-the-rail). |
108
+
109
+ ## Connect an iPhone Duo
110
+
111
+ For Capacitor iOS, run `npx cap sync ios`. Build with Xcode 27.1 or newer and link against the iOS 27.1 SDK or later to receive the actual rail edge, safe-area inset, and hinge posture. The plugin uses Swift Package Manager; existing CocoaPods apps can follow [Native UI Shell setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell#enable-the-shell).
112
+
113
+ Replace the browser-only startup above with this after `ion-app` is mounted:
114
+
115
+ ```ts
116
+ import { Capacitor, type PluginListenerHandle } from '@capacitor/core';
117
+ import { enableVerticalControlArea, IonicNativeUIShell } from '@rdlabo/ionic-theme-ios27/vertical-bars';
118
+
119
+ const rail = await enableVerticalControlArea();
120
+ let layoutListener: PluginListenerHandle | undefined;
121
+
122
+ if (Capacitor.getPlatform() === 'ios') {
123
+ // The runtime already monitors device layout; only subscribe.
124
+ layoutListener = await IonicNativeUIShell.addListener('deviceLayoutChange', ({ placement }) =>
125
+ rail.setPlacement(placement),
126
+ );
127
+ rail.setPlacement((await IonicNativeUIShell.getDeviceLayout()).placement);
128
+ }
129
+
130
+ // Call when the application owner is disposed.
131
+ const stopVerticalArea = async () => {
132
+ await layoutListener?.remove();
133
+ await rail.destroy();
134
+ };
135
+ ```
136
+
137
+ `setPlacement()` applies the measured inset and resolves the logical edge through the document direction. A `null` edge restores the ordinary layout. Devices without a rail and apps built with older SDKs report `null`, so this example restores the ordinary layout there. To deliberately preview a DOM rail on such an iOS build, have your application choose a fixed edge with `rail.setPlacement('trailing')` instead of applying that null placement. This simulates the layout; it does not provide a real system rail or hinge measurements.
138
+
139
+ On supported iOS, controls in the rail use the system SwiftUI appearance; your custom Web styling still applies to ordinary content and horizontal controls. Web and Android use Web clones.
140
+
141
+ ## Use hinge posture without projecting controls
142
+
143
+ If your existing theme needs only a posture-driven split pane or a layout switch, do not start a projection runtime or add `.ios-theme-vertical-bars`. Use `getDeviceLayout()` and `deviceLayoutChange` directly, pairing `startDeviceLayoutMonitoring()` with `stopDeviceLayoutMonitoring()` and removing the listener when finished.
144
+
145
+ See [Read the device layout](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#read-the-device-layout) for the subscription example, null values, and monitoring lifetime. See [Adapt the split pane](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#adapt-the-split-pane) for the opt-in width rules and half-open state.
146
+
147
+ ## Shared layout rules and API
148
+
149
+ Safe-area handling, overlays, RTL, control eligibility, Web simulation, and the handle API are documented in [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo). Those rules apply to this standalone setup too.
@@ -4,9 +4,11 @@ title: iPhone Duo support (experimental)
4
4
 
5
5
  # iPhone Duo support (experimental)
6
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.
7
+ Adapt your Ionic app to iPhone Duo: place navigation and actions in its vertical system rail, and adjust your split pane as the device opens and closes. Existing Ionic markup remains the source of labels, icons, routing, and click handlers.
8
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.
9
+ **New here?** Start with [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme) to preview the side layout in Chrome. This page explains device events, placement, split panes, and the API.
10
+
11
+ Available in `1.2.0-0` as an **experimental** feature alongside [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell). APIs and supported behavior may change. The real system rail and hinge reporting require iOS 27.1 or later and an app built with Xcode 27.1 or newer.
10
12
 
11
13
  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
14
 
@@ -18,9 +20,12 @@ The pieces map to distinct responsibilities. The plugin reports **device facts**
18
20
 
19
21
  ## Choose what to adopt
20
22
 
23
+ To keep your existing theme and add only the standalone support, follow [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme). This page covers the shared device-layout rules and API.
24
+
21
25
  | Goal | Stylesheet | Runtime |
22
26
  | ------------------------------------------------ | ------------------- | ------------------------------------------------------------------ |
23
- | Hinge posture only (split pane, layout switches) | `vertical-bars.css` | none — subscribe to the plugin directly |
27
+ | Hinge posture only (layout switches) | none | none — subscribe to the plugin directly |
28
+ | Posture-driven split-pane width | `vertical-bars.css` | none — subscribe to the plugin directly |
24
29
  | Vertical rail for tabs and toolbar actions | `vertical-bars.css` | `enableVerticalControlArea()` |
25
30
  | Native shell plus the rail | `vertical-bars.css` | `enableNativeUIShell()` — already includes rail and posture support |
26
31
 
@@ -91,17 +96,24 @@ The mode is component-mode independent: an app can keep Ionic `mode: 'md'` on iO
91
96
  Start the standalone runtime once at application startup:
92
97
 
93
98
  ```ts
94
- import { Capacitor } from '@capacitor/core';
99
+ import { Capacitor, type PluginListenerHandle } from '@capacitor/core';
95
100
  import { enableVerticalControlArea, IonicNativeUIShell } from '@rdlabo/ionic-theme-ios27/vertical-bars';
96
101
 
97
102
  // Start on Chrome too; the Web projection stays idle until the class is present.
98
103
  const rail = await enableVerticalControlArea();
104
+ let layoutListener: PluginListenerHandle | undefined;
99
105
 
100
106
  if (Capacitor.getPlatform() === 'ios') {
101
107
  // The runtime already monitors device layout; only subscribe.
102
- await IonicNativeUIShell.addListener('deviceLayoutChange', ({ placement }) => rail.setPlacement(placement));
108
+ layoutListener = await IonicNativeUIShell.addListener('deviceLayoutChange', ({ placement }) => rail.setPlacement(placement));
103
109
  rail.setPlacement((await IonicNativeUIShell.getDeviceLayout()).placement);
104
110
  }
111
+
112
+ // Call when the application owner is disposed.
113
+ const stopVerticalArea = async () => {
114
+ await layoutListener?.remove();
115
+ await rail.destroy();
116
+ };
105
117
  ```
106
118
 
107
119
  `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.
package/docs/migration.md CHANGED
@@ -4,6 +4,29 @@ title: Migration
4
4
 
5
5
  # Migration
6
6
 
7
+ ## Native UI Shell transition adapter
8
+
9
+ Use `withNativeUIShellTransition()` to keep your existing Ionic navigation animation while coordinating Native UI Shell controls.
10
+
11
+ - If you already use this package's `iosTransitionAnimation`, no configuration change is needed. It now uses the shared adapter internally; do not add another wrapper.
12
+ - If you use Ionic's default animation without a `navAnimation` option, follow [Keep Ionic's default animation](./iphone-duo-with-original-theme.md#keep-ionics-default-animation). The example selects Ionic's standard iOS or MD builder from the transition mode.
13
+ - If you use a custom navigation animation with Native UI Shell or the standalone Vertical Control Area, wrap your existing builder when configuring Ionic:
14
+
15
+ ```diff
16
+ + import { withNativeUIShellTransition } from '@rdlabo/ionic-theme-ios27/vertical-bars';
17
+
18
+ const ionicConfig = {
19
+ - navAnimation: existingTransition,
20
+ + navAnimation: withNativeUIShellTransition(existingTransition),
21
+ };
22
+ ```
23
+
24
+ Merge this option into your existing Ionic configuration before initialization. The adapter preserves the animation's effects, duration, and easing while coordinating native retirement, swipe progress, and cancellation. Keep your existing theme stylesheet imports and Native UI Shell or Vertical Control Area startup.
25
+
26
+ Use the adapter only for navigation; leave modal and popover animations unchanged. Your builder must create a fresh `Animation` for each navigation because Ionic destroys it afterward. Keep lifecycle events for control registration and transitions without animation. If the custom builder animates a horizontal back button separately, exclude that effect while `.ios-theme-vertical-bars` is active.
27
+
28
+ See [Connect your navigation animation](./iphone-duo-with-original-theme.md#3-connect-your-navigation-animation) for the setup and supported scope.
29
+
7
30
  ## From the iOS 26 theme
8
31
 
9
32
  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.
@@ -116,7 +116,7 @@ FAB support covers the standard circular glass appearance, text and resolved sta
116
116
 
117
117
  Custom host animation or transition declarations on the FAB, list or button keep the group on the Web until removed. Button transforms support the standard identity and hidden-child scale(0), not custom scaling. For children inside a `display:none` list, browsers can report a computed transform of `none` even when a custom transform is declared. Such transforms are checked when layout becomes available; the whole FAB then returns to Web if needed. The plugin does not parse application stylesheets or temporarily open lists to predict hidden layout.
118
118
 
119
- The connection in `src/transition/ios.transition.ts` waits for native retirement before starting the Web animation. Interactive progress and completion/cancellation are queued while that retirement is in progress. Stationary shared tabs are retained. First render and transitions without an animation builder are covered by the startup runtime and Ionic lifecycle events.
119
+ The built-in `iosTransitionAnimation` waits for native retirement before starting the Web animation. Interactive progress and completion/cancellation are queued while that retirement is in progress. Stationary shared tabs are retained. First render and transitions without an animation builder are covered by the startup runtime and Ionic lifecycle events. Default Ionic and custom navigation builders can use the same integration through `withNativeUIShellTransition()`, exported from both the package root and `/vertical-bars`. See [Connect your navigation animation](./iphone-duo-with-original-theme.md#3-connect-your-navigation-animation) for the setup.
120
120
 
121
121
  Tab switches skip the Web/native crossfade so a retiring UIKit snapshot cannot linger over the next tab. Detection uses the router URL versus the still-selected tab on `ionViewWillLeave` (and vanilla `ionTabsWillChange` / `ionTabsDidChange` DOM events). Stack pushes and pops keep the normal 180ms handoff.
122
122
 
@@ -169,7 +169,7 @@ The native material and control appearance follow the running iOS version; an iO
169
169
 
170
170
  ## Support iPhone Duo (experimental)
171
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.
172
+ iPhone Duo support is experimental, including standalone use without Native UI Shell. It is available in the `1.2.0-0` prerelease alongside Native UI Shell. Its APIs and supported behavior may change.
173
173
 
174
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
175
 
@@ -179,6 +179,8 @@ The SwiftUI surface is clipped and hit-tested to the system rail. Web content re
179
179
 
180
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
181
 
182
+ For standalone setup while keeping your existing theme, see [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme).
183
+
182
184
  ## Native UI Shell API
183
185
 
184
186
  The generated reference below documents the handle returned by `enableNativeUIShell()`. The underlying Capacitor bridge and its control-snapshot protocol are implementation details.
@@ -49,10 +49,12 @@ These classes do not reposition a separate `ion-fab`; leave room for it when cho
49
49
 
50
50
  ## Support iPhone Duo (experimental)
51
51
 
52
- iPhone Duo support is experimental. It is planned for release alongside Native UI Shell. Its APIs and supported behavior may change.
52
+ iPhone Duo support is experimental. It is available in the `1.2.0-0` prerelease alongside Native UI Shell. Its APIs and supported behavior may change.
53
53
 
54
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
55
 
56
+ For standalone setup while keeping your existing theme, see [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme).
57
+
56
58
  ## Two-line inset list items
57
59
 
58
60
  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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rdlabo/ionic-theme-ios27",
3
3
  "private": false,
4
- "version": "1.2.0-0",
4
+ "version": "1.2.0-beta.pr225.sha520dc52f7232",
5
5
  "description": "iOS27 Theme for Ionic Framework",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ export {
8
8
  export * from './tab-bar-searchable';
9
9
  export * from './searchbar';
10
10
  export * from './transition/ios.transition';
11
+ export { withNativeUIShellTransition } from './native-integration/transition';
11
12
  export { registerSegmentEffect } from './segment';
12
13
 
13
14
  export const registerTabBarEffect = (targetElement: HTMLElement): registeredEffect | undefined => {
@@ -44,13 +44,19 @@ export const isNativeUIShell = (element: HTMLElement) => element.hasAttribute('d
44
44
  export const suspendNativeUIShell = async (scopes: HTMLElement[]): Promise<(canceled?: boolean) => void> =>
45
45
  (await runtimes.get(scopes[0]?.ownerDocument)?.suspend(scopes)) ?? (() => {});
46
46
 
47
+ const connectedTransitions = new WeakSet<Animation>();
48
+ const cancellationTransitions = new WeakSet<Animation>();
49
+
47
50
  /** Ionic write hooks cannot await the bridge. Gate playback, including interactive playback. */
48
51
  export const connectNativeUIShellTransition = (animation: Animation, entering: HTMLElement, leaving?: HTMLElement) => {
49
- if (leaving)
52
+ if (leaving && !cancellationTransitions.has(animation)) {
53
+ cancellationTransitions.add(animation);
50
54
  animation.onFinish((step) => {
51
55
  if (step === 0) leaving.dispatchEvent(new CustomEvent(VERTICAL_BARS_TRANSITION_CANCELED, { bubbles: true, detail: { entering } }));
52
56
  });
53
- if (!runtimes.has(entering.ownerDocument)) return;
57
+ }
58
+ if (connectedTransitions.has(animation) || !runtimes.has(entering.ownerDocument)) return;
59
+ connectedTransitions.add(animation);
54
60
  const scopes = leaving ? [entering, leaving] : [entering];
55
61
  const play = animation.play.bind(animation);
56
62
  const progressStart = animation.progressStart.bind(animation);
@@ -0,0 +1,15 @@
1
+ import type { AnimationBuilder } from '@ionic/core';
2
+ import { connectNativeUIShellTransition } from './index';
3
+
4
+ /**
5
+ * Keeps an existing Ionic navigation animation and coordinates native control handoff.
6
+ * Register the returned builder as navAnimation. Do not use it for overlay animations.
7
+ * The builder must return a fresh Animation for each navigation; Ionic destroys it afterward.
8
+ */
9
+ export const withNativeUIShellTransition =
10
+ (builder: AnimationBuilder): AnimationBuilder =>
11
+ (baseEl, opts) => {
12
+ const animation = builder(baseEl, opts);
13
+ if (opts?.enteringEl) connectNativeUIShellTransition(animation, opts.enteringEl, opts.leavingEl);
14
+ return animation;
15
+ };
@@ -2,7 +2,7 @@ import { createIosTransitionAnimation, shadow } from '@rdlabo/ionic-theme-utils'
2
2
  import type { Animation } from '@ionic/core';
3
3
  import type { TransitionOptions } from './index';
4
4
  import { getIonPageElement } from './index';
5
- import { connectNativeUIShellTransition } from '../native-integration';
5
+ import { withNativeUIShellTransition } from '../native-integration/transition';
6
6
 
7
7
  export { shadow };
8
8
 
@@ -13,7 +13,6 @@ export interface IosTransitionConfig {
13
13
  const transitionConfig = {
14
14
  offLeftPercent: 30,
15
15
  getIonPageElement,
16
- connectNativeUIShellTransition,
17
16
  shouldAnimateFixedBackButton: (navEl: HTMLElement) => !navEl.closest('ion-app.ios-theme-vertical-bars'),
18
17
  radius: 0,
19
18
  };
@@ -22,5 +21,6 @@ export const setConfig = (config: Partial<IosTransitionConfig>): void => {
22
21
  Object.assign(transitionConfig, config);
23
22
  };
24
23
 
25
- export const iosTransitionAnimation: (navEl: HTMLElement, opts: TransitionOptions) => Animation =
26
- createIosTransitionAnimation<TransitionOptions>(transitionConfig);
24
+ export const iosTransitionAnimation: (navEl: HTMLElement, opts: TransitionOptions) => Animation = withNativeUIShellTransition(
25
+ createIosTransitionAnimation<TransitionOptions>(transitionConfig),
26
+ );
@@ -1,3 +1,4 @@
1
1
  export { enableVerticalControlArea, setVerticalControlAreaPlacement, IonicNativeUIShell } from './native';
2
2
  export { HingeStatus } from './native';
3
+ export { withNativeUIShellTransition } from './native-integration/transition';
3
4
  export type { DeviceLayout, NativeUIShellStatus, NativeUIShellSuspension, VerticalBarEdge, VerticalControlAreaHandle } from './native';