@rdlabo/ionic-theme-ios27 1.2.0-2 → 1.2.0-4

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 (180) hide show
  1. package/README.md +15 -2
  2. package/dist/_virtual/_rolldown/runtime.js +13 -0
  3. package/dist/css/ionic-theme-ios27.css +1 -1
  4. package/dist/css/vertical-bars.css +1 -1
  5. package/dist/focus-controller/index.d.ts +4 -2
  6. package/dist/focus-controller/index.d.ts.map +1 -1
  7. package/dist/focus-controller/index.js +100 -57
  8. package/dist/focus-controller/index.js.map +1 -0
  9. package/dist/index.d.ts +10 -8
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +54 -60
  12. package/dist/index.js.map +1 -0
  13. package/dist/native/components/index.d.ts +13 -10
  14. package/dist/native/components/index.d.ts.map +1 -1
  15. package/dist/native/components/index.js +39 -41
  16. package/dist/native/components/index.js.map +1 -0
  17. package/dist/native/components/ion-back-button.d.ts +6 -1
  18. package/dist/native/components/ion-back-button.d.ts.map +1 -1
  19. package/dist/native/components/ion-back-button.js +19 -13
  20. package/dist/native/components/ion-back-button.js.map +1 -0
  21. package/dist/native/components/ion-button.d.ts +8 -2
  22. package/dist/native/components/ion-button.d.ts.map +1 -1
  23. package/dist/native/components/ion-button.js +22 -17
  24. package/dist/native/components/ion-button.js.map +1 -0
  25. package/dist/native/components/ion-buttons.d.ts +8 -2
  26. package/dist/native/components/ion-buttons.d.ts.map +1 -1
  27. package/dist/native/components/ion-buttons.js +29 -33
  28. package/dist/native/components/ion-buttons.js.map +1 -0
  29. package/dist/native/components/ion-fab.d.ts +6 -1
  30. package/dist/native/components/ion-fab.d.ts.map +1 -1
  31. package/dist/native/components/ion-fab.js +131 -151
  32. package/dist/native/components/ion-fab.js.map +1 -0
  33. package/dist/native/components/ion-menu-button.d.ts +8 -2
  34. package/dist/native/components/ion-menu-button.d.ts.map +1 -1
  35. package/dist/native/components/ion-menu-button.js +29 -19
  36. package/dist/native/components/ion-menu-button.js.map +1 -0
  37. package/dist/native/components/ion-segment.d.ts +6 -1
  38. package/dist/native/components/ion-segment.d.ts.map +1 -1
  39. package/dist/native/components/ion-segment.js +29 -23
  40. package/dist/native/components/ion-segment.js.map +1 -0
  41. package/dist/native/components/ion-tab-bar.d.ts +6 -1
  42. package/dist/native/components/ion-tab-bar.d.ts.map +1 -1
  43. package/dist/native/components/ion-tab-bar.js +49 -40
  44. package/dist/native/components/ion-tab-bar.js.map +1 -0
  45. package/dist/native/components/searchable-tabs.d.ts +17 -15
  46. package/dist/native/components/searchable-tabs.d.ts.map +1 -1
  47. package/dist/native/components/searchable-tabs.js +382 -435
  48. package/dist/native/components/searchable-tabs.js.map +1 -0
  49. package/dist/native/definitions.d.ts +118 -111
  50. package/dist/native/definitions.d.ts.map +1 -1
  51. package/dist/native/definitions.js +0 -1
  52. package/dist/native/foldable/fold-state.d.ts +5 -0
  53. package/dist/native/foldable/fold-state.d.ts.map +1 -0
  54. package/dist/native/foldable/fold-state.js +14 -0
  55. package/dist/native/foldable/fold-state.js.map +1 -0
  56. package/dist/native/foldable/types.d.ts +12 -0
  57. package/dist/native/foldable/types.d.ts.map +1 -0
  58. package/dist/native/foldable/types.js +0 -0
  59. package/dist/native/index.d.ts +6 -3
  60. package/dist/native/index.d.ts.map +1 -1
  61. package/dist/native/index.js +228 -204
  62. package/dist/native/index.js.map +1 -0
  63. package/dist/native/prehide.d.ts +4 -2
  64. package/dist/native/prehide.d.ts.map +1 -1
  65. package/dist/native/prehide.js +220 -242
  66. package/dist/native/prehide.js.map +1 -0
  67. package/dist/native/runtime.d.ts +3 -1
  68. package/dist/native/runtime.d.ts.map +1 -1
  69. package/dist/native/runtime.js +626 -724
  70. package/dist/native/runtime.js.map +1 -0
  71. package/dist/native/shared/candidate.d.ts +16 -11
  72. package/dist/native/shared/candidate.d.ts.map +1 -1
  73. package/dist/native/shared/candidate.js +96 -95
  74. package/dist/native/shared/candidate.js.map +1 -0
  75. package/dist/native/shared/crossfade.d.ts +6 -4
  76. package/dist/native/shared/crossfade.d.ts.map +1 -1
  77. package/dist/native/shared/crossfade.js +49 -45
  78. package/dist/native/shared/crossfade.js.map +1 -0
  79. package/dist/native/shared/dom.d.ts +7 -5
  80. package/dist/native/shared/dom.d.ts.map +1 -1
  81. package/dist/native/shared/dom.js +163 -211
  82. package/dist/native/shared/dom.js.map +1 -0
  83. package/dist/native/shared/events.d.ts +8 -6
  84. package/dist/native/shared/events.d.ts.map +1 -1
  85. package/dist/native/shared/events.js +12 -7
  86. package/dist/native/shared/events.js.map +1 -0
  87. package/dist/native/shared/icons.d.ts +4 -2
  88. package/dist/native/shared/icons.d.ts.map +1 -1
  89. package/dist/native/shared/icons.js +59 -53
  90. package/dist/native/shared/icons.js.map +1 -0
  91. package/dist/native/shared/modal.d.ts +3 -1
  92. package/dist/native/shared/modal.d.ts.map +1 -1
  93. package/dist/native/shared/modal.js +156 -139
  94. package/dist/native/shared/modal.js.map +1 -0
  95. package/dist/native/vertical-bars-web.d.ts +3 -1
  96. package/dist/native/vertical-bars-web.d.ts.map +1 -1
  97. package/dist/native/vertical-bars-web.js +332 -346
  98. package/dist/native/vertical-bars-web.js.map +1 -0
  99. package/dist/native-integration/index.d.ts +10 -8
  100. package/dist/native-integration/index.d.ts.map +1 -1
  101. package/dist/native-integration/index.js +102 -100
  102. package/dist/native-integration/index.js.map +1 -0
  103. package/dist/native-integration/transition.d.ts +3 -1
  104. package/dist/native-integration/transition.d.ts.map +1 -1
  105. package/dist/native-integration/transition.js +15 -6
  106. package/dist/native-integration/transition.js.map +1 -0
  107. package/dist/searchbar/index.d.ts +4 -2
  108. package/dist/searchbar/index.d.ts.map +1 -1
  109. package/dist/searchbar/index.js +64 -56
  110. package/dist/searchbar/index.js.map +1 -0
  111. package/dist/segment/index.d.ts +3 -1
  112. package/dist/segment/index.d.ts.map +1 -1
  113. package/dist/segment/index.js +465 -324
  114. package/dist/segment/index.js.map +1 -0
  115. package/dist/sheets-of-glass/animations.d.ts +13 -12
  116. package/dist/sheets-of-glass/animations.d.ts.map +1 -1
  117. package/dist/sheets-of-glass/animations.js +496 -306
  118. package/dist/sheets-of-glass/animations.js.map +1 -0
  119. package/dist/sheets-of-glass/index.d.ts +3 -1
  120. package/dist/sheets-of-glass/index.d.ts.map +1 -1
  121. package/dist/sheets-of-glass/index.js +294 -357
  122. package/dist/sheets-of-glass/index.js.map +1 -0
  123. package/dist/sheets-of-glass/interfaces.d.ts +11 -9
  124. package/dist/sheets-of-glass/interfaces.d.ts.map +1 -1
  125. package/dist/sheets-of-glass/interfaces.js +0 -1
  126. package/dist/tab-bar-searchable/animations/enter.d.ts +4 -2
  127. package/dist/tab-bar-searchable/animations/enter.d.ts.map +1 -1
  128. package/dist/tab-bar-searchable/animations/enter.js +50 -69
  129. package/dist/tab-bar-searchable/animations/enter.js.map +1 -0
  130. package/dist/tab-bar-searchable/animations/leave.d.ts +4 -2
  131. package/dist/tab-bar-searchable/animations/leave.d.ts.map +1 -1
  132. package/dist/tab-bar-searchable/animations/leave.js +48 -62
  133. package/dist/tab-bar-searchable/animations/leave.js.map +1 -0
  134. package/dist/tab-bar-searchable/index.d.ts +5 -3
  135. package/dist/tab-bar-searchable/index.d.ts.map +1 -1
  136. package/dist/tab-bar-searchable/index.js +125 -109
  137. package/dist/tab-bar-searchable/index.js.map +1 -0
  138. package/dist/transition/index.d.ts +9 -8
  139. package/dist/transition/index.d.ts.map +1 -1
  140. package/dist/transition/index.js +203 -192
  141. package/dist/transition/index.js.map +1 -0
  142. package/dist/transition/ios.transition.d.ts +7 -5
  143. package/dist/transition/ios.transition.d.ts.map +1 -1
  144. package/dist/transition/ios.transition.js +15 -11
  145. package/dist/transition/ios.transition.js.map +1 -0
  146. package/dist/utils.d.ts +10 -8
  147. package/dist/utils.d.ts.map +1 -1
  148. package/dist/utils.js +58 -66
  149. package/dist/utils.js.map +1 -0
  150. package/dist/vertical-bars.d.ts +5 -4
  151. package/dist/vertical-bars.js +4 -2
  152. package/docs/iphone-duo-with-original-theme.md +18 -29
  153. package/docs/iphone-duo.md +44 -204
  154. package/docs/native-ui-shell.md +15 -11
  155. package/docs/special-markup.md +2 -0
  156. package/docs/vertical-bars.md +283 -0
  157. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +1 -1
  158. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +61 -16
  159. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +118 -0
  160. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +17 -8
  161. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +2 -0
  162. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +40 -0
  163. package/package.json +9 -6
  164. package/src/native/components/index.ts +3 -2
  165. package/src/native/components/ion-button.ts +3 -2
  166. package/src/native/components/ion-buttons.ts +4 -3
  167. package/src/native/components/ion-menu-button.ts +3 -2
  168. package/src/native/components/searchable-tabs.ts +8 -11
  169. package/src/native/definitions.ts +22 -1
  170. package/src/native/foldable/fold-state.ts +9 -0
  171. package/src/native/foldable/types.ts +4 -0
  172. package/src/native/index.ts +17 -3
  173. package/src/native/prehide.ts +3 -1
  174. package/src/native/runtime.ts +14 -9
  175. package/src/native/shared/candidate.ts +39 -10
  176. package/src/native/shared/dom.ts +4 -4
  177. package/src/styles/vertical-bars.scss +12 -2
  178. package/src/tab-bar-searchable/index.ts +40 -9
  179. package/src/vertical-bars.ts +9 -1
  180. package/dist/vertical-bars.d.ts.map +0 -1
@@ -40,7 +40,7 @@ Native appearance follows the applied class, system or always-dark theme CSS. Sy
40
40
 
41
41
  ## Supported markup
42
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/iphone-duo#toolbar-actions).
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
44
 
45
45
  | Ionic component | Supported appearance and placement | Native rendering |
46
46
  | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
@@ -52,12 +52,14 @@ The table below describes the ordinary Native UI Shell. Vertical Bars uses the s
52
52
  | `ion-segment` | Fixed toolbar, non-scrollable, text **or** one icon per item | `UISegmentedControl` |
53
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
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. `ionic-theme-disabled`, `ios-theme-disabled`, and the legacy `ios26-disabled` on an element or ancestor always exclude it. A disabled theme on one tab/segment item keeps its whole group on the Web.
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
56
 
57
- Use `ios-theme-shell-disabled` to disable only the iOS Native UI Shell while keeping the Web theme. It excludes the element and all its descendants. Adding or removing the class at runtime automatically restores Web rendering or re-evaluates native eligibility.
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 unreleased.
58
60
 
59
61
  ```html
60
- <ion-toolbar class="ios-theme-shell-disabled">
62
+ <ion-toolbar data-shell="disabled">
61
63
  <ion-button>Web glass button</ion-button>
62
64
  </ion-toolbar>
63
65
  ```
@@ -84,19 +86,21 @@ The DOM owns labels, SVG content, placement, selected values and application beh
84
86
 
85
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`.
86
88
 
87
- 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, preserving their colors. Tab SVGs that follow text color use native template rendering, so icons and labels change selection color together without waiting for another bridge image. Explicit multicolor 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.
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.
88
90
 
89
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.
90
92
 
91
93
  ## Searchable tabs
92
94
 
93
- 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`; searchable groups use a persistent `UITabBarController`, `UITab` / `UISearchTab`, and `UISearchController`. The original Capacitor WebView continues to render results and handle navigation.
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. When native search is unavailable, the Web search presentation also preserves vertical tabs.
94
98
 
95
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.
96
100
 
97
- While a registration is alive, Native UI Shell keeps the searchable controller even when the page is transitioning or temporarily unavailable (`available: false`), instead of demoting back to a separate ordinary control identity. Resting chrome (including Album before search opens) uses the same `UITabBar` + `ShellTabBar.fit` path as ordinary tabs, with the search trigger pinned to the FAB when available; `UISearchTab` is shown only while the search session is active. Flipping availability or entering/leaving search crossfades between those layers. Register before the destination page finishes entering (for example in `ionViewWillEnter`) so the first visit does not paint ordinary tabs and then swap.
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`), instead of demoting back to a separate ordinary control identity. Resting chrome (including Album before search opens) uses the same `UITabBar` + `ShellTabBar.fit` path as ordinary tabs, with the search trigger pinned to the FAB when available; `UISearchTab` is shown only while the search session is active. Flipping availability or entering/leaving search crossfades between those layers. Register before the destination page finishes entering (for example in `ionViewWillEnter`) so the first visit does not paint ordinary tabs and then swap.
98
102
 
99
- 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 (no Ionic `fit` mid-session). Leave re-fits ordinary tabs to `ion-tab-bar` and search to the FAB. 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 and the search icon are projected from Ionic, including `ion-icon name`.
103
+ With horizontal tabs, 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 (no Ionic `fit` mid-session). Leave re-fits ordinary tabs to `ion-tab-bar` and search to the FAB. 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 and the search icon are projected from Ionic, including `ion-icon name`.
100
104
 
101
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`.
102
106
 
@@ -175,11 +179,11 @@ iPhone Duo support is experimental, including standalone use without Native UI S
175
179
 
176
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.
177
181
 
178
- On supported iOS versions, adding `.ios-theme-vertical-bars` changes only controls that the system relocates into the physical side rail. Native UI Shell presents eligible tabs, back navigation, menu buttons, and toolbar actions through a SwiftUI `TabView` and toolbar once the class is applied. When the OS reports a rail edge — on iPhone Duo linked against iOS 27.1 or later — it must agree with the applied placement; a disagreeing report keeps the rail on the Web. Older toolchains that cannot report an edge trust the DOM placement directly. SwiftUI owns their adaptive placement and Liquid Glass appearance; Ionic remains the source of labels, icons, selected/disabled state, routing, form submission, and click handlers.
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.
179
183
 
180
- The SwiftUI surface is clipped and hit-tested to the system rail. Web content remains visible and interactive outside that physical region. The runtime optimistically updates tab selection before forwarding the action to the original `ion-tab-button`, using the same event and stale-revision protection as the other native controls. A topmost full-width modal supplies its own eligible toolbar actions and rail bounds, including when it is a sheet. Centered modals, menus, and popovers keep their own toolbar layout. Modal placement follows the visible dialog width rather than its type or hinge posture.
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.
181
185
 
182
- In Vertical Bars, fixed-toolbar `ion-button` actions use an `ion-icon` or SVG with `slot="icon-only"`. All fills (`default`, `clear`, `solid`, and `outline`) and Ionic colors are eligible; submit buttons follow the same placement rule. Solid actions retain their background color and outline actions retain their border color and width. Actions without `icon-only` stay horizontal. Scrolling and explicitly opted-out controls retain their Web presentation. 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.
186
+ **Unreleased button appearance options:** the next release 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.
183
187
 
184
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).
185
189
 
@@ -53,6 +53,8 @@ iPhone Duo support is experimental. It is available in the `1.2.0-0` prerelease
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
+ See [Vertical Bars](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/vertical-bars) for rail layout classes, control eligibility and native button appearance.
57
+
56
58
  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
59
 
58
60
  ## Two-line inset list items
@@ -0,0 +1,283 @@
1
+ ---
2
+ title: Vertical Bars (experimental)
3
+ ---
4
+
5
+ # Vertical Bars (experimental)
6
+
7
+ Vertical Bars moves eligible Ionic navigation and actions into a side rail while keeping the original components as the source of labels, icons and behavior. It works with this theme or an existing Ionic theme, independently of hinge posture and the full Native UI Shell.
8
+
9
+ Use this page for layout, control eligibility, native button appearance and the runtime API. For device events and split panes, see [iPhone Duo support](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo). For a step-by-step browser preview and native setup, start with [iPhone Duo with your existing theme](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme).
10
+
11
+ Vertical Bars is experimental; APIs and supported behavior may change. The button appearance options below are unreleased.
12
+
13
+ ## Enable Vertical Bars
14
+
15
+ Load the opt-in stylesheet:
16
+
17
+ ```scss
18
+ @use '@rdlabo/ionic-theme-ios27/dist/css/vertical-bars.css';
19
+ ```
20
+
21
+ After `ion-app` is mounted, start one runtime:
22
+
23
+ ```ts
24
+ import { enableVerticalControlArea } from '@rdlabo/ionic-theme-ios27/vertical-bars';
25
+
26
+ const rail = await enableVerticalControlArea();
27
+ ```
28
+
29
+ Add the layout class below for browser simulation, or [apply device placement](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo#project-controls-into-the-rail) for an iPhone Duo. The application owns placement and cleanup: call `await rail.destroy()` when its owner is disposed. If you already use `enableNativeUIShell()`, keep that runtime; it includes Vertical Bars. Do not start both.
30
+
31
+ The `/vertical-bars` entry point requires `@capacitor/core`, including in browser builds. CSS-only use needs no runtime. For native setup and navigation transitions, follow the [existing-theme guide](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/iphone-duo-with-original-theme).
32
+
33
+ On supported Capacitor iOS, eligible controls use native SwiftUI rendering. Web, Android and unavailable native projection use Web clones. A browser preview can verify layout and actions, but cannot compare native button appearance.
34
+
35
+ ## Reserve the vertical rail
36
+
37
+ Add `.ios-theme-vertical-bars` to `ion-app` to reserve the rail region on the physical right, or add `.ios-theme-vertical-bars-left` as well to use the physical left:
38
+
39
+ ```html
40
+ <ion-app class="ios-theme-vertical-bars">...</ion-app>
41
+ ```
42
+
43
+ The classes are physical — `-left` always means the physical left edge — because CSS and the native renderer work in physical coordinates. `setVerticalControlAreaPlacement` applies the logical `verticalBarEdge` reported by the device plugin and resolves it through the document's direction, so an RTL app does not need its own conversion.
44
+
45
+ For Chrome development, no native plugin is needed — the class alone reserves `80px` to simulate iPhone Duo. When `setVerticalControlAreaPlacement` receives `{ edge, nativeEdge, inset }`, the inset replaces the fallback width, even when it is less than `80px`. Override `--ios-theme-vertical-bars-safe-area-left` or `--ios-theme-vertical-bars-safe-area-right` when simulating a different layout.
46
+
47
+ This keeps routers and component backgrounds full-viewport. `ion-content` moves its scroll foreground, `ion-toolbar` moves its container foreground, and `ion-fab` adjusts only when placed beside the system UI. The corresponding Ionic safe-area variable is reset inside those foreground components so descendants do not add the inset again.
48
+
49
+ `ion-modal` applies the same foreground correction when its visible dialog spans the viewport width. With the Vertical Control Area runtime enabled, the topmost full-width modal also projects eligible toolbar buttons into its own rail; centered dialogs keep their toolbar buttons and receive no page-rail inset. This includes full-width sheet modals: their rail follows the visible sheet bounds as the breakpoint changes. Eligibility follows the visible dialog width, not the hinge posture or modal type. `ion-menu` and `ion-popover` are handled as separate surfaces: their internal foreground components do not receive the main-page conversion and retain Ionic's standard safe-area handling. A menu presented beside the system UI keeps Ionic's full-viewport animation host and offsets only its visible container by the corresponding inset; a menu from the opposite side is unchanged. Left and right remain physical coordinates in RTL, while Ionic's `side="start"` and `side="end"` values remain logical.
50
+
51
+ The mode is component-mode independent: an app can keep Ionic `mode: 'md'` on iOS and still enable Vertical Bars. No component needs `mode="ios"`.
52
+
53
+ ## Native rendering
54
+
55
+ On supported iOS versions, adding `.ios-theme-vertical-bars` changes only controls that the system relocates into the physical side rail. Native UI Shell presents eligible tabs, back navigation, menu buttons, and toolbar actions through a SwiftUI `TabView` and toolbar once the class is applied. When the OS reports a rail edge — on iPhone Duo linked against iOS 27.1 or later — it must agree with the applied placement; a disagreeing report keeps the rail on the Web. Older toolchains that cannot report an edge trust the DOM placement directly. SwiftUI owns their adaptive placement and Liquid Glass appearance; Ionic remains the source of labels, icons, selected/disabled state, routing, form submission, and click handlers.
56
+
57
+ The SwiftUI surface is clipped and hit-tested to the system rail. Web content remains visible and interactive outside that physical region. The runtime optimistically updates tab selection before forwarding the action to the original `ion-tab-button`, using the same event and stale-revision protection as the other native controls. Overlay layout follows the [rail reservation rules](#reserve-the-vertical-rail).
58
+
59
+
60
+ ## Toolbar actions
61
+
62
+ A standard `ion-back-button` can project from outside a fixed toolbar, including routed content or a persistent app shell. `ion-menu-button` and other toolbar actions require a fixed toolbar.
63
+
64
+ An `ion-button` moves into the rail when it contains an `ion-icon` or SVG with `slot="icon-only"`. The button must be in a fixed `ion-toolbar` directly inside `ion-header` or `ion-footer`, outside scrolling `ion-content`.
65
+
66
+ | Icon markup | Placement |
67
+ | --- | --- |
68
+ | `slot="icon-only"` | Vertical rail |
69
+ | `slot="start"`, `slot="end"`, or no slot | Original horizontal toolbar |
70
+ | No icon | Original horizontal toolbar |
71
+
72
+ ```html
73
+ <ion-header>
74
+ <ion-toolbar>
75
+ <ion-buttons slot="end">
76
+ <ion-button aria-label="Done">
77
+ <ion-icon name="checkmark-outline" slot="icon-only"></ion-icon>
78
+ </ion-button>
79
+ </ion-buttons>
80
+ </ion-toolbar>
81
+ </ion-header>
82
+ ```
83
+
84
+ All fills (`default`, `clear`, `solid`, and `outline`) and Ionic colors follow this placement rule. Keep accessible names and the original click or form-submit handlers on the source buttons. `type="submit"` and `.button-submit` do not select a different placement.
85
+
86
+ The rule applies to individual buttons and buttons inside `ion-buttons`, on ordinary pages and in the topmost full-width modal. Centered modals, menus, and popovers keep their own toolbar layout. Add `.ios-theme-horizontal-only` to a group or individual button to keep it horizontal. Placement is chosen when a routed page enters; changing an existing button's content or icon slot does not move it between the toolbar and rail until the page leaves and re-enters.
87
+
88
+ ### Choose button appearance
89
+
90
+ **Unreleased:** `buttonProjection` and the local projection settings below are part of the next release. They are not available in the `1.2.0-1` package used by the introductory guide. The following rules describe this branch.
91
+
92
+ For native vertical `ion-button` and `ion-menu-button` actions, choose who controls appearance:
93
+
94
+ | `buttonProjection` | Appearance |
95
+ | --- | --- |
96
+ | `'system'` (default) | SwiftUI styles the buttons and tints their icons. Ionic fill, colors and borders are not applied. |
97
+ | `'source'` | Projects the supported Ionic fill and computed colors described in [Source fill rules](#source-fill-rules). |
98
+
99
+ ```ts
100
+ const rail = await enableVerticalControlArea({ buttonProjection: 'source' });
101
+ ```
102
+
103
+ Both `enableVerticalControlArea()` and `enableNativeUIShell()` accept the option. Use one runtime, and destroy it before restarting with different options. Either mode preserves actions, disabled state and grouping. These appearance settings do not affect horizontal controls, source elements or Web fallback clones, so compare the native appearance on supported iOS.
104
+
105
+ **Migration:** the default changes from source styling to `system`. Set `buttonProjection: 'source'` to retain the previous projection behavior. This does not change the feature's experimental status.
106
+
107
+ ### Override individual buttons or groups
108
+
109
+ Use `data-projection` for local exceptions. Existing classes remain supported:
110
+
111
+ | Attribute | Equivalent class |
112
+ | --- | --- |
113
+ | `data-projection="source"` | `ios-theme-projection-source` |
114
+ | `data-projection="system"` | `ios-theme-projection-system` |
115
+
116
+ ```html
117
+ <ion-buttons data-projection="source">
118
+ <ion-button fill="solid" aria-label="Add">
119
+ <ion-icon name="add-outline" slot="icon-only"></ion-icon>
120
+ </ion-button>
121
+ <ion-button data-projection="system" aria-label="Search">
122
+ <ion-icon name="search-outline" slot="icon-only"></ion-icon>
123
+ </ion-button>
124
+ </ion-buttons>
125
+ ```
126
+
127
+ The first matching setting wins:
128
+
129
+ 1. The button's local setting.
130
+ 2. Its nearest `ion-buttons` local setting.
131
+ 3. The startup `buttonProjection` option, or `system` if omitted.
132
+
133
+ On the same element, a valid `data-projection` value takes precedence over the classes. Empty or unknown values are ignored. Without a valid attribute, `system` wins if both classes are present. Removing an attribute falls back to the element's classes, then the next level above. Attribute and class changes apply without restarting the runtime; grouping and placement stay unchanged.
134
+
135
+ Only native vertical `ion-button` and `ion-menu-button` actions interpret these settings. Other ancestors, back buttons, tabs and FABs do not. They do not change Web styling. To keep a control or subtree on the Web entirely, use [`data-shell="disabled"`](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell#supported-markup).
136
+
137
+ ### Source fill rules
138
+
139
+ For an `ion-button` resolved to `source`, fill is chosen separately from the projection mode:
140
+
141
+ | Button | Effective fill |
142
+ | --- | --- |
143
+ | Explicit `fill="clear"`, `"solid"` or `"outline"` | The explicit value |
144
+ | Omitted fill or `fill="default"` inside `ion-buttons` | `clear` |
145
+ | Omitted fill or `fill="default"` outside `ion-buttons` | `buttonDefaultFill` |
146
+
147
+ `buttonDefaultFill` accepts only `'solid'` or `null`; omission is equivalent to `null`. Use `'solid'` for Ionic's default button design, or `null` for this theme's default glass design. It applies to every button resolved to `source`, including a local exception under a global `system` setting.
148
+
149
+ ```ts
150
+ // Keep system styling globally; local source buttons use Ionic's default fill.
151
+ const rail = await enableVerticalControlArea({ buttonDefaultFill: 'solid' });
152
+ ```
153
+
154
+ ```html
155
+ <!-- In a fixed toolbar, outside ion-buttons: omitted fill resolves to solid. -->
156
+ <ion-button data-projection="source" aria-label="Add">
157
+ <ion-icon name="add-outline" slot="icon-only"></ion-icon>
158
+ </ion-button>
159
+ ```
160
+
161
+ | Effective fill | Native appearance in `source` mode |
162
+ | --- | --- |
163
+ | `clear` | Icon color, without a glass background; CSS backgrounds are ignored |
164
+ | `solid` | Computed background color tints a prominent glass button |
165
+ | `outline` | Computed border color and width, with native glass |
166
+ | `null` | Native glass with source icon colors |
167
+
168
+ Inside `ion-buttons`, explicitly set `fill="solid"` to project a background; `buttonDefaultFill: 'solid'` does not override the group's clear default. Native Liquid Glass tinting can differ from the CSS color, especially for translucent backgrounds. With the iOS theme, ordinary `ion-buttons` retain group projection; `ion-buttons.ios-theme-disabled` projects eligible buttons individually. Local projection settings do not change that grouping rule.
169
+
170
+ ## Tab bar
171
+
172
+ When the app contains `ion-tabs`, its tab bar moves into the reserved region and uses the native Duo edge spacing; the Ionic `slot` value does not select a different position. Without native projection, the stable Web rail is icon-only, matching the native resting presentation. While the user presses and drags across that rail, every icon-and-label tab reveals its label so the pending destination stays identifiable. The Web tab bar receives pointer input in the simulated system region. Native tabs and a restored Web tab bar fade in over 180ms; disappearance remains immediate. Reduced motion disables this fade. Use `ion-menu` when navigation should become a sidebar; this mode does not convert tabs into a menu. Web clones also work when no `ion-tabs` exists. Disabling the mode or leaving the page removes the native ownership or Web clones and restores their sources. Override `--ios-theme-vertical-bars-toolbar-top` when the simulated system controls use a different vertical layout.
173
+
174
+ ## Vertical Control Area API
175
+
176
+ The generated reference below documents the handle returned by `enableVerticalControlArea()`.
177
+
178
+ <docgen-index>
179
+
180
+ * [`setPlacement(...)`](#setplacement)
181
+ * [`getStatus()`](#getstatus)
182
+ * [`suspend()`](#suspend)
183
+ * [`destroy()`](#destroy)
184
+ * [Interfaces](#interfaces)
185
+ * [Type Aliases](#type-aliases)
186
+
187
+ </docgen-index>
188
+
189
+ <docgen-api>
190
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
191
+
192
+ ### setPlacement(...)
193
+
194
+ ```typescript
195
+ setPlacement(placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean | undefined) => void
196
+ ```
197
+
198
+ Applies the application's chosen placement to both Web and native controls.
199
+
200
+ | Param | Type |
201
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------- |
202
+ | **`placement`** | <code><a href="#verticalbaredge">VerticalBarEdge</a> \| <a href="#verticalbarplacement">VerticalBarPlacement</a></code> |
203
+ | **`rtl`** | <code>boolean</code> |
204
+
205
+ --------------------
206
+
207
+
208
+ ### getStatus()
209
+
210
+ ```typescript
211
+ getStatus() => NativeUIShellStatus
212
+ ```
213
+
214
+ Returns the current Web/native projection state.
215
+
216
+ **Returns:** <code><a href="#nativeuishellstatus">NativeUIShellStatus</a></code>
217
+
218
+ --------------------
219
+
220
+
221
+ ### suspend()
222
+
223
+ ```typescript
224
+ suspend() => Promise<NativeUIShellSuspension>
225
+ ```
226
+
227
+ Restores projected controls to the Web until the returned lease is resumed.
228
+
229
+ **Returns:** <code>Promise&lt;<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>&gt;</code>
230
+
231
+ --------------------
232
+
233
+
234
+ ### destroy()
235
+
236
+ ```typescript
237
+ destroy() => Promise<void>
238
+ ```
239
+
240
+ Stops synchronization, restores Web controls and releases native resources.
241
+
242
+ --------------------
243
+
244
+
245
+ ### Interfaces
246
+
247
+
248
+ #### VerticalBarPlacement
249
+
250
+ | Prop | Type | Description |
251
+ | ---------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
252
+ | **`edge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | |
253
+ | **`inset`** | <code>number</code> | Explicit rail width in CSS pixels; omitted to use the stylesheet's safe-area rules. |
254
+ | **`nativeEdge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | Native logical edge reported by the application's device plugin. Null or an unregistered edge uses a Web rail in verticalBarsOnly mode, or the ordinary Native UI Shell layout otherwise. Omission keeps the last supplied value. |
255
+
256
+
257
+ #### NativeUIShellStatus
258
+
259
+ | Prop | Type |
260
+ | --------------- | ------------------------------------------- |
261
+ | **`state`** | <code>'native' \| 'stopped' \| 'web'</code> |
262
+ | **`projected`** | <code>number</code> |
263
+ | **`updates`** | <code>number</code> |
264
+ | **`reason`** | <code>string</code> |
265
+
266
+
267
+ #### NativeUIShellSuspension
268
+
269
+ | Method | Signature | Description |
270
+ | ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
271
+ | **resume** | () =&gt; Promise&lt;void&gt; | Releases this suspension. Native projection resumes after all active suspensions are released. |
272
+
273
+
274
+ ### Type Aliases
275
+
276
+
277
+ #### VerticalBarEdge
278
+
279
+ Logical edge in the reading direction, matching UIVerticalBarEdge and capacitor-foldable.
280
+
281
+ <code>'leading' | 'trailing' | null</code>
282
+
283
+ </docgen-api>
@@ -35,7 +35,7 @@ final class ShellSearchHost: UIView {
35
35
  }
36
36
 
37
37
  // Preserve UISearchBar's delegate while returning its clear action to Ionic.
38
- private final class ShellSearchInputDelegate: NSObject, UITextFieldDelegate {
38
+ final class ShellSearchInputDelegate: NSObject, UITextFieldDelegate {
39
39
  weak var original: UITextFieldDelegate?
40
40
  var clear: (() -> Void)?
41
41
  override func responds(to selector: Selector!) -> Bool {
@@ -6,6 +6,7 @@ protocol ShellVerticalBarsControlling: AnyObject {
6
6
  var view: UIView { get }
7
7
  func attach(to owner: UIViewController, in parent: UIView)
8
8
  @MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String)
9
+ var ownsKeyboardChrome: Bool { get }
9
10
  func detach()
10
11
  }
11
12
 
@@ -16,6 +17,7 @@ final class ShellVerticalBarsModel: ObservableObject {
16
17
  let label: String
17
18
  let accessibilityLabel: String
18
19
  let image: UIImage?
20
+ let clear: Bool
19
21
  let background: Color?
20
22
  let borderColor: Color?
21
23
  let borderWidth: CGFloat
@@ -30,6 +32,7 @@ final class ShellVerticalBarsModel: ObservableObject {
30
32
  let slot: ShellControl.ToolbarSlot?
31
33
  }
32
34
 
35
+ @Published var search: ShellVerticalSearchModel?
33
36
  @Published var back: Item?
34
37
  @Published var groups: [Group] = []
35
38
  @Published var tabs: [Item] = []
@@ -45,6 +48,7 @@ final class ShellVerticalBarsModel: ObservableObject {
45
48
  Item(id: source.id, label: source.content.label,
46
49
  accessibilityLabel: source.content.accessibilityLabel,
47
50
  image: rendering.image(source.content),
51
+ clear: source.content.buttonFill == .clear,
48
52
  background: source.content.backgroundColor.map { Color(uiColor: rendering.color($0)) },
49
53
  borderColor: source.content.borderColor.map { Color(uiColor: rendering.color($0)) },
50
54
  borderWidth: source.content.borderWidth ?? 0,
@@ -123,6 +127,21 @@ private func verticalBarsButton(_ item: ShellVerticalBarsModel.Item, model: Shel
123
127
  }
124
128
  }
125
129
 
130
+ @available(iOS 26.0, *)
131
+ private struct ShellVerticalBarsButton: ToolbarContent {
132
+ @ObservedObject var model: ShellVerticalBarsModel
133
+ let id: String
134
+ let placement: ToolbarItemPlacement
135
+
136
+ // Observe here so same-ID updates also refresh the shared toolbar background.
137
+ var body: some ToolbarContent {
138
+ if let item = model.groups.lazy.flatMap(\.items).first(where: { $0.id == id }) {
139
+ ToolbarItem(placement: placement) { verticalBarsButton(item, model: model) }
140
+ .sharedBackgroundVisibility(item.clear ? .hidden : .automatic)
141
+ }
142
+ }
143
+ }
144
+
126
145
  @available(iOS 26.0, *)
127
146
  private func verticalBarsBackButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
128
147
  Button { model.activate(item.id) } label: { Image(systemName: "chevron.backward") }
@@ -163,6 +182,7 @@ private struct ShellVerticalBarsView: View {
163
182
  }
164
183
  }
165
184
  }
185
+ .modifier(ShellVerticalBarsSearch(model: model))
166
186
  .modifier(ShellVerticalBarsCompression())
167
187
  .background(Color.clear)
168
188
  }
@@ -179,6 +199,16 @@ private struct ShellVerticalBarsPage: View {
179
199
  }
180
200
  }
181
201
 
202
+ @available(iOS 26.0, *)
203
+ private struct ShellVerticalBarsSearch: ViewModifier {
204
+ @ObservedObject var model: ShellVerticalBarsModel
205
+ @ViewBuilder func body(content: Content) -> some View {
206
+ if let search = model.search {
207
+ content.modifier(ShellVerticalSearchModifier(model: search))
208
+ } else { content }
209
+ }
210
+ }
211
+
182
212
  @available(iOS 26.0, *)
183
213
  private struct ShellVerticalBarsCompression: ViewModifier {
184
214
  @ViewBuilder func body(content: Content) -> some View {
@@ -225,20 +255,22 @@ private struct ShellVerticalBarsToolbar: ViewModifier {
225
255
  .axisBehavior(.verticalPreferred)
226
256
  }
227
257
  ForEach(model.groups.filter { $0.slot == .start }) { group in
228
- ToolbarItemGroup(placement: .topBarLeading) {
229
- ForEach(group.items) { item in
230
- verticalBarsButton(item, model: model)
231
- }
258
+ if group.id != model.groups.first(where: { $0.slot == .start })?.id {
259
+ ToolbarSpacer(.fixed, placement: .topBarLeading)
260
+ }
261
+ ForEach(group.items) { item in
262
+ ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarLeading)
263
+ .axisBehavior(.verticalPreferred)
232
264
  }
233
- .axisBehavior(.verticalPreferred)
234
265
  }
235
266
  ForEach(model.groups.filter { $0.slot != .start }) { group in
236
- ToolbarItemGroup(placement: .topBarTrailing) {
237
- ForEach(group.items) { item in
238
- verticalBarsButton(item, model: model)
239
- }
267
+ if group.id != model.groups.first(where: { $0.slot != .start })?.id {
268
+ ToolbarSpacer(.fixed, placement: .topBarTrailing)
269
+ }
270
+ ForEach(group.items) { item in
271
+ ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarTrailing)
272
+ .axisBehavior(.verticalPreferred)
240
273
  }
241
- .axisBehavior(.verticalPreferred)
242
274
  }
243
275
  }
244
276
  }
@@ -257,10 +289,11 @@ private struct ShellVerticalBarsLegacyToolbar: ViewModifier {
257
289
  }
258
290
  }
259
291
  ForEach(model.groups) { group in
260
- ToolbarItemGroup(placement: .primaryAction) {
261
- ForEach(group.items) { item in
262
- verticalBarsButton(item, model: model)
263
- }
292
+ if group.id != model.groups.first?.id {
293
+ ToolbarSpacer(.fixed, placement: .primaryAction)
294
+ }
295
+ ForEach(group.items) { item in
296
+ ShellVerticalBarsButton(model: model, id: item.id, placement: .primaryAction)
264
297
  }
265
298
  }
266
299
  }
@@ -271,9 +304,11 @@ private struct ShellVerticalBarsLegacyToolbar: ViewModifier {
271
304
  final class ShellVerticalBarsController: ShellVerticalBarsControlling {
272
305
  private final class TransparentHostingController<Content: View>: UIHostingController<Content> {
273
306
  var railEdge = "right"
307
+ var layoutSearch: (() -> Void)?
274
308
  override func viewDidLayoutSubviews() {
275
309
  super.viewDidLayoutSubviews()
276
310
  makeFullSizeSurfacesTransparent(in: view)
311
+ layoutSearch?()
277
312
  }
278
313
 
279
314
  private func makeFullSizeSurfacesTransparent(in surface: UIView) {
@@ -323,7 +358,7 @@ final class ShellVerticalBarsController: ShellVerticalBarsControlling {
323
358
  private func containsBarSurface(in view: UIView, at point: CGPoint) -> Bool {
324
359
  guard !view.isHidden, view.alpha > 0.05 else { return false }
325
360
  let name = NSStringFromClass(type(of: view))
326
- if (name.contains("TabBar") || name.contains("Platter") || name.contains("Pocket") || name.contains("Sidebar")),
361
+ if (name.contains("TabBar") || name.contains("Platter") || name.contains("Pocket") || name.contains("Sidebar") || view is UISearchBar || view is UINavigationBar),
327
362
  convert(view.bounds, from: view).contains(point) { return true }
328
363
  return view.subviews.contains { containsBarSurface(in: $0, at: point) }
329
364
  }
@@ -333,14 +368,20 @@ final class ShellVerticalBarsController: ShellVerticalBarsControlling {
333
368
  private lazy var controller = TransparentHostingController(rootView: ShellVerticalBarsView(model: model))
334
369
  private let container = RailContainer()
335
370
  private weak var owner: UIViewController?
371
+ private let changed: (String, ShellSearchPhase, String, Bool, Int) -> Int
336
372
 
337
373
  var view: UIView { container }
338
374
 
339
- init(activate: @escaping (String) -> Void) {
375
+ var ownsKeyboardChrome: Bool { model.search?.ownsKeyboardChrome == true }
376
+
377
+ init(activate: @escaping (String) -> Void,
378
+ changed: @escaping (String, ShellSearchPhase, String, Bool, Int) -> Int) {
379
+ self.changed = changed
340
380
  model.activate = activate
341
381
  container.backgroundColor = .clear
342
382
  controller.view.backgroundColor = .clear
343
383
  controller.view.isOpaque = false
384
+ controller.layoutSearch = { [weak self] in self?.model.search?.configureField() }
344
385
  }
345
386
 
346
387
  func attach(to owner: UIViewController, in parent: UIView) {
@@ -361,6 +402,10 @@ final class ShellVerticalBarsController: ShellVerticalBarsControlling {
361
402
  container.railEdge = edge
362
403
  controller.railEdge = edge
363
404
  let tabsAppear = model.tabs.isEmpty && controls.contains { $0.kind == .tabBar && !$0.items.isEmpty }
405
+ if let search = controls.first(where: { $0.search != nil })?.search {
406
+ if model.search?.configuration.id == search.id { model.search?.apply(search) }
407
+ else { model.search = ShellVerticalSearchModel(search, in: container, activate: model.activate, changed: changed) }
408
+ } else { model.search = nil }
364
409
  model.apply(controls, rendering: rendering)
365
410
  if tabsAppear { ShellCrossfade.enter(container, duration: ShellCrossfade.duration(180)) }
366
411
  controller.overrideUserInterfaceStyle = controls.contains(where: \.dark) ? .dark : .light