@swiftbrowser/native 0.0.0-stage → 0.10.0

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 (107) hide show
  1. package/README.md +143 -2
  2. package/bin/swiftbrowser-native.mjs +49 -0
  3. package/dist/bridge.d.ts +50 -0
  4. package/dist/bridge.d.ts.map +1 -0
  5. package/dist/bridge.js +10 -0
  6. package/dist/bridge.js.map +1 -0
  7. package/dist/build.d.ts +47 -0
  8. package/dist/build.d.ts.map +1 -0
  9. package/dist/build.js +156 -0
  10. package/dist/build.js.map +1 -0
  11. package/dist/devBuild.d.ts +34 -0
  12. package/dist/devBuild.d.ts.map +1 -0
  13. package/dist/devBuild.js +133 -0
  14. package/dist/devBuild.js.map +1 -0
  15. package/dist/encoding.d.ts +32 -0
  16. package/dist/encoding.d.ts.map +1 -0
  17. package/dist/encoding.js +136 -0
  18. package/dist/encoding.js.map +1 -0
  19. package/dist/fetch.d.ts +117 -0
  20. package/dist/fetch.d.ts.map +1 -0
  21. package/dist/fetch.js +272 -0
  22. package/dist/fetch.js.map +1 -0
  23. package/dist/index.d.ts +20 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +20 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/native.d.ts +19 -0
  28. package/dist/native.d.ts.map +1 -0
  29. package/dist/native.js +17 -0
  30. package/dist/native.js.map +1 -0
  31. package/dist/refresh.d.ts +18 -0
  32. package/dist/refresh.d.ts.map +1 -0
  33. package/dist/refresh.js +18 -0
  34. package/dist/refresh.js.map +1 -0
  35. package/dist/run.d.ts +27 -0
  36. package/dist/run.d.ts.map +1 -0
  37. package/dist/run.js +76 -0
  38. package/dist/run.js.map +1 -0
  39. package/dist/runtime.d.ts +30 -0
  40. package/dist/runtime.d.ts.map +1 -0
  41. package/dist/runtime.js +193 -0
  42. package/dist/runtime.js.map +1 -0
  43. package/dist/stubs/react-dom-client.d.ts +8 -0
  44. package/dist/stubs/react-dom-client.d.ts.map +1 -0
  45. package/dist/stubs/react-dom-client.js +11 -0
  46. package/dist/stubs/react-dom-client.js.map +1 -0
  47. package/dist/stubs/react-dom.d.ts +9 -0
  48. package/dist/stubs/react-dom.d.ts.map +1 -0
  49. package/dist/stubs/react-dom.js +13 -0
  50. package/dist/stubs/react-dom.js.map +1 -0
  51. package/dist/url.d.ts +48 -0
  52. package/dist/url.d.ts.map +1 -0
  53. package/dist/url.js +213 -0
  54. package/dist/url.js.map +1 -0
  55. package/dist/vendor.d.ts +11 -0
  56. package/dist/vendor.d.ts.map +1 -0
  57. package/dist/vendor.js +27 -0
  58. package/dist/vendor.js.map +1 -0
  59. package/dist/wasmHost.d.ts +35 -0
  60. package/dist/wasmHost.d.ts.map +1 -0
  61. package/dist/wasmHost.js +120 -0
  62. package/dist/wasmHost.js.map +1 -0
  63. package/package.json +73 -4
  64. package/src/bridge.ts +58 -0
  65. package/src/build.ts +185 -0
  66. package/src/devBuild.ts +152 -0
  67. package/src/encoding.ts +130 -0
  68. package/src/fetch.ts +305 -0
  69. package/src/index.ts +20 -0
  70. package/src/native.ts +35 -0
  71. package/src/refresh.ts +29 -0
  72. package/src/run.ts +98 -0
  73. package/src/runtime.ts +208 -0
  74. package/src/stubs/react-dom-client.ts +17 -0
  75. package/src/stubs/react-dom.ts +15 -0
  76. package/src/types/react-refresh.d.ts +38 -0
  77. package/src/url.ts +202 -0
  78. package/src/vendor.ts +30 -0
  79. package/src/wasmHost.ts +147 -0
  80. package/swift/Package.swift +31 -0
  81. package/swift/Sources/SwiftBrowserNative/Host/HostBridge.swift +228 -0
  82. package/swift/Sources/SwiftBrowserNative/Host/JSHost.swift +42 -0
  83. package/swift/Sources/SwiftBrowserNative/Host/JavaScriptCoreHost.swift +102 -0
  84. package/swift/Sources/SwiftBrowserNative/Host/ShimAliases.swift +25 -0
  85. package/swift/Sources/SwiftBrowserNative/Host/WebHost.swift +138 -0
  86. package/swift/Sources/SwiftBrowserNative/Runtime/NativeApp.swift +251 -0
  87. package/swift/Sources/SwiftBrowserNative/Runtime/NativeRegistry.swift +53 -0
  88. package/swift/Sources/SwiftBrowserNative/SwiftBrowserNative.swift +6 -0
  89. package/swift/Sources/SwiftBrowserNative/Views/Conversions.swift +361 -0
  90. package/swift/Sources/SwiftBrowserNative/Views/ElementView.swift +169 -0
  91. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Controls.swift +268 -0
  92. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Lists.swift +175 -0
  93. package/swift/Sources/SwiftBrowserNative/Views/Kinds/More.swift +196 -0
  94. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Navigation.swift +246 -0
  95. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Shapes.swift +108 -0
  96. package/swift/Sources/SwiftBrowserNative/Views/Kinds/Stacks.swift +189 -0
  97. package/swift/Sources/SwiftBrowserNative/Views/Kinds/TextAndImages.swift +102 -0
  98. package/swift/Sources/SwiftBrowserNative/Views/NativeRootView.swift +190 -0
  99. package/swift/Sources/SwiftBrowserNative/Views/StyleModifier.swift +182 -0
  100. package/swift/Sources/SwiftBrowserProtocol/JSONValue.swift +268 -0
  101. package/swift/Sources/SwiftBrowserProtocol/Op.swift +100 -0
  102. package/swift/Sources/SwiftBrowserProtocol/Props.swift +469 -0
  103. package/swift/Sources/SwiftBrowserProtocol/RendererEvent.swift +117 -0
  104. package/swift/Sources/SwiftBrowserProtocol/Style.swift +148 -0
  105. package/swift/Sources/SwiftBrowserProtocol/Values.swift +279 -0
  106. package/swift/Sources/SwiftBrowserTree/ElementNode.swift +65 -0
  107. package/swift/Sources/SwiftBrowserTree/OpTree.swift +168 -0
@@ -0,0 +1,251 @@
1
+ #if canImport(SwiftUI)
2
+ import Foundation
3
+ import Observation
4
+ import SwiftBrowserProtocol
5
+ import SwiftBrowserTree
6
+ import SwiftUI
7
+ #if canImport(FoundationBridge)
8
+ import FoundationBridge // `Bundle.main` (ShimAliases.swift) in a default argument needs the import here
9
+ #endif
10
+
11
+ /// A JavaScript app and the tree it draws: the engine, the bridge, and the
12
+ /// `OpTree` the SwiftUI views read. One per app; `NativeRootView` shows it.
13
+ ///
14
+ /// ```swift
15
+ /// @main struct MyApp: App {
16
+ /// @State private var app = NativeApp()
17
+ /// var body: some Scene {
18
+ /// WindowGroup { NativeRootView(app: app).task { app.loadBundledApp() } }
19
+ /// }
20
+ /// }
21
+ /// ```
22
+ ///
23
+ /// Every call across the bridge is synchronous on the main thread: a tap's
24
+ /// `dispatch` returns after JavaScript handled it and its render pass
25
+ /// reached the tree (docs/native.md, "Synchronous calls").
26
+ @MainActor
27
+ @Observable
28
+ public final class NativeApp {
29
+ public let tree: OpTree
30
+ public let host: JSHost
31
+ private var bridge: HostBridge?
32
+ /// The last error JavaScript raised, shown over the app in Debug builds.
33
+ public private(set) var error: String?
34
+ /// Whether a script was loaded and its `__sbDispatch` is there to call.
35
+ public private(set) var isLoaded = false
36
+ /// The environment last sent to the app.
37
+ public private(set) var colorScheme: String?
38
+ public private(set) var dynamicTypeSize: String?
39
+ public private(set) var horizontalSizeClass: String?
40
+
41
+ #if canImport(JavaScriptCore)
42
+ public convenience init() {
43
+ self.init(host: JavaScriptCoreHost())
44
+ }
45
+ #endif
46
+
47
+ public init(host: JSHost) {
48
+ self.host = host
49
+ tree = OpTree()
50
+ tree.animate = { spec, body in
51
+ if let animation = spec.map(Animation.init(spec:)) {
52
+ withAnimation(animation, body)
53
+ } else {
54
+ body()
55
+ }
56
+ }
57
+ tree.onEvent = { [weak self] event in self?.dispatch(event) }
58
+ #if canImport(JavaScriptCore)
59
+ (host as? JavaScriptCoreHost)?.onError = { [weak self] error in self?.error = error.description }
60
+ #endif
61
+ bridge = HostBridge(host: host, tree: tree)
62
+ }
63
+
64
+ /// Evaluates the app's bundle (what `@swiftbrowser/native` built: it calls
65
+ /// `run(<App/>)` at the end, which renders the first pass into the tree
66
+ /// before this returns). The environment, if known, is sent right after.
67
+ public func load(script: String, name: String = "app.js") {
68
+ error = nil
69
+ if isLoaded { reset() }
70
+ do {
71
+ try host.evaluate(script, name: name)
72
+ isLoaded = host.hasGlobal("__sbDispatch")
73
+ if !isLoaded { error = "\(name) did not call run(): no __sbDispatch" }
74
+ sendEnvironment()
75
+ } catch {
76
+ self.error = "\(error)"
77
+ }
78
+ }
79
+
80
+ /// Starts over for a reload: a fresh engine (the old one's globals and
81
+ /// timers go with it) and an empty tree, which the views are observing.
82
+ private func reset() {
83
+ bridge?.invalidate()
84
+ isLoaded = false
85
+ try? tree.apply(tree.root.children.map { Op.remove(id: $0.id) } + [.commit(animation: nil)])
86
+ host.reset()
87
+ bridge = HostBridge(host: host, tree: tree)
88
+ }
89
+
90
+ /// Loads a bundled resource, `app.js` by default.
91
+ public func load(resource: String = "app", extension ext: String = "js", bundle: Bundle = .main) {
92
+ guard let url = bundle.url(forResource: resource, withExtension: ext) else {
93
+ error = "\(resource).\(ext) is not in the app bundle (build it with `npm run build` in the app's JavaScript project)"
94
+ return
95
+ }
96
+ do {
97
+ load(script: try String(contentsOf: url, encoding: .utf8), name: "\(resource).\(ext)")
98
+ } catch {
99
+ self.error = "\(error)"
100
+ }
101
+ }
102
+
103
+ #if !canImport(FoundationBridge)
104
+ /// Loads the bundle from a URL (a dev server's `http://localhost:5173/app.js`),
105
+ /// then re-evaluates whenever `reload` is called. Debug builds use it to
106
+ /// pick up a rebuilt bundle without reinstalling the app.
107
+ public func load(url: URL) async {
108
+ do {
109
+ let (data, _) = try await URLSession.shared.data(from: url)
110
+ load(script: String(decoding: data, as: UTF8.self), name: url.lastPathComponent)
111
+ } catch {
112
+ self.error = "\(url): \(error.localizedDescription)"
113
+ }
114
+ }
115
+
116
+ /// The dev loop over `swiftbrowser-native dev`'s server (`url` is the
117
+ /// server, `http://localhost:5173/`; an `app.js` URL from earlier builds
118
+ /// names the same server). Polls `manifest.json` every `interval` until
119
+ /// the task is cancelled: a new `vendor` half starts the app over (a fresh
120
+ /// engine and tree, as `swiftbrowser dev` reloads a page); a new `app`
121
+ /// half is evaluated in place and React Fast Refresh keeps the mounted
122
+ /// components' state where their hooks are unchanged (docs/native.md).
123
+ /// A server without a manifest is read as one bundle at `app.js`.
124
+ public func watch(url: URL, interval: Duration = .seconds(1)) async {
125
+ let base = url.pathExtension == "js" ? url.deletingLastPathComponent() : url
126
+ var vendorHash: String?
127
+ var appHash: String?
128
+ var legacyETag: String?
129
+ while !Task.isCancelled {
130
+ if let manifest = await fetchJSON(base.appendingPathComponent("manifest.json")) {
131
+ let vendor = manifest["vendor"].string ?? ""
132
+ let app = manifest["app"].string ?? ""
133
+ if vendor != vendorHash {
134
+ if let vendorCode = await fetchText(base.appendingPathComponent("vendor.js")),
135
+ let appCode = await fetchText(base.appendingPathComponent("app.js"))
136
+ {
137
+ load(scripts: [("vendor.js", vendorCode), ("app.js", appCode)])
138
+ vendorHash = vendor
139
+ appHash = app
140
+ }
141
+ } else if app != appHash {
142
+ if let appCode = await fetchText(base.appendingPathComponent("app.js")) {
143
+ refresh(script: appCode)
144
+ appHash = app
145
+ }
146
+ }
147
+ } else if let probe = await head(base.appendingPathComponent("app.js")) {
148
+ let etag = probe.0
149
+ if etag != legacyETag {
150
+ legacyETag = etag
151
+ await load(url: base.appendingPathComponent("app.js"))
152
+ }
153
+ } else if !isLoaded {
154
+ error = "waiting for the dev server at \(base)"
155
+ }
156
+ try? await Task.sleep(for: interval)
157
+ }
158
+ }
159
+
160
+ #endif
161
+
162
+ /// How many Fast Refreshes applied since the last full load.
163
+ public private(set) var refreshes = 0
164
+
165
+ /// Evaluates the app half again in the running engine: its modules
166
+ /// re-register their components and `run` refreshes the mounted ones.
167
+ public func refresh(script: String, name: String = "app.js") {
168
+ guard isLoaded else {
169
+ load(script: script, name: name)
170
+ return
171
+ }
172
+ error = nil
173
+ do {
174
+ try host.evaluate(script, name: name)
175
+ refreshes += 1
176
+ } catch {
177
+ self.error = "\(error)"
178
+ }
179
+ }
180
+
181
+ /// Evaluates several scripts as one load (the vendor half, then the app half).
182
+ public func load(scripts: [(name: String, script: String)]) {
183
+ error = nil
184
+ if isLoaded { reset() }
185
+ refreshes = 0
186
+ do {
187
+ for (name, script) in scripts { try host.evaluate(script, name: name) }
188
+ isLoaded = host.hasGlobal("__sbDispatch")
189
+ if !isLoaded { error = "the scripts did not call run(): no __sbDispatch" }
190
+ sendEnvironment()
191
+ } catch {
192
+ self.error = "\(error)"
193
+ }
194
+ }
195
+
196
+ #if !canImport(FoundationBridge)
197
+ private func fetchText(_ url: URL) async -> String? {
198
+ var request = URLRequest(url: url)
199
+ request.cachePolicy = .reloadIgnoringLocalCacheData
200
+ guard let result = try? await URLSession.shared.data(for: request),
201
+ (result.1 as? HTTPURLResponse).map({ (200..<300).contains($0.statusCode) }) ?? true
202
+ else { return nil }
203
+ return String(decoding: result.0, as: UTF8.self)
204
+ }
205
+
206
+ private func fetchJSON(_ url: URL) async -> JSONValue? {
207
+ guard let text = await fetchText(url) else { return nil }
208
+ return try? JSONValue.parse(text)
209
+ }
210
+
211
+ /// A `HEAD` request's `ETag` and status, or nil when the server is not there.
212
+ private func head(_ url: URL) async -> (String, Int)? {
213
+ var request = URLRequest(url: url)
214
+ request.httpMethod = "HEAD"
215
+ request.cachePolicy = .reloadIgnoringLocalCacheData
216
+ guard let result = try? await URLSession.shared.data(for: request), let http = result.1 as? HTTPURLResponse else { return nil }
217
+ return (http.value(forHTTPHeaderField: "ETag") ?? "", http.statusCode)
218
+ }
219
+ #endif
220
+
221
+ /// Delivers a renderer event to the app; returns after its render pass applied.
222
+ public func dispatch(_ event: RendererEvent) {
223
+ guard isLoaded else { return }
224
+ do {
225
+ try host.call("__sbDispatch", [.string(event.jsonString)])
226
+ } catch {
227
+ self.error = "\(error)"
228
+ }
229
+ }
230
+
231
+ /// Tells the app the device environment; sent once at load and on change.
232
+ public func setEnvironment(colorScheme: String? = nil, dynamicTypeSize: String? = nil, horizontalSizeClass: String? = nil) {
233
+ var changed = false
234
+ if let colorScheme, colorScheme != self.colorScheme { self.colorScheme = colorScheme; changed = true }
235
+ if let dynamicTypeSize, dynamicTypeSize != self.dynamicTypeSize { self.dynamicTypeSize = dynamicTypeSize; changed = true }
236
+ if let horizontalSizeClass, horizontalSizeClass != self.horizontalSizeClass { self.horizontalSizeClass = horizontalSizeClass; changed = true }
237
+ if changed { sendEnvironment() }
238
+ }
239
+
240
+ private func sendEnvironment() {
241
+ guard isLoaded, colorScheme != nil || dynamicTypeSize != nil || horizontalSizeClass != nil else { return }
242
+ dispatch(.environment(colorScheme: colorScheme, dynamicTypeSize: dynamicTypeSize, horizontalSizeClass: horizontalSizeClass))
243
+ }
244
+
245
+ /// The scene moved between the foreground, inactive and the background.
246
+ public func setScenePhase(_ phase: String) {
247
+ guard isLoaded else { return }
248
+ dispatch(.environment(scenePhase: phase))
249
+ }
250
+ }
251
+ #endif
@@ -0,0 +1,53 @@
1
+ #if canImport(SwiftUI)
2
+ import Foundation
3
+ import SwiftBrowserProtocol
4
+ import SwiftBrowserTree
5
+ import SwiftUI
6
+
7
+ /// What a registered native view gets: the element's `props` as the app
8
+ /// wrote them, its children (SwiftUI views the app put inside it) and a way
9
+ /// to send a value back, which reaches the element's `onEvent` in JavaScript.
10
+ public struct NativeViewContext {
11
+ public let id: Int
12
+ public let props: JSONValue
13
+ public let children: [ElementNode]
14
+ public let send: (JSONValue) -> Void
15
+
16
+ /// The children as a view (built on the main actor, as views are).
17
+ @MainActor public var content: some View {
18
+ ForEach(children) { ElementView(node: $0) }
19
+ }
20
+ }
21
+
22
+ /// The escape hatch (docs/native.md): the `native` element kind, which only
23
+ /// a JavaScript producer emits, names a view the Swift app registered.
24
+ ///
25
+ /// ```swift
26
+ /// NativeRegistry.register("MapView") { context in
27
+ /// MapView(region: context.props["region"]) { context.send(["tapped": true]) }
28
+ /// }
29
+ /// ```
30
+ ///
31
+ /// ```tsx
32
+ /// <Native name="MapView" props={{ region }} onEvent={(value) => …} />
33
+ /// ```
34
+ @MainActor
35
+ public enum NativeRegistry {
36
+ public typealias Builder = (NativeViewContext) -> AnyView
37
+ private static var builders: [String: Builder] = [:]
38
+
39
+ public static func register<V: View>(_ name: String, @ViewBuilder _ builder: @escaping (NativeViewContext) -> V) {
40
+ builders[name] = { AnyView(builder($0)) }
41
+ }
42
+
43
+ public static func unregister(_ name: String) {
44
+ builders[name] = nil
45
+ }
46
+
47
+ public static func isRegistered(_ name: String) -> Bool { builders[name] != nil }
48
+
49
+ static func view(name: String, context: NativeViewContext) -> AnyView? {
50
+ builders[name]?(context)
51
+ }
52
+ }
53
+ #endif
@@ -0,0 +1,6 @@
1
+ // SwiftBrowserNative: the render ops drawn by SwiftUI, with the app's logic
2
+ // in JavaScriptCore (docs/native.md). Everything in this module is for Apple
3
+ // platforms; each file is wrapped in `#if canImport(SwiftUI)` so the package
4
+ // builds and its core tests run on Linux too.
5
+ @_exported import SwiftBrowserProtocol
6
+ @_exported import SwiftBrowserTree
@@ -0,0 +1,361 @@
1
+ #if canImport(SwiftUI)
2
+ import Foundation
3
+ import SwiftBrowserProtocol
4
+ import SwiftUI
5
+ #if canImport(UIKit)
6
+ import UIKit
7
+ #endif
8
+
9
+ // The protocol's value types as SwiftUI's. Names are SwiftUI's own (the shim
10
+ // emits them from the real API), so most of these are lookups.
11
+
12
+ extension Color {
13
+ init(spec: SwiftBrowserProtocol.ColorSpec) {
14
+ switch spec {
15
+ case .rgba(let r, let g, let b, let a):
16
+ self = Color(.sRGB, red: r, green: g, blue: b, opacity: a)
17
+ case .named(let name, let opacity):
18
+ self = Color.named(name).opacity(opacity)
19
+ case .asset(let name, let opacity):
20
+ self = Color(name, bundle: .main).opacity(opacity)
21
+ }
22
+ }
23
+
24
+ /// The semantic names of docs/ops-protocol.md ("Color") and UIKit's.
25
+ static func named(_ name: String) -> Color {
26
+ switch name {
27
+ case "primary", "label": return .primary
28
+ case "secondary", "secondaryLabel": return .secondary
29
+ case "foreground": return .primary
30
+ case "accent", "tint": return .accentColor
31
+ case "red": return .red
32
+ case "orange": return .orange
33
+ case "yellow": return .yellow
34
+ case "green": return .green
35
+ case "mint": return .mint
36
+ case "teal": return .teal
37
+ case "cyan": return .cyan
38
+ case "blue": return .blue
39
+ case "indigo": return .indigo
40
+ case "purple": return .purple
41
+ case "pink": return .pink
42
+ case "brown": return .brown
43
+ case "gray": return .gray
44
+ case "clear": return .clear
45
+ case "white": return .white
46
+ case "black": return .black
47
+ default: break
48
+ }
49
+ #if canImport(UIKit)
50
+ switch name {
51
+ case "systemBackground": return Color(uiColor: .systemBackground)
52
+ case "secondarySystemBackground": return Color(uiColor: .secondarySystemBackground)
53
+ case "tertiarySystemBackground": return Color(uiColor: .tertiarySystemBackground)
54
+ case "systemGroupedBackground": return Color(uiColor: .systemGroupedBackground)
55
+ case "secondarySystemGroupedBackground": return Color(uiColor: .secondarySystemGroupedBackground)
56
+ case "tertiarySystemGroupedBackground": return Color(uiColor: .tertiarySystemGroupedBackground)
57
+ case "tertiaryLabel": return Color(uiColor: .tertiaryLabel)
58
+ case "quaternaryLabel": return Color(uiColor: .quaternaryLabel)
59
+ case "placeholderText": return Color(uiColor: .placeholderText)
60
+ case "separator": return Color(uiColor: .separator)
61
+ case "opaqueSeparator": return Color(uiColor: .opaqueSeparator)
62
+ case "systemFill": return Color(uiColor: .systemFill)
63
+ case "secondarySystemFill": return Color(uiColor: .secondarySystemFill)
64
+ case "tertiarySystemFill": return Color(uiColor: .tertiarySystemFill)
65
+ case "quaternarySystemFill": return Color(uiColor: .quaternarySystemFill)
66
+ case "systemGray2": return Color(uiColor: .systemGray2)
67
+ case "systemGray3": return Color(uiColor: .systemGray3)
68
+ case "systemGray4": return Color(uiColor: .systemGray4)
69
+ case "systemGray5": return Color(uiColor: .systemGray5)
70
+ case "systemGray6": return Color(uiColor: .systemGray6)
71
+ default: break
72
+ }
73
+ #else
74
+ switch name {
75
+ case "systemBackground", "secondarySystemGroupedBackground", "tertiarySystemBackground": return Color(white: 1)
76
+ case "secondarySystemBackground", "systemGroupedBackground", "tertiarySystemGroupedBackground": return Color(white: 0.95)
77
+ case "tertiaryLabel", "quaternaryLabel", "placeholderText": return Color.gray.opacity(0.5)
78
+ case "separator", "opaqueSeparator": return Color.gray.opacity(0.3)
79
+ case "systemFill", "secondarySystemFill", "tertiarySystemFill", "quaternarySystemFill": return Color.gray.opacity(0.2)
80
+ case "systemGray2", "systemGray3", "systemGray4", "systemGray5", "systemGray6": return Color.gray.opacity(0.4)
81
+ default: break
82
+ }
83
+ #endif
84
+ // An unknown name is visible, as the web renderer draws unknown asset colors.
85
+ return Color(.sRGB, red: 1, green: 0, blue: 1, opacity: 1)
86
+ }
87
+ }
88
+
89
+ extension AnyShapeStyle {
90
+ /// Any `ShapeStyle` of the protocol, type-erased.
91
+ init(spec: ShapeStyleSpec) {
92
+ switch spec {
93
+ case .color(.named("tint", let opacity)):
94
+ self = opacity < 1 ? AnyShapeStyle(TintShapeStyle().opacity(opacity)) : AnyShapeStyle(TintShapeStyle())
95
+ case .color(let color):
96
+ self = AnyShapeStyle(Color(spec: color))
97
+ case .hierarchical(let level, let opacity):
98
+ let style: AnyShapeStyle
99
+ switch level {
100
+ case 2: style = AnyShapeStyle(HierarchicalShapeStyle.secondary)
101
+ case 3: style = AnyShapeStyle(HierarchicalShapeStyle.tertiary)
102
+ case 4: style = AnyShapeStyle(HierarchicalShapeStyle.quaternary)
103
+ default: style = AnyShapeStyle(HierarchicalShapeStyle.primary)
104
+ }
105
+ self = opacity < 1 ? AnyShapeStyle(style.opacity(opacity)) : style
106
+ case .linearGradient(let stops, let start, let end, let opacity):
107
+ let gradient = LinearGradient(gradient: Gradient(stops: stops.map(Gradient.Stop.init(spec:))), startPoint: UnitPoint(spec: start), endPoint: UnitPoint(spec: end))
108
+ self = opacity < 1 ? AnyShapeStyle(gradient.opacity(opacity)) : AnyShapeStyle(gradient)
109
+ case .radialGradient(let stops, let center, let startRadius, let endRadius, let opacity):
110
+ let gradient = RadialGradient(gradient: Gradient(stops: stops.map(Gradient.Stop.init(spec:))), center: UnitPoint(spec: center), startRadius: startRadius, endRadius: endRadius)
111
+ self = opacity < 1 ? AnyShapeStyle(gradient.opacity(opacity)) : AnyShapeStyle(gradient)
112
+ case .angularGradient(let stops, let center, let startAngle, let endAngle, let opacity):
113
+ let gradient = AngularGradient(gradient: Gradient(stops: stops.map(Gradient.Stop.init(spec:))), center: UnitPoint(spec: center), startAngle: .degrees(startAngle), endAngle: .degrees(endAngle))
114
+ self = opacity < 1 ? AnyShapeStyle(gradient.opacity(opacity)) : AnyShapeStyle(gradient)
115
+ case .colorGradient(let color, let opacity):
116
+ let gradient = Color(spec: color).gradient
117
+ self = opacity < 1 ? AnyShapeStyle(gradient.opacity(opacity)) : AnyShapeStyle(gradient)
118
+ case .material(let name, let opacity):
119
+ let material: Material
120
+ switch name {
121
+ case "ultraThin": material = .ultraThinMaterial
122
+ case "thin": material = .thinMaterial
123
+ case "thick": material = .thickMaterial
124
+ case "ultraThick": material = .ultraThickMaterial
125
+ default: material = .regularMaterial
126
+ }
127
+ self = opacity < 1 ? AnyShapeStyle(material.opacity(opacity)) : AnyShapeStyle(material)
128
+ }
129
+ }
130
+ }
131
+
132
+ extension Gradient.Stop {
133
+ init(spec: GradientStopSpec) {
134
+ self.init(color: Color(spec: spec.color), location: spec.location)
135
+ }
136
+ }
137
+
138
+ extension UnitPoint {
139
+ init(spec: UnitPointSpec) {
140
+ self.init(x: spec.x, y: spec.y)
141
+ }
142
+ }
143
+
144
+ extension Font {
145
+ init(spec: FontSpec) {
146
+ let weight = spec.weight.map(Font.Weight.init(name:))
147
+ let design = spec.design.map(Font.Design.init(name:))
148
+ var font: Font
149
+ if let size = spec.size {
150
+ font = .system(size: size, weight: weight, design: design)
151
+ } else {
152
+ let style = Font.TextStyle(name: spec.textStyle ?? "body")
153
+ font = .system(style, design: design, weight: weight)
154
+ }
155
+ if spec.monospacedDigit { font = font.monospacedDigit() }
156
+ self = font
157
+ }
158
+ }
159
+
160
+ extension Font.Weight {
161
+ init(name: String) {
162
+ switch name {
163
+ case "ultraLight": self = .ultraLight
164
+ case "thin": self = .thin
165
+ case "light": self = .light
166
+ case "medium": self = .medium
167
+ case "semibold": self = .semibold
168
+ case "bold": self = .bold
169
+ case "heavy": self = .heavy
170
+ case "black": self = .black
171
+ default: self = .regular
172
+ }
173
+ }
174
+ }
175
+
176
+ extension Font.Design {
177
+ init(name: String) {
178
+ switch name {
179
+ case "serif": self = .serif
180
+ case "rounded": self = .rounded
181
+ case "monospaced": self = .monospaced
182
+ default: self = .default
183
+ }
184
+ }
185
+ }
186
+
187
+ extension Font.TextStyle {
188
+ init(name: String) {
189
+ switch name {
190
+ case "largeTitle": self = .largeTitle
191
+ case "title": self = .title
192
+ case "title2": self = .title2
193
+ case "title3": self = .title3
194
+ case "headline": self = .headline
195
+ case "subheadline": self = .subheadline
196
+ case "callout": self = .callout
197
+ case "footnote": self = .footnote
198
+ case "caption": self = .caption
199
+ case "caption2": self = .caption2
200
+ default: self = .body
201
+ }
202
+ }
203
+ }
204
+
205
+ extension Alignment {
206
+ init(name: String) {
207
+ switch name {
208
+ case "leading": self = .leading
209
+ case "trailing": self = .trailing
210
+ case "top": self = .top
211
+ case "bottom": self = .bottom
212
+ case "topLeading": self = .topLeading
213
+ case "topTrailing": self = .topTrailing
214
+ case "bottomLeading": self = .bottomLeading
215
+ case "bottomTrailing": self = .bottomTrailing
216
+ default: self = .center
217
+ }
218
+ }
219
+ }
220
+
221
+ extension HorizontalAlignment {
222
+ init(name: String) {
223
+ switch name {
224
+ case "leading": self = .leading
225
+ case "trailing": self = .trailing
226
+ default: self = .center
227
+ }
228
+ }
229
+ }
230
+
231
+ extension VerticalAlignment {
232
+ init(name: String) {
233
+ switch name {
234
+ case "top": self = .top
235
+ case "bottom": self = .bottom
236
+ case "firstTextBaseline": self = .firstTextBaseline
237
+ case "lastTextBaseline": self = .lastTextBaseline
238
+ default: self = .center
239
+ }
240
+ }
241
+ }
242
+
243
+ extension TextAlignment {
244
+ init(name: String) {
245
+ switch name {
246
+ case "leading": self = .leading
247
+ case "trailing": self = .trailing
248
+ default: self = .center
249
+ }
250
+ }
251
+ }
252
+
253
+ extension Edge {
254
+ init?(name: String) {
255
+ switch name {
256
+ case "top": self = .top
257
+ case "leading": self = .leading
258
+ case "bottom": self = .bottom
259
+ case "trailing": self = .trailing
260
+ default: return nil
261
+ }
262
+ }
263
+ }
264
+
265
+ extension Edge.Set {
266
+ init(names: [String]) {
267
+ var set: Edge.Set = []
268
+ for name in names {
269
+ if let edge = Edge(name: name) { set.insert(Edge.Set(edge)) }
270
+ }
271
+ self = set
272
+ }
273
+ }
274
+
275
+ extension CGFloat {
276
+ init?(dimension: DimensionSpec?) {
277
+ switch dimension {
278
+ case .points(let value)?: self = value
279
+ case .infinity?: self = .infinity
280
+ case nil: return nil
281
+ }
282
+ }
283
+ }
284
+
285
+ extension Animation {
286
+ /// A `commit`'s or `.animation(_:value:)`'s animation.
287
+ init(spec: AnimationSpec) {
288
+ let duration = spec.duration ?? 0.35
289
+ var animation: Animation
290
+ switch spec.type {
291
+ case "linear": animation = .linear(duration: duration)
292
+ case "easeIn": animation = .easeIn(duration: duration)
293
+ case "easeOut": animation = .easeOut(duration: duration)
294
+ case "easeInOut": animation = .easeInOut(duration: duration)
295
+ case "spring": animation = .spring(duration: duration, bounce: spec.bounce ?? 0)
296
+ default: animation = spec.duration == nil ? .default : .easeInOut(duration: duration)
297
+ }
298
+ if let delay = spec.delay, delay > 0 { animation = animation.delay(delay) }
299
+ if spec.repeatsForever {
300
+ animation = animation.repeatForever(autoreverses: spec.autoreverses)
301
+ } else if let count = spec.repeatCount, count > 1 {
302
+ animation = animation.repeatCount(count, autoreverses: spec.autoreverses)
303
+ }
304
+ self = animation
305
+ }
306
+ }
307
+
308
+ extension AnyTransition {
309
+ /// A `transition` field: one part, or several combined.
310
+ init(specs: [TransitionSpec]) {
311
+ guard let first = specs.first else {
312
+ self = .identity
313
+ return
314
+ }
315
+ var transition = AnyTransition(spec: first)
316
+ for spec in specs.dropFirst() { transition = transition.combined(with: AnyTransition(spec: spec)) }
317
+ self = transition
318
+ }
319
+
320
+ init(spec: TransitionSpec) {
321
+ switch spec.kind {
322
+ case "opacity": self = .opacity
323
+ case "scale": self = .scale(scale: spec.scale ?? 0.5)
324
+ case "slide": self = .slide
325
+ case "move": self = .move(edge: spec.edge.flatMap(Edge.init(name:)) ?? .bottom)
326
+ case "offset": self = .offset(x: spec.x ?? 0, y: spec.y ?? 0)
327
+ case "asymmetric":
328
+ self = .asymmetric(insertion: AnyTransition(specs: spec.insertion ?? []), removal: AnyTransition(specs: spec.removal ?? []))
329
+ default: self = .identity
330
+ }
331
+ }
332
+ }
333
+
334
+ extension EdgeInsets {
335
+ /// `.padding(...)` edges: a missing edge is not padded.
336
+ init(spec: EdgeInsetsSpec) {
337
+ self.init(top: spec.top ?? 0, leading: spec.leading ?? 0, bottom: spec.bottom ?? 0, trailing: spec.trailing ?? 0)
338
+ }
339
+ }
340
+
341
+ extension DynamicTypeSize {
342
+ /// The protocol's name for the size (`xSmall` ... `accessibility5`).
343
+ var protocolName: String {
344
+ switch self {
345
+ case .xSmall: return "xSmall"
346
+ case .small: return "small"
347
+ case .medium: return "medium"
348
+ case .large: return "large"
349
+ case .xLarge: return "xLarge"
350
+ case .xxLarge: return "xxLarge"
351
+ case .xxxLarge: return "xxxLarge"
352
+ case .accessibility1: return "accessibility1"
353
+ case .accessibility2: return "accessibility2"
354
+ case .accessibility3: return "accessibility3"
355
+ case .accessibility4: return "accessibility4"
356
+ case .accessibility5: return "accessibility5"
357
+ @unknown default: return "large"
358
+ }
359
+ }
360
+ }
361
+ #endif