craft-native 0.0.293 → 0.0.299
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/dist/android/src/index.d.ts +24 -0
- package/dist/android/src/index.js +31 -0
- package/dist/android/templates/CraftNativeShell.kt.template +32 -1
- package/dist/android/templates/MainActivityNative.kt.template +153 -12
- package/dist/api/app.d.ts +2 -2
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +21 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +24 -0
- package/dist/ios/src/index.js +31 -0
- package/dist/ios/templates/CraftApp.swift +153 -3
- package/dist/ios/templates/CraftHybrid.swift +1 -1
- package/dist/ios/templates/CraftNativeActions.swift +1 -1
- package/dist/ios/templates/CraftNativeScreen.swift +294 -23
- package/dist/ios/templates/CraftWatchApp.swift.template +4 -0
- package/dist/types.d.ts +15 -0
- package/package.json +1 -1
|
@@ -102,6 +102,96 @@ enum CraftPageAppearance {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/// The app-wide UIKit appearance override. SwiftUI's `preferredColorScheme`
|
|
106
|
+
/// only controls the SwiftUI environment; native screen controllers need the
|
|
107
|
+
/// UIKit override as well so their trait collection, controls and navigation
|
|
108
|
+
/// chrome resolve the same way. A runtime choice is kept here so screens
|
|
109
|
+
/// created after the choice inherit it instead of reverting to the bundle
|
|
110
|
+
/// config.
|
|
111
|
+
enum CraftInterfaceStyleController {
|
|
112
|
+
/// The app's bundled choice is the default for newly-created screens.
|
|
113
|
+
/// A runtime choice is separate: it is the only value that should
|
|
114
|
+
/// override a screen's own generated config.
|
|
115
|
+
private static var configured: UIUserInterfaceStyle = .unspecified
|
|
116
|
+
private static var runtimeSelection: UIUserInterfaceStyle?
|
|
117
|
+
|
|
118
|
+
static func configuredStyle(for config: CraftConfig) -> UIUserInterfaceStyle {
|
|
119
|
+
switch config.appearance {
|
|
120
|
+
case "system": return .unspecified
|
|
121
|
+
case "light": return .light
|
|
122
|
+
case "dark": return .dark
|
|
123
|
+
default: return config.darkMode ? .dark : .light
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static func configure(_ config: CraftConfig) {
|
|
128
|
+
configured = configuredStyle(for: config)
|
|
129
|
+
runtimeSelection = nil
|
|
130
|
+
applyToWindows()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static func style(for config: CraftConfig) -> UIUserInterfaceStyle {
|
|
134
|
+
runtimeSelection ?? configuredStyle(for: config)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static func colorScheme(for config: CraftConfig) -> ColorScheme? {
|
|
138
|
+
switch style(for: config) {
|
|
139
|
+
case .light: return .light
|
|
140
|
+
case .dark: return .dark
|
|
141
|
+
default: return nil
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Returns false rather than silently accepting a typo from JavaScript.
|
|
146
|
+
@discardableResult
|
|
147
|
+
static func set(_ value: String) -> Bool {
|
|
148
|
+
let style: UIUserInterfaceStyle
|
|
149
|
+
switch value {
|
|
150
|
+
case "system": style = .unspecified
|
|
151
|
+
case "light": style = .light
|
|
152
|
+
case "dark": style = .dark
|
|
153
|
+
default: return false
|
|
154
|
+
}
|
|
155
|
+
runtimeSelection = style
|
|
156
|
+
applyToWindows()
|
|
157
|
+
NotificationCenter.default.post(name: .craftColorSchemeChanged, object: nil)
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static func currentName() -> String {
|
|
162
|
+
let requested = runtimeSelection ?? UIApplication.shared.connectedScenes
|
|
163
|
+
.compactMap { ($0 as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow })?.traitCollection.userInterfaceStyle }
|
|
164
|
+
.first ?? .unspecified
|
|
165
|
+
let style = requested == .unspecified
|
|
166
|
+
? (UIApplication.shared.connectedScenes
|
|
167
|
+
.compactMap { ($0 as? UIWindowScene)?.windows.first(where: { $0.isKeyWindow })?.traitCollection.userInterfaceStyle }
|
|
168
|
+
.first { $0 != .unspecified } ?? UITraitCollection.current.userInterfaceStyle)
|
|
169
|
+
: requested
|
|
170
|
+
return style == .dark ? "dark" : "light"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static func apply(to controller: UIViewController, config: CraftConfig) {
|
|
174
|
+
controller.overrideUserInterfaceStyle = style(for: config)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private static func applyToWindows() {
|
|
178
|
+
let style = runtimeSelection ?? configured
|
|
179
|
+
for scene in UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }) {
|
|
180
|
+
for window in scene.windows {
|
|
181
|
+
window.overrideUserInterfaceStyle = style
|
|
182
|
+
apply(style, to: window.rootViewController)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private static func apply(_ style: UIUserInterfaceStyle, to controller: UIViewController?) {
|
|
188
|
+
guard let controller else { return }
|
|
189
|
+
controller.overrideUserInterfaceStyle = style
|
|
190
|
+
controller.children.forEach { apply(style, to: $0) }
|
|
191
|
+
if let presented = controller.presentedViewController { apply(style, to: presented) }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
105
195
|
/// Tells the shell when SwiftUI's colour scheme changes: the one appearance
|
|
106
196
|
/// setting with no notification of its own.
|
|
107
197
|
struct CraftColorSchemeObserver: View {
|
|
@@ -195,6 +285,7 @@ struct CraftRecoveryBudget {
|
|
|
195
285
|
final class CraftAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
|
196
286
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
|
197
287
|
let config = CraftConfig.bundled()
|
|
288
|
+
CraftInterfaceStyleController.configure(config)
|
|
198
289
|
// First, so a crash anywhere in the rest of launch is reported too.
|
|
199
290
|
CraftCrashReporter.shared.start(config)
|
|
200
291
|
UNUserNotificationCenter.current().delegate = self
|
|
@@ -606,7 +697,7 @@ struct CraftApp: App {
|
|
|
606
697
|
.ignoresSafeArea(.keyboard, edges: .bottom)
|
|
607
698
|
}
|
|
608
699
|
}
|
|
609
|
-
.preferredColorScheme(appState.
|
|
700
|
+
.preferredColorScheme(appState.appearanceScheme)
|
|
610
701
|
.environmentObject(appState)
|
|
611
702
|
.onOpenURL { url in
|
|
612
703
|
// Handle deep links and universal links
|
|
@@ -689,6 +780,10 @@ final class CraftChrome: ObservableObject {
|
|
|
689
780
|
/// The selected tab highlight's corner radius. Nil preserves the default
|
|
690
781
|
/// capsule, while a finite non-negative value opts into a rounded rect.
|
|
691
782
|
private(set) var tabSelectionCornerRadius: CGFloat?
|
|
783
|
+
/// The configured static shape, retained when the page updates only its
|
|
784
|
+
/// tabs or selected item at runtime.
|
|
785
|
+
private var configuredTabSelectionCornerRadius: CGFloat?
|
|
786
|
+
private(set) var tabSelectionColor: UIColor?
|
|
692
787
|
|
|
693
788
|
weak var webView: WKWebView?
|
|
694
789
|
/// What the tab bar covers at the bottom of the screen, in points.
|
|
@@ -723,9 +818,13 @@ final class CraftChrome: ObservableObject {
|
|
|
723
818
|
/// The tab bar from `tabs` in craft.config.json, there before the page
|
|
724
819
|
/// loads; the page's own `tabBar` message replaces it later. `splash`
|
|
725
820
|
/// false when the first screen is native and draws itself.
|
|
726
|
-
func configure(tabs configured: [CraftConfig.Tab], selected: String?, splash: Bool) {
|
|
821
|
+
func configure(tabs configured: [CraftConfig.Tab], selected: String?, splash: Bool, appearance: CraftConfig.TabBar? = nil) {
|
|
727
822
|
splashVisible = splash
|
|
728
823
|
tabSwipeEnabled = true
|
|
824
|
+
configuredTabSelectionCornerRadius = appearance?.selection?.cornerRadius
|
|
825
|
+
.flatMap { $0.isFinite && $0 >= 0 ? CGFloat($0) : nil }
|
|
826
|
+
tabSelectionCornerRadius = configuredTabSelectionCornerRadius
|
|
827
|
+
tabSelectionColor = appearance?.selection?.color.flatMap { UIColor(hex: $0) }
|
|
729
828
|
guard !configured.isEmpty else { return }
|
|
730
829
|
tabs = configured.map { Tab(id: $0.id, title: $0.title, symbol: $0.symbol ?? "circle", badge: nil, selectedSymbol: Tab.normalizedSelectedSymbol($0.selectedSymbol), selectedIconTint: $0.selectedIconTint, selectedIconPulse: $0.selectedIconPulse) }
|
|
731
830
|
self.selected = selected ?? configured.first?.id
|
|
@@ -856,6 +955,7 @@ final class CraftChrome: ObservableObject {
|
|
|
856
955
|
tint = light.map { light in dark.map { dark in UIColor { $0.userInterfaceStyle == .dark ? dark : light } } ?? light }
|
|
857
956
|
}
|
|
858
957
|
if let swipeEnabled = body["swipeEnabled"] as? Bool { tabSwipeEnabled = swipeEnabled }
|
|
958
|
+
if !body.keys.contains("selection") { tabSelectionCornerRadius = configuredTabSelectionCornerRadius }
|
|
859
959
|
withAnimation(Self.motion(.easeOut(duration: 0.2))) { tabBarVisible = !tabs.isEmpty && !hiddenByScreen }
|
|
860
960
|
publishLayout()
|
|
861
961
|
case "selectTab":
|
|
@@ -1312,6 +1412,7 @@ private struct CraftTabSelectionShape: Shape {
|
|
|
1312
1412
|
}
|
|
1313
1413
|
|
|
1314
1414
|
private struct CraftTabSelectionIndicator: View {
|
|
1415
|
+
@ObservedObject private var chrome = CraftChrome.shared
|
|
1315
1416
|
@ObservedObject private var progress = CraftChrome.shared.tabSwipeProgress
|
|
1316
1417
|
let selected: Int?
|
|
1317
1418
|
let count: Int
|
|
@@ -1323,7 +1424,7 @@ private struct CraftTabSelectionIndicator: View {
|
|
|
1323
1424
|
let width = geometry.size.width / CGFloat(max(1, count))
|
|
1324
1425
|
CraftTabSelectionShape(cornerRadius: cornerRadius)
|
|
1325
1426
|
.frame(width: width, height: geometry.size.height)
|
|
1326
|
-
.foregroundStyle(Color.primary.opacity(0.09))
|
|
1427
|
+
.foregroundStyle(chrome.tabSelectionColor.map { Color($0) } ?? Color.primary.opacity(0.09))
|
|
1327
1428
|
.offset(x: position * width)
|
|
1328
1429
|
}
|
|
1329
1430
|
}
|
|
@@ -1557,6 +1658,8 @@ class DeepLinkManager {
|
|
|
1557
1658
|
// MARK: - App State
|
|
1558
1659
|
class AppState: ObservableObject {
|
|
1559
1660
|
@Published var config: CraftConfig
|
|
1661
|
+
@Published private(set) var appearanceScheme: ColorScheme?
|
|
1662
|
+
private var appearanceObserver: NSObjectProtocol?
|
|
1560
1663
|
|
|
1561
1664
|
init() {
|
|
1562
1665
|
if let configURL = Bundle.main.url(forResource: "craft.config", withExtension: "json"),
|
|
@@ -1565,11 +1668,26 @@ class AppState: ObservableObject {
|
|
|
1565
1668
|
} else {
|
|
1566
1669
|
self.config = CraftConfig()
|
|
1567
1670
|
}
|
|
1671
|
+
self.appearanceScheme = CraftInterfaceStyleController.colorScheme(for: config)
|
|
1672
|
+
appearanceObserver = NotificationCenter.default.addObserver(
|
|
1673
|
+
forName: .craftColorSchemeChanged,
|
|
1674
|
+
object: nil,
|
|
1675
|
+
queue: .main,
|
|
1676
|
+
) { [weak self] _ in
|
|
1677
|
+
guard let self else { return }
|
|
1678
|
+
self.appearanceScheme = CraftInterfaceStyleController.colorScheme(for: self.config)
|
|
1679
|
+
}
|
|
1568
1680
|
// Before the first frame: the hybrid start screen, and the tab bar
|
|
1569
1681
|
// from the config, are decided here, if the app delegate has not
|
|
1570
1682
|
// already (it does at launch; the first call decides).
|
|
1571
1683
|
CraftHybrid.shared.configure(config)
|
|
1572
1684
|
}
|
|
1685
|
+
|
|
1686
|
+
deinit {
|
|
1687
|
+
if let appearanceObserver {
|
|
1688
|
+
NotificationCenter.default.removeObserver(appearanceObserver)
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1573
1691
|
}
|
|
1574
1692
|
|
|
1575
1693
|
extension CraftConfig {
|
|
@@ -1706,6 +1824,9 @@ struct CraftConfig: Codable {
|
|
|
1706
1824
|
var nativeScreens: [String: String]? = nil
|
|
1707
1825
|
/// The tab bar to show before the page describes its own.
|
|
1708
1826
|
var tabs: [Tab]? = nil
|
|
1827
|
+
/// Static styling for the selected tab indicator; the page may still
|
|
1828
|
+
/// override its shape through `tabBar.set({ selection })`.
|
|
1829
|
+
var tabBar: TabBar? = nil
|
|
1709
1830
|
/// Hybrid: web-storage keys the page's own code writes, mirrored into the
|
|
1710
1831
|
/// Keychain under another name for native screens (`{"auth_token": "auth.token"}`).
|
|
1711
1832
|
var shareStorage: [String: String]? = nil
|
|
@@ -1733,6 +1854,15 @@ struct CraftConfig: Codable {
|
|
|
1733
1854
|
var selectedIconPulse: Bool? = nil
|
|
1734
1855
|
}
|
|
1735
1856
|
|
|
1857
|
+
struct TabBar: Codable {
|
|
1858
|
+
var selection: TabSelection? = nil
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
struct TabSelection: Codable {
|
|
1862
|
+
var cornerRadius: Double? = nil
|
|
1863
|
+
var color: String? = nil
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1736
1866
|
struct BackgroundRefresh: Codable {
|
|
1737
1867
|
var enabled: Bool = false
|
|
1738
1868
|
/// The BGTaskScheduler identifier; `<bundleId>.refresh` if unset.
|
|
@@ -2535,6 +2665,18 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
2535
2665
|
resolveCallback(callbackId, result: CraftSymbols.image(named: body["name"] as? String ?? "", options: body) ?? NSNull())
|
|
2536
2666
|
case "statusBarSetStyle":
|
|
2537
2667
|
resolveCallback(callbackId, result: CraftStatusBar.setStyle(body["style"] as? String ?? "default"))
|
|
2668
|
+
case "setAppearance":
|
|
2669
|
+
guard let appearance = body["appearance"] as? String else {
|
|
2670
|
+
rejectCallback(callbackId, error: "setAppearance needs light, dark or system", code: "INVALID_ARGUMENT")
|
|
2671
|
+
return
|
|
2672
|
+
}
|
|
2673
|
+
guard CraftInterfaceStyleController.set(appearance) else {
|
|
2674
|
+
rejectCallback(callbackId, error: "setAppearance needs light, dark or system", code: "INVALID_ARGUMENT")
|
|
2675
|
+
return
|
|
2676
|
+
}
|
|
2677
|
+
resolveCallback(callbackId, result: true)
|
|
2678
|
+
case "getAppearance":
|
|
2679
|
+
resolveCallback(callbackId, result: CraftInterfaceStyleController.currentName())
|
|
2538
2680
|
case "chromeSetUnderPageColor":
|
|
2539
2681
|
// What shows past the page's edges when it rubber-bands.
|
|
2540
2682
|
if #available(iOS 15.0, *), let webView, let css = body["color"] as? String, let color = UIColor(css: css) {
|
|
@@ -5596,6 +5738,14 @@ struct CraftWebView: UIViewRepresentable {
|
|
|
5596
5738
|
getInfo: function() { return craft.getDeviceInfo(); },
|
|
5597
5739
|
getCapabilities: function() { return Promise.resolve(Object.assign({}, craft.capabilities)); }
|
|
5598
5740
|
};
|
|
5741
|
+
craft.app = Object.assign(craft.app || {}, {
|
|
5742
|
+
setAppearance: function(appearance) {
|
|
5743
|
+
return craft._invoke('setAppearance', {appearance: appearance});
|
|
5744
|
+
},
|
|
5745
|
+
getAppearance: function() {
|
|
5746
|
+
return craft._invoke('getAppearance', {});
|
|
5747
|
+
}
|
|
5748
|
+
});
|
|
5599
5749
|
// Feedback, as on Android and the web, where there is no
|
|
5600
5750
|
// motor to fire: an app that left enableHaptics off gets
|
|
5601
5751
|
// nothing played and a settled promise, so `await
|
|
@@ -380,7 +380,7 @@ final class CraftHybrid: NSObject {
|
|
|
380
380
|
}
|
|
381
381
|
startsNative = isEnabled && routes.match(startPath) != nil
|
|
382
382
|
// The splash waits for a page; an all-native app's first screen draws itself.
|
|
383
|
-
CraftChrome.shared.configure(tabs: config.tabs ?? [], selected: tabIds(from: config.tabs ?? []).isEmpty ? nil : CraftHybridRoutes.tab(owning: startPath, among: tabIds(from: config.tabs ?? [])), splash: hasWeb && !startsNative)
|
|
383
|
+
CraftChrome.shared.configure(tabs: config.tabs ?? [], selected: tabIds(from: config.tabs ?? []).isEmpty ? nil : CraftHybridRoutes.tab(owning: startPath, among: tabIds(from: config.tabs ?? [])), splash: hasWeb && !startsNative, appearance: config.tabBar)
|
|
384
384
|
|
|
385
385
|
let script = config.nativeApp.flatMap { $0.isEmpty ? nil : $0 }
|
|
386
386
|
appScriptRoute = script != nil && Self.hasBundle && (isEnabled || !hasWeb) ? script : nil
|
|
@@ -42,7 +42,7 @@ enum CraftNativeActions {
|
|
|
42
42
|
static func capabilities(config: CraftConfig) -> [String] {
|
|
43
43
|
// `speech`, `audio` (background sound) and `eventStream` (a fetch read
|
|
44
44
|
// as it arrives) need nothing the app has to declare.
|
|
45
|
-
var values = ["device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "dialog", "speech", "audio", "eventStream", "linking", "badge", "animation", "viewFocus", "viewMeasure", "viewReparent", "viewNonModalPresentation"]
|
|
45
|
+
var values = ["device", "clipboard", "haptics", "storage", "lifecycle", "fetch", "dialog", "speech", "audio", "eventStream", "linking", "badge", "animation", "viewFocus", "viewMeasure", "viewReparent", "viewNonModalPresentation", "nativePlatformControls"]
|
|
46
46
|
values.append("storageCompareAndSet")
|
|
47
47
|
values.append("storageSetIfAbsent")
|
|
48
48
|
values.append("viewScroll")
|