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

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 -280
  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/iphone-duo.md +62 -35
  34. package/docs/migration.md +17 -26
  35. package/docs/setup.md +92 -0
  36. package/docs/special-markup.md +41 -10
  37. package/docs/using-ion-item-group.md +2 -2
  38. package/docs/vertical-bars-layout.md +67 -0
  39. package/package.json +12 -42
  40. package/src/index.ts +3 -1
  41. package/src/layout/index.ts +29 -0
  42. package/src/segment/index.ts +3 -3
  43. package/src/sheets-of-glass/index.ts +7 -7
  44. package/src/styles/components/ion-toolbar.scss +1 -1
  45. package/src/styles/vertical-bars.scss +15 -18
  46. package/src/tab-bar-searchable/dismiss-button.ts +73 -4
  47. package/src/tab-bar-searchable/index.ts +20 -3
  48. package/src/transition/ios.transition.ts +7 -4
  49. package/src/{native-integration/index.ts → transition/native-ui.ts} +17 -46
  50. package/Package.swift +0 -21
  51. package/dist/_virtual/_rolldown/runtime.js +0 -13
  52. package/dist/native/components/index.d.ts +0 -19
  53. package/dist/native/components/index.d.ts.map +0 -1
  54. package/dist/native/components/index.js +0 -40
  55. package/dist/native/components/index.js.map +0 -1
  56. package/dist/native/components/ion-back-button.d.ts +0 -9
  57. package/dist/native/components/ion-back-button.d.ts.map +0 -1
  58. package/dist/native/components/ion-back-button.js +0 -20
  59. package/dist/native/components/ion-back-button.js.map +0 -1
  60. package/dist/native/components/ion-button.d.ts +0 -10
  61. package/dist/native/components/ion-button.d.ts.map +0 -1
  62. package/dist/native/components/ion-button.js +0 -23
  63. package/dist/native/components/ion-button.js.map +0 -1
  64. package/dist/native/components/ion-buttons.d.ts +0 -11
  65. package/dist/native/components/ion-buttons.d.ts.map +0 -1
  66. package/dist/native/components/ion-buttons.js +0 -30
  67. package/dist/native/components/ion-buttons.js.map +0 -1
  68. package/dist/native/components/ion-fab.d.ts +0 -10
  69. package/dist/native/components/ion-fab.d.ts.map +0 -1
  70. package/dist/native/components/ion-fab.js +0 -132
  71. package/dist/native/components/ion-fab.js.map +0 -1
  72. package/dist/native/components/ion-menu-button.d.ts +0 -13
  73. package/dist/native/components/ion-menu-button.d.ts.map +0 -1
  74. package/dist/native/components/ion-menu-button.js +0 -31
  75. package/dist/native/components/ion-menu-button.js.map +0 -1
  76. package/dist/native/components/ion-segment.d.ts +0 -11
  77. package/dist/native/components/ion-segment.d.ts.map +0 -1
  78. package/dist/native/components/ion-segment.js +0 -30
  79. package/dist/native/components/ion-segment.js.map +0 -1
  80. package/dist/native/components/ion-tab-bar.d.ts +0 -11
  81. package/dist/native/components/ion-tab-bar.d.ts.map +0 -1
  82. package/dist/native/components/ion-tab-bar.js +0 -50
  83. package/dist/native/components/ion-tab-bar.js.map +0 -1
  84. package/dist/native/components/searchable-tabs.d.ts +0 -20
  85. package/dist/native/components/searchable-tabs.d.ts.map +0 -1
  86. package/dist/native/components/searchable-tabs.js +0 -383
  87. package/dist/native/components/searchable-tabs.js.map +0 -1
  88. package/dist/native/definitions.d.ts +0 -152
  89. package/dist/native/definitions.d.ts.map +0 -1
  90. package/dist/native/definitions.js +0 -0
  91. package/dist/native/foldable/fold-state.d.ts +0 -5
  92. package/dist/native/foldable/fold-state.d.ts.map +0 -1
  93. package/dist/native/foldable/fold-state.js +0 -14
  94. package/dist/native/foldable/fold-state.js.map +0 -1
  95. package/dist/native/foldable/types.d.ts +0 -12
  96. package/dist/native/foldable/types.d.ts.map +0 -1
  97. package/dist/native/foldable/types.js +0 -0
  98. package/dist/native/index.d.ts +0 -11
  99. package/dist/native/index.d.ts.map +0 -1
  100. package/dist/native/index.js +0 -242
  101. package/dist/native/index.js.map +0 -1
  102. package/dist/native/prehide.d.ts +0 -7
  103. package/dist/native/prehide.d.ts.map +0 -1
  104. package/dist/native/prehide.js +0 -230
  105. package/dist/native/prehide.js.map +0 -1
  106. package/dist/native/runtime.d.ts +0 -5
  107. package/dist/native/runtime.d.ts.map +0 -1
  108. package/dist/native/runtime.js +0 -640
  109. package/dist/native/runtime.js.map +0 -1
  110. package/dist/native/shared/candidate.d.ts +0 -21
  111. package/dist/native/shared/candidate.d.ts.map +0 -1
  112. package/dist/native/shared/candidate.js +0 -98
  113. package/dist/native/shared/candidate.js.map +0 -1
  114. package/dist/native/shared/crossfade.d.ts +0 -10
  115. package/dist/native/shared/crossfade.d.ts.map +0 -1
  116. package/dist/native/shared/crossfade.js +0 -50
  117. package/dist/native/shared/crossfade.js.map +0 -1
  118. package/dist/native/shared/dom.d.ts +0 -46
  119. package/dist/native/shared/dom.d.ts.map +0 -1
  120. package/dist/native/shared/dom.js +0 -172
  121. package/dist/native/shared/dom.js.map +0 -1
  122. package/dist/native/shared/events.d.ts +0 -11
  123. package/dist/native/shared/events.d.ts.map +0 -1
  124. package/dist/native/shared/events.js +0 -13
  125. package/dist/native/shared/events.js.map +0 -1
  126. package/dist/native/shared/icons.d.ts +0 -8
  127. package/dist/native/shared/icons.d.ts.map +0 -1
  128. package/dist/native/shared/icons.js +0 -61
  129. package/dist/native/shared/icons.js.map +0 -1
  130. package/dist/native/shared/modal.d.ts +0 -10
  131. package/dist/native/shared/modal.d.ts.map +0 -1
  132. package/dist/native/shared/modal.js +0 -160
  133. package/dist/native/shared/modal.js.map +0 -1
  134. package/dist/native/vertical-bars-web-search.d.ts +0 -9
  135. package/dist/native/vertical-bars-web-search.d.ts.map +0 -1
  136. package/dist/native/vertical-bars-web-search.js +0 -84
  137. package/dist/native/vertical-bars-web-search.js.map +0 -1
  138. package/dist/native/vertical-bars-web.d.ts +0 -5
  139. package/dist/native/vertical-bars-web.d.ts.map +0 -1
  140. package/dist/native/vertical-bars-web.js +0 -341
  141. package/dist/native/vertical-bars-web.js.map +0 -1
  142. package/dist/native-integration/index.d.ts +0 -23
  143. package/dist/native-integration/index.d.ts.map +0 -1
  144. package/dist/native-integration/index.js.map +0 -1
  145. package/dist/native-integration/transition.d.ts +0 -5
  146. package/dist/native-integration/transition.d.ts.map +0 -1
  147. package/dist/native-integration/transition.js +0 -16
  148. package/dist/native-integration/transition.js.map +0 -1
  149. package/dist/vertical-bars.d.ts +0 -5
  150. package/dist/vertical-bars.js +0 -4
  151. package/docs/e2e-testing.md +0 -51
  152. package/docs/feedback.md +0 -65
  153. package/docs/iphone-duo-with-original-theme.md +0 -158
  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,285 +0,0 @@
1
- ---
2
- title: Vertical Bars (preview)
3
- ---
4
-
5
- # Vertical Bars (preview)
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
- Available in `1.2.0` as a **preview** feature. APIs and supported behavior may change.
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
- A toolbar whose content has all moved into the rail collapses while projection is active. Toolbars containing a title, direct text, other content or a horizontal-only control remain visible. Removing projection restores the original toolbar.
89
-
90
- ### Choose button appearance
91
-
92
- `buttonProjection` and the local projection settings below are available in `1.2.0`.
93
-
94
- For native vertical `ion-button` and `ion-menu-button` actions, choose who controls appearance:
95
-
96
- | `buttonProjection` | Appearance |
97
- | --- | --- |
98
- | `'system'` (default) | SwiftUI styles the buttons and tints their icons. Ionic fill, colors and borders are not applied. |
99
- | `'source'` | Projects the supported Ionic fill and computed colors described in [Source fill rules](#source-fill-rules). |
100
-
101
- ```ts
102
- const rail = await enableVerticalControlArea({ buttonProjection: 'source' });
103
- ```
104
-
105
- 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. Disabled icons use the native disabled appearance. These appearance settings do not affect horizontal controls, source elements or Web fallback clones, so compare the native appearance on supported iOS.
106
-
107
- **Migration from the experimental releases:** the default changes from source styling to `system`. Set `buttonProjection: 'source'` to retain the previous projection behavior.
108
-
109
- ### Override individual buttons or groups
110
-
111
- Use `data-projection` for local exceptions. Existing classes remain supported:
112
-
113
- | Attribute | Equivalent class |
114
- | --- | --- |
115
- | `data-projection="source"` | `ios-theme-projection-source` |
116
- | `data-projection="system"` | `ios-theme-projection-system` |
117
-
118
- ```html
119
- <ion-buttons data-projection="source">
120
- <ion-button fill="solid" aria-label="Add">
121
- <ion-icon name="add-outline" slot="icon-only"></ion-icon>
122
- </ion-button>
123
- <ion-button data-projection="system" aria-label="Search">
124
- <ion-icon name="search-outline" slot="icon-only"></ion-icon>
125
- </ion-button>
126
- </ion-buttons>
127
- ```
128
-
129
- The first matching setting wins:
130
-
131
- 1. The button's local setting.
132
- 2. Its nearest `ion-buttons` local setting.
133
- 3. The startup `buttonProjection` option, or `system` if omitted.
134
-
135
- 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.
136
-
137
- 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).
138
-
139
- ### Source fill rules
140
-
141
- For an `ion-button` resolved to `source`, fill is chosen separately from the projection mode:
142
-
143
- | Button | Effective fill |
144
- | --- | --- |
145
- | Explicit `fill="clear"`, `"solid"` or `"outline"` | The explicit value |
146
- | Omitted fill or `fill="default"` inside `ion-buttons` | `clear` |
147
- | Omitted fill or `fill="default"` outside `ion-buttons` | `buttonDefaultFill` |
148
-
149
- `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.
150
-
151
- ```ts
152
- // Keep system styling globally; local source buttons use Ionic's default fill.
153
- const rail = await enableVerticalControlArea({ buttonDefaultFill: 'solid' });
154
- ```
155
-
156
- ```html
157
- <!-- In a fixed toolbar, outside ion-buttons: omitted fill resolves to solid. -->
158
- <ion-button data-projection="source" aria-label="Add">
159
- <ion-icon name="add-outline" slot="icon-only"></ion-icon>
160
- </ion-button>
161
- ```
162
-
163
- | Effective fill | Native appearance in `source` mode |
164
- | --- | --- |
165
- | `clear` | Icon color, without a glass background; CSS backgrounds are ignored |
166
- | `solid` | Computed background color tints a prominent glass button |
167
- | `outline` | Computed border color and width, with native glass |
168
- | `null` | Native glass with source icon colors |
169
-
170
- 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.
171
-
172
- ## Tab bar
173
-
174
- 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.
175
-
176
- ## Vertical Control Area API
177
-
178
- The generated reference below documents the handle returned by `enableVerticalControlArea()`.
179
-
180
- <docgen-index>
181
-
182
- * [`setPlacement(...)`](#setplacement)
183
- * [`getStatus()`](#getstatus)
184
- * [`suspend()`](#suspend)
185
- * [`destroy()`](#destroy)
186
- * [Interfaces](#interfaces)
187
- * [Type Aliases](#type-aliases)
188
-
189
- </docgen-index>
190
-
191
- <docgen-api>
192
- <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
193
-
194
- ### setPlacement(...)
195
-
196
- ```typescript
197
- setPlacement(placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean | undefined) => void
198
- ```
199
-
200
- Applies the application's chosen placement to both Web and native controls.
201
-
202
- | Param | Type |
203
- | --------------- | ----------------------------------------------------------------------------------------------------------------------- |
204
- | **`placement`** | <code><a href="#verticalbaredge">VerticalBarEdge</a> \| <a href="#verticalbarplacement">VerticalBarPlacement</a></code> |
205
- | **`rtl`** | <code>boolean</code> |
206
-
207
- --------------------
208
-
209
-
210
- ### getStatus()
211
-
212
- ```typescript
213
- getStatus() => NativeUIShellStatus
214
- ```
215
-
216
- Returns the current Web/native projection state.
217
-
218
- **Returns:** <code><a href="#nativeuishellstatus">NativeUIShellStatus</a></code>
219
-
220
- --------------------
221
-
222
-
223
- ### suspend()
224
-
225
- ```typescript
226
- suspend() => Promise<NativeUIShellSuspension>
227
- ```
228
-
229
- Restores projected controls to the Web until the returned lease is resumed.
230
-
231
- **Returns:** <code>Promise&lt;<a href="#nativeuishellsuspension">NativeUIShellSuspension</a>&gt;</code>
232
-
233
- --------------------
234
-
235
-
236
- ### destroy()
237
-
238
- ```typescript
239
- destroy() => Promise<void>
240
- ```
241
-
242
- Stops synchronization, restores Web controls and releases native resources.
243
-
244
- --------------------
245
-
246
-
247
- ### Interfaces
248
-
249
-
250
- #### VerticalBarPlacement
251
-
252
- | Prop | Type | Description |
253
- | ---------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
254
- | **`edge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | |
255
- | **`inset`** | <code>number</code> | Explicit rail width in CSS pixels; omitted to use the stylesheet's safe-area rules. |
256
- | **`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. |
257
-
258
-
259
- #### NativeUIShellStatus
260
-
261
- | Prop | Type |
262
- | --------------- | ------------------------------------------- |
263
- | **`state`** | <code>'native' \| 'stopped' \| 'web'</code> |
264
- | **`projected`** | <code>number</code> |
265
- | **`updates`** | <code>number</code> |
266
- | **`reason`** | <code>string</code> |
267
-
268
-
269
- #### NativeUIShellSuspension
270
-
271
- | Method | Signature | Description |
272
- | ---------- | ---------------------------- | ---------------------------------------------------------------------------------------------- |
273
- | **resume** | () =&gt; Promise&lt;void&gt; | Releases this suspension. Native projection resumes after all active suspensions are released. |
274
-
275
-
276
- ### Type Aliases
277
-
278
-
279
- #### VerticalBarEdge
280
-
281
- Logical edge in the reading direction, matching UIVerticalBarEdge and capacitor-foldable.
282
-
283
- <code>'leading' | 'trailing' | null</code>
284
-
285
- </docgen-api>
@@ -1,71 +0,0 @@
1
- import UIKit
2
-
3
- enum ShellButton {
4
- private final class BadgeLabel: UILabel {}
5
- // These Ionic components share the same native UIButton presentation.
6
- static let kinds: [ShellComponent] = [.button, .backButton, .menuButton]
7
-
8
- @available(iOS 26.0, *)
9
- static func make(_ node: ShellControl, rendering: ShellRendering, activate: @escaping (String) -> Void) -> UIButton? {
10
- guard node.items.count == 1, let item = node.items.first else { return nil }
11
- let control = render(item.content, glass: true, rendering: rendering, activate: activate)
12
- control.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
13
- return control
14
- }
15
-
16
- @available(iOS 26.0, *)
17
- static func render(_ item: ShellItemContent, glass: Bool, existing: UIButton? = nil, rendering: ShellRendering, activate: @escaping (String) -> Void) -> UIButton {
18
- var configuration: UIButton.Configuration = glass ? .glass() : .plain()
19
- configuration.title = item.label
20
- configuration.image = rendering.image(item)
21
- configuration.imagePadding = item.imagePadding ?? 4
22
- configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: item.contentInsetLeading ?? 4,
23
- bottom: 0, trailing: item.contentInsetTrailing ?? 4)
24
- configuration.imagePlacement = item.iconPosition == .trailing ? .trailing : .leading
25
- configuration.baseForegroundColor = rendering.color(item.color)
26
- configuration.titleLineBreakMode = .byTruncatingTail
27
- let size = item.fontSize
28
- let weight = item.fontWeight
29
- configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
30
- var outgoing = incoming
31
- outgoing.font = UIFont.systemFont(ofSize: size, weight: weight >= 600 ? .semibold : weight >= 500 ? .medium : .regular)
32
- return outgoing
33
- }
34
- let button = existing ?? UIButton(configuration: configuration)
35
- if existing != nil { button.configuration = configuration }
36
- button.titleLabel?.numberOfLines = 1
37
- // Web and UIKit can differ by a fraction of a point in text measurement.
38
- // Absorb that rounding without widening the projected button.
39
- button.titleLabel?.adjustsFontSizeToFitWidth = true
40
- button.titleLabel?.minimumScaleFactor = 0.99
41
- button.isEnabled = !item.disabled
42
- button.accessibilityLabel = item.accessibilityLabel
43
- button.accessibilityIdentifier = item.id
44
- if item.selected { button.accessibilityTraits.insert(.selected) }
45
- button.subviews.filter { $0 is BadgeLabel }.forEach { $0.removeFromSuperview() }
46
- button.accessibilityValue = item.badge?.value
47
- if let badge = item.badge {
48
- let label = BadgeLabel()
49
- label.text = badge.value
50
- label.font = .systemFont(ofSize: 11, weight: .semibold)
51
- label.textColor = rendering.color(badge.textColor)
52
- label.backgroundColor = rendering.color(badge.color)
53
- label.textAlignment = .center
54
- label.layer.cornerRadius = 8
55
- label.clipsToBounds = true
56
- label.isAccessibilityElement = false
57
- label.translatesAutoresizingMaskIntoConstraints = false
58
- button.addSubview(label)
59
- NSLayoutConstraint.activate([
60
- label.centerXAnchor.constraint(equalTo: button.centerXAnchor, constant: 14),
61
- label.topAnchor.constraint(equalTo: button.topAnchor, constant: 1),
62
- label.widthAnchor.constraint(greaterThanOrEqualToConstant: max(16, label.intrinsicContentSize.width + 6)),
63
- label.heightAnchor.constraint(equalToConstant: 16)
64
- ])
65
- }
66
- if existing == nil {
67
- button.addAction(UIAction { _ in activate(item.id) }, for: .touchUpInside)
68
- }
69
- return button
70
- }
71
- }
@@ -1,24 +0,0 @@
1
- import UIKit
2
-
3
- enum ShellButtons {
4
- static let kind = ShellComponent.buttons
5
-
6
- @available(iOS 26.0, *)
7
- static func make(_ node: ShellControl, scale: CGFloat, rendering: ShellRendering, activate: @escaping (String) -> Void) -> UIView {
8
- let items = node.items
9
- let rtl = node.rtl
10
- let effect = UIGlassEffect(style: .regular)
11
- let control = UIVisualEffectView(effect: effect)
12
- control.layer.cornerRadius = node.frame.height * scale / 2
13
- control.clipsToBounds = true
14
- control.accessibilityIdentifier = node.id
15
- for item in items {
16
- let button = ShellButton.render(item.content, glass: false, rendering: rendering, activate: activate)
17
- button.semanticContentAttribute = rtl ? .forceRightToLeft : .forceLeftToRight
18
- let rect = item.frame.rect
19
- button.frame = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale)
20
- control.contentView.addSubview(button)
21
- }
22
- return control
23
- }
24
- }
@@ -1,18 +0,0 @@
1
- import UIKit
2
-
3
- // Static composition of supported wire kinds. Component files own their names and rendering.
4
- enum ShellComponents {
5
- static let supported = Set(ShellButton.kinds + [ShellButtons.kind, ShellTabBar.kind, ShellSegment.kind, ShellFab.kind])
6
-
7
- @available(iOS 26.0, *)
8
- static func make(_ node: ShellControl, scale: CGFloat, rendering: ShellRendering,
9
- tabDelegate: UITabBarDelegate, activate: @escaping (String) -> Void) -> UIView? {
10
- switch node.kind {
11
- case .button, .backButton, .menuButton: return ShellButton.make(node, rendering: rendering, activate: activate)
12
- case .buttons: return ShellButtons.make(node, scale: scale, rendering: rendering, activate: activate)
13
- case .tabBar: return ShellTabBar.make(node, rendering: rendering, delegate: tabDelegate)
14
- case .segment: return ShellSegment.make(node, scale: scale, rendering: rendering, activate: activate)
15
- case .fab: return ShellFab()
16
- }
17
- }
18
- }
@@ -1,83 +0,0 @@
1
- import UIKit
2
-
3
- final class ShellFab: UIView {
4
- static let kind = ShellComponent.fab
5
- var buttons: [String: UIButton] = [:]
6
- var content: [String: ShellItemContent] = [:]
7
- override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
8
- let hit = super.hitTest(point, with: event)
9
- return hit === self ? nil : hit
10
- }
11
-
12
- @available(iOS 26.0, *)
13
- func apply(_ node: ShellControl, scale: CGFloat, rendering: ShellRendering, activate: @escaping (String) -> Void) {
14
- let items = node.items
15
- let retained = Set(items.map(\.id))
16
- var lostFocus = false
17
- for id in Array(self.buttons.keys) where !retained.contains(id) {
18
- lostFocus = lostFocus || self.buttons[id]?.accessibilityElementIsFocused() == true
19
- self.buttons.removeValue(forKey: id)?.removeFromSuperview()
20
- self.content.removeValue(forKey: id)
21
- }
22
- self.accessibilityIdentifier = node.id
23
- for item in items {
24
- let id = item.id
25
- let artwork = item.content.fabArtwork
26
- let previous = self.buttons[id]
27
- let button = previous ?? ShellButton.render(artwork, glass: true, rendering: rendering, activate: activate)
28
- if previous == nil {
29
- self.buttons[id] = button
30
- self.addSubview(button)
31
- #if DEBUG
32
- NSLog("[Native UI Shell] Created FAB button %@ %p", id, unsafeBitCast(button, to: Int.self))
33
- #endif
34
- } else if self.content[id] != artwork {
35
- let changedIcon = self.content[id]?.selected != item.content.selected
36
- let duration = UIAccessibility.isReduceMotionEnabled ? 0 : min(item.content.iconTransition ?? 0, 1)
37
- if changedIcon, let imageView = button.imageView, duration > 0 {
38
- UIView.transition(with: imageView, duration: duration,
39
- options: [.transitionCrossDissolve, .beginFromCurrentState, .allowUserInteraction]) {
40
- _ = ShellButton.render(artwork, glass: true, existing: button, rendering: rendering, activate: activate)
41
- }
42
- } else {
43
- UIView.performWithoutAnimation { _ = ShellButton.render(artwork, glass: true, existing: button, rendering: rendering, activate: activate) }
44
- }
45
- }
46
- self.content[id] = artwork
47
- button.accessibilityTraits.remove(.selected)
48
- if id == items.first?.id && items.count > 1 {
49
- button.accessibilityValue = item.content.selected
50
- ? NSLocalizedString("Expanded", comment: "FAB accessibility state")
51
- : NSLocalizedString("Collapsed", comment: "FAB accessibility state")
52
- } else {
53
- button.accessibilityValue = nil
54
- }
55
- let hidden = item.visible == false
56
- if hidden && !button.isHidden && button.accessibilityElementIsFocused() { lostFocus = true }
57
- button.isHidden = hidden
58
- button.isUserInteractionEnabled = button.isEnabled && !hidden
59
- button.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
60
- let rect = item.frame.rect
61
- let frame = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale)
62
- if button.frame != frame { button.frame = frame }
63
- }
64
- let ordered = items.compactMap { self.buttons[$0.id] }.filter { !$0.isHidden }
65
- self.accessibilityElements = ordered
66
- if lostFocus, let main = ordered.first, main.isEnabled, self.window != nil {
67
- UIAccessibility.post(notification: .layoutChanged, argument: main)
68
- }
69
- }
70
- }
71
-
72
- extension ShellItemContent {
73
- var fabArtwork: Self {
74
- var artwork = self
75
- if selected {
76
- artwork.icon = closeIcon
77
- artwork.iconWidth = closeIconWidth
78
- artwork.iconHeight = closeIconHeight
79
- artwork.label = ""
80
- }
81
- return artwork
82
- }
83
- }