@rdlabo/ionic-theme-ios27 1.2.1 → 2.0.0-beta.pr304.sha56daf151ad80

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 +151 -0
  29. package/docs/add-native-ui.md +53 -0
  30. package/docs/animation.md +1 -1
  31. package/docs/eslint.md +1 -1
  32. package/docs/features.md +3 -3
  33. package/docs/iphone-duo-with-original-theme.md +44 -29
  34. package/docs/iphone-duo.md +29 -32
  35. package/docs/migration.md +12 -24
  36. package/docs/setup.md +82 -0
  37. package/docs/special-markup.md +5 -5
  38. package/docs/using-ion-item-group.md +2 -2
  39. package/docs/vertical-bars-layout.md +47 -0
  40. package/package.json +12 -42
  41. package/src/index.ts +3 -1
  42. package/src/layout/index.ts +29 -0
  43. package/src/segment/index.ts +3 -3
  44. package/src/sheets-of-glass/index.ts +7 -7
  45. package/src/styles/components/ion-toolbar.scss +1 -1
  46. package/src/styles/vertical-bars.scss +15 -18
  47. package/src/tab-bar-searchable/dismiss-button.ts +73 -4
  48. package/src/tab-bar-searchable/index.ts +20 -3
  49. package/src/transition/ios.transition.ts +7 -4
  50. package/src/{native-integration/index.ts → transition/native-ui.ts} +17 -46
  51. package/Package.swift +0 -21
  52. package/dist/_virtual/_rolldown/runtime.js +0 -13
  53. package/dist/native/components/index.d.ts +0 -19
  54. package/dist/native/components/index.d.ts.map +0 -1
  55. package/dist/native/components/index.js +0 -40
  56. package/dist/native/components/index.js.map +0 -1
  57. package/dist/native/components/ion-back-button.d.ts +0 -9
  58. package/dist/native/components/ion-back-button.d.ts.map +0 -1
  59. package/dist/native/components/ion-back-button.js +0 -20
  60. package/dist/native/components/ion-back-button.js.map +0 -1
  61. package/dist/native/components/ion-button.d.ts +0 -10
  62. package/dist/native/components/ion-button.d.ts.map +0 -1
  63. package/dist/native/components/ion-button.js +0 -23
  64. package/dist/native/components/ion-button.js.map +0 -1
  65. package/dist/native/components/ion-buttons.d.ts +0 -11
  66. package/dist/native/components/ion-buttons.d.ts.map +0 -1
  67. package/dist/native/components/ion-buttons.js +0 -30
  68. package/dist/native/components/ion-buttons.js.map +0 -1
  69. package/dist/native/components/ion-fab.d.ts +0 -10
  70. package/dist/native/components/ion-fab.d.ts.map +0 -1
  71. package/dist/native/components/ion-fab.js +0 -132
  72. package/dist/native/components/ion-fab.js.map +0 -1
  73. package/dist/native/components/ion-menu-button.d.ts +0 -13
  74. package/dist/native/components/ion-menu-button.d.ts.map +0 -1
  75. package/dist/native/components/ion-menu-button.js +0 -31
  76. package/dist/native/components/ion-menu-button.js.map +0 -1
  77. package/dist/native/components/ion-segment.d.ts +0 -11
  78. package/dist/native/components/ion-segment.d.ts.map +0 -1
  79. package/dist/native/components/ion-segment.js +0 -30
  80. package/dist/native/components/ion-segment.js.map +0 -1
  81. package/dist/native/components/ion-tab-bar.d.ts +0 -11
  82. package/dist/native/components/ion-tab-bar.d.ts.map +0 -1
  83. package/dist/native/components/ion-tab-bar.js +0 -50
  84. package/dist/native/components/ion-tab-bar.js.map +0 -1
  85. package/dist/native/components/searchable-tabs.d.ts +0 -20
  86. package/dist/native/components/searchable-tabs.d.ts.map +0 -1
  87. package/dist/native/components/searchable-tabs.js +0 -383
  88. package/dist/native/components/searchable-tabs.js.map +0 -1
  89. package/dist/native/definitions.d.ts +0 -152
  90. package/dist/native/definitions.d.ts.map +0 -1
  91. package/dist/native/definitions.js +0 -0
  92. package/dist/native/foldable/fold-state.d.ts +0 -5
  93. package/dist/native/foldable/fold-state.d.ts.map +0 -1
  94. package/dist/native/foldable/fold-state.js +0 -14
  95. package/dist/native/foldable/fold-state.js.map +0 -1
  96. package/dist/native/foldable/types.d.ts +0 -12
  97. package/dist/native/foldable/types.d.ts.map +0 -1
  98. package/dist/native/foldable/types.js +0 -0
  99. package/dist/native/index.d.ts +0 -11
  100. package/dist/native/index.d.ts.map +0 -1
  101. package/dist/native/index.js +0 -242
  102. package/dist/native/index.js.map +0 -1
  103. package/dist/native/prehide.d.ts +0 -7
  104. package/dist/native/prehide.d.ts.map +0 -1
  105. package/dist/native/prehide.js +0 -230
  106. package/dist/native/prehide.js.map +0 -1
  107. package/dist/native/runtime.d.ts +0 -5
  108. package/dist/native/runtime.d.ts.map +0 -1
  109. package/dist/native/runtime.js +0 -640
  110. package/dist/native/runtime.js.map +0 -1
  111. package/dist/native/shared/candidate.d.ts +0 -21
  112. package/dist/native/shared/candidate.d.ts.map +0 -1
  113. package/dist/native/shared/candidate.js +0 -98
  114. package/dist/native/shared/candidate.js.map +0 -1
  115. package/dist/native/shared/crossfade.d.ts +0 -10
  116. package/dist/native/shared/crossfade.d.ts.map +0 -1
  117. package/dist/native/shared/crossfade.js +0 -50
  118. package/dist/native/shared/crossfade.js.map +0 -1
  119. package/dist/native/shared/dom.d.ts +0 -46
  120. package/dist/native/shared/dom.d.ts.map +0 -1
  121. package/dist/native/shared/dom.js +0 -172
  122. package/dist/native/shared/dom.js.map +0 -1
  123. package/dist/native/shared/events.d.ts +0 -11
  124. package/dist/native/shared/events.d.ts.map +0 -1
  125. package/dist/native/shared/events.js +0 -13
  126. package/dist/native/shared/events.js.map +0 -1
  127. package/dist/native/shared/icons.d.ts +0 -8
  128. package/dist/native/shared/icons.d.ts.map +0 -1
  129. package/dist/native/shared/icons.js +0 -61
  130. package/dist/native/shared/icons.js.map +0 -1
  131. package/dist/native/shared/modal.d.ts +0 -10
  132. package/dist/native/shared/modal.d.ts.map +0 -1
  133. package/dist/native/shared/modal.js +0 -160
  134. package/dist/native/shared/modal.js.map +0 -1
  135. package/dist/native/vertical-bars-web-search.d.ts +0 -9
  136. package/dist/native/vertical-bars-web-search.d.ts.map +0 -1
  137. package/dist/native/vertical-bars-web-search.js +0 -84
  138. package/dist/native/vertical-bars-web-search.js.map +0 -1
  139. package/dist/native/vertical-bars-web.d.ts +0 -5
  140. package/dist/native/vertical-bars-web.d.ts.map +0 -1
  141. package/dist/native/vertical-bars-web.js +0 -341
  142. package/dist/native/vertical-bars-web.js.map +0 -1
  143. package/dist/native-integration/index.d.ts +0 -23
  144. package/dist/native-integration/index.d.ts.map +0 -1
  145. package/dist/native-integration/index.js.map +0 -1
  146. package/dist/native-integration/transition.d.ts +0 -5
  147. package/dist/native-integration/transition.d.ts.map +0 -1
  148. package/dist/native-integration/transition.js +0 -16
  149. package/dist/native-integration/transition.js.map +0 -1
  150. package/dist/vertical-bars.d.ts +0 -5
  151. package/dist/vertical-bars.js +0 -4
  152. package/docs/e2e-testing.md +0 -51
  153. package/docs/feedback.md +0 -65
  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,437 +0,0 @@
1
- import Combine
2
- import SwiftUI
3
- import UIKit
4
-
5
- protocol ShellVerticalBarsControlling: AnyObject {
6
- var view: UIView { get }
7
- func attach(to owner: UIViewController, in parent: UIView)
8
- @MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String)
9
- var ownsKeyboardChrome: Bool { get }
10
- func detach()
11
- }
12
-
13
- @available(iOS 26.0, *)
14
- final class ShellVerticalBarsModel: ObservableObject {
15
- struct Item: Identifiable {
16
- let id: String
17
- let label: String
18
- let accessibilityLabel: String
19
- let image: UIImage?
20
- let clear: Bool
21
- let background: Color?
22
- let borderColor: Color?
23
- let borderWidth: CGFloat
24
- let badge: ShellBadge?
25
- let disabled: Bool
26
- var selected: Bool
27
- }
28
-
29
- struct Group: Identifiable {
30
- let id: String
31
- let items: [Item]
32
- let slot: ShellControl.ToolbarSlot?
33
- }
34
-
35
- @Published var search: ShellVerticalSearchModel?
36
- @Published var back: Item?
37
- @Published var groups: [Group] = []
38
- @Published var tabs: [Item] = []
39
- @Published var selection = ""
40
- var activate: (String) -> Void = { _ in }
41
- private var domSelection = ""
42
- private var pendingSelection: ShellTabBar.PendingSelection?
43
- private var pendingExpiryWork: DispatchWorkItem?
44
-
45
- func apply(_ controls: [ShellControl], rendering: ShellRendering,
46
- now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()) {
47
- func item(_ source: ShellItem) -> Item {
48
- let image = rendering.image(source.content)
49
- return Item(id: source.id, label: source.content.label,
50
- accessibilityLabel: source.content.accessibilityLabel,
51
- // UIKit toolbar extraction uses the UIImage's rendering mode.
52
- image: source.content.disabled ? image?.withRenderingMode(.alwaysTemplate) : image,
53
- clear: source.content.buttonFill == .clear,
54
- background: source.content.backgroundColor.map { Color(uiColor: rendering.color($0)) },
55
- borderColor: source.content.borderColor.map { Color(uiColor: rendering.color($0)) },
56
- borderWidth: source.content.borderWidth ?? 0,
57
- badge: source.content.badge, disabled: source.content.disabled,
58
- selected: source.content.selected)
59
- }
60
- withAnimation(.smooth(duration: 0.3)) {
61
- back = controls.first(where: { $0.kind == .backButton })?.items.first.map(item)
62
- groups = controls.compactMap { control in
63
- guard [.button, .buttons, .menuButton].contains(control.kind) else { return nil }
64
- return Group(id: control.id, items: control.items.map(item), slot: control.toolbarSlot)
65
- }
66
- }
67
- tabs = controls.first(where: { $0.kind == .tabBar })?.items.map(item) ?? []
68
- domSelection = tabs.first(where: \.selected)?.id ?? ""
69
- reconcileSelection(now: now)
70
- }
71
-
72
- func select(_ id: String, now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent(), ttl: CFTimeInterval = 1) {
73
- guard let item = tabs.first(where: { $0.id == id }), !item.disabled else { return }
74
- pendingSelection = .init(id: id, until: now + ttl)
75
- selection = id
76
- pendingExpiryWork?.cancel()
77
- let work = DispatchWorkItem { [weak self] in self?.reconcileSelection() }
78
- pendingExpiryWork = work
79
- DispatchQueue.main.asyncAfter(deadline: .now() + ttl + 0.02, execute: work)
80
- activate(id)
81
- }
82
-
83
- private func reconcileSelection(now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()) {
84
- guard let pending = pendingSelection else { selection = domSelection; return }
85
- let item = tabs.first(where: { $0.id == pending.id })
86
- if item == nil || item!.disabled || now >= pending.until || domSelection == pending.id {
87
- pendingSelection = nil
88
- pendingExpiryWork?.cancel()
89
- pendingExpiryWork = nil
90
- selection = domSelection
91
- } else {
92
- selection = pending.id
93
- }
94
- }
95
- }
96
-
97
- @available(iOS 26.0, *)
98
- private struct ShellVerticalBarsLabel: View {
99
- let item: ShellVerticalBarsModel.Item
100
-
101
- var body: some View {
102
- if let image = item.image {
103
- Label {
104
- Text(item.label.isEmpty ? item.accessibilityLabel : item.label)
105
- } icon: {
106
- Image(uiImage: image)
107
- }
108
- } else {
109
- Text(item.label)
110
- }
111
- }
112
- }
113
-
114
- @available(iOS 26.0, *)
115
- @ViewBuilder
116
- private func verticalBarsButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
117
- let button = Button { model.activate(item.id) } label: { ShellVerticalBarsLabel(item: item) }
118
- .disabled(item.disabled)
119
- .accessibilityLabel(item.accessibilityLabel)
120
- .accessibilityIdentifier(item.id)
121
- if let background = item.background {
122
- button.buttonStyle(.glassProminent).tint(background)
123
- } else {
124
- button.overlay {
125
- if let borderColor = item.borderColor {
126
- Capsule().strokeBorder(borderColor, lineWidth: item.borderWidth).allowsHitTesting(false)
127
- }
128
- }
129
- }
130
- }
131
-
132
- @available(iOS 26.0, *)
133
- private struct ShellVerticalBarsButton: ToolbarContent {
134
- @ObservedObject var model: ShellVerticalBarsModel
135
- let id: String
136
- let placement: ToolbarItemPlacement
137
-
138
- // Observe here so same-ID updates also refresh the shared toolbar background.
139
- var body: some ToolbarContent {
140
- if let item = model.groups.lazy.flatMap(\.items).first(where: { $0.id == id }) {
141
- ToolbarItem(placement: placement) { verticalBarsButton(item, model: model) }
142
- .sharedBackgroundVisibility(item.clear ? .hidden : .automatic)
143
- }
144
- }
145
- }
146
-
147
- @available(iOS 26.0, *)
148
- private func verticalBarsBackButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
149
- Button { model.activate(item.id) } label: { Image(systemName: "chevron.backward") }
150
- .disabled(item.disabled)
151
- .accessibilityLabel(item.accessibilityLabel)
152
- .accessibilityIdentifier("BackButton")
153
- }
154
-
155
- @available(iOS 26.0, *)
156
- private struct ShellVerticalBarsBadge: ViewModifier {
157
- let badge: ShellBadge?
158
-
159
- @ViewBuilder func body(content: Content) -> some View {
160
- if let badge { content.badge(badge.value) }
161
- else { content }
162
- }
163
- }
164
-
165
- @available(iOS 26.0, *)
166
- private struct ShellVerticalBarsView: View {
167
- @ObservedObject var model: ShellVerticalBarsModel
168
-
169
- var body: some View {
170
- Group {
171
- if model.tabs.isEmpty {
172
- ShellVerticalBarsPage(model: model)
173
- } else {
174
- TabView(selection: Binding(get: { model.selection }, set: { model.select($0) })) {
175
- ForEach(model.tabs) { item in
176
- ShellVerticalBarsPage(model: model)
177
- .tag(item.id)
178
- .tabItem { ShellVerticalBarsLabel(item: item) }
179
- .modifier(ShellVerticalBarsBadge(badge: item.badge))
180
- .disabled(item.disabled)
181
- .accessibilityLabel(item.accessibilityLabel)
182
- .accessibilityIdentifier(item.id)
183
- }
184
- }
185
- }
186
- }
187
- .modifier(ShellVerticalBarsSearch(model: model))
188
- .modifier(ShellVerticalBarsCompression())
189
- .background(Color.clear)
190
- }
191
- }
192
-
193
- @available(iOS 26.0, *)
194
- private struct ShellVerticalBarsPage: View {
195
- @ObservedObject var model: ShellVerticalBarsModel
196
-
197
- var body: some View {
198
- NavigationStack {
199
- Color.clear.modifier(ShellVerticalBarsToolbarAdapter(model: model))
200
- .toolbar {
201
- if model.search?.configuration.available == true {
202
- // Let the system adapt bottom-bar search to the vertical rail.
203
- DefaultToolbarItem(kind: .search, placement: .bottomBar)
204
- }
205
- }
206
- }
207
- }
208
- }
209
-
210
- @available(iOS 26.0, *)
211
- private struct ShellVerticalBarsSearch: ViewModifier {
212
- @ObservedObject var model: ShellVerticalBarsModel
213
- @ViewBuilder func body(content: Content) -> some View {
214
- if let search = model.search {
215
- content.modifier(ShellVerticalSearchModifier(model: search))
216
- } else { content }
217
- }
218
- }
219
-
220
- @available(iOS 26.0, *)
221
- private struct ShellVerticalBarsCompression: ViewModifier {
222
- @ViewBuilder func body(content: Content) -> some View {
223
- #if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
224
- if #available(iOS 27.1, *) {
225
- content.toolbarVerticalCompressionBehavior(.prefersToolbarItems)
226
- } else {
227
- content
228
- }
229
- #else
230
- content
231
- #endif
232
- }
233
- }
234
-
235
- @available(iOS 26.0, *)
236
- private struct ShellVerticalBarsToolbarAdapter: ViewModifier {
237
- @ObservedObject var model: ShellVerticalBarsModel
238
-
239
- @ViewBuilder func body(content: Content) -> some View {
240
- #if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
241
- if #available(iOS 27.1, *) {
242
- content.modifier(ShellVerticalBarsToolbar(model: model))
243
- } else {
244
- content.modifier(ShellVerticalBarsLegacyToolbar(model: model))
245
- }
246
- #else
247
- content.modifier(ShellVerticalBarsLegacyToolbar(model: model))
248
- #endif
249
- }
250
- }
251
-
252
- #if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
253
- @available(iOS 27.1, *)
254
- private struct ShellVerticalBarsToolbar: ViewModifier {
255
- @ObservedObject var model: ShellVerticalBarsModel
256
-
257
- func body(content: Content) -> some View {
258
- content.toolbar {
259
- if let back = model.back {
260
- ToolbarItem(placement: .navigation) {
261
- verticalBarsBackButton(back, model: model)
262
- }
263
- .axisBehavior(.verticalPreferred)
264
- }
265
- ForEach(model.groups.filter { $0.slot == .start }) { group in
266
- if group.id != model.groups.first(where: { $0.slot == .start })?.id {
267
- ToolbarSpacer(.fixed, placement: .topBarLeading)
268
- }
269
- ForEach(group.items) { item in
270
- ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarLeading)
271
- .axisBehavior(.verticalPreferred)
272
- }
273
- }
274
- ForEach(model.groups.filter { $0.slot != .start }) { group in
275
- if group.id != model.groups.first(where: { $0.slot != .start })?.id {
276
- ToolbarSpacer(.fixed, placement: .topBarTrailing)
277
- }
278
- ForEach(group.items) { item in
279
- ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarTrailing)
280
- .axisBehavior(.verticalPreferred)
281
- }
282
- }
283
- }
284
- }
285
- }
286
- #endif
287
-
288
- @available(iOS 26.0, *)
289
- private struct ShellVerticalBarsLegacyToolbar: ViewModifier {
290
- @ObservedObject var model: ShellVerticalBarsModel
291
-
292
- func body(content: Content) -> some View {
293
- content.toolbar {
294
- if let back = model.back {
295
- ToolbarItem(placement: .navigation) {
296
- verticalBarsBackButton(back, model: model)
297
- }
298
- }
299
- ForEach(model.groups) { group in
300
- if group.id != model.groups.first?.id {
301
- ToolbarSpacer(.fixed, placement: .primaryAction)
302
- }
303
- ForEach(group.items) { item in
304
- ShellVerticalBarsButton(model: model, id: item.id, placement: .primaryAction)
305
- }
306
- }
307
- }
308
- }
309
- }
310
-
311
- @available(iOS 26.0, *)
312
- final class ShellVerticalBarsController: ShellVerticalBarsControlling {
313
- private final class TransparentHostingController<Content: View>: UIHostingController<Content> {
314
- var railEdge = "right"
315
- var layoutSearch: (() -> Void)?
316
- override func viewDidLayoutSubviews() {
317
- super.viewDidLayoutSubviews()
318
- makeFullSizeSurfacesTransparent(in: view)
319
- layoutSearch?()
320
- }
321
-
322
- private func makeFullSizeSurfacesTransparent(in surface: UIView) {
323
- let frame = surface.convert(surface.bounds, to: view)
324
- guard !(surface is UIVisualEffectView) else { return }
325
- let coversHost = frame.insetBy(dx: -1, dy: -1).contains(view.bounds)
326
- // SwiftUI may add an opaque backing behind the rail controls, sized to
327
- // the rail or to an expanded tab bar. Keep that backing clear without
328
- // touching the glass controls or materials.
329
- let coversRail = frame.minY <= 1 && frame.maxY >= view.bounds.maxY - 1 &&
330
- (railEdge == "left" ? frame.minX <= 1 : frame.maxX >= view.bounds.maxX - 1)
331
- if coversHost || coversRail {
332
- surface.backgroundColor = .clear
333
- surface.isOpaque = false
334
- }
335
- surface.subviews.forEach(makeFullSizeSurfacesTransparent)
336
- }
337
- }
338
-
339
- private final class RailContainer: UIView {
340
- var railEdge = "right"
341
-
342
- private var railWidth: CGFloat {
343
- let inset = railEdge == "left" ? safeAreaInsets.left : safeAreaInsets.right
344
- return inset > 0 ? inset : 80
345
- }
346
-
347
- // The rail is not masked: expanded rail content (a widened tab bar) is
348
- // allowed to draw over the WebView. Inside the base rail touches behave
349
- // as before; beyond it touches are captured only where they land inside
350
- // an actual bar surface so empty overlap still belongs to the WebView.
351
- // The SwiftUI hosting scaffold reports a full-size hosting view for
352
- // every point, so the hit result cannot tell bar content from empty
353
- // space — the bar frames decide instead.
354
- override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
355
- let hit = super.hitTest(point, with: event)
356
- let inRail = railEdge == "left" ? point.x <= railWidth : point.x >= bounds.maxX - railWidth
357
- if inRail { return hit }
358
- // Expanded system search leaves the rail; its field and close button
359
- // must receive touches instead of passing them through to the WebView.
360
- var ancestor = hit
361
- while let view = ancestor, view !== self {
362
- if view is UIControl { return hit }
363
- ancestor = view.superview
364
- }
365
- guard hit != nil, containsBarSurface(at: point) else { return nil }
366
- return hit
367
- }
368
-
369
- private func containsBarSurface(at point: CGPoint) -> Bool {
370
- containsBarSurface(in: self, at: point)
371
- }
372
-
373
- private func containsBarSurface(in view: UIView, at point: CGPoint) -> Bool {
374
- guard !view.isHidden, view.alpha > 0.05 else { return false }
375
- let name = NSStringFromClass(type(of: view))
376
- if (name.contains("TabBar") || name.contains("Platter") || name.contains("Pocket") || name.contains("Sidebar") || view is UISearchBar || view is UINavigationBar),
377
- convert(view.bounds, from: view).contains(point) { return true }
378
- return view.subviews.contains { containsBarSurface(in: $0, at: point) }
379
- }
380
- }
381
-
382
- private let model = ShellVerticalBarsModel()
383
- private lazy var controller = TransparentHostingController(rootView: ShellVerticalBarsView(model: model))
384
- private let container = RailContainer()
385
- private weak var owner: UIViewController?
386
- private let changed: (String, ShellSearchPhase, String, Bool, Int) -> Int
387
-
388
- var view: UIView { container }
389
-
390
- var ownsKeyboardChrome: Bool { model.search?.ownsKeyboardChrome == true }
391
-
392
- init(activate: @escaping (String) -> Void,
393
- changed: @escaping (String, ShellSearchPhase, String, Bool, Int) -> Int) {
394
- self.changed = changed
395
- model.activate = activate
396
- container.backgroundColor = .clear
397
- controller.view.backgroundColor = .clear
398
- controller.view.isOpaque = false
399
- controller.layoutSearch = { [weak self] in self?.model.search?.configureField() }
400
- }
401
-
402
- func attach(to owner: UIViewController, in parent: UIView) {
403
- guard self.owner !== owner || container.superview !== parent else { return }
404
- detach()
405
- self.owner = owner
406
- owner.addChild(controller)
407
- container.frame = parent.bounds
408
- container.autoresizingMask = [.flexibleWidth, .flexibleHeight]
409
- controller.view.frame = container.bounds
410
- controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
411
- container.addSubview(controller.view)
412
- parent.addSubview(container)
413
- controller.didMove(toParent: owner)
414
- }
415
-
416
- @MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String) {
417
- container.railEdge = edge
418
- controller.railEdge = edge
419
- let tabsAppear = model.tabs.isEmpty && controls.contains { $0.kind == .tabBar && !$0.items.isEmpty }
420
- if let search = controls.first(where: { $0.search != nil })?.search {
421
- if model.search?.configuration.id == search.id { model.search?.apply(search) }
422
- else { model.search = ShellVerticalSearchModel(search, in: container, activate: model.activate, changed: changed) }
423
- } else { model.search = nil }
424
- model.apply(controls, rendering: rendering)
425
- if tabsAppear { ShellCrossfade.enter(container, duration: ShellCrossfade.duration(180)) }
426
- controller.overrideUserInterfaceStyle = controls.contains(where: \.dark) ? .dark : .light
427
- }
428
-
429
- func detach() {
430
- guard controller.parent != nil else { return }
431
- controller.willMove(toParent: nil)
432
- container.removeFromSuperview()
433
- controller.view.removeFromSuperview()
434
- controller.removeFromParent()
435
- owner = nil
436
- }
437
- }
@@ -1,118 +0,0 @@
1
- import Combine
2
- import SwiftUI
3
- import UIKit
4
-
5
- @available(iOS 26.0, *)
6
- final class ShellVerticalSearchModel: ObservableObject {
7
- @Published private(set) var configuration: ShellSearch
8
- @Published var text: String
9
- @Published var presented: Bool
10
- @Published var focused: Bool
11
- private weak var railView: UIView?
12
- private let activate: (String) -> Void
13
- private let changed: (String, ShellSearchPhase, String, Bool, Int) -> Int
14
- private let inputDelegate = ShellSearchInputDelegate()
15
- private var editingSequence = 0
16
- private var valueVersion: Int
17
-
18
- init(_ configuration: ShellSearch, in railView: UIView,
19
- activate: @escaping (String) -> Void,
20
- changed: @escaping (String, ShellSearchPhase, String, Bool, Int) -> Int) {
21
- self.configuration = configuration
22
- self.railView = railView
23
- self.activate = activate
24
- self.changed = changed
25
- text = configuration.value
26
- presented = configuration.available && configuration.active
27
- focused = configuration.focused
28
- valueVersion = configuration.valueVersion
29
- inputDelegate.clear = { [weak self] in self?.emit(.clear) }
30
- }
31
-
32
- var ownsKeyboardChrome: Bool { presented && (focused || textField?.isFirstResponder == true) }
33
-
34
- private var textField: UISearchTextField? {
35
- func find(_ view: UIView) -> UISearchTextField? {
36
- guard !view.isHidden, view.alpha > 0.01 else { return nil }
37
- if let field = view as? UISearchTextField { return field }
38
- for child in view.subviews {
39
- if let field = find(child) { return field }
40
- }
41
- return nil
42
- }
43
- return railView.flatMap(find)
44
- }
45
-
46
- func apply(_ next: ShellSearch) {
47
- let previous = configuration
48
- configuration = next
49
- if next.valueVersion != valueVersion || next.editSequence >= editingSequence {
50
- valueVersion = next.valueVersion
51
- text = next.value
52
- }
53
- presented = next.available && next.active
54
- // The first Web acknowledgement still carries the old focus state.
55
- if !next.active { focused = false }
56
- else if previous.focused != next.focused { focused = next.focused }
57
- }
58
-
59
- func input(_ value: String) {
60
- text = value
61
- emit(.input)
62
- }
63
-
64
- func present(_ value: Bool) {
65
- presented = value
66
- if value != configuration.active {
67
- activate(value ? configuration.trigger.id : configuration.closeId)
68
- }
69
- }
70
-
71
- func focus(_ value: Bool) {
72
- focused = value
73
- emit(value ? .focus : .blur)
74
- }
75
-
76
- func commit() { emit(.commit) }
77
-
78
- func configureField() {
79
- guard let field = textField else { return }
80
- field.accessibilityIdentifier = configuration.id
81
- field.accessibilityLabel = configuration.field.accessibilityLabel
82
- field.isEnabled = !configuration.disabled
83
- field.autocapitalizationType = .none
84
- field.autocorrectionType = .no
85
- field.spellCheckingType = .no
86
- field.clearButtonMode = .always
87
- if field.delegate !== inputDelegate {
88
- inputDelegate.original = field.delegate
89
- field.delegate = inputDelegate
90
- }
91
- }
92
-
93
- private func emit(_ phase: ShellSearchPhase) {
94
- editingSequence = changed(configuration.id, phase, text,
95
- textField?.markedTextRange != nil, valueVersion)
96
- }
97
- }
98
-
99
- @available(iOS 26.0, *)
100
- struct ShellVerticalSearchModifier: ViewModifier {
101
- @ObservedObject var model: ShellVerticalSearchModel
102
- @FocusState private var focused: Bool
103
-
104
- @ViewBuilder func body(content: Content) -> some View {
105
- if model.configuration.available {
106
- content
107
- .searchable(text: Binding(get: { model.text }, set: model.input),
108
- isPresented: Binding(get: { model.presented }, set: model.present),
109
- placement: .toolbar, prompt: Text(model.configuration.placeholder))
110
- .searchToolbarBehavior(.minimize)
111
- .searchPresentationToolbarBehavior(.avoidHidingContent)
112
- .searchFocused($focused)
113
- .onSubmit(of: .search, model.commit)
114
- .onChange(of: focused) { _, value in model.focus(value) }
115
- .onChange(of: model.focused) { _, value in focused = value }
116
- } else { content }
117
- }
118
- }