@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,218 +0,0 @@
1
- import UIKit
2
-
3
- enum ShellTabBar {
4
- struct Anchor: Decodable, Equatable {
5
- let x: Double
6
- let y: Double
7
- var isValid: Bool { [0, 0.5, 1].contains(x) && [0, 1].contains(y) }
8
- }
9
-
10
- /// Optimistic tab selection until the Web selected state catches up (or times out).
11
- struct PendingSelection: Equatable {
12
- var id: String
13
- var until: CFAbsoluteTime
14
- static func start(_ id: String, ttl: CFTimeInterval = 1) -> PendingSelection {
15
- PendingSelection(id: id, until: CFAbsoluteTimeGetCurrent() + ttl)
16
- }
17
- }
18
-
19
- static let kind = ShellComponent.tabBar
20
- /// Tab bars waiting for an iOS 26 title-layout warmup after the next `fit`.
21
- private static var pendingTitleWarmup = Set<ObjectIdentifier>()
22
-
23
- static func make(_ node: ShellControl, rendering: ShellRendering, delegate: UITabBarDelegate) -> UITabBar {
24
- let control = UITabBar()
25
- configureLayout(control)
26
- control.delegate = delegate
27
- update(control, node: node, rendering: rendering)
28
- return control
29
- }
30
-
31
- static func configureLayout(_ tabBar: UITabBar) {
32
- // The reader accepts icon-top tabs. Keep that layout local to the native
33
- // bar instead of letting the iPad idiom turn it into inline items.
34
- if #available(iOS 17.0, *) {
35
- tabBar.traitOverrides.horizontalSizeClass = .compact
36
- tabBar.traitOverrides.verticalSizeClass = .regular
37
- }
38
- tabBar.itemPositioning = .fill
39
- }
40
-
41
- static func update(_ tabBar: UITabBar, node: ShellControl, rendering: ShellRendering) {
42
- var pending: PendingSelection?
43
- update(tabBar, node: node, rendering: rendering, pendingSelection: &pending)
44
- }
45
-
46
- static func update(_ tabBar: UITabBar, node: ShellControl, rendering: ShellRendering,
47
- pendingSelection: inout PendingSelection?) {
48
- let items = node.items
49
- let ids = items.map(\.id)
50
- var needsTitleWarmup = false
51
- // Keep UIKit's item identities and interaction state across DOM selection updates.
52
- if tabBar.items?.map({ $0.accessibilityIdentifier ?? "" }) != ids {
53
- tabBar.items = items.map { item in
54
- let tab = UITabBarItem(title: item.content.label, image: rendering.image(item.content), tag: 0)
55
- tab.accessibilityIdentifier = item.id
56
- return tab
57
- }
58
- needsTitleWarmup = true
59
- }
60
- let nativeItems = tabBar.items ?? []
61
- for (tab, item) in zip(nativeItems, items) {
62
- let title = item.content.label
63
- if tab.title != title {
64
- tab.title = title
65
- needsTitleWarmup = true
66
- }
67
- let icon = rendering.image(item.content)
68
- if tab.image !== icon {
69
- tab.image = icon
70
- needsTitleWarmup = true
71
- }
72
- if tab.selectedImage !== icon { tab.selectedImage = icon }
73
- tab.isEnabled = !item.content.disabled
74
- tab.accessibilityLabel = item.content.accessibilityLabel
75
- if applyTypography(item.content, to: tab) { needsTitleWarmup = true }
76
- applyBadge(item.content.badge, to: tab, rendering: rendering)
77
- }
78
- let domSelected = zip(nativeItems, items).first { $0.1.content.selected }
79
- let selectedItem = domSelected?.0
80
- if let pending = pendingSelection {
81
- let pendingItem = zip(nativeItems, items).first { $0.1.id == pending.id }
82
- let expired = CFAbsoluteTimeGetCurrent() >= pending.until
83
- let unavailable = pendingItem == nil || pendingItem!.1.content.disabled
84
- if unavailable || expired {
85
- // Ionic rejected the tap, the item vanished, or the echo timed out.
86
- pendingSelection = nil
87
- if tabBar.selectedItem !== selectedItem { tabBar.selectedItem = selectedItem }
88
- } else if domSelected?.1.id == pending.id {
89
- pendingSelection = nil
90
- if tabBar.selectedItem !== selectedItem { tabBar.selectedItem = selectedItem }
91
- } else if tabBar.selectedItem !== pendingItem!.0 {
92
- // Keep the optimistic native selection until the Web selected state catches up.
93
- tabBar.selectedItem = pendingItem!.0
94
- }
95
- } else if tabBar.selectedItem !== selectedItem {
96
- tabBar.selectedItem = selectedItem
97
- }
98
- tabBar.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
99
- tabBar.accessibilityIdentifier = node.id
100
- // Warm after `fit` settles geometry — not on every motion-driven frame sync.
101
- if needsTitleWarmup { pendingTitleWarmup.insert(ObjectIdentifier(tabBar)) }
102
- }
103
-
104
- @discardableResult
105
- static func applyTypography(_ content: ShellItemContent, to item: UITabBarItem) -> Bool {
106
- let weight: UIFont.Weight
107
- switch content.fontWeight {
108
- case ..<150: weight = .ultraLight
109
- case ..<250: weight = .thin
110
- case ..<350: weight = .light
111
- case ..<450: weight = .regular
112
- case ..<550: weight = .medium
113
- case ..<650: weight = .semibold
114
- case ..<750: weight = .bold
115
- case ..<850: weight = .heavy
116
- default: weight = .black
117
- }
118
- let font = UIFont.systemFont(ofSize: content.fontSize, weight: weight)
119
- var changed = false
120
- for state in [UIControl.State.normal, .selected] {
121
- if item.titleTextAttributes(for: state)?[.font] as? UIFont != font {
122
- item.setTitleTextAttributes([.font: font], for: state)
123
- changed = true
124
- }
125
- }
126
- return changed
127
- }
128
-
129
- static func applyBadge(_ badge: ShellBadge?, to item: UITabBarItem, rendering: ShellRendering) {
130
- // nil removes a badge; an empty string keeps the native notification dot.
131
- item.badgeValue = badge?.value
132
- item.accessibilityValue = badge?.value
133
- item.badgeColor = badge.map { rendering.color($0.color) }
134
- let attributes = badge.map { [NSAttributedString.Key.foregroundColor: rendering.color($0.textColor)] }
135
- item.setBadgeTextAttributes(attributes, for: .normal)
136
- item.setBadgeTextAttributes(attributes, for: .selected)
137
- }
138
-
139
- static func fit(_ tabBar: UITabBar, node: ShellControl, bounds: CGRect) -> Bool {
140
- // Place a new bar before asking UIKit for its safe-area-dependent size.
141
- // Measuring at the initial zero frame makes a restored iPad bar 5pt shorter.
142
- if tabBar.bounds.isEmpty {
143
- tabBar.frame = bounds
144
- tabBar.layoutIfNeeded()
145
- }
146
- let fitted = tabBar.sizeThatFits(bounds.size)
147
- var size = CGSize(width: bounds.width,
148
- height: max(bounds.height, fitted.height))
149
- if tabBar.bounds.size != size { tabBar.bounds.size = size }
150
- tabBar.layoutIfNeeded()
151
- // UIKit's tab content occupies only part of UITabBar's outer frame.
152
- // Align that content to ion-tab-bar, whose CSS already owns placement.
153
- guard var content = contentFrame(tabBar) else {
154
- return false
155
- }
156
- // Reserve ion-tab-bar's width for the tab content, excluding UIKit's outer gutters.
157
- // UIKit can cap the content width for a small number of items.
158
- // Do not keep expanding the outer frame beyond its natural size.
159
- size.width = min(bounds.width + tabBar.bounds.width - content.width,
160
- max(bounds.width, fitted.width))
161
- if abs(tabBar.bounds.width - size.width) > 0.1 {
162
- tabBar.bounds.size = size
163
- tabBar.layoutIfNeeded()
164
- guard let measured = contentFrame(tabBar) else {
165
- return false
166
- }
167
- content = measured
168
- }
169
- // UIKit owns the adaptive platter size, including changes after badge/title updates.
170
- // Preserve the DOM placement anchor instead of requiring identical item widths.
171
- let anchor = node.tabBarAnchor
172
- let x = anchor?.x ?? 0
173
- let y = anchor?.y ?? 0
174
- let center = CGPoint(x: bounds.minX + (bounds.width - content.width) * x - content.minX + size.width / 2,
175
- y: bounds.minY + (bounds.height - content.height) * y - content.minY + size.height / 2)
176
- if tabBar.center != center { tabBar.center = center }
177
- if pendingTitleWarmup.remove(ObjectIdentifier(tabBar)) != nil {
178
- warmUnselectedTitleLayout(tabBar)
179
- }
180
- return true
181
- }
182
-
183
- /// iOS 26 only: programmatically visit each unselected item so UIKit measures
184
- /// its title the same way a user tap would. Selection does not call the
185
- /// tab-bar delegate. No-op on iOS 27+, where first paint already matches Web.
186
- static func warmUnselectedTitleLayout(_ tabBar: UITabBar) {
187
- let version = ProcessInfo.processInfo.operatingSystemVersion
188
- guard version.majorVersion == 26, let items = tabBar.items, items.count > 1 else { return }
189
- let selected = tabBar.selectedItem
190
- UIView.performWithoutAnimation {
191
- for item in items where item !== selected {
192
- tabBar.selectedItem = item
193
- tabBar.layoutIfNeeded()
194
- }
195
- if tabBar.selectedItem !== selected {
196
- tabBar.selectedItem = selected
197
- tabBar.layoutIfNeeded()
198
- }
199
- }
200
- }
201
-
202
- private static func contentFrame(_ tabBar: UITabBar) -> CGRect? {
203
- contentFrames(tabBar).reduce(nil) { (result: CGRect?, frame) in result.map { $0.union(frame) } ?? frame }
204
- }
205
-
206
- static func contentFrames(_ tabBar: UITabBar) -> [CGRect] {
207
- contentViews(tabBar).map { $0.frame }
208
- }
209
-
210
- static func contentViews(_ tabBar: UITabBar) -> [UIView] {
211
- func containsControl(_ view: UIView) -> Bool {
212
- view is UIControl || view.subviews.contains(where: containsControl)
213
- }
214
- return tabBar.subviews
215
- .filter { !$0.isHidden && $0.alpha > 0 && containsControl($0) }
216
- .filter { !$0.frame.isEmpty }
217
- }
218
- }