@rdlabo/ionic-theme-ios27 1.2.1 → 2.0.0-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +24 -279
  2. package/dist/css/components/ion-toolbar.css +1 -1
  3. package/dist/css/ionic-theme-ios27.css +1 -1
  4. package/dist/css/vertical-bars.css +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/layout/index.d.ts +18 -0
  10. package/dist/layout/index.d.ts.map +1 -0
  11. package/dist/layout/index.js +26 -0
  12. package/dist/layout/index.js.map +1 -0
  13. package/dist/segment/index.js +3 -3
  14. package/dist/segment/index.js.map +1 -1
  15. package/dist/sheets-of-glass/index.js +7 -7
  16. package/dist/sheets-of-glass/index.js.map +1 -1
  17. package/dist/tab-bar-searchable/dismiss-button.js +53 -3
  18. package/dist/tab-bar-searchable/dismiss-button.js.map +1 -1
  19. package/dist/tab-bar-searchable/index.js +18 -3
  20. package/dist/tab-bar-searchable/index.js.map +1 -1
  21. package/dist/transition/ios.transition.d.ts.map +1 -1
  22. package/dist/transition/ios.transition.js +7 -4
  23. package/dist/transition/ios.transition.js.map +1 -1
  24. package/dist/transition/native-ui.d.ts +6 -0
  25. package/dist/transition/native-ui.d.ts.map +1 -0
  26. package/dist/{native-integration/index.js → transition/native-ui.js} +16 -30
  27. package/dist/transition/native-ui.js.map +1 -0
  28. package/docs/adaptive-themes.md +161 -0
  29. package/docs/add-native-ui.md +53 -0
  30. package/docs/animation.md +19 -3
  31. package/docs/eslint.md +3 -1
  32. package/docs/features.md +9 -5
  33. package/docs/migration.md +33 -22
  34. package/docs/setup.md +94 -0
  35. package/docs/special-markup.md +41 -10
  36. package/docs/using-ion-item-group.md +2 -2
  37. package/docs/vertical-bars-layout.md +137 -0
  38. package/package.json +12 -42
  39. package/src/index.ts +3 -1
  40. package/src/layout/index.ts +29 -0
  41. package/src/segment/index.ts +3 -3
  42. package/src/sheets-of-glass/index.ts +7 -7
  43. package/src/styles/components/ion-toolbar.scss +1 -1
  44. package/src/styles/vertical-bars.scss +15 -18
  45. package/src/tab-bar-searchable/dismiss-button.ts +73 -4
  46. package/src/tab-bar-searchable/index.ts +20 -3
  47. package/src/transition/ios.transition.ts +7 -4
  48. package/src/{native-integration/index.ts → transition/native-ui.ts} +17 -46
  49. package/Package.swift +0 -21
  50. package/dist/_virtual/_rolldown/runtime.js +0 -13
  51. package/dist/native/components/index.d.ts +0 -19
  52. package/dist/native/components/index.d.ts.map +0 -1
  53. package/dist/native/components/index.js +0 -40
  54. package/dist/native/components/index.js.map +0 -1
  55. package/dist/native/components/ion-back-button.d.ts +0 -9
  56. package/dist/native/components/ion-back-button.d.ts.map +0 -1
  57. package/dist/native/components/ion-back-button.js +0 -20
  58. package/dist/native/components/ion-back-button.js.map +0 -1
  59. package/dist/native/components/ion-button.d.ts +0 -10
  60. package/dist/native/components/ion-button.d.ts.map +0 -1
  61. package/dist/native/components/ion-button.js +0 -23
  62. package/dist/native/components/ion-button.js.map +0 -1
  63. package/dist/native/components/ion-buttons.d.ts +0 -11
  64. package/dist/native/components/ion-buttons.d.ts.map +0 -1
  65. package/dist/native/components/ion-buttons.js +0 -30
  66. package/dist/native/components/ion-buttons.js.map +0 -1
  67. package/dist/native/components/ion-fab.d.ts +0 -10
  68. package/dist/native/components/ion-fab.d.ts.map +0 -1
  69. package/dist/native/components/ion-fab.js +0 -132
  70. package/dist/native/components/ion-fab.js.map +0 -1
  71. package/dist/native/components/ion-menu-button.d.ts +0 -13
  72. package/dist/native/components/ion-menu-button.d.ts.map +0 -1
  73. package/dist/native/components/ion-menu-button.js +0 -31
  74. package/dist/native/components/ion-menu-button.js.map +0 -1
  75. package/dist/native/components/ion-segment.d.ts +0 -11
  76. package/dist/native/components/ion-segment.d.ts.map +0 -1
  77. package/dist/native/components/ion-segment.js +0 -30
  78. package/dist/native/components/ion-segment.js.map +0 -1
  79. package/dist/native/components/ion-tab-bar.d.ts +0 -11
  80. package/dist/native/components/ion-tab-bar.d.ts.map +0 -1
  81. package/dist/native/components/ion-tab-bar.js +0 -50
  82. package/dist/native/components/ion-tab-bar.js.map +0 -1
  83. package/dist/native/components/searchable-tabs.d.ts +0 -20
  84. package/dist/native/components/searchable-tabs.d.ts.map +0 -1
  85. package/dist/native/components/searchable-tabs.js +0 -383
  86. package/dist/native/components/searchable-tabs.js.map +0 -1
  87. package/dist/native/definitions.d.ts +0 -152
  88. package/dist/native/definitions.d.ts.map +0 -1
  89. package/dist/native/definitions.js +0 -0
  90. package/dist/native/foldable/fold-state.d.ts +0 -5
  91. package/dist/native/foldable/fold-state.d.ts.map +0 -1
  92. package/dist/native/foldable/fold-state.js +0 -14
  93. package/dist/native/foldable/fold-state.js.map +0 -1
  94. package/dist/native/foldable/types.d.ts +0 -12
  95. package/dist/native/foldable/types.d.ts.map +0 -1
  96. package/dist/native/foldable/types.js +0 -0
  97. package/dist/native/index.d.ts +0 -11
  98. package/dist/native/index.d.ts.map +0 -1
  99. package/dist/native/index.js +0 -242
  100. package/dist/native/index.js.map +0 -1
  101. package/dist/native/prehide.d.ts +0 -7
  102. package/dist/native/prehide.d.ts.map +0 -1
  103. package/dist/native/prehide.js +0 -230
  104. package/dist/native/prehide.js.map +0 -1
  105. package/dist/native/runtime.d.ts +0 -5
  106. package/dist/native/runtime.d.ts.map +0 -1
  107. package/dist/native/runtime.js +0 -640
  108. package/dist/native/runtime.js.map +0 -1
  109. package/dist/native/shared/candidate.d.ts +0 -21
  110. package/dist/native/shared/candidate.d.ts.map +0 -1
  111. package/dist/native/shared/candidate.js +0 -98
  112. package/dist/native/shared/candidate.js.map +0 -1
  113. package/dist/native/shared/crossfade.d.ts +0 -10
  114. package/dist/native/shared/crossfade.d.ts.map +0 -1
  115. package/dist/native/shared/crossfade.js +0 -50
  116. package/dist/native/shared/crossfade.js.map +0 -1
  117. package/dist/native/shared/dom.d.ts +0 -46
  118. package/dist/native/shared/dom.d.ts.map +0 -1
  119. package/dist/native/shared/dom.js +0 -172
  120. package/dist/native/shared/dom.js.map +0 -1
  121. package/dist/native/shared/events.d.ts +0 -11
  122. package/dist/native/shared/events.d.ts.map +0 -1
  123. package/dist/native/shared/events.js +0 -13
  124. package/dist/native/shared/events.js.map +0 -1
  125. package/dist/native/shared/icons.d.ts +0 -8
  126. package/dist/native/shared/icons.d.ts.map +0 -1
  127. package/dist/native/shared/icons.js +0 -61
  128. package/dist/native/shared/icons.js.map +0 -1
  129. package/dist/native/shared/modal.d.ts +0 -10
  130. package/dist/native/shared/modal.d.ts.map +0 -1
  131. package/dist/native/shared/modal.js +0 -160
  132. package/dist/native/shared/modal.js.map +0 -1
  133. package/dist/native/vertical-bars-web-search.d.ts +0 -9
  134. package/dist/native/vertical-bars-web-search.d.ts.map +0 -1
  135. package/dist/native/vertical-bars-web-search.js +0 -84
  136. package/dist/native/vertical-bars-web-search.js.map +0 -1
  137. package/dist/native/vertical-bars-web.d.ts +0 -5
  138. package/dist/native/vertical-bars-web.d.ts.map +0 -1
  139. package/dist/native/vertical-bars-web.js +0 -341
  140. package/dist/native/vertical-bars-web.js.map +0 -1
  141. package/dist/native-integration/index.d.ts +0 -23
  142. package/dist/native-integration/index.d.ts.map +0 -1
  143. package/dist/native-integration/index.js.map +0 -1
  144. package/dist/native-integration/transition.d.ts +0 -5
  145. package/dist/native-integration/transition.d.ts.map +0 -1
  146. package/dist/native-integration/transition.js +0 -16
  147. package/dist/native-integration/transition.js.map +0 -1
  148. package/dist/vertical-bars.d.ts +0 -5
  149. package/dist/vertical-bars.js +0 -4
  150. package/docs/e2e-testing.md +0 -51
  151. package/docs/feedback.md +0 -65
  152. package/docs/iphone-duo-with-original-theme.md +0 -158
  153. package/docs/iphone-duo.md +0 -150
  154. package/docs/native-ui-shell.md +0 -285
  155. package/docs/vertical-bars.md +0 -285
  156. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButton.swift +0 -71
  157. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButtons.swift +0 -24
  158. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellComponents.swift +0 -18
  159. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellFab.swift +0 -83
  160. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +0 -432
  161. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSegment.swift +0 -91
  162. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +0 -221
  163. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +0 -437
  164. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +0 -118
  165. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +0 -396
  166. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellCrossfade.swift +0 -35
  167. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellHost.swift +0 -8
  168. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellRendering.swift +0 -27
  169. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +0 -135
  170. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +0 -650
  171. package/src/native/components/index.ts +0 -53
  172. package/src/native/components/ion-back-button.ts +0 -19
  173. package/src/native/components/ion-button.ts +0 -24
  174. package/src/native/components/ion-buttons.ts +0 -38
  175. package/src/native/components/ion-fab.ts +0 -151
  176. package/src/native/components/ion-menu-button.ts +0 -24
  177. package/src/native/components/ion-segment.ts +0 -27
  178. package/src/native/components/ion-tab-bar.ts +0 -50
  179. package/src/native/components/searchable-tabs.ts +0 -459
  180. package/src/native/definitions.ts +0 -191
  181. package/src/native/foldable/fold-state.ts +0 -9
  182. package/src/native/foldable/types.ts +0 -4
  183. package/src/native/index.ts +0 -281
  184. package/src/native/prehide.ts +0 -265
  185. package/src/native/runtime.ts +0 -781
  186. package/src/native/shared/candidate.ts +0 -150
  187. package/src/native/shared/crossfade.ts +0 -50
  188. package/src/native/shared/dom.ts +0 -286
  189. package/src/native/shared/events.ts +0 -8
  190. package/src/native/shared/icons.ts +0 -57
  191. package/src/native/shared/modal.ts +0 -157
  192. package/src/native/vertical-bars-web-search.ts +0 -80
  193. package/src/native/vertical-bars-web.ts +0 -411
  194. package/src/native-integration/transition.ts +0 -15
  195. package/src/vertical-bars.ts +0 -11
@@ -1,158 +0,0 @@
1
- ---
2
- title: iPhone Duo with your existing theme (preview)
3
- ---
4
-
5
- # iPhone Duo with your existing theme (preview)
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` as a **preview** 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
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
- #### Use this package's iOS animation
66
-
67
- If you already use the iOS 27 transition, keep this configuration. It includes the native adapter and excludes the horizontal back-button effect in vertical layouts; no additional wrapper is needed. Importing this JavaScript entry point does not load the theme stylesheets.
68
-
69
- ```ts
70
- import { iosTransitionAnimation } from '@rdlabo/ionic-theme-ios27';
71
-
72
- const ionicConfig = {
73
- navAnimation: iosTransitionAnimation,
74
- };
75
- ```
76
-
77
- Apply this option to your existing iOS-mode configuration and keep your MD configuration.
78
-
79
- #### Keep your custom animation
80
-
81
- If your app uses another builder for `navAnimation`, wrap it:
82
-
83
- ```ts
84
- import type { AnimationBuilder } from '@ionic/core';
85
- import { withNativeUIShellTransition } from '@rdlabo/ionic-theme-ios27/vertical-bars';
86
-
87
- // Pass the animation builder your app already uses.
88
- const configureNavigation = (existingTransition: AnimationBuilder) => ({
89
- navAnimation: withNativeUIShellTransition(existingTransition),
90
- });
91
- ```
92
-
93
- 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.
94
-
95
- 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.
96
-
97
- ### 4. Start the controls after the app root is mounted
98
-
99
- Call this once from your application startup after `ion-app` exists in the DOM:
100
-
101
- ```ts
102
- import { enableVerticalControlArea } from '@rdlabo/ionic-theme-ios27/vertical-bars';
103
-
104
- const rail = await enableVerticalControlArea();
105
- ```
106
-
107
- **What you should see:** your existing tab bar moves to the side, and fixed-toolbar buttons with an `ion-icon` or SVG using `slot="icon-only"` 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.
108
-
109
- Use your existing Ionic click handlers, routing, and form associations. All button fills (`default`, `clear`, `solid`, and `outline`) use the same `icon-only` rule, including submit buttons. Actions without that slot remain horizontal. Add `.ios-theme-horizontal-only` to an `ion-buttons` group or individual `ion-button` to keep an action in the horizontal toolbar.
110
-
111
- 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).
112
-
113
- ### Optional: choose native button appearance
114
-
115
- `buttonProjection` and local projection settings are available in `1.2.0`. See [Choose button appearance](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#choose-button-appearance) for availability and migration details.
116
-
117
- The new default is `system`: SwiftUI styles vertical buttons and tints their icons. If your existing theme should supply their fill and colors, use:
118
-
119
- ```ts
120
- const rail = await enableVerticalControlArea({ buttonProjection: 'source', buttonDefaultFill: 'solid' });
121
- ```
122
-
123
- The `solid` default suits ordinary Ionic buttons. Buttons inside `ion-buttons` still default to clear; set `fill="solid"` explicitly to project their background. For one-off exceptions, use the [local projection settings](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#override-individual-buttons-or-groups). These settings affect native vertical buttons only; Web clones keep their existing appearance.
124
-
125
- ### If the preview does not appear
126
-
127
- | What you see | What to check |
128
- | --- | --- |
129
- | No space at the side | Load `vertical-bars.css` and put the class on `ion-app`. |
130
- | Space appears, but controls stay horizontal | Start `enableVerticalControlArea()` after mounting the app root. Use existing tabs or `slot="icon-only"` actions in a fixed header/footer toolbar. |
131
- | One action stays horizontal | Check for `slot="icon-only"` on the icon and a fixed toolbar outside scrolling content. Explicitly excluded controls and controls in centered modals stay horizontal; `fill` and `type="submit"` do not prevent movement. See [control requirements](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions). |
132
-
133
- ## Connect an iPhone Duo
134
-
135
- Install [`@erkamyaman/capacitor-foldable`](https://github.com/erkamyaman/capacitor-foldable) for device state:
136
-
137
- ```bash
138
- npm install @erkamyaman/capacitor-foldable
139
- npx cap sync ios
140
- ```
141
-
142
- Use Capacitor 8.5 or later and build with Xcode 27.1 or newer for actual rail placement and hinge posture on iOS 27.1. Native UI Shell 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). Keep this package's `vertical-bars.css`; the device plugin's `ionic-tabs.css` is not needed with our rail projection.
143
-
144
- Replace the browser-only startup with the [device placement setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#project-controls-into-the-rail). That setup sends initial values and `barPlacementChange` events to `setVerticalControlAreaPlacement({ edge: verticalBarEdge, nativeEdge: verticalBarEdge, inset })`, with subscriptions and cleanup kept in your application.
145
-
146
- Pass both the requested and native edge with Foldable's measured inset. The placement API resolves RTL. A null edge restores the ordinary layout.
147
-
148
- On supported iOS, controls in the rail use native SwiftUI rendering; your custom Web styling still applies to ordinary content and horizontal controls. Web and Android use Web clones.
149
-
150
- ## Use hinge posture without projecting controls
151
-
152
- 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`. Pass `Foldable.getFoldState()` results and `foldStateChange` events to `applyFoldStateClasses(root, fold)`, removing the listener when finished. There is no separate start/stop monitoring call.
153
-
154
- 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.
155
-
156
- ## Shared layout rules and API
157
-
158
- Safe-area handling, overlays, RTL, control eligibility, Web simulation, and the handle API are documented in [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars). Those rules apply to this standalone setup too.
@@ -1,150 +0,0 @@
1
- ---
2
- title: iPhone Duo support (preview)
3
- ---
4
-
5
- # iPhone Duo support (preview)
6
-
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
-
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 and split panes. [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars) documents control projection and the runtime API.
10
-
11
- Available in `1.2.0` as a **preview** feature alongside [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell). APIs and supported behavior may change before stable status. Stable status is planned after the official release of Xcode 27.1. The real system rail and hinge reporting require iOS 27.1 or later and an app built with Xcode 27.1 or newer.
12
-
13
- This package provides two independent pieces for that hardware. Each works **without the iOS 27 theme stylesheets** and **without the full Native UI Shell**:
14
-
15
- - `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.
16
- - `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.
17
-
18
- Device state belongs to [`@erkamyaman/capacitor-foldable`](https://github.com/erkamyaman/capacitor-foldable). The **application** subscribes to its events and chooses its layout. This package's **stylesheet and runtime** apply that decision by reserving space, projecting controls and adapting the split pane. The theme does not monitor hinge state or bar placement.
19
-
20
- ## Choose what to adopt
21
-
22
- 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.
23
-
24
- | Goal | Stylesheet | Runtime |
25
- | ------------------------------------------------ | ------------------- | ------------------------------------------------------------------ |
26
- | Hinge posture only (layout switches) | none | none — subscribe to `Foldable` directly |
27
- | Posture-driven split-pane width | `vertical-bars.css` | none — subscribe to `Foldable` directly |
28
- | Vertical rail for tabs and toolbar actions | `vertical-bars.css` | `enableVerticalControlArea()` |
29
- | Native shell plus the rail | `vertical-bars.css` | `enableNativeUIShell()` — includes rail projection |
30
-
31
- ```scss
32
- @use '@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css';
33
- ```
34
-
35
- 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.
36
-
37
- 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.
38
-
39
- ## Read the device layout
40
-
41
- Install the device-state plugin in the application, then sync the native project:
42
-
43
- ```bash
44
- npm install @erkamyaman/capacitor-foldable
45
- npx cap sync
46
- ```
47
-
48
- Use Capacitor 8.5 or later and build with Xcode 27.1 or newer for iPhone Duo's iOS 27.1 APIs. The dependency is needed for device-driven layout, not for the theme's CSS, browser simulation, or native control projection alone. Do not import the plugin's `ionic-tabs.css` alongside this package's rail projection; both would reposition the same tabs.
49
-
50
- After `ion-app` is mounted, pass device state to `applyFoldStateClasses`. The application owns subscriptions and cleanup. No projection runtime is needed for posture-driven layout.
51
-
52
- ```ts
53
- import { Foldable } from '@erkamyaman/capacitor-foldable';
54
- import { applyFoldStateClasses } from '@rdlabo/ionic-theme-ios27/vertical-bars';
55
-
56
- const root = document.querySelector('ion-app')!;
57
- const listener = await Foldable.addListener('foldStateChange', (fold) => applyFoldStateClasses(root, fold));
58
- applyFoldStateClasses(root, await Foldable.getFoldState());
59
- ```
60
-
61
- When the application owner is disposed:
62
-
63
- ```ts
64
- await listener.remove();
65
- ```
66
-
67
- Subscribe to changes, then read the current state. The same helper handles initial values and events.
68
-
69
- `applyFoldStateClasses` keeps one of `ios-theme-fold-flat`, `ios-theme-fold-half-opened`, and `ios-theme-fold-closed` on the supplied root, preserving unrelated classes. It also sets `ios-theme-fold-expanded` for a half-opened state or a flat state with hinge geometry. A flat state without geometry (including the Web fallback) and a closed state clear that class. The helper does not subscribe to the plugin or change Ionic's split-pane `when` property.
70
-
71
- For rail placement, use `setVerticalControlAreaPlacement` as shown below. Pass the reported logical edge as both `edge` and `nativeEdge`, together with the measured `inset`. Leading is the physical left in LTR and the physical right in RTL. A null edge restores the ordinary layout; an inset of zero clears the explicit width. No start/stop monitoring calls are needed.
72
-
73
- WebView corner radius remains a rendering concern: `configureNativeTransition()` uses the shell's `getWebViewMetrics()` API, independently of `Foldable`.
74
-
75
- **Migration:** the theme's former `DeviceLayout`, `HingeStatus`, `getDeviceLayout()`, `deviceLayoutChange`, and start/stop device-layout monitoring APIs have been removed. Replace device subscriptions with the `Foldable` APIs above; use `getWebViewMetrics()` for one-shot radius reads. Foldable can infer Duo bar placement from safe-area insets when the app is built without the iOS 27.1 SDK. Hinge data still requires the newer SDK. Apps can also request a fixed rail placement independently of the reported edge.
76
-
77
- ## Reserve the vertical rail
78
-
79
- Load `vertical-bars.css` as shown above. For class-based browser simulation, safe-area handling, RTL and overlay layout, see [Reserve the vertical rail](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#reserve-the-vertical-rail). On device, the placement helper below applies the layout classes and measured inset.
80
-
81
- ## Project controls into the rail
82
-
83
- Start the standalone runtime once after `ion-app` is mounted, and apply the plugin's placement with `setVerticalControlAreaPlacement`:
84
-
85
- ```ts
86
- import { Foldable } from '@erkamyaman/capacitor-foldable';
87
- import { setVerticalControlAreaPlacement, enableVerticalControlArea } from '@rdlabo/ionic-theme-ios27/vertical-bars';
88
-
89
- const rail = await enableVerticalControlArea();
90
- const listener = await Foldable.addListener('barPlacementChange', ({ verticalBarEdge, inset }) =>
91
- setVerticalControlAreaPlacement({ edge: verticalBarEdge, nativeEdge: verticalBarEdge, inset }),
92
- );
93
- const { verticalBarEdge, inset } = await Foldable.getBarPlacement();
94
- setVerticalControlAreaPlacement({ edge: verticalBarEdge, nativeEdge: verticalBarEdge, inset });
95
- ```
96
-
97
- When the application owner is disposed:
98
-
99
- ```ts
100
- await listener.remove();
101
- await rail.destroy();
102
- ```
103
-
104
- Supply `nativeEdge` on both the initial read and each event so the renderer knows which rail the system actually provides. The measured `inset` is passed through instead of assuming a fixed width. Devices without a reported rail, including Web and Android, return a null edge and retain the ordinary layout. For browser simulation, use the [class-based preview](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#reserve-the-vertical-rail) without connecting device placement.
105
-
106
- Start either `enableVerticalControlArea()` or the full `enableNativeUIShell()` — not both. If the app already uses Native UI Shell, keep that runtime and use the same `setVerticalControlAreaPlacement` callback. The application owner removes its listeners and destroys its runtime on teardown.
107
-
108
- 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. See [Toolbar actions](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions) for control eligibility.
109
-
110
- ### Toolbar actions
111
-
112
- See [Vertical Bars: Toolbar actions](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions) for eligible markup, placement, button appearance and local overrides.
113
-
114
- ### Tab bar
115
-
116
- See [Vertical Bars: Tab bar](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#tab-bar) for navigation, labels and Web fallback behavior.
117
-
118
- ## Adapt the split pane
119
-
120
- 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:
121
-
122
- ```html
123
- <ion-split-pane
124
- class="split-pane-fold-layout"
125
- contentId="main-content"
126
- when="(min-width: 900px)"
127
- >
128
- <ion-menu contentId="main-content">...</ion-menu>
129
- <div id="main-content">...</div>
130
- </ion-split-pane>
131
- ```
132
-
133
- Set the ordinary split-pane width to 320pt in the application's stylesheet, and let the half-open class change only the width value:
134
-
135
- ```css
136
- ion-split-pane.split-pane-fold-layout {
137
- --ios-theme-menu-width: var(--ios-theme-split-pane-width);
138
- --side-width: var(--ios-theme-menu-width);
139
- --side-max-width: var(--ios-theme-menu-width);
140
- transition: --ios-theme-split-pane-width 300ms ease;
141
- }
142
- ```
143
-
144
- The registered `--ios-theme-split-pane-width` defaults to `320px`. `applyFoldStateClasses` sets `ios-theme-fold-half-opened` on `ion-app`; the stylesheet then sets only descendant split panes with `split-pane-fold-layout` to `50vw`. Add this opt-in class once; no state-dependent class binding is needed. Other split panes retain their existing width.
145
-
146
- Ionic's `when` still controls whether the menu is persistent. The example chooses a fixed 900px breakpoint. If your app needs different breakpoints for folded and ordinary displays, use the `ios-theme-fold-expanded` class applied by the helper to select that policy in your application's layout code. The helper only updates state classes, not `when`. This layout does not enable Vertical Bars or move an overlay menu.
147
-
148
- ## Vertical Control Area API
149
-
150
- The runtime handle reference is maintained in [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#vertical-control-area-api).
@@ -1,285 +0,0 @@
1
- ---
2
- title: Native UI Shell (Preview)
3
- ---
4
-
5
- # Native UI Shell (Preview)
6
-
7
- Native UI Shell is available as a **preview** in `1.2.0`. Its API and supported controls may change before stable status. Stable status is planned after the official release of Xcode 27.1.
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 or SwiftUI and the system's Liquid Glass material. Page content, scrolling, application state and routing remain in Ionic's WebView.
10
-
11
- ## Background
12
-
13
- Basecamp described this hybrid approach in [Hybrid sweet spot: Native navigation, web content](https://signalvnoise.com/posts/3743-hybrid-sweet-spot-native-navigation-web-content) (May 8, 2014): keep Web content at the core and use native UI where it improves the experience. The [Capacitor 1.0.0 Alpha announcement](https://ionic.io/blog/announcing-capacitor-1-0-0-alpha) (February 27, 2018) explicitly included **Native UI Shell** in its roadmap and linked to that article. Combining native UI with Web content was part of Capacitor's direction from the beginning.
14
-
15
- This package revisits that idea for Ionic and Liquid Glass. Existing Ionic markup defines the shell: eligible toolbar controls, tabs, fixed FABs and searchable tabs acquire native presentation. UIKit handles their appearance and interaction; the bridge synchronizes DOM state and returns actions to the original Ionic components. Ionic continues to own the navigation stack, page transitions and application logic. The shell's scope is the supported fixed controls described below.
16
-
17
- ## Enable the shell
18
-
19
- After installing the theme CSS described in the README, enable the shell once at application startup:
20
-
21
- ```ts
22
- import { enableNativeUIShell } from '@rdlabo/ionic-theme-ios27/native';
23
-
24
- void enableNativeUIShell();
25
- ```
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
-
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.
36
-
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()`.
38
-
39
- Native appearance follows the applied class, system or always-dark theme CSS. System theme changes are synchronized while the runtime is active.
40
-
41
- ## Supported markup
42
-
43
- The table below describes the ordinary Native UI Shell. Vertical Bars uses the separate [toolbar action rules](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions).
44
-
45
- | Ionic component | Supported appearance and placement | Native rendering |
46
- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
47
- | `ion-button` | `fill="default"`, standard glass, in a fixed header/footer toolbar | Glass `UIButton` |
48
- | `ion-buttons` | Fixed toolbar, two or more direct clear `ion-button` / `ion-menu-button` children sharing the theme glass | One `UIGlassEffect` surface with independent native buttons |
49
- | `ion-back-button` | Standard icon and color in a fixed header/footer toolbar | Glass `UIButton`, using the resolved Ionic label/icon |
50
- | `ion-menu-button` | Fixed toolbar, inside the theme glass `ion-buttons` | Glass `UIButton`; original Ionic menu toggle |
51
- | `ion-tab-bar` | Fixed tabs, icon-only/label-only items, one icon per item, dot/text badges, selection and disabled state | `UITabBar` and `UITabBarItem` |
52
- | `ion-segment` | Fixed toolbar, non-scrollable, text **or** one icon per item | `UISegmentedControl` |
53
- | `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 |
54
-
55
- 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. `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.
56
-
57
- Use `data-shell="disabled"` (or the equivalent `ios-theme-shell-disabled` class) 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 attribute or class at runtime automatically restores Web rendering or re-evaluates native eligibility.
58
-
59
- Only the exact value `disabled` opts out; an empty or unknown value is ignored. Remove the attribute to re-enable projection. If the class is also present, remove both to re-enable projection. This does not disable clicks or change the Web theme. The data attribute is available in `1.2.0`.
60
-
61
- ```html
62
- <ion-toolbar data-shell="disabled">
63
- <ion-button>Web glass button</ion-button>
64
- </ion-toolbar>
65
- ```
66
-
67
- 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.
68
-
69
- 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. Collapsed headers, opted-out controls and departed pages are excluded. Full-width foreground modals can participate in Vertical Bars as described below; other overlay surfaces keep their own layout.
70
-
71
- 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.
72
-
73
- Outside Vertical Bars, standalone clear, solid and outline buttons are excluded. A glass `ion-buttons` group of two or more clear buttons is projected as one surface; its children retain separate actions. Menu buttons can also share that group. A single menu button uses its parent `ion-buttons` as the glass surface, so no Web glass is left underneath the native button. A menu button outside this theme glass remains on the Web. Mixed fills, unsupported children, or a theme-disabled child keep the group on the Web. Single clear buttons remain on the Web. Custom button colors, custom back icons/colors, collapsing headers, toolbars inside scrolling content, modal content, scrollable/expanded segments and segment-view integration remain on the Web. Complex slots and unsupported SVG features also fall back to Web. The plugin does not translate arbitrary application CSS into UIKit styles.
74
-
75
- Native glass samples the Web content actually drawn behind it. Existing toolbar backgrounds and header blur still affect that content. For content to scroll beneath a header, use Ionic's normal translucent-header/fullscreen-content layout; the plugin does not move page content or override an application-owned opaque toolbar background.
76
-
77
- While a Web input owns the software keyboard, ordinary controls return to Web rendering and are re-evaluated when it closes. This also covers iPad layouts where the keyboard does not move the visual viewport. A native search field keeps its own native keyboard and search surface.
78
-
79
- When an existing control becomes unsupported, its Web source is painted before its native cover is removed. This avoids a blank handoff, but Web/UIKit updates are not atomic and can briefly overlap. This boundary behavior differs from ordinary page navigation, where unchanged shared native tabs are retained.
80
-
81
- ## State and events
82
-
83
- Menu buttons use Ionic's resolved default/configured icon or a supported slot icon/label. Native activation clicks the original `ion-menu-button`, preserving `menu` targeting. Non-default menu button types (`submit` / `reset`) remain on the Web. `disabled`, `autoHide`, menu availability and split-pane visibility follow the actual DOM. Opening a menu restores Web controls and retires their native covers; closing it reprojects eligible controls.
84
-
85
- The DOM owns labels, SVG content, placement, selected values and application behavior. The placement reference is `ion-tab-bar` itself. The native tab content uses that rectangle as its sizing proposal and placement anchor, accounting for UITabBar's larger outer frame: `tab-bar-position-start`, `tab-bar-position-center`, and `tab-bar-position-end` control the horizontal anchor (including RTL), while `slot="bottom"` retains the bottom edge and `slot="top"` retains the top edge. Class changes are reconciled automatically. UIKit owns internal margins and platter sizing, while the supported stacked layout and label typography follow the Web. UIKit can cap the content width even with fill positioning. On viewports at least 768px wide, the Web theme caps standard two-, three-, four- and five-item bars near the iPad stacked platter measurements (188, 274, 336 and 414pt). Smaller viewports retain the phone sizing. The standard bar is 62pt high, with a 54pt selection and a 4pt inset. Adjacent buttons overlap like UIKit’s controls; icon and label alignment is checked against simulator screenshots. These are default appearance targets, not a guarantee for custom fonts, icons or labels; UIKit still determines its intrinsic width. A different native width or height does not reject projection. Badge and title updates retain native item identities and remeasure the platter at the same placement anchor. The plugin does not stretch icons or modify UIKit's internal controls to force CSS item widths. The plugin measures the view subtree containing the native tab controls without private class names or fixed inset corrections; an unrecognized layout restores Web rendering. The runtime watches structural changes, affected shadow roots, sizing, page lifecycle events and overlays. Ancestor `display: none`, `hidden`, theme classes, component removal and disabled changes are reconciled automatically. Native touch events are checked against the current DOM and revision before clicking the original Ionic element.
86
-
87
- For forms, retain `ion-button type="submit"` and the form's existing submit handler. An external form is still passed as `[form]="formRef"`. The plugin does not call `form.submit()`, add a second submit path or change Angular form ownership. Segment values retain their original type because the original `ion-segment-button` is clicked; programmatic value changes do not emit a synthetic `ionChange`.
88
-
89
- Labels use native text. Local static SVGs and resolved `ion-icon` SVGs (including `name` and changes to `name`) are rasterized at the display scale and cached. Source projection preserves their colors; system projection of vertical toolbar buttons uses template icons tinted by SwiftUI. Tab SVGs that follow text color use native template rendering, so icons and labels change selection color together without waiting for another bridge image. Multicolor tab artwork retains its original colors. External references, `<use>`, animation, embedded HTML/images, SVG text and stylesheets are excluded. Web fonts and arbitrary slot layouts are not reproduced exactly.
90
-
91
- The native host accepts input only within native controls. Tab interaction and accessibility are provided by the standard [UITabBar](https://developer.apple.com/documentation/uikit/uitabbar) control. Empty areas pass touches to the WebView. Ionic iOS hides empty badges by default. An empty visible `ion-badge` becomes a native notification dot; a non-empty badge displays its text. Badge background and text colors come from computed DOM styles, including Ionic `color` palettes. Hidden or removed badges clear the native badge. Tabs use UIKit’s standard tab control for selection, touch behavior, badges and accessibility, retaining item identity across selection updates. UIKit owns the item layout within the measured bar, so arbitrary CSS item placement is not reproduced. Native controls expose names, disabled/selected traits and badges to accessibility; the source is hidden from Web accessibility while projected. This is not a guarantee of identical VoiceOver traversal between Web and UIKit.
92
-
93
- ## Searchable tabs
94
-
95
- Existing `attachTabBarSearchable(tabBar, fabButton, footer)` registrations automatically use native search when their bottom tab bar and glass search controls are supported. No new component option, route, native setup or page listener is required. Ordinary tab bars continue using `UITabBar`; horizontal searchable groups use a persistent `UITabBarController`, `UITab` / `UISearchTab`, and `UISearchController`. The original Capacitor WebView continues to render results and handle navigation.
96
-
97
- With vertical bars, the native rail keeps every tab and its selection. SwiftUI’s `searchable` and `searchToolbarBehavior(.minimize)` provide the system search button, field, dismissal control and transitions within the same native navigation surface. Apple determines their placement for the current device layout and focuses the native field when system search opens. Input, focus, clear and submit events use the same bridge as horizontal search. As soon as vertical bars are enabled, the Web projection places the registered search button above visible vertical tabs, or at the bottom of the rail when the tabs are hidden, and forwards activation to the original FAB. Native search replaces that Web projection when available. The Web search presentation also preserves vertical tabs.
98
-
99
- Search registration does not bypass placement restrictions. Its searchbar and close button must be in fixed footer toolbars. Its trigger must belong to an `ion-fab[slot="fixed"]` directly inside `ion-content`, or directly on the existing non-scrolling `.ion-page` layout. A wrapper inside scrolling content is not a fixed slot.
100
-
101
- For horizontal tabs, while a registration is alive, Native UI Shell keeps the searchable controller even when the page is transitioning or temporarily unavailable (`available: false`). The same controller manages resting tabs and active search. When resting, its view is fitted to the source `ion-tab-bar` to preserve the ordinary tab platter’s source width. UIKit can limit the platter width for small tab groups. UIKit owns the search button's placement beside the tabs. Register before the destination page finishes entering (for example in `ionViewWillEnter`) so the first visit does not paint ordinary tabs and then swap.
102
-
103
- Opening search preserves the selected Ionic tab and does not automatically show the keyboard (`automaticallyActivatesSearch` stays off). Tap the field or call `ion-searchbar.setFocus()` for the keyboard. While search is active, Native UI Shell freezes Web layout projection and holds Capacitor Keyboard resize at `none`; UIKit owns tab/search chrome without frame fitting during the session. Closing search restores the ordinary tabs to their source placement. Ordinary native tabs keep an optimistic selection until the Web `selected` state catches up. Input events and application `value` updates continue across the bridge until search closes. The trigger's resolved SVG, accessibility label and the search icon are projected from Ionic, including `ion-icon name`.
104
-
105
- Native edits pass through Ionic's input handlers, preserving `ionInput` debounce, `ionChange`, `ionFocus`, `ionBlur`, and `ionClear`. Programmatic `value` changes do not emit `ionInput`; synchronous application corrections and stale native input are distinguished. Native editing owns marked text and the caret. Returning through the footer's close action retains the value and does not emit `ionCancel` or `ionClear`.
106
-
107
- The first supported search configuration uses iOS-mode glass searchbars with the standard search keyboard, default clear control, no internal cancel button, and default autocorrection/capitalization settings. Custom input modes, return-key hints, min/max length, autocomplete, autocorrection, spellcheck, clear icons and classic searchbars remain on the Web. `disabled`, `placeholder`, `value` and `setFocus()` are synchronized for supported groups. General standalone searchbars are outside this feature.
108
-
109
- Page retirement, overlays, theme exclusion and lost native ownership close the native presentation and preserve the latest synchronized/application value. A later search starts from that closed state. The registration survives cached page transitions; reattaching on every return is not required. Bridge waits are finite. When an opening request loses the bridge, its pending Enter can complete through the existing Web animation. A disconnected bridge cannot recover native characters that were never delivered to JavaScript.
110
-
111
- Replacing the registered `ion-searchbar` or its input retires the old editing session. The replacement keeps its own application value and starts a new native session when reopened.
112
-
113
- While enabled, Native UI Shell suppresses the WebView’s top scroll-edge effect because Ionic already paints the header edge. This prevents a second dark gradient when the OS and Web themes differ. The original setting is restored on destroy.
114
-
115
- The native search controller remains visible over its own keyboard. Other projected controls are hidden while a Web input opens the keyboard. UIKit's own accessibility and Reduce Motion behavior apply to the standard controls; full VoiceOver traversal is not a verified parity guarantee.
116
-
117
- ## Transitions and recovery
118
-
119
- FABs retain Ionic's `activated`, per-child `show`, `close()` and original click handlers. Multiple lists, initial expansion, small buttons and `edge` use each button's measured layout. The native side reflects Ionic's staggered visibility without adding another timer or open/close controller. Main-button icon changes crossfade using the resolved `closeIcon`; Reduce Motion disables this crossfade. Native FAB button instances persist across open/close updates. The source FAB stays projected throughout ordinary opening and closing.
120
-
121
- FAB support covers the standard circular glass appearance, text and resolved static SVG/`ion-icon` content, including RTL icon mirroring. One unsupported child keeps the whole FAB in Web rendering, including when its list is closed. Colored solid buttons, submit/reset or href FABs, custom host backgrounds/shapes/motion, non-fixed-slot placement and unsupported artwork remain Web. For example, the demo's red-background `floating-action-button-fixed` page retains its existing Web appearance. A fixed-slot FAB must be a direct child of `ion-content`; a `slot="fixed"` attribute on a page sibling is not a content slot.
122
-
123
- 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.
124
-
125
- 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.
126
-
127
- 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.
128
-
129
- Standard Ionic overlays suspend native projection until dismissal. Unsupported searchable-tab configurations use the existing Web animation and share this ownership with Web glass gestures. CSS motion of supported containing surfaces also causes temporary Web rendering.
130
-
131
- During retirement, the source is restored and allowed to paint before its native cover is removed. During acquisition, the source is hidden only after a successful, current native response. Delayed responses are revalidated per control: existing eligible controls retain their native cover while content updates catch up. Only removed or ineligible sources return to Web; newly acquired sources require an exact acknowledgement. Ordinary page mutations never call the global clear operation. UIKit tab instances and items are retained, and equal frames/selections are not reapplied. Duplicate/stale activations are discarded. WebKit and UIKit still render separately: the implementation avoids an intentional blank frame, but does not provide an OS-level atomic compositing guarantee. Validate custom transitions and overlays on the app's supported simulators before rollout; unknown overlay systems are outside the automatic integration contract.
132
-
133
- If a bridge update fails or times out, the runtime stops and restores Web rendering. It does not automatically reconnect. `getStatus()` reports `stopped` and the reason; to retry deliberately, call `destroy()` on that handle and then `enableNativeUIShell()` again.
134
-
135
- For diagnostics or application teardown:
136
-
137
- ```ts
138
- const shell = await enableNativeUIShell(); // repeated calls share the runtime
139
- console.log(shell.getStatus()); // state, projected control count, update count, failure reason
140
- await shell.destroy(); // restore DOM, remove native controls and release listeners/cache
141
- ```
142
-
143
- 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:
144
-
145
- ```ts
146
- const shell = await enableNativeUIShell({
147
- enabled: true,
148
- controls: {
149
- tabs: true,
150
- },
151
- });
152
-
153
- // Equivalent to leaving Native UI Shell off; all controls remain on the Web.
154
- const disabledShell = await enableNativeUIShell({ enabled: false });
155
- ```
156
-
157
- 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`.
158
-
159
- 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:
160
-
161
- ```ts
162
- const suspension = await shell.suspend();
163
-
164
- try {
165
- await modal.present();
166
- await modal.onDidDismiss();
167
- } finally {
168
- await suspension.resume();
169
- }
170
- ```
171
-
172
- 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.
173
-
174
- 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.
175
-
176
- ## Support iPhone Duo (preview)
177
-
178
- iPhone Duo support, including standalone vertical bars without Native UI Shell, is available as a **preview** in `1.2.0` alongside Native UI Shell. Its APIs and supported behavior may change.
179
-
180
- 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.
181
-
182
- Vertical Bars uses a SwiftUI `TabView` and toolbar in the system rail. Ionic remains the source of labels, icons, selected/disabled state and actions. See [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars) for rail layout, native fallback and supported overlays.
183
-
184
- In Vertical Bars, fixed-toolbar `ion-button` actions need an `ion-icon` or SVG with `slot="icon-only"`. All fills and Ionic colors are eligible; submit buttons follow the same placement rule. Add `.ios-theme-horizontal-only` to a button or its `ion-buttons` group to keep it horizontal. See [Toolbar actions](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#toolbar-actions) for placement and exclusion rules.
185
-
186
- **Native button appearance:** `1.2.0` defaults native vertical buttons to `buttonProjection: 'system'`. Use `source` to project Ionic fill and colors, with `data-projection="source|system"` or the equivalent `ios-theme-projection-source` / `ios-theme-projection-system` classes for local exceptions. These settings affect native vertical `ion-button` and `ion-menu-button` actions only. Horizontal controls and Web clones keep their existing behavior. See [Choose button appearance](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars#choose-button-appearance) for migration, priority and fill rules.
187
-
188
- 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).
189
-
190
- ## Native UI Shell API
191
-
192
- The generated reference below documents the handle returned by `enableNativeUIShell()`. The underlying Capacitor bridge and its control-snapshot protocol are implementation details.
193
-
194
- <docgen-index>
195
-
196
- * [`getStatus()`](#getstatus)
197
- * [`suspend()`](#suspend)
198
- * [`destroy()`](#destroy)
199
- * [Interfaces](#interfaces)
200
-
201
- </docgen-index>
202
-
203
- <docgen-api>
204
- <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
205
-
206
- ### getStatus()
207
-
208
- ```typescript
209
- getStatus() => NativeUIShellStatus
210
- ```
211
-
212
- Returns the current Web/native projection state.
213
-
214
- **Returns:** <code><a href="#nativeuishellstatus">NativeUIShellStatus</a></code>
215
-
216
- --------------------
217
-
218
-
219
- ### suspend()
220
-
221
- ```typescript
222
- suspend() => Promise<NativeUIShellSuspension>
223
- ```
224
-
225
- Restores projected controls to the Web until the returned lease is resumed.
226
-
227
- **Returns:** <code>Promise&lt;<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>&gt;</code>
228
-
229
- --------------------
230
-
231
-
232
- ### destroy()
233
-
234
- ```typescript
235
- destroy() => Promise<void>
236
- ```
237
-
238
- Stops synchronization, restores Web controls and releases native resources.
239
-
240
- --------------------
241
-
242
-
243
- ### Interfaces
244
-
245
-
246
- #### NativeUIShellStatus
247
-
248
- | Prop | Type |
249
- | --------------- | ------------------------------------------- |
250
- | **`state`** | <code>'native' \| 'stopped' \| 'web'</code> |
251
- | **`projected`** | <code>number</code> |
252
- | **`updates`** | <code>number</code> |
253
- | **`reason`** | <code>string</code> |
254
-
255
-
256
- #### NativeUIShellSuspension
257
-
258
- | Method | Signature | Description |
259
- | ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
260
- | **resume** | () =&gt; Promise&lt;void&gt; | Releases this suspension. Native projection resumes after all active suspensions are released. |
261
-
262
- </docgen-api>
263
-
264
- ## Source layout
265
-
266
- 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.
267
-
268
- On iOS, [`Components`](../ios/Sources/IonicNativeUIShellPlugin/Components) owns UIKit control creation, updates and component names. `ShellButton` shares the native button implementation used by ordinary, back and menu buttons. `Shared` owns the host view, typed snapshots, geometry, colors and image cache. Capacitor decodes each complete snapshot once using `Decodable`; renderers consume typed models and compare content with `Equatable`. Invalid batches are rejected before visible controls are changed. `IonicNativeUIShellPlugin.swift` coordinates Capacitor calls, revisions and native view lifetimes.
269
-
270
- ## Demo and verification
271
-
272
- The demo includes a `native-ui-shell` page for exercising fixed controls. From the repository root, build the library and run the browser tests:
273
-
274
- ```sh
275
- npm ci
276
- npm run build
277
- cd demo
278
- npm ci
279
- npx --no-install playwright install chromium
280
- npx --no-install playwright test e2e/native-ui-shell.spec.ts e2e/native-ui-shell-edge.spec.ts
281
- ```
282
-
283
- For native interaction and placement tests, use Xcode 26 or later, XcodeGen and a booted iOS 26+ Simulator. From the repository root, run `sh scripts/verify-native-ui-shell.sh SIMULATOR_UDID`. This also builds an independent Swift Package Manager consumer from the npm package. Run `sh scripts/verify-native-search.sh SIMULATOR_UDID` for searchable-tab integration, or add `edge` to that command for placement, navigation and keyboard edge cases. The scripts print the location of their local test artifacts.
284
-
285
- Search controllers retain their UIKit-managed transition and are excluded from the ordinary control acquisition crossfade.