@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.
- package/README.md +24 -280
- package/dist/css/components/ion-toolbar.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/vertical-bars.css +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.ts +18 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +26 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/segment/index.js +3 -3
- package/dist/segment/index.js.map +1 -1
- package/dist/sheets-of-glass/index.js +7 -7
- package/dist/sheets-of-glass/index.js.map +1 -1
- package/dist/tab-bar-searchable/dismiss-button.js +53 -3
- package/dist/tab-bar-searchable/dismiss-button.js.map +1 -1
- package/dist/tab-bar-searchable/index.js +18 -3
- package/dist/tab-bar-searchable/index.js.map +1 -1
- package/dist/transition/ios.transition.d.ts.map +1 -1
- package/dist/transition/ios.transition.js +7 -4
- package/dist/transition/ios.transition.js.map +1 -1
- package/dist/transition/native-ui.d.ts +6 -0
- package/dist/transition/native-ui.d.ts.map +1 -0
- package/dist/{native-integration/index.js → transition/native-ui.js} +16 -30
- package/dist/transition/native-ui.js.map +1 -0
- package/docs/adaptive-themes.md +151 -0
- package/docs/add-native-ui.md +53 -0
- package/docs/animation.md +1 -1
- package/docs/eslint.md +1 -1
- package/docs/features.md +3 -3
- package/docs/iphone-duo-with-original-theme.md +44 -29
- package/docs/iphone-duo.md +29 -32
- package/docs/migration.md +12 -24
- package/docs/setup.md +82 -0
- package/docs/special-markup.md +5 -5
- package/docs/using-ion-item-group.md +2 -2
- package/docs/vertical-bars-layout.md +47 -0
- package/package.json +12 -42
- package/src/index.ts +3 -1
- package/src/layout/index.ts +29 -0
- package/src/segment/index.ts +3 -3
- package/src/sheets-of-glass/index.ts +7 -7
- package/src/styles/components/ion-toolbar.scss +1 -1
- package/src/styles/vertical-bars.scss +15 -18
- package/src/tab-bar-searchable/dismiss-button.ts +73 -4
- package/src/tab-bar-searchable/index.ts +20 -3
- package/src/transition/ios.transition.ts +7 -4
- package/src/{native-integration/index.ts → transition/native-ui.ts} +17 -46
- package/Package.swift +0 -21
- package/dist/_virtual/_rolldown/runtime.js +0 -13
- package/dist/native/components/index.d.ts +0 -19
- package/dist/native/components/index.d.ts.map +0 -1
- package/dist/native/components/index.js +0 -40
- package/dist/native/components/index.js.map +0 -1
- package/dist/native/components/ion-back-button.d.ts +0 -9
- package/dist/native/components/ion-back-button.d.ts.map +0 -1
- package/dist/native/components/ion-back-button.js +0 -20
- package/dist/native/components/ion-back-button.js.map +0 -1
- package/dist/native/components/ion-button.d.ts +0 -10
- package/dist/native/components/ion-button.d.ts.map +0 -1
- package/dist/native/components/ion-button.js +0 -23
- package/dist/native/components/ion-button.js.map +0 -1
- package/dist/native/components/ion-buttons.d.ts +0 -11
- package/dist/native/components/ion-buttons.d.ts.map +0 -1
- package/dist/native/components/ion-buttons.js +0 -30
- package/dist/native/components/ion-buttons.js.map +0 -1
- package/dist/native/components/ion-fab.d.ts +0 -10
- package/dist/native/components/ion-fab.d.ts.map +0 -1
- package/dist/native/components/ion-fab.js +0 -132
- package/dist/native/components/ion-fab.js.map +0 -1
- package/dist/native/components/ion-menu-button.d.ts +0 -13
- package/dist/native/components/ion-menu-button.d.ts.map +0 -1
- package/dist/native/components/ion-menu-button.js +0 -31
- package/dist/native/components/ion-menu-button.js.map +0 -1
- package/dist/native/components/ion-segment.d.ts +0 -11
- package/dist/native/components/ion-segment.d.ts.map +0 -1
- package/dist/native/components/ion-segment.js +0 -30
- package/dist/native/components/ion-segment.js.map +0 -1
- package/dist/native/components/ion-tab-bar.d.ts +0 -11
- package/dist/native/components/ion-tab-bar.d.ts.map +0 -1
- package/dist/native/components/ion-tab-bar.js +0 -50
- package/dist/native/components/ion-tab-bar.js.map +0 -1
- package/dist/native/components/searchable-tabs.d.ts +0 -20
- package/dist/native/components/searchable-tabs.d.ts.map +0 -1
- package/dist/native/components/searchable-tabs.js +0 -383
- package/dist/native/components/searchable-tabs.js.map +0 -1
- package/dist/native/definitions.d.ts +0 -152
- package/dist/native/definitions.d.ts.map +0 -1
- package/dist/native/definitions.js +0 -0
- package/dist/native/foldable/fold-state.d.ts +0 -5
- package/dist/native/foldable/fold-state.d.ts.map +0 -1
- package/dist/native/foldable/fold-state.js +0 -14
- package/dist/native/foldable/fold-state.js.map +0 -1
- package/dist/native/foldable/types.d.ts +0 -12
- package/dist/native/foldable/types.d.ts.map +0 -1
- package/dist/native/foldable/types.js +0 -0
- package/dist/native/index.d.ts +0 -11
- package/dist/native/index.d.ts.map +0 -1
- package/dist/native/index.js +0 -242
- package/dist/native/index.js.map +0 -1
- package/dist/native/prehide.d.ts +0 -7
- package/dist/native/prehide.d.ts.map +0 -1
- package/dist/native/prehide.js +0 -230
- package/dist/native/prehide.js.map +0 -1
- package/dist/native/runtime.d.ts +0 -5
- package/dist/native/runtime.d.ts.map +0 -1
- package/dist/native/runtime.js +0 -640
- package/dist/native/runtime.js.map +0 -1
- package/dist/native/shared/candidate.d.ts +0 -21
- package/dist/native/shared/candidate.d.ts.map +0 -1
- package/dist/native/shared/candidate.js +0 -98
- package/dist/native/shared/candidate.js.map +0 -1
- package/dist/native/shared/crossfade.d.ts +0 -10
- package/dist/native/shared/crossfade.d.ts.map +0 -1
- package/dist/native/shared/crossfade.js +0 -50
- package/dist/native/shared/crossfade.js.map +0 -1
- package/dist/native/shared/dom.d.ts +0 -46
- package/dist/native/shared/dom.d.ts.map +0 -1
- package/dist/native/shared/dom.js +0 -172
- package/dist/native/shared/dom.js.map +0 -1
- package/dist/native/shared/events.d.ts +0 -11
- package/dist/native/shared/events.d.ts.map +0 -1
- package/dist/native/shared/events.js +0 -13
- package/dist/native/shared/events.js.map +0 -1
- package/dist/native/shared/icons.d.ts +0 -8
- package/dist/native/shared/icons.d.ts.map +0 -1
- package/dist/native/shared/icons.js +0 -61
- package/dist/native/shared/icons.js.map +0 -1
- package/dist/native/shared/modal.d.ts +0 -10
- package/dist/native/shared/modal.d.ts.map +0 -1
- package/dist/native/shared/modal.js +0 -160
- package/dist/native/shared/modal.js.map +0 -1
- package/dist/native/vertical-bars-web-search.d.ts +0 -9
- package/dist/native/vertical-bars-web-search.d.ts.map +0 -1
- package/dist/native/vertical-bars-web-search.js +0 -84
- package/dist/native/vertical-bars-web-search.js.map +0 -1
- package/dist/native/vertical-bars-web.d.ts +0 -5
- package/dist/native/vertical-bars-web.d.ts.map +0 -1
- package/dist/native/vertical-bars-web.js +0 -341
- package/dist/native/vertical-bars-web.js.map +0 -1
- package/dist/native-integration/index.d.ts +0 -23
- package/dist/native-integration/index.d.ts.map +0 -1
- package/dist/native-integration/index.js.map +0 -1
- package/dist/native-integration/transition.d.ts +0 -5
- package/dist/native-integration/transition.d.ts.map +0 -1
- package/dist/native-integration/transition.js +0 -16
- package/dist/native-integration/transition.js.map +0 -1
- package/dist/vertical-bars.d.ts +0 -5
- package/dist/vertical-bars.js +0 -4
- package/docs/e2e-testing.md +0 -51
- package/docs/feedback.md +0 -65
- package/docs/native-ui-shell.md +0 -285
- package/docs/vertical-bars.md +0 -285
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButton.swift +0 -71
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButtons.swift +0 -24
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellComponents.swift +0 -18
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellFab.swift +0 -83
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +0 -432
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSegment.swift +0 -91
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +0 -221
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +0 -437
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +0 -118
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +0 -396
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellCrossfade.swift +0 -35
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellHost.swift +0 -8
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellRendering.swift +0 -27
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +0 -135
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +0 -650
- package/src/native/components/index.ts +0 -53
- package/src/native/components/ion-back-button.ts +0 -19
- package/src/native/components/ion-button.ts +0 -24
- package/src/native/components/ion-buttons.ts +0 -38
- package/src/native/components/ion-fab.ts +0 -151
- package/src/native/components/ion-menu-button.ts +0 -24
- package/src/native/components/ion-segment.ts +0 -27
- package/src/native/components/ion-tab-bar.ts +0 -50
- package/src/native/components/searchable-tabs.ts +0 -459
- package/src/native/definitions.ts +0 -191
- package/src/native/foldable/fold-state.ts +0 -9
- package/src/native/foldable/types.ts +0 -4
- package/src/native/index.ts +0 -281
- package/src/native/prehide.ts +0 -265
- package/src/native/runtime.ts +0 -781
- package/src/native/shared/candidate.ts +0 -150
- package/src/native/shared/crossfade.ts +0 -50
- package/src/native/shared/dom.ts +0 -286
- package/src/native/shared/events.ts +0 -8
- package/src/native/shared/icons.ts +0 -57
- package/src/native/shared/modal.ts +0 -157
- package/src/native/vertical-bars-web-search.ts +0 -80
- package/src/native/vertical-bars-web.ts +0 -411
- package/src/native-integration/transition.ts +0 -15
- package/src/vertical-bars.ts +0 -11
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
import Capacitor
|
|
2
|
-
import UIKit
|
|
3
|
-
import WebKit
|
|
4
|
-
|
|
5
|
-
@objc(IonicNativeUIShellPlugin)
|
|
6
|
-
public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDelegate {
|
|
7
|
-
public let identifier = "IonicNativeUIShellPlugin"
|
|
8
|
-
public let jsName = "IonicNativeUIShell"
|
|
9
|
-
public let pluginMethods: [CAPPluginMethod] = [
|
|
10
|
-
CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise),
|
|
11
|
-
CAPPluginMethod(name: "getWebViewMetrics", returnType: CAPPluginReturnPromise),
|
|
12
|
-
CAPPluginMethod(name: "update", returnType: CAPPluginReturnPromise),
|
|
13
|
-
CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise)
|
|
14
|
-
]
|
|
15
|
-
private var host: ShellHost?
|
|
16
|
-
private var verticalBars: ShellVerticalBarsControlling?
|
|
17
|
-
private var controls: [String: UIView] = [:]
|
|
18
|
-
private var searchControllers: [String: ShellSearchControlling] = [:]
|
|
19
|
-
private var fingerprints: [String: ShellControl] = [:]
|
|
20
|
-
private let rendering = ShellRendering()
|
|
21
|
-
private var revision = 0
|
|
22
|
-
private var sequence = 0
|
|
23
|
-
private var keyboardVisible = false
|
|
24
|
-
private var pendingTabSelections: [String: ShellTabBar.PendingSelection] = [:]
|
|
25
|
-
private var pendingTabExpiryWorks: [String: DispatchWorkItem] = [:]
|
|
26
|
-
private var restoreTopEdge: (() -> Void)?
|
|
27
|
-
private var observers: [NSObjectProtocol] = []
|
|
28
|
-
private var lastWebViewRadius: Double?
|
|
29
|
-
|
|
30
|
-
public override func load() {
|
|
31
|
-
for name in [UIApplication.didEnterBackgroundNotification, UIResponder.keyboardWillChangeFrameNotification, UIResponder.keyboardWillHideNotification,
|
|
32
|
-
UIDevice.orientationDidChangeNotification] {
|
|
33
|
-
observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] notification in
|
|
34
|
-
guard let self else { return }
|
|
35
|
-
let keyboard = name == UIResponder.keyboardWillChangeFrameNotification || name == UIResponder.keyboardWillHideNotification
|
|
36
|
-
if keyboard {
|
|
37
|
-
if name == UIResponder.keyboardWillHideNotification { self.keyboardVisible = false }
|
|
38
|
-
else if let window = self.bridge?.webView?.window,
|
|
39
|
-
let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
|
40
|
-
let local = window.convert(frame, from: window.screen.coordinateSpace)
|
|
41
|
-
let overlap = window.bounds.intersection(local)
|
|
42
|
-
self.keyboardVisible = !overlap.isNull && overlap.width > 0 && overlap.height > 0
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if !keyboard {
|
|
46
|
-
self.host?.isHidden = true
|
|
47
|
-
self.verticalBars?.view.isHidden = true
|
|
48
|
-
}
|
|
49
|
-
var searchOwnsKeyboard = self.verticalBars?.ownsKeyboardChrome == true
|
|
50
|
-
self.searchControllers.values.forEach { controller in
|
|
51
|
-
if controller.ownsKeyboardChrome { searchOwnsKeyboard = true }
|
|
52
|
-
if !keyboard { controller.surface.isHidden = true }
|
|
53
|
-
}
|
|
54
|
-
if keyboard {
|
|
55
|
-
if !searchOwnsKeyboard {
|
|
56
|
-
self.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh")
|
|
57
|
-
}
|
|
58
|
-
} else if name == UIDevice.orientationDidChangeNotification {
|
|
59
|
-
self.notifyWebViewMetricsChange()
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
for name in [UIApplication.didBecomeActiveNotification, UIResponder.keyboardDidHideNotification,
|
|
64
|
-
UIDevice.orientationDidChangeNotification] {
|
|
65
|
-
observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
|
|
66
|
-
if name == UIResponder.keyboardDidHideNotification { self?.keyboardVisible = false }
|
|
67
|
-
self?.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh", data: name == UIApplication.didBecomeActiveNotification ? "{\"retireSearch\":true}" : "{}")
|
|
68
|
-
if name == UIApplication.didBecomeActiveNotification { self?.notifyWebViewMetricsChange() }
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
deinit {
|
|
74
|
-
observers.forEach(NotificationCenter.default.removeObserver)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private func webViewMetrics() -> JSObject? {
|
|
78
|
-
guard #available(iOS 26.0, *), let webView = bridge?.webView else { return nil }
|
|
79
|
-
webView.layoutIfNeeded()
|
|
80
|
-
return ["radius": Double(webView.effectiveRadius(corner: .topLeft))]
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private func notifyWebViewMetricsChange() {
|
|
84
|
-
let metrics = webViewMetrics() ?? ["radius": 0]
|
|
85
|
-
let radius = metrics["radius"] as? Double ?? 0
|
|
86
|
-
guard radius != lastWebViewRadius else { return }
|
|
87
|
-
lastWebViewRadius = radius
|
|
88
|
-
notifyListeners("webViewMetricsChange", data: metrics)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
@objc func getWebViewMetrics(_ call: CAPPluginCall) {
|
|
92
|
-
DispatchQueue.main.async { [weak self] in
|
|
93
|
-
call.resolve(self?.webViewMetrics() ?? ["radius": 0])
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
@objc func configure(_ call: CAPPluginCall) {
|
|
98
|
-
DispatchQueue.main.async { [weak self] in
|
|
99
|
-
// A new JS context starts revision numbering again (live reload / navigation).
|
|
100
|
-
self?.restoreTopEdge?()
|
|
101
|
-
self?.restoreTopEdge = nil
|
|
102
|
-
self?.removeControls()
|
|
103
|
-
self?.revision = 0
|
|
104
|
-
if #available(iOS 26.0, *) {
|
|
105
|
-
self?.bridge?.webView?.layoutIfNeeded()
|
|
106
|
-
// Ionic already paints the header edge; a second native effect can
|
|
107
|
-
// add a dark scrim when the OS and Web themes differ.
|
|
108
|
-
if call.getBool("verticalBarsOnly") != true, let effect = self?.bridge?.webView?.scrollView.topEdgeEffect {
|
|
109
|
-
let hidden = effect.isHidden
|
|
110
|
-
effect.isHidden = true
|
|
111
|
-
self?.restoreTopEdge = { [weak effect] in effect?.isHidden = hidden }
|
|
112
|
-
}
|
|
113
|
-
call.resolve(["supported": true])
|
|
114
|
-
}
|
|
115
|
-
else { call.resolve(["supported": false]) }
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
@objc func clear(_ call: CAPPluginCall) {
|
|
120
|
-
DispatchQueue.main.async { [weak self] in
|
|
121
|
-
guard let self else { call.resolve(); return }
|
|
122
|
-
let next = call.getInt("revision") ?? 0
|
|
123
|
-
if next >= self.revision {
|
|
124
|
-
self.revision = next
|
|
125
|
-
self.restoreTopEdge?()
|
|
126
|
-
self.restoreTopEdge = nil
|
|
127
|
-
self.removeControls()
|
|
128
|
-
}
|
|
129
|
-
call.resolve()
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private func removeControl(_ id: String, duration: TimeInterval = 0) {
|
|
134
|
-
searchControllers.removeValue(forKey: id)?.detach()
|
|
135
|
-
if let control = controls.removeValue(forKey: id) { ShellCrossfade.retire(control, duration: duration) }
|
|
136
|
-
fingerprints.removeValue(forKey: id)
|
|
137
|
-
pendingTabSelections.removeValue(forKey: id)
|
|
138
|
-
pendingTabExpiryWorks.removeValue(forKey: id)?.cancel()
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
private func removeControls(duration: TimeInterval = 0) {
|
|
142
|
-
Array(controls.keys).forEach { removeControl($0, duration: duration) }
|
|
143
|
-
host?.removeFromSuperview()
|
|
144
|
-
host = nil
|
|
145
|
-
verticalBars?.detach()
|
|
146
|
-
verticalBars = nil
|
|
147
|
-
rendering.clear()
|
|
148
|
-
pendingTabSelections.removeAll()
|
|
149
|
-
pendingTabExpiryWorks.values.forEach { $0.cancel() }
|
|
150
|
-
pendingTabExpiryWorks.removeAll()
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
private func syncTabBar(_ tabBar: UITabBar, id: String, node: ShellControl) {
|
|
154
|
-
var pending = pendingTabSelections[id]
|
|
155
|
-
ShellTabBar.update(tabBar, node: node, rendering: rendering, pendingSelection: &pending)
|
|
156
|
-
if let pending {
|
|
157
|
-
pendingTabSelections[id] = pending
|
|
158
|
-
} else {
|
|
159
|
-
pendingTabSelections.removeValue(forKey: id)
|
|
160
|
-
pendingTabExpiryWorks.removeValue(forKey: id)?.cancel()
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private func schedulePendingTabExpiry(_ id: String, until: CFAbsoluteTime) {
|
|
165
|
-
pendingTabExpiryWorks[id]?.cancel()
|
|
166
|
-
let delay = max(0, until - CFAbsoluteTimeGetCurrent()) + 0.02
|
|
167
|
-
let work = DispatchWorkItem { [weak self] in
|
|
168
|
-
guard let self else { return }
|
|
169
|
-
self.pendingTabExpiryWorks.removeValue(forKey: id)
|
|
170
|
-
guard let pending = self.pendingTabSelections[id],
|
|
171
|
-
CFAbsoluteTimeGetCurrent() >= pending.until,
|
|
172
|
-
let tabBar = self.controls[id] as? UITabBar,
|
|
173
|
-
let node = self.fingerprints[id] else { return }
|
|
174
|
-
self.syncTabBar(tabBar, id: id, node: node)
|
|
175
|
-
}
|
|
176
|
-
pendingTabExpiryWorks[id] = work
|
|
177
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
@objc func update(_ call: CAPPluginCall) {
|
|
181
|
-
DispatchQueue.main.async { [weak self] in
|
|
182
|
-
guard let self else { call.reject("Plugin released"); return }
|
|
183
|
-
guard #available(iOS 26.0, *) else { call.reject("Requires iOS 26"); return }
|
|
184
|
-
let next = call.getInt("revision") ?? 0
|
|
185
|
-
guard next > self.revision else { call.resolve(["revision": self.revision]); return }
|
|
186
|
-
guard let webView = self.bridge?.webView, let parent = webView.superview else {
|
|
187
|
-
call.reject("WebView unavailable"); return
|
|
188
|
-
}
|
|
189
|
-
// Decode and validate the entire batch before changing visible controls.
|
|
190
|
-
guard let snapshot = try? call.decode(ShellSnapshot.self), snapshot.isValid else {
|
|
191
|
-
call.reject("Invalid control snapshot"); return
|
|
192
|
-
}
|
|
193
|
-
self.notifyWebViewMetricsChange()
|
|
194
|
-
let duration = ShellCrossfade.duration(snapshot.transitionDuration)
|
|
195
|
-
let existing = Set(self.controls.keys)
|
|
196
|
-
let verticalBars = snapshot.controls.filter { $0.placement == .verticalBars }
|
|
197
|
-
let snapshots = snapshot.controls.filter { $0.placement != .verticalBars }
|
|
198
|
-
let verticalSearchActive = verticalBars.contains { $0.search.map { $0.available && $0.active } ?? false }
|
|
199
|
-
let width = snapshot.viewportWidth
|
|
200
|
-
self.revision = next
|
|
201
|
-
if snapshots.isEmpty && verticalBars.isEmpty {
|
|
202
|
-
self.removeControls(duration: duration)
|
|
203
|
-
call.resolve(["revision": next]); return
|
|
204
|
-
}
|
|
205
|
-
let scale = webView.bounds.width / width
|
|
206
|
-
let retained = Set(snapshots.map(\.id))
|
|
207
|
-
for id in Array(self.controls.keys) where !retained.contains(id) {
|
|
208
|
-
self.removeControl(id, duration: duration)
|
|
209
|
-
}
|
|
210
|
-
var rejectedControls: [String] = []
|
|
211
|
-
var fabs: [(ShellFab, ShellControl)] = []
|
|
212
|
-
var searches: [(ShellSearchControlling, ShellControl, CGRect, CGRect, UIView?, Bool)] = []
|
|
213
|
-
var rejectedSearches: [String] = []
|
|
214
|
-
if verticalBars.isEmpty || (self.keyboardVisible && !verticalSearchActive) {
|
|
215
|
-
self.verticalBars?.detach()
|
|
216
|
-
self.verticalBars = nil
|
|
217
|
-
if self.keyboardVisible { rejectedControls.append(contentsOf: verticalBars.map(\.id)) }
|
|
218
|
-
} else if let owner = self.bridge?.viewController {
|
|
219
|
-
let rail = self.verticalBars ?? ShellVerticalBarsController(
|
|
220
|
-
activate: { [weak self] id in self?.activate(id) },
|
|
221
|
-
changed: { [weak self] id, phase, value, composing, valueVersion in
|
|
222
|
-
self?.searchChanged(id, phase: phase, value: value, composing: composing, valueVersion: valueVersion) ?? 0
|
|
223
|
-
})
|
|
224
|
-
self.verticalBars = rail
|
|
225
|
-
rail.attach(to: owner, in: owner.view)
|
|
226
|
-
if let frame = snapshot.verticalBarFrame {
|
|
227
|
-
rail.view.frame = webView.convert(frame.rect.applying(CGAffineTransform(scaleX: scale, y: scale)), to: owner.view)
|
|
228
|
-
} else {
|
|
229
|
-
rail.view.frame = owner.view.bounds
|
|
230
|
-
}
|
|
231
|
-
rail.apply(verticalBars, rendering: self.rendering, edge: snapshot.verticalBarEdge ?? "right")
|
|
232
|
-
rail.view.isHidden = false
|
|
233
|
-
} else {
|
|
234
|
-
rejectedControls.append(contentsOf: verticalBars.map(\.id))
|
|
235
|
-
}
|
|
236
|
-
if snapshots.isEmpty {
|
|
237
|
-
self.host?.removeFromSuperview()
|
|
238
|
-
self.host = nil
|
|
239
|
-
call.resolve(["revision": next, "rejectedControls": rejectedControls])
|
|
240
|
-
return
|
|
241
|
-
}
|
|
242
|
-
let host = self.host ?? ShellHost()
|
|
243
|
-
self.host = host
|
|
244
|
-
host.frame = parent.bounds
|
|
245
|
-
host.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
246
|
-
host.backgroundColor = .clear
|
|
247
|
-
host.isAccessibilityElement = false
|
|
248
|
-
UIView.performWithoutAnimation {
|
|
249
|
-
if host.superview !== parent { parent.addSubview(host) }
|
|
250
|
-
for node in snapshots {
|
|
251
|
-
let id = node.id
|
|
252
|
-
let previous = self.controls[id]
|
|
253
|
-
let previousBounds = previous?.bounds
|
|
254
|
-
let previousCenter = previous?.center
|
|
255
|
-
let reject = {
|
|
256
|
-
// Keep the old cover until JS paints Web and omits this ID.
|
|
257
|
-
if let previous, self.controls[id] === previous {
|
|
258
|
-
previous.bounds = previousBounds!
|
|
259
|
-
previous.center = previousCenter!
|
|
260
|
-
previous.layoutIfNeeded()
|
|
261
|
-
} else { self.removeControl(id) }
|
|
262
|
-
rejectedControls.append(id)
|
|
263
|
-
}
|
|
264
|
-
// Only native search owns its keyboard; other controls return to Web.
|
|
265
|
-
if self.keyboardVisible && self.searchControllers[id]?.ownsKeyboard != true {
|
|
266
|
-
reject(); continue
|
|
267
|
-
}
|
|
268
|
-
let local = node.frame.rect
|
|
269
|
-
let bounds = webView.convert(CGRect(x: local.minX * scale, y: local.minY * scale,
|
|
270
|
-
width: local.width * scale, height: local.height * scale), to: parent)
|
|
271
|
-
if let search = node.search {
|
|
272
|
-
guard let owner = self.bridge?.viewController else { rejectedSearches.append(id); continue }
|
|
273
|
-
let controller: ShellSearchControlling
|
|
274
|
-
let previousCover = self.controls[id]
|
|
275
|
-
let replacing = self.searchControllers[id]
|
|
276
|
-
if let existing = replacing { controller = existing }
|
|
277
|
-
else {
|
|
278
|
-
// Keep the ordinary UITabBar cover until the search controller applies.
|
|
279
|
-
controller = ShellSearchController()
|
|
280
|
-
controller.activate = { [weak self] id in self?.activate(id) }
|
|
281
|
-
controller.changed = { [weak self] id, phase, value, composing, valueVersion in
|
|
282
|
-
self?.searchChanged(id, phase: phase, value: value, composing: composing, valueVersion: valueVersion) ?? 0
|
|
283
|
-
}
|
|
284
|
-
controller.attach(to: owner, in: owner.view)
|
|
285
|
-
self.searchControllers[id] = controller
|
|
286
|
-
self.controls[id] = controller.surface
|
|
287
|
-
}
|
|
288
|
-
let trigger = search.trigger.frame.rect
|
|
289
|
-
let searchBarFrame = webView.convert(CGRect(x: webView.bounds.minX + local.minX * scale,
|
|
290
|
-
y: webView.bounds.minY + local.minY * scale, width: local.width * scale, height: local.height * scale), to: owner.view)
|
|
291
|
-
let triggerFrame = webView.convert(CGRect(x: webView.bounds.minX + trigger.minX * scale,
|
|
292
|
-
y: webView.bounds.minY + trigger.minY * scale, width: trigger.width * scale, height: trigger.height * scale), to: owner.view)
|
|
293
|
-
searches.append((controller, node, searchBarFrame, triggerFrame, previousCover, replacing == nil))
|
|
294
|
-
continue
|
|
295
|
-
} else if self.searchControllers[id] != nil {
|
|
296
|
-
self.removeControl(id)
|
|
297
|
-
}
|
|
298
|
-
if self.fingerprints[id] != node {
|
|
299
|
-
if node.kind == ShellFab.kind {
|
|
300
|
-
let fab = (self.controls[id] as? ShellFab) ?? ShellFab()
|
|
301
|
-
if fab.superview == nil { host.addSubview(fab) }
|
|
302
|
-
self.controls[id] = fab
|
|
303
|
-
fabs.append((fab, node))
|
|
304
|
-
} else if let segment = self.controls[id] as? ShellSegment, node.kind == ShellSegment.kind {
|
|
305
|
-
segment.update(node, scale: scale, rendering: self.rendering)
|
|
306
|
-
} else if let tabBar = self.controls[id] as? UITabBar, node.kind == ShellTabBar.kind {
|
|
307
|
-
self.syncTabBar(tabBar, id: id, node: node)
|
|
308
|
-
} else {
|
|
309
|
-
guard let control = ShellComponents.make(node, scale: scale, rendering: self.rendering, tabDelegate: self,
|
|
310
|
-
activate: { [weak self] id in self?.activate(id) }) else { reject(); continue }
|
|
311
|
-
self.controls.removeValue(forKey: id)?.removeFromSuperview()
|
|
312
|
-
self.controls[id] = control
|
|
313
|
-
host.addSubview(control)
|
|
314
|
-
}
|
|
315
|
-
self.fingerprints[id] = node
|
|
316
|
-
} else if let tabBar = self.controls[id] as? UITabBar, self.pendingTabSelections[id] != nil {
|
|
317
|
-
// Resolve an in-flight native tap even when other fingerprint fields are unchanged.
|
|
318
|
-
self.syncTabBar(tabBar, id: id, node: node)
|
|
319
|
-
}
|
|
320
|
-
guard let control = self.controls[id] else { reject(); continue }
|
|
321
|
-
if let tabBar = control as? UITabBar {
|
|
322
|
-
guard ShellTabBar.fit(tabBar, node: node, bounds: bounds) else { reject(); continue }
|
|
323
|
-
} else if control.frame != bounds {
|
|
324
|
-
control.frame = bounds
|
|
325
|
-
}
|
|
326
|
-
control.overrideUserInterfaceStyle = node.dark ? .dark : .light
|
|
327
|
-
}
|
|
328
|
-
host.isHidden = false
|
|
329
|
-
host.layoutIfNeeded()
|
|
330
|
-
}
|
|
331
|
-
// FABs update in place. Only icon content animates; the glass surface
|
|
332
|
-
// and Ionic's staggered child visibility keep their own identities.
|
|
333
|
-
for (fab, node) in fabs {
|
|
334
|
-
fab.apply(node, scale: scale, rendering: self.rendering, activate: { [weak self] id in self?.activate(id) })
|
|
335
|
-
}
|
|
336
|
-
for (controller, node, frame, triggerFrame, previousCover, created) in searches {
|
|
337
|
-
let id = node.id
|
|
338
|
-
let webFrame = webView.convert(webView.bounds, to: controller.surface.superview)
|
|
339
|
-
if !controller.apply(node, webFrame: webFrame, barFrame: frame, triggerFrame: triggerFrame, rendering: self.rendering) {
|
|
340
|
-
if created {
|
|
341
|
-
self.searchControllers.removeValue(forKey: id)
|
|
342
|
-
controller.detach()
|
|
343
|
-
if let previousCover, previousCover.superview != nil {
|
|
344
|
-
self.controls[id] = previousCover
|
|
345
|
-
} else {
|
|
346
|
-
self.controls.removeValue(forKey: id)?.removeFromSuperview()
|
|
347
|
-
self.fingerprints.removeValue(forKey: id)
|
|
348
|
-
self.pendingTabSelections[id] = nil
|
|
349
|
-
self.pendingTabExpiryWorks.removeValue(forKey: id)?.cancel()
|
|
350
|
-
}
|
|
351
|
-
} else {
|
|
352
|
-
self.removeControl(id)
|
|
353
|
-
}
|
|
354
|
-
rejectedSearches.append(id)
|
|
355
|
-
} else {
|
|
356
|
-
if created, let previousCover, previousCover !== controller.surface {
|
|
357
|
-
previousCover.removeFromSuperview()
|
|
358
|
-
}
|
|
359
|
-
controller.surface.isHidden = self.keyboardVisible && !controller.ownsKeyboard
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
for (id, control) in self.controls where !existing.contains(id) && self.searchControllers[id] == nil {
|
|
363
|
-
ShellCrossfade.enter(control, duration: duration)
|
|
364
|
-
}
|
|
365
|
-
let complete = { call.resolve(["revision": next, "rejectedSearches": rejectedSearches, "rejectedControls": rejectedControls]) }
|
|
366
|
-
if let coordinator = searches.first?.0.transitionCoordinator,
|
|
367
|
-
coordinator.animate(alongsideTransition: nil, completion: { _ in complete() }) { return }
|
|
368
|
-
complete()
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
|
|
374
|
-
guard item.isEnabled, let itemId = item.accessibilityIdentifier else { return }
|
|
375
|
-
if let controlId = controls.first(where: { $0.value === tabBar })?.key {
|
|
376
|
-
let pending = ShellTabBar.PendingSelection.start(itemId)
|
|
377
|
-
pendingTabSelections[controlId] = pending
|
|
378
|
-
schedulePendingTabExpiry(controlId, until: pending.until)
|
|
379
|
-
}
|
|
380
|
-
activate(itemId)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
private func searchChanged(_ id: String, phase: ShellSearchPhase, value: String,
|
|
384
|
-
composing: Bool, valueVersion: Int) -> Int {
|
|
385
|
-
sequence += 1
|
|
386
|
-
notifyListeners("search", data: ["id": id, "phase": phase.rawValue, "value": value,
|
|
387
|
-
"composing": composing, "valueVersion": valueVersion, "sequence": sequence, "revision": revision])
|
|
388
|
-
return sequence
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
private func activate(_ id: String) {
|
|
392
|
-
sequence += 1
|
|
393
|
-
notifyListeners("activate", data: ["id": id, "revision": revision, "sequence": sequence])
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import UIKit
|
|
2
|
-
|
|
3
|
-
@MainActor enum ShellCrossfade {
|
|
4
|
-
static func duration(_ milliseconds: Double?) -> TimeInterval {
|
|
5
|
-
UIAccessibility.isReduceMotionEnabled ? 0 : (milliseconds ?? 0) / 1000
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
static func enter(_ view: UIView, duration: TimeInterval) {
|
|
9
|
-
guard duration > 0 else { return }
|
|
10
|
-
view.alpha = 0
|
|
11
|
-
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
|
12
|
-
view.alpha = 1
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
static func retire(_ view: UIView, duration: TimeInterval) {
|
|
17
|
-
// The outgoing copy is visual only: it cannot keep accepting actions or
|
|
18
|
-
// VoiceOver focus once the Web source has been restored.
|
|
19
|
-
if duration > 0, let host = view.superview, let parent = host.superview,
|
|
20
|
-
!view.isHidden, !host.isHidden, let copy = view.snapshotView(afterScreenUpdates: false) {
|
|
21
|
-
copy.frame = host.convert(view.frame, to: parent)
|
|
22
|
-
copy.alpha = CGFloat(view.layer.presentation()?.opacity ?? Float(view.alpha))
|
|
23
|
-
copy.isUserInteractionEnabled = false
|
|
24
|
-
copy.isAccessibilityElement = false
|
|
25
|
-
copy.accessibilityElementsHidden = true
|
|
26
|
-
parent.insertSubview(copy, aboveSubview: host)
|
|
27
|
-
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseInOut]) {
|
|
28
|
-
copy.alpha = 0
|
|
29
|
-
} completion: { _ in
|
|
30
|
-
copy.removeFromSuperview()
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
view.removeFromSuperview()
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import UIKit
|
|
2
|
-
|
|
3
|
-
final class ShellRendering {
|
|
4
|
-
private var images: [String: UIImage] = [:]
|
|
5
|
-
func clear() { images.removeAll() }
|
|
6
|
-
|
|
7
|
-
func image(_ item: ShellItemContent) -> UIImage? {
|
|
8
|
-
guard let data = item.icon, let width = item.iconWidth, width > 0 else { return nil }
|
|
9
|
-
let template = item.iconTemplate == true
|
|
10
|
-
let key = (template ? "template:" : "original:") + data
|
|
11
|
-
if let cached = images[key] { return cached }
|
|
12
|
-
guard let bytes = Data(base64Encoded: data), let raw = UIImage(data: bytes), let cg = raw.cgImage else { return nil }
|
|
13
|
-
let result = UIImage(cgImage: cg, scale: CGFloat(cg.width) / width, orientation: .up)
|
|
14
|
-
.withRenderingMode(template ? .alwaysTemplate : .alwaysOriginal)
|
|
15
|
-
if images.count >= 128 { images.removeAll() }
|
|
16
|
-
images[key] = result
|
|
17
|
-
return result
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
func color(_ css: String?) -> UIColor {
|
|
21
|
-
guard let css else { return .label }
|
|
22
|
-
let numbers = css.components(separatedBy: CharacterSet(charactersIn: "0123456789.").inverted).compactMap(Double.init)
|
|
23
|
-
guard numbers.count >= 3 else { return .label }
|
|
24
|
-
return UIColor(red: numbers[0] / 255, green: numbers[1] / 255, blue: numbers[2] / 255,
|
|
25
|
-
alpha: numbers.count > 3 ? numbers[3] : 1)
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import CoreGraphics
|
|
3
|
-
|
|
4
|
-
enum ShellComponent: String, Decodable {
|
|
5
|
-
case button = "ion-button", buttons = "ion-buttons"
|
|
6
|
-
case backButton = "ion-back-button", menuButton = "ion-menu-button"
|
|
7
|
-
case tabBar = "ion-tab-bar", segment = "ion-segment", fab = "ion-fab"
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
struct ShellSnapshot: Decodable {
|
|
11
|
-
let revision: Int
|
|
12
|
-
let transitionDuration: Double?
|
|
13
|
-
let viewportWidth: Double
|
|
14
|
-
let verticalBarFrame: ShellFrame?
|
|
15
|
-
let verticalBarEdge: String?
|
|
16
|
-
let controls: [ShellControl]
|
|
17
|
-
|
|
18
|
-
var isValid: Bool {
|
|
19
|
-
revision >= 0 && (transitionDuration.map { $0.isFinite && $0 >= 0 && $0 <= 500 } ?? true) && viewportWidth.isFinite && viewportWidth > 0 && controls.count <= 100 &&
|
|
20
|
-
(verticalBarFrame.map { $0.isValid } ?? true) &&
|
|
21
|
-
(verticalBarEdge == nil || verticalBarEdge == "left" || verticalBarEdge == "right") &&
|
|
22
|
-
Set(controls.map(\.id)).count == controls.count && controls.allSatisfy(\.isValid)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Frames and content decode from the same flat wire object. Keeping them separate
|
|
27
|
-
// lets FABs compare artwork without treating movement or visibility as new content.
|
|
28
|
-
struct ShellFrame: Decodable, Equatable {
|
|
29
|
-
let x: Double
|
|
30
|
-
let y: Double
|
|
31
|
-
let width: Double
|
|
32
|
-
let height: Double
|
|
33
|
-
var rect: CGRect { CGRect(x: x, y: y, width: width, height: height) }
|
|
34
|
-
var isValid: Bool { [x, y, width, height].allSatisfy(\.isFinite) && width > 0 && height > 0 }
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
struct ShellControl: Decodable, Equatable {
|
|
38
|
-
enum Placement: String, Decodable { case verticalBars = "vertical-bars" }
|
|
39
|
-
enum ToolbarSlot: String, Decodable { case start, end }
|
|
40
|
-
let id: String
|
|
41
|
-
let kind: ShellComponent
|
|
42
|
-
let placement: Placement?
|
|
43
|
-
let toolbarSlot: ToolbarSlot?
|
|
44
|
-
let frame: ShellFrame
|
|
45
|
-
let items: [ShellItem]
|
|
46
|
-
let dark: Bool
|
|
47
|
-
let rtl: Bool
|
|
48
|
-
let tabBarAnchor: ShellTabBar.Anchor?
|
|
49
|
-
let search: ShellSearch?
|
|
50
|
-
|
|
51
|
-
private enum CodingKeys: String, CodingKey { case id, kind, placement, toolbarSlot, items, dark, rtl, tabBarAnchor, search }
|
|
52
|
-
|
|
53
|
-
init(from decoder: Decoder) throws {
|
|
54
|
-
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
55
|
-
id = try values.decode(String.self, forKey: .id)
|
|
56
|
-
kind = try values.decode(ShellComponent.self, forKey: .kind)
|
|
57
|
-
placement = try values.decodeIfPresent(Placement.self, forKey: .placement)
|
|
58
|
-
toolbarSlot = try values.decodeIfPresent(ToolbarSlot.self, forKey: .toolbarSlot)
|
|
59
|
-
frame = try ShellFrame(from: decoder)
|
|
60
|
-
items = try values.decode([ShellItem].self, forKey: .items)
|
|
61
|
-
dark = try values.decode(Bool.self, forKey: .dark)
|
|
62
|
-
rtl = try values.decode(Bool.self, forKey: .rtl)
|
|
63
|
-
tabBarAnchor = try values.decodeIfPresent(ShellTabBar.Anchor.self, forKey: .tabBarAnchor)
|
|
64
|
-
search = try values.decodeIfPresent(ShellSearch.self, forKey: .search)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
var isValid: Bool {
|
|
68
|
-
ShellComponents.supported.contains(kind) && !id.isEmpty && frame.isValid && !items.isEmpty && items.count <= 30 &&
|
|
69
|
-
(!ShellButton.kinds.contains(kind) || items.count == 1) &&
|
|
70
|
-
(toolbarSlot == nil || (placement == .verticalBars && [.button, .buttons, .menuButton].contains(kind))) &&
|
|
71
|
-
Set(items.map(\.id)).count == items.count && items.allSatisfy(\.isValid) &&
|
|
72
|
-
(tabBarAnchor.map { kind == .tabBar && $0.isValid } ?? true) &&
|
|
73
|
-
(search.map { kind == .tabBar && $0.isValid } ?? true)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
struct ShellItem: Decodable, Equatable {
|
|
78
|
-
let frame: ShellFrame
|
|
79
|
-
let content: ShellItemContent
|
|
80
|
-
let visible: Bool?
|
|
81
|
-
var id: String { content.id }
|
|
82
|
-
|
|
83
|
-
private enum CodingKeys: String, CodingKey { case visible }
|
|
84
|
-
|
|
85
|
-
init(from decoder: Decoder) throws {
|
|
86
|
-
frame = try ShellFrame(from: decoder)
|
|
87
|
-
content = try ShellItemContent(from: decoder)
|
|
88
|
-
visible = try decoder.container(keyedBy: CodingKeys.self).decodeIfPresent(Bool.self, forKey: .visible)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
var isValid: Bool { frame.isValid && content.isValid }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
struct ShellBadge: Decodable, Equatable {
|
|
95
|
-
let value: String
|
|
96
|
-
let color: String
|
|
97
|
-
let textColor: String
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
struct ShellItemContent: Decodable, Equatable {
|
|
101
|
-
enum IconPosition: String, Decodable { case leading, trailing, top }
|
|
102
|
-
enum ButtonFill: String, Decodable { case clear, solid, outline }
|
|
103
|
-
let id: String
|
|
104
|
-
var label: String
|
|
105
|
-
let accessibilityLabel: String
|
|
106
|
-
let disabled: Bool
|
|
107
|
-
let selected: Bool
|
|
108
|
-
let fontSize: Double
|
|
109
|
-
let fontWeight: Double
|
|
110
|
-
let color: String
|
|
111
|
-
let buttonFill: ButtonFill?
|
|
112
|
-
let backgroundColor: String?
|
|
113
|
-
let borderColor: String?
|
|
114
|
-
let borderWidth: CGFloat?
|
|
115
|
-
let badge: ShellBadge?
|
|
116
|
-
var icon: String?
|
|
117
|
-
var iconWidth: Double?
|
|
118
|
-
var iconHeight: Double?
|
|
119
|
-
let iconPosition: IconPosition?
|
|
120
|
-
let imagePadding: Double?
|
|
121
|
-
let contentInsetLeading: Double?
|
|
122
|
-
let contentInsetTrailing: Double?
|
|
123
|
-
let iconTemplate: Bool?
|
|
124
|
-
let closeIcon: String?
|
|
125
|
-
let closeIconWidth: Double?
|
|
126
|
-
let closeIconHeight: Double?
|
|
127
|
-
let iconTransition: Double?
|
|
128
|
-
|
|
129
|
-
var isValid: Bool {
|
|
130
|
-
!id.isEmpty && fontSize.isFinite && fontSize >= 0 && fontWeight.isFinite &&
|
|
131
|
-
[iconWidth, iconHeight, closeIconWidth, closeIconHeight].compactMap { $0 }.allSatisfy { $0.isFinite && $0 > 0 } &&
|
|
132
|
-
[imagePadding, contentInsetLeading, contentInsetTrailing].compactMap { $0 }.allSatisfy { $0.isFinite } &&
|
|
133
|
-
(iconTransition.map { $0.isFinite && $0 >= 0 } ?? true)
|
|
134
|
-
}
|
|
135
|
-
}
|