@rdlabo/ionic-theme-ios27 1.2.1-beta.pr275.shad339e7688db7 → 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 (248) 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 -191
  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 -267
  101. package/dist/native/index.js.map +0 -1
  102. package/dist/native/overlays/content.d.ts +0 -4
  103. package/dist/native/overlays/content.d.ts.map +0 -1
  104. package/dist/native/overlays/content.js +0 -51
  105. package/dist/native/overlays/content.js.map +0 -1
  106. package/dist/native/overlays/controller.d.ts +0 -8
  107. package/dist/native/overlays/controller.d.ts.map +0 -1
  108. package/dist/native/overlays/controller.js +0 -285
  109. package/dist/native/overlays/controller.js.map +0 -1
  110. package/dist/native/overlays/dialog-animation.d.ts +0 -9
  111. package/dist/native/overlays/dialog-animation.d.ts.map +0 -1
  112. package/dist/native/overlays/dialog-animation.js +0 -53
  113. package/dist/native/overlays/dialog-animation.js.map +0 -1
  114. package/dist/native/overlays/dismiss-animation.d.ts +0 -4
  115. package/dist/native/overlays/dismiss-animation.d.ts.map +0 -1
  116. package/dist/native/overlays/dismiss-animation.js +0 -18
  117. package/dist/native/overlays/dismiss-animation.js.map +0 -1
  118. package/dist/native/overlays/focus.d.ts +0 -5
  119. package/dist/native/overlays/focus.d.ts.map +0 -1
  120. package/dist/native/overlays/focus.js +0 -59
  121. package/dist/native/overlays/focus.js.map +0 -1
  122. package/dist/native/overlays/popover.d.ts +0 -12
  123. package/dist/native/overlays/popover.d.ts.map +0 -1
  124. package/dist/native/overlays/popover.js +0 -110
  125. package/dist/native/overlays/popover.js.map +0 -1
  126. package/dist/native/overlays/styles.d.ts +0 -11
  127. package/dist/native/overlays/styles.d.ts.map +0 -1
  128. package/dist/native/overlays/styles.js +0 -167
  129. package/dist/native/overlays/styles.js.map +0 -1
  130. package/dist/native/prehide.d.ts +0 -7
  131. package/dist/native/prehide.d.ts.map +0 -1
  132. package/dist/native/prehide.js +0 -230
  133. package/dist/native/prehide.js.map +0 -1
  134. package/dist/native/runtime.d.ts +0 -7
  135. package/dist/native/runtime.d.ts.map +0 -1
  136. package/dist/native/runtime.js +0 -710
  137. package/dist/native/runtime.js.map +0 -1
  138. package/dist/native/shared/bridge.d.ts +0 -6
  139. package/dist/native/shared/bridge.d.ts.map +0 -1
  140. package/dist/native/shared/bridge.js +0 -18
  141. package/dist/native/shared/bridge.js.map +0 -1
  142. package/dist/native/shared/candidate.d.ts +0 -21
  143. package/dist/native/shared/candidate.d.ts.map +0 -1
  144. package/dist/native/shared/candidate.js +0 -98
  145. package/dist/native/shared/candidate.js.map +0 -1
  146. package/dist/native/shared/crossfade.d.ts +0 -12
  147. package/dist/native/shared/crossfade.d.ts.map +0 -1
  148. package/dist/native/shared/crossfade.js +0 -67
  149. package/dist/native/shared/crossfade.js.map +0 -1
  150. package/dist/native/shared/dom.d.ts +0 -46
  151. package/dist/native/shared/dom.d.ts.map +0 -1
  152. package/dist/native/shared/dom.js +0 -172
  153. package/dist/native/shared/dom.js.map +0 -1
  154. package/dist/native/shared/events.d.ts +0 -11
  155. package/dist/native/shared/events.d.ts.map +0 -1
  156. package/dist/native/shared/events.js +0 -13
  157. package/dist/native/shared/events.js.map +0 -1
  158. package/dist/native/shared/icons.d.ts +0 -8
  159. package/dist/native/shared/icons.d.ts.map +0 -1
  160. package/dist/native/shared/icons.js +0 -61
  161. package/dist/native/shared/icons.js.map +0 -1
  162. package/dist/native/shared/modal.d.ts +0 -10
  163. package/dist/native/shared/modal.d.ts.map +0 -1
  164. package/dist/native/shared/modal.js +0 -160
  165. package/dist/native/shared/modal.js.map +0 -1
  166. package/dist/native/shared/projection-id.d.ts +0 -4
  167. package/dist/native/shared/projection-id.d.ts.map +0 -1
  168. package/dist/native/shared/projection-id.js +0 -7
  169. package/dist/native/shared/projection-id.js.map +0 -1
  170. package/dist/native/shared/vertical-bars-geometry.d.ts +0 -10
  171. package/dist/native/shared/vertical-bars-geometry.d.ts.map +0 -1
  172. package/dist/native/shared/vertical-bars-geometry.js +0 -85
  173. package/dist/native/shared/vertical-bars-geometry.js.map +0 -1
  174. package/dist/native/vertical-bars-web-search.d.ts +0 -10
  175. package/dist/native/vertical-bars-web-search.d.ts.map +0 -1
  176. package/dist/native/vertical-bars-web-search.js +0 -90
  177. package/dist/native/vertical-bars-web-search.js.map +0 -1
  178. package/dist/native/vertical-bars-web.d.ts +0 -5
  179. package/dist/native/vertical-bars-web.d.ts.map +0 -1
  180. package/dist/native/vertical-bars-web.js +0 -411
  181. package/dist/native/vertical-bars-web.js.map +0 -1
  182. package/dist/native-integration/index.d.ts +0 -23
  183. package/dist/native-integration/index.d.ts.map +0 -1
  184. package/dist/native-integration/index.js.map +0 -1
  185. package/dist/native-integration/transition.d.ts +0 -5
  186. package/dist/native-integration/transition.d.ts.map +0 -1
  187. package/dist/native-integration/transition.js +0 -16
  188. package/dist/native-integration/transition.js.map +0 -1
  189. package/dist/vertical-bars.d.ts +0 -5
  190. package/dist/vertical-bars.js +0 -4
  191. package/docs/e2e-testing.md +0 -51
  192. package/docs/feedback.md +0 -65
  193. package/docs/iphone-duo-with-original-theme.md +0 -158
  194. package/docs/native-ui-shell.md +0 -325
  195. package/docs/vertical-bars.md +0 -300
  196. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButton.swift +0 -71
  197. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButtons.swift +0 -24
  198. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellComponents.swift +0 -18
  199. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellFab.swift +0 -80
  200. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +0 -432
  201. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSegment.swift +0 -91
  202. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +0 -218
  203. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +0 -565
  204. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +0 -118
  205. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +0 -502
  206. package/ios/Sources/IonicNativeUIShellPlugin/Overlays/ShellOverlayController.swift +0 -119
  207. package/ios/Sources/IonicNativeUIShellPlugin/Overlays/ShellOverlayHost.swift +0 -75
  208. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellCrossfade.swift +0 -35
  209. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellHost.swift +0 -8
  210. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellRendering.swift +0 -27
  211. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +0 -135
  212. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellVerticalBarsGeometry.swift +0 -52
  213. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +0 -788
  214. package/src/native/components/index.ts +0 -53
  215. package/src/native/components/ion-back-button.ts +0 -19
  216. package/src/native/components/ion-button.ts +0 -24
  217. package/src/native/components/ion-buttons.ts +0 -38
  218. package/src/native/components/ion-fab.ts +0 -151
  219. package/src/native/components/ion-menu-button.ts +0 -24
  220. package/src/native/components/ion-segment.ts +0 -27
  221. package/src/native/components/ion-tab-bar.ts +0 -50
  222. package/src/native/components/searchable-tabs.ts +0 -459
  223. package/src/native/definitions.ts +0 -218
  224. package/src/native/foldable/fold-state.ts +0 -9
  225. package/src/native/foldable/types.ts +0 -4
  226. package/src/native/index.ts +0 -315
  227. package/src/native/overlays/content.ts +0 -51
  228. package/src/native/overlays/controller.ts +0 -349
  229. package/src/native/overlays/dialog-animation.ts +0 -53
  230. package/src/native/overlays/dismiss-animation.ts +0 -15
  231. package/src/native/overlays/focus.ts +0 -64
  232. package/src/native/overlays/popover.ts +0 -120
  233. package/src/native/overlays/styles.ts +0 -181
  234. package/src/native/prehide.ts +0 -265
  235. package/src/native/runtime.ts +0 -885
  236. package/src/native/shared/bridge.ts +0 -18
  237. package/src/native/shared/candidate.ts +0 -150
  238. package/src/native/shared/crossfade.ts +0 -67
  239. package/src/native/shared/dom.ts +0 -286
  240. package/src/native/shared/events.ts +0 -8
  241. package/src/native/shared/icons.ts +0 -57
  242. package/src/native/shared/modal.ts +0 -157
  243. package/src/native/shared/projection-id.ts +0 -2
  244. package/src/native/shared/vertical-bars-geometry.ts +0 -80
  245. package/src/native/vertical-bars-web-search.ts +0 -86
  246. package/src/native/vertical-bars-web.ts +0 -485
  247. package/src/native-integration/transition.ts +0 -15
  248. package/src/vertical-bars.ts +0 -11
@@ -1,565 +0,0 @@
1
- import Combine
2
- import SwiftUI
3
- import UIKit
4
-
5
- protocol ShellVerticalBarsControlling: AnyObject {
6
- var view: UIView { get }
7
- func measuredFrames(relativeTo webView: UIView) -> [String: CGRect]
8
- @MainActor func attach(to owner: UIViewController, in parent: UIView)
9
- @MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String, animated: Bool)
10
- var ownsKeyboardChrome: Bool { get }
11
- func presentPopover(anchor: String, host: ShellOverlayHost, completion: @escaping (Bool) -> Void) -> Bool
12
- @MainActor func detach(duration: TimeInterval)
13
- }
14
-
15
- @available(iOS 26.0, *)
16
- final class ShellVerticalBarsModel: ObservableObject {
17
- struct Item: Identifiable {
18
- let id: String
19
- let label: String
20
- let accessibilityLabel: String
21
- let image: UIImage?
22
- let clear: Bool
23
- let background: Color?
24
- let borderColor: Color?
25
- let borderWidth: CGFloat
26
- let badge: ShellBadge?
27
- let disabled: Bool
28
- var selected: Bool
29
- }
30
-
31
- struct Group: Identifiable {
32
- let id: String
33
- let items: [Item]
34
- let slot: ShellControl.ToolbarSlot?
35
- }
36
-
37
- @Published var popoverAnchor: String?
38
- var popoverHost: ShellOverlayHost?
39
- var popoverPresented: ((Bool) -> Void)?
40
- @Published var search: ShellVerticalSearchModel?
41
- @Published var back: Item?
42
- @Published var groups: [Group] = []
43
- @Published var tabs: [Item] = []
44
- @Published var selection = ""
45
- var activate: (String) -> Void = { _ in }
46
- private var domSelection = ""
47
- private var pendingSelection: ShellTabBar.PendingSelection?
48
- private var pendingExpiryWork: DispatchWorkItem?
49
-
50
- func apply(_ controls: [ShellControl], rendering: ShellRendering, animated: Bool = true,
51
- now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()) {
52
- func item(_ source: ShellItem) -> Item {
53
- let image = rendering.image(source.content)
54
- return Item(id: source.id, label: source.content.label,
55
- accessibilityLabel: source.content.accessibilityLabel,
56
- // UIKit toolbar extraction uses the UIImage's rendering mode.
57
- image: source.content.disabled ? image?.withRenderingMode(.alwaysTemplate) : image,
58
- clear: source.content.buttonFill == .clear,
59
- background: source.content.backgroundColor.map { Color(uiColor: rendering.color($0)) },
60
- borderColor: source.content.borderColor.map { Color(uiColor: rendering.color($0)) },
61
- borderWidth: source.content.borderWidth ?? 0,
62
- badge: source.content.badge, disabled: source.content.disabled,
63
- selected: source.content.selected)
64
- }
65
- withAnimation(animated && !UIAccessibility.isReduceMotionEnabled ? .smooth(duration: 0.3) : nil) {
66
- back = controls.first(where: { $0.kind == .backButton })?.items.first.map(item)
67
- groups = controls.compactMap { control in
68
- guard [.button, .buttons, .menuButton].contains(control.kind) else { return nil }
69
- return Group(id: control.id, items: control.items.map(item), slot: control.toolbarSlot)
70
- }
71
- }
72
- tabs = controls.first(where: { $0.kind == .tabBar })?.items.map(item) ?? []
73
- domSelection = tabs.first(where: \.selected)?.id ?? ""
74
- reconcileSelection(now: now)
75
- }
76
-
77
- func select(_ id: String, now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent(), ttl: CFTimeInterval = 1) {
78
- guard let item = tabs.first(where: { $0.id == id }), !item.disabled else { return }
79
- pendingSelection = .init(id: id, until: now + ttl)
80
- selection = id
81
- pendingExpiryWork?.cancel()
82
- let work = DispatchWorkItem { [weak self] in self?.reconcileSelection() }
83
- pendingExpiryWork = work
84
- DispatchQueue.main.asyncAfter(deadline: .now() + ttl + 0.02, execute: work)
85
- activate(id)
86
- }
87
-
88
- private func reconcileSelection(now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()) {
89
- guard let pending = pendingSelection else { selection = domSelection; return }
90
- let item = tabs.first(where: { $0.id == pending.id })
91
- if item == nil || item!.disabled || now >= pending.until || domSelection == pending.id {
92
- pendingSelection = nil
93
- pendingExpiryWork?.cancel()
94
- pendingExpiryWork = nil
95
- selection = domSelection
96
- } else {
97
- selection = pending.id
98
- }
99
- }
100
- }
101
-
102
- @available(iOS 26.0, *)
103
- private struct ShellVerticalBarsLabel: View {
104
- let item: ShellVerticalBarsModel.Item
105
-
106
- var body: some View {
107
- if let image = item.image {
108
- Label {
109
- Text(item.label.isEmpty ? item.accessibilityLabel : item.label)
110
- } icon: {
111
- Image(uiImage: image)
112
- }
113
- } else {
114
- Text(item.label)
115
- }
116
- }
117
- }
118
-
119
- @available(iOS 26.0, *)
120
- @ViewBuilder
121
- private func verticalBarsButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
122
- let button = Button { model.activate(item.id) } label: { ShellVerticalBarsLabel(item: item) }
123
- .disabled(item.disabled)
124
- .accessibilityLabel(item.accessibilityLabel)
125
- .accessibilityIdentifier(item.id)
126
- if let background = item.background {
127
- button.buttonStyle(.glassProminent).tint(background)
128
- } else {
129
- button.overlay {
130
- if let borderColor = item.borderColor {
131
- Capsule().strokeBorder(borderColor, lineWidth: item.borderWidth).allowsHitTesting(false)
132
- }
133
- }
134
- }
135
- }
136
-
137
- @available(iOS 26.0, *)
138
- private struct ShellVerticalPopoverContent: UIViewControllerRepresentable {
139
- let host: ShellOverlayHost
140
- func makeUIViewController(context: Context) -> ShellOverlayHost { host }
141
- func updateUIViewController(_ controller: ShellOverlayHost, context: Context) {}
142
- }
143
-
144
- @available(iOS 26.0, *)
145
- private struct ShellVerticalBarsButton: ToolbarContent {
146
- @ObservedObject var model: ShellVerticalBarsModel
147
- let id: String
148
- let placement: ToolbarItemPlacement
149
-
150
- // Observe here so same-ID updates also refresh the shared toolbar background.
151
- var body: some ToolbarContent {
152
- if let item = model.groups.lazy.flatMap(\.items).first(where: { $0.id == id }) {
153
- ToolbarItem(placement: placement) {
154
- verticalBarsButton(item, model: model)
155
- .popover(isPresented: Binding(
156
- get: { model.popoverAnchor == item.id },
157
- set: { shown in
158
- if !shown && model.popoverAnchor == item.id {
159
- model.popoverAnchor = nil
160
- model.popoverHost?.event("dismiss")
161
- }
162
- }
163
- )) {
164
- if let host = model.popoverHost {
165
- ShellVerticalPopoverContent(host: host)
166
- .frame(width: host.preferredContentSize.width, height: host.preferredContentSize.height)
167
- .preferredColorScheme(host.options?["dark"] as? Bool == true ? .dark : .light)
168
- .presentationCompactAdaptation(.popover)
169
- .interactiveDismissDisabled(host.options?["backdropDismiss"] as? Bool == false)
170
- .onAppear {
171
- guard model.popoverHost === host else { return }
172
- host.anchoredVisible = true
173
- let completion = model.popoverPresented
174
- model.popoverPresented = nil
175
- completion?(true)
176
- }
177
- .onDisappear {
178
- if model.popoverHost === host { model.popoverHost = nil }
179
- host.completeDismissal()
180
- }
181
- }
182
- }
183
- }
184
- .sharedBackgroundVisibility(item.clear ? .hidden : .automatic)
185
- }
186
- }
187
- }
188
-
189
- private struct ShellVerticalBarGeometryProbe: UIViewRepresentable {
190
- func makeUIView(context: Context) -> UIView {
191
- let view = UIView()
192
- view.isUserInteractionEnabled = false
193
- view.accessibilityIdentifier = "BackButton"
194
- return view
195
- }
196
- func updateUIView(_ uiView: UIView, context: Context) {}
197
- }
198
-
199
- @available(iOS 26.0, *)
200
- private func verticalBarsBackButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
201
- Button { model.activate(item.id) } label: { Image(systemName: "chevron.backward") }
202
- .disabled(item.disabled)
203
- .accessibilityLabel(item.accessibilityLabel)
204
- .accessibilityIdentifier("BackButton")
205
- .background(ShellVerticalBarGeometryProbe())
206
- }
207
-
208
- @available(iOS 26.0, *)
209
- private struct ShellVerticalBarsBadge: ViewModifier {
210
- let badge: ShellBadge?
211
-
212
- @ViewBuilder func body(content: Content) -> some View {
213
- if let badge { content.badge(badge.value) }
214
- else { content }
215
- }
216
- }
217
-
218
- @available(iOS 26.0, *)
219
- private struct ShellVerticalBarsView: View {
220
- @ObservedObject var model: ShellVerticalBarsModel
221
-
222
- var body: some View {
223
- Group {
224
- if model.tabs.isEmpty {
225
- ShellVerticalBarsPage(model: model)
226
- } else {
227
- TabView(selection: Binding(get: { model.selection }, set: { model.select($0) })) {
228
- ForEach(model.tabs) { item in
229
- ShellVerticalBarsPage(model: model)
230
- .tag(item.id)
231
- .tabItem { ShellVerticalBarsLabel(item: item) }
232
- .modifier(ShellVerticalBarsBadge(badge: item.badge))
233
- .disabled(item.disabled)
234
- .accessibilityLabel(item.accessibilityLabel)
235
- .accessibilityIdentifier(item.id)
236
- }
237
- }
238
- }
239
- }
240
- .modifier(ShellVerticalBarsSearch(model: model))
241
- .modifier(ShellVerticalBarsCompression())
242
- .background(Color.clear)
243
- }
244
- }
245
-
246
- @available(iOS 26.0, *)
247
- private struct ShellVerticalBarsPage: View {
248
- @ObservedObject var model: ShellVerticalBarsModel
249
-
250
- var body: some View {
251
- NavigationStack {
252
- Color.clear.modifier(ShellVerticalBarsToolbarAdapter(model: model))
253
- .toolbar {
254
- if model.search?.configuration.available == true {
255
- // Let the system adapt bottom-bar search to the vertical rail.
256
- DefaultToolbarItem(kind: .search, placement: .bottomBar)
257
- }
258
- }
259
- }
260
- }
261
- }
262
-
263
- @available(iOS 26.0, *)
264
- private struct ShellVerticalBarsSearch: ViewModifier {
265
- @ObservedObject var model: ShellVerticalBarsModel
266
- @ViewBuilder func body(content: Content) -> some View {
267
- if let search = model.search {
268
- content.modifier(ShellVerticalSearchModifier(model: search))
269
- } else { content }
270
- }
271
- }
272
-
273
- @available(iOS 26.0, *)
274
- private struct ShellVerticalBarsCompression: ViewModifier {
275
- @ViewBuilder func body(content: Content) -> some View {
276
- #if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
277
- if #available(iOS 27.1, *) {
278
- content.toolbarVerticalCompressionBehavior(.prefersToolbarItems)
279
- } else {
280
- content
281
- }
282
- #else
283
- content
284
- #endif
285
- }
286
- }
287
-
288
- @available(iOS 26.0, *)
289
- private struct ShellVerticalBarsToolbarAdapter: ViewModifier {
290
- @ObservedObject var model: ShellVerticalBarsModel
291
-
292
- @ViewBuilder func body(content: Content) -> some View {
293
- #if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
294
- if #available(iOS 27.1, *) {
295
- content.modifier(ShellVerticalBarsToolbar(model: model))
296
- } else {
297
- content.modifier(ShellVerticalBarsLegacyToolbar(model: model))
298
- }
299
- #else
300
- content.modifier(ShellVerticalBarsLegacyToolbar(model: model))
301
- #endif
302
- }
303
- }
304
-
305
- #if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
306
- @available(iOS 27.1, *)
307
- private struct ShellVerticalBarsToolbar: ViewModifier {
308
- @ObservedObject var model: ShellVerticalBarsModel
309
-
310
- func body(content: Content) -> some View {
311
- content.toolbar {
312
- if let back = model.back {
313
- ToolbarItem(placement: .navigation) {
314
- verticalBarsBackButton(back, model: model)
315
- }
316
- .axisBehavior(.verticalPreferred)
317
- }
318
- ForEach(model.groups.filter { $0.slot == .start }) { group in
319
- if group.id != model.groups.first(where: { $0.slot == .start })?.id {
320
- ToolbarSpacer(.fixed, placement: .topBarLeading)
321
- }
322
- ForEach(group.items) { item in
323
- ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarLeading)
324
- .axisBehavior(.verticalPreferred)
325
- }
326
- }
327
- ForEach(model.groups.filter { $0.slot != .start }) { group in
328
- if group.id != model.groups.first(where: { $0.slot != .start })?.id {
329
- ToolbarSpacer(.fixed, placement: .topBarTrailing)
330
- }
331
- ForEach(group.items) { item in
332
- ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarTrailing)
333
- .axisBehavior(.verticalPreferred)
334
- }
335
- }
336
- }
337
- }
338
- }
339
- #endif
340
-
341
- @available(iOS 26.0, *)
342
- private struct ShellVerticalBarsLegacyToolbar: ViewModifier {
343
- @ObservedObject var model: ShellVerticalBarsModel
344
-
345
- func body(content: Content) -> some View {
346
- content.toolbar {
347
- if let back = model.back {
348
- ToolbarItem(placement: .navigation) {
349
- verticalBarsBackButton(back, model: model)
350
- }
351
- }
352
- ForEach(model.groups) { group in
353
- if group.id != model.groups.first?.id {
354
- ToolbarSpacer(.fixed, placement: .primaryAction)
355
- }
356
- ForEach(group.items) { item in
357
- ShellVerticalBarsButton(model: model, id: item.id, placement: .primaryAction)
358
- }
359
- }
360
- }
361
- }
362
- }
363
-
364
- @available(iOS 26.0, *)
365
- final class ShellVerticalBarsController: ShellVerticalBarsControlling {
366
- private final class TransparentHostingController<Content: View>: UIHostingController<Content> {
367
- var railEdge = "right"
368
- var layoutSearch: (() -> Void)?
369
- override func viewDidLayoutSubviews() {
370
- super.viewDidLayoutSubviews()
371
- makeFullSizeSurfacesTransparent(in: view)
372
- layoutSearch?()
373
- }
374
-
375
- private func makeFullSizeSurfacesTransparent(in surface: UIView) {
376
- let frame = surface.convert(surface.bounds, to: view)
377
- guard !(surface is UIVisualEffectView) else { return }
378
- let coversHost = frame.insetBy(dx: -1, dy: -1).contains(view.bounds)
379
- // SwiftUI may add an opaque backing behind the rail controls, sized to
380
- // the rail or to an expanded tab bar. Keep that backing clear without
381
- // touching the glass controls or materials.
382
- let coversRail = frame.minY <= 1 && frame.maxY >= view.bounds.maxY - 1 &&
383
- (railEdge == "left" ? frame.minX <= 1 : frame.maxX >= view.bounds.maxX - 1)
384
- if coversHost || coversRail {
385
- surface.backgroundColor = .clear
386
- surface.isOpaque = false
387
- }
388
- surface.subviews.forEach(makeFullSizeSurfacesTransparent)
389
- }
390
- }
391
-
392
- private final class RailContainer: UIView {
393
- var railEdge = "right"
394
-
395
- private var railWidth: CGFloat {
396
- let inset = railEdge == "left" ? safeAreaInsets.left : safeAreaInsets.right
397
- return inset > 0 ? inset : 80
398
- }
399
-
400
- // The rail is not masked: expanded rail content (a widened tab bar) is
401
- // allowed to draw over the WebView. Inside the base rail touches behave
402
- // as before; beyond it touches are captured only where they land inside
403
- // an actual bar surface so empty overlap still belongs to the WebView.
404
- // The SwiftUI hosting scaffold reports a full-size hosting view for
405
- // every point, so the hit result cannot tell bar content from empty
406
- // space — the bar frames decide instead.
407
- override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
408
- let hit = super.hitTest(point, with: event)
409
- let inRail = railEdge == "left" ? point.x <= railWidth : point.x >= bounds.maxX - railWidth
410
- if inRail { return hit }
411
- // Expanded system search leaves the rail; its field and close button
412
- // must receive touches instead of passing them through to the WebView.
413
- var ancestor = hit
414
- while let view = ancestor, view !== self {
415
- if view is UIControl { return hit }
416
- ancestor = view.superview
417
- }
418
- guard hit != nil, containsBarSurface(at: point) else { return nil }
419
- return hit
420
- }
421
-
422
- private func containsBarSurface(at point: CGPoint) -> Bool {
423
- containsBarSurface(in: self, at: point)
424
- }
425
-
426
- private func containsBarSurface(in view: UIView, at point: CGPoint) -> Bool {
427
- guard !view.isHidden, view.alpha > 0.05 else { return false }
428
- let name = NSStringFromClass(type(of: view))
429
- if (name.contains("TabBar") || name.contains("Platter") || name.contains("Pocket") || name.contains("Sidebar") || view is UISearchBar || view is UINavigationBar),
430
- convert(view.bounds, from: view).contains(point) { return true }
431
- return view.subviews.contains { containsBarSurface(in: $0, at: point) }
432
- }
433
- }
434
-
435
- private let model = ShellVerticalBarsModel()
436
- private lazy var controller = TransparentHostingController(rootView: ShellVerticalBarsView(model: model))
437
- private let container = RailContainer()
438
- private weak var owner: UIViewController?
439
- private let changed: (String, ShellSearchPhase, String, Bool, Int) -> Int
440
-
441
- var view: UIView { container }
442
- func measuredFrames(relativeTo webView: UIView) -> [String: CGRect] {
443
- var frames: [String: CGRect] = [:]
444
- let labels = Dictionary(grouping: model.tabs, by: \.accessibilityLabel)
445
- var tabViews: [UIView] = []
446
- func visit(_ view: UIView) {
447
- guard !view.isHidden else { return }
448
- if let control = view as? UIControl,
449
- let label = control.accessibilityLabel, let items = labels[label], items.count == 1,
450
- !control.isHidden, control.bounds.width > 0, control.bounds.height > 0 {
451
- if frames[items[0].id] != nil { return }
452
- frames[items[0].id] = control.convert(control.bounds, to: webView)
453
- tabViews.append(control)
454
- }
455
- view.subviews.forEach(visit)
456
- }
457
- visit(container)
458
- if let first = tabViews.first, tabViews.count >= model.tabs.count {
459
- var ancestor: UIView? = first.superview
460
- var platter: UIView?
461
- while let current = ancestor, current !== container {
462
- if current.bounds.width >= webView.bounds.width * 0.5 { break }
463
- if current.bounds.width > 0, current.bounds.height > 0,
464
- tabViews.allSatisfy({ $0.isDescendant(of: current) }) { platter = current }
465
- ancestor = current.superview
466
- }
467
- if let platter { frames["__tabBar"] = platter.convert(platter.bounds, to: webView) }
468
- }
469
- return frames
470
- }
471
-
472
- var ownsKeyboardChrome: Bool { model.search?.ownsKeyboardChrome == true }
473
-
474
- init(activate: @escaping (String) -> Void,
475
- changed: @escaping (String, ShellSearchPhase, String, Bool, Int) -> Int) {
476
- self.changed = changed
477
- model.activate = activate
478
- container.backgroundColor = .clear
479
- controller.view.backgroundColor = .clear
480
- controller.view.isOpaque = false
481
- controller.layoutSearch = { [weak self] in self?.model.search?.configureField() }
482
- }
483
-
484
- @MainActor func attach(to owner: UIViewController, in parent: UIView) {
485
- guard self.owner !== owner || container.superview !== parent else { return }
486
- detach()
487
- self.owner = owner
488
- owner.addChild(controller)
489
- container.frame = parent.bounds
490
- container.autoresizingMask = [.flexibleWidth, .flexibleHeight]
491
- controller.view.frame = container.bounds
492
- controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
493
- container.addSubview(controller.view)
494
- parent.addSubview(container)
495
- controller.didMove(toParent: owner)
496
- }
497
-
498
- @MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String, animated: Bool = true) {
499
- container.railEdge = edge
500
- controller.railEdge = edge
501
- if let search = controls.first(where: { $0.search != nil })?.search {
502
- if model.search?.configuration.id == search.id { model.search?.apply(search) }
503
- else { model.search = ShellVerticalSearchModel(search, in: container, activate: model.activate, changed: changed) }
504
- } else { model.search = nil }
505
- model.apply(controls, rendering: rendering, animated: animated)
506
- controller.overrideUserInterfaceStyle = controls.contains(where: \.dark) ? .dark : .light
507
- }
508
-
509
- func presentPopover(anchor: String, host: ShellOverlayHost, completion: @escaping (Bool) -> Void) -> Bool {
510
- guard container.window != nil, model.popoverAnchor == nil,
511
- model.groups.flatMap(\.items).contains(where: { $0.id == anchor && !$0.disabled }) else { return false }
512
- model.popoverHost = host
513
- model.popoverPresented = completion
514
- host.anchoredVisible = false
515
- host.cancelAnchored = { [weak self] in
516
- // Native presentation callbacks and their deadline run on the main queue.
517
- MainActor.assumeIsolated { self?.detach() }
518
- }
519
- host.dismissAnchored = { [weak self, weak host] in
520
- guard let host else { return }
521
- guard let self, self.model.popoverHost === host else {
522
- host.completeDismissal()
523
- return
524
- }
525
- // A backdrop tap clears the binding before SwiftUI finishes closing.
526
- // Keep the live content and completion until onDisappear, including
527
- // when dismissal has already started through the system binding.
528
- if self.model.popoverAnchor != nil {
529
- var transaction = Transaction()
530
- transaction.disablesAnimations = host.options?["animated"] as? Bool == false
531
- withTransaction(transaction) { self.model.popoverAnchor = nil }
532
- }
533
- // Before onAppear there will be no onDisappear to finish cleanup.
534
- if !host.anchoredVisible {
535
- let completion = self.model.popoverPresented
536
- self.model.popoverPresented = nil
537
- self.model.popoverHost = nil
538
- completion?(false)
539
- }
540
- }
541
- var transaction = Transaction()
542
- transaction.disablesAnimations = host.options?["animated"] as? Bool == false
543
- withTransaction(transaction) { model.popoverAnchor = anchor }
544
- return true
545
- }
546
-
547
- @MainActor func detach(duration: TimeInterval = 0) {
548
- let host = model.popoverHost
549
- let presented = model.popoverPresented
550
- model.popoverPresented = nil
551
- model.popoverAnchor = nil
552
- model.popoverHost = nil
553
- if controller.parent != nil {
554
- controller.willMove(toParent: nil)
555
- ShellCrossfade.retire(container, duration: duration)
556
- controller.view.removeFromSuperview()
557
- controller.removeFromParent()
558
- }
559
- owner = nil
560
- host?.dismissAnchored = nil
561
- host?.cancelAnchored = nil
562
- presented?(false)
563
- host?.completeDismissal()
564
- }
565
- }
@@ -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
- }