@swiftbrowser/swift 0.2.0 → 0.3.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.
- package/Package.swift +3 -3
- package/Sources/SwiftUI/API/AlignmentGuides.swift +6 -3
- package/Sources/SwiftUI/API/ColorSpaces.swift +11 -11
- package/Sources/SwiftUI/API/Effects.swift +20 -3
- package/Sources/SwiftUI/API/Environment.swift +7 -0
- package/Sources/SwiftUI/API/Gauge.swift +23 -8
- package/Sources/SwiftUI/API/Layout.swift +4 -0
- package/Sources/SwiftUI/API/Modifiers.swift +8 -2
- package/Sources/SwiftUI/API/Picker.swift +3 -2
- package/Sources/SwiftUI/API/Primitives.swift +4 -2
- package/Sources/SwiftUI/API/SectionSupplements.swift +5 -15
- package/Sources/SwiftUI/API/ShapeStyle.swift +82 -1
- package/Sources/SwiftUI/API/View.swift +37 -1
- package/Sources/SwiftUI/Localization/Localization.swift +28 -2
- package/Sources/UIKit/ColorConversions.swift +1 -1
- package/Sources/UIKit/UIKit.swift +9 -9
- package/manifest.mjs +3 -1
- package/package.json +1 -1
package/Package.swift
CHANGED
|
@@ -102,9 +102,9 @@ let package = Package(
|
|
|
102
102
|
// (`canImport(OSLog)`) and the macro must link it: the CLI sets
|
|
103
103
|
// SWIFT_BUILD_SCRIPT_ENVIRONMENT, which turns that logging off
|
|
104
104
|
// (toolchain.ts). This repository's own builds always compile
|
|
105
|
-
// swift-syntax (SwiftDataTests links the macro target)
|
|
106
|
-
//
|
|
107
|
-
//
|
|
105
|
+
// swift-syntax (SwiftDataTests links the macro target): without
|
|
106
|
+
// that variable, the end of its Package.swift has the macro link
|
|
107
|
+
// os too (lines manifest.mjs leaves out of the published one).
|
|
108
108
|
],
|
|
109
109
|
path: "Sources/SwiftDataMacros"
|
|
110
110
|
),
|
|
@@ -26,9 +26,12 @@ extension HorizontalAlignment {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/// Where a list row's separator starts: accepted, the page draws full-width
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
/// Where a list row's separator starts: accepted, the page draws full-width
|
|
30
|
+
/// separators. A guide of its own, which no stack or frame aligns by: as
|
|
31
|
+
/// `.leading`, Ice Cubes' `.alignmentGuide(.listRowSeparatorLeading) { _ in -100 }`
|
|
32
|
+
/// moved its embedded posts 100 points right in their leading `VStack`.
|
|
33
|
+
public static let listRowSeparatorLeading = HorizontalAlignment(name: "listRowSeparatorLeading")
|
|
34
|
+
public static let listRowSeparatorTrailing = HorizontalAlignment(name: "listRowSeparatorTrailing")
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
extension VerticalAlignment {
|
|
@@ -93,7 +93,7 @@ extension Color {
|
|
|
93
93
|
case "primary": return dark ? resolved(1, 1, 1, a) : resolved(0, 0, 0, a)
|
|
94
94
|
case "secondary":
|
|
95
95
|
return dark ? resolved(0.922, 0.922, 0.961, 0.6 * a) : resolved(0.235, 0.235, 0.263, 0.6 * a)
|
|
96
|
-
case "accent": return dark ? resolved(0
|
|
96
|
+
case "accent": return dark ? resolved(0, 0.569, 1, a) : resolved(0, 0.533, 1, a)
|
|
97
97
|
case "clear": return resolved(0, 0, 0, 0)
|
|
98
98
|
default:
|
|
99
99
|
guard let (light, darkValue) = Color.semanticValues[name] else { return resolved(0.5, 0.5, 0.5, a) }
|
|
@@ -113,18 +113,18 @@ extension Color {
|
|
|
113
113
|
static let semanticValues: [String: (light: (Double, Double, Double, Double), dark: (Double, Double, Double, Double))] = {
|
|
114
114
|
func rgb(_ r: Double, _ g: Double, _ b: Double, _ a: Double = 1) -> (Double, Double, Double, Double) { (r / 255, g / 255, b / 255, a) }
|
|
115
115
|
return [
|
|
116
|
-
"red": (rgb(255,
|
|
117
|
-
"orange": (rgb(255,
|
|
118
|
-
"yellow": (rgb(255, 204, 0), rgb(255, 214,
|
|
116
|
+
"red": (rgb(255, 56, 60), rgb(255, 66, 69)),
|
|
117
|
+
"orange": (rgb(255, 141, 40), rgb(255, 146, 48)),
|
|
118
|
+
"yellow": (rgb(255, 204, 0), rgb(255, 214, 0)),
|
|
119
119
|
"green": (rgb(52, 199, 89), rgb(48, 209, 88)),
|
|
120
|
-
"mint": (rgb(0,
|
|
121
|
-
"teal": (rgb(
|
|
122
|
-
"cyan": (rgb(
|
|
123
|
-
"blue": (rgb(0,
|
|
124
|
-
"indigo": (rgb(
|
|
125
|
-
"purple": (rgb(
|
|
120
|
+
"mint": (rgb(0, 200, 179), rgb(0, 218, 195)),
|
|
121
|
+
"teal": (rgb(0, 195, 208), rgb(0, 210, 224)),
|
|
122
|
+
"cyan": (rgb(0, 192, 232), rgb(60, 211, 254)),
|
|
123
|
+
"blue": (rgb(0, 136, 255), rgb(0, 145, 255)),
|
|
124
|
+
"indigo": (rgb(97, 85, 245), rgb(109, 124, 255)),
|
|
125
|
+
"purple": (rgb(203, 48, 224), rgb(219, 52, 242)),
|
|
126
126
|
"pink": (rgb(255, 45, 85), rgb(255, 55, 95)),
|
|
127
|
-
"brown": (rgb(
|
|
127
|
+
"brown": (rgb(172, 127, 94), rgb(183, 138, 102)),
|
|
128
128
|
"gray": (rgb(142, 142, 147), rgb(142, 142, 147)),
|
|
129
129
|
"systemBackground": (rgb(255, 255, 255), rgb(0, 0, 0)),
|
|
130
130
|
"secondarySystemBackground": (rgb(242, 242, 247), rgb(28, 28, 30)),
|
|
@@ -227,13 +227,30 @@ nonisolated extension View {
|
|
|
227
227
|
public func contentTransition(_ transition: ContentTransition) -> some View { self }
|
|
228
228
|
public func textSelection<S: TextSelectability>(_ selectability: S) -> some View { self }
|
|
229
229
|
public func toolbarRole(_ role: ToolbarRole) -> some View { self }
|
|
230
|
-
|
|
230
|
+
/// The size of the controls inside; a bordered button's height follows it
|
|
231
|
+
/// (the page: `large` and `extraLarge` are taller).
|
|
232
|
+
public func controlSize(_ size: ControlSize) -> some View {
|
|
233
|
+
environment(\.controlSize, size)
|
|
234
|
+
}
|
|
231
235
|
}
|
|
232
236
|
|
|
233
237
|
public enum Prominence: Hashable, Sendable { case standard, increased }
|
|
234
238
|
|
|
235
|
-
/// `.controlSize`:
|
|
236
|
-
|
|
239
|
+
/// `.controlSize`: bordered buttons take it (props `controlSize` when it is not
|
|
240
|
+
/// `regular`); other controls keep one size.
|
|
241
|
+
public enum ControlSize: Hashable, CaseIterable, Sendable {
|
|
242
|
+
case mini, small, regular, large, extraLarge
|
|
243
|
+
|
|
244
|
+
var _name: String {
|
|
245
|
+
switch self {
|
|
246
|
+
case .mini: "mini"
|
|
247
|
+
case .small: "small"
|
|
248
|
+
case .regular: "regular"
|
|
249
|
+
case .large: "large"
|
|
250
|
+
case .extraLarge: "extraLarge"
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
237
254
|
|
|
238
255
|
public struct SymbolVariants: Hashable, Sendable {
|
|
239
256
|
let name: String
|
|
@@ -166,6 +166,12 @@ public struct EnvironmentValues {
|
|
|
166
166
|
set { self[_ButtonStyleKey.self] = newValue }
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/// `.controlSize(_:)`; `regular` by default.
|
|
170
|
+
public var controlSize: ControlSize {
|
|
171
|
+
get { self[_ControlSizeKey.self] }
|
|
172
|
+
set { self[_ControlSizeKey.self] = newValue }
|
|
173
|
+
}
|
|
174
|
+
|
|
169
175
|
var _textFieldStyle: String {
|
|
170
176
|
get { self[_TextFieldStyleKey.self] }
|
|
171
177
|
set { self[_TextFieldStyleKey.self] = newValue }
|
|
@@ -214,6 +220,7 @@ enum _LayoutDirectionKey: EnvironmentKey { static let defaultValue = LayoutDirec
|
|
|
214
220
|
enum _FontKey: EnvironmentKey { static let defaultValue: Font? = nil }
|
|
215
221
|
enum _DisplayScaleKey: EnvironmentKey { static let defaultValue: CGFloat = 3 }
|
|
216
222
|
enum _ButtonStyleKey: EnvironmentKey { static let defaultValue = "automatic" }
|
|
223
|
+
enum _ControlSizeKey: EnvironmentKey { static let defaultValue = ControlSize.regular }
|
|
217
224
|
enum _TextFieldStyleKey: EnvironmentKey { static let defaultValue = "plain" }
|
|
218
225
|
|
|
219
226
|
// MARK: - @Environment
|
|
@@ -35,6 +35,12 @@ public struct AccessoryCircularCapacityGaugeStyle: GaugeStyle, _NamedGaugeStyle,
|
|
|
35
35
|
var _name: String { "accessoryCircularCapacity" }
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/// A determinate `ProgressView`'s gauge: the label above the bar in the
|
|
39
|
+
/// body font, the current value label under it (the page lays it out).
|
|
40
|
+
struct _ProgressGaugeStyle: GaugeStyle, _NamedGaugeStyle, Sendable {
|
|
41
|
+
var _name: String { "progress" }
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
extension GaugeStyle where Self == DefaultGaugeStyle {
|
|
39
45
|
public static var automatic: DefaultGaugeStyle { DefaultGaugeStyle() }
|
|
40
46
|
}
|
|
@@ -163,10 +169,11 @@ extension Gauge: _HostView {
|
|
|
163
169
|
|
|
164
170
|
// MARK: - ProgressView
|
|
165
171
|
|
|
166
|
-
/// Progress toward a total. A determinate progress view draws as a
|
|
167
|
-
/// `
|
|
168
|
-
///
|
|
169
|
-
/// symbol
|
|
172
|
+
/// Progress toward a total. A determinate progress view draws as a gauge
|
|
173
|
+
/// (style `progress`: its label above the bar, its current value label under
|
|
174
|
+
/// it); an indeterminate one (no value) is
|
|
175
|
+
/// the spinner symbol (`progress.indicator`, which the page animates) with its
|
|
176
|
+
/// label under it, as iOS draws them.
|
|
170
177
|
public struct ProgressView<Label: View, CurrentValueLabel: View>: View {
|
|
171
178
|
/// Progress in 0…1, nil when indeterminate.
|
|
172
179
|
let fraction: Double?
|
|
@@ -178,16 +185,24 @@ public struct ProgressView<Label: View, CurrentValueLabel: View>: View {
|
|
|
178
185
|
Gauge(value: fraction, in: 0...1) {
|
|
179
186
|
if let label { label }
|
|
180
187
|
} currentValueLabel: {
|
|
181
|
-
if let currentValueLabel { currentValueLabel }
|
|
188
|
+
if let currentValueLabel { currentValueLabel.foregroundStyle(.secondary) }
|
|
182
189
|
}
|
|
183
|
-
.gaugeStyle(
|
|
190
|
+
.gaugeStyle(_ProgressGaugeStyle())
|
|
184
191
|
} else if let label {
|
|
185
|
-
|
|
192
|
+
VStack(spacing: 8) {
|
|
193
|
+
activityIndicator
|
|
194
|
+
label.foregroundStyle(.secondary)
|
|
195
|
+
}
|
|
186
196
|
} else {
|
|
187
|
-
|
|
197
|
+
activityIndicator
|
|
188
198
|
}
|
|
189
199
|
}
|
|
190
200
|
|
|
201
|
+
/// iOS's activity indicator is 20pt across whatever the font around it.
|
|
202
|
+
private var activityIndicator: some View {
|
|
203
|
+
Image(systemName: "progress.indicator").font(.system(size: 20)).foregroundStyle(.secondary)
|
|
204
|
+
}
|
|
205
|
+
|
|
191
206
|
static func fraction<V: BinaryFloatingPoint>(_ value: V?, total: V) -> Double? {
|
|
192
207
|
guard let value, total > 0 else { return nil }
|
|
193
208
|
return max(0, min(1, Double(value) / Double(total)))
|
|
@@ -81,6 +81,10 @@ extension _ShapeView: _HostView {
|
|
|
81
81
|
object["dash"] = .array(style.dash.map(_JSON.number))
|
|
82
82
|
object["dashPhase"] = .number(style.dashPhase)
|
|
83
83
|
}
|
|
84
|
+
} else if let style = strokeStyle, !style.dash.isEmpty {
|
|
85
|
+
// A built-in shape's border can only be dashed or not: the page
|
|
86
|
+
// draws CSS dashes for any pattern.
|
|
87
|
+
object["dash"] = .array(style.dash.map(_JSON.number))
|
|
84
88
|
}
|
|
85
89
|
stroke = .object(object)
|
|
86
90
|
}
|
|
@@ -134,14 +134,20 @@ extension _StyledGroup: _GroupView {
|
|
|
134
134
|
// child of its list, which is how the layout recognizes it).
|
|
135
135
|
if let section = content as? any _SectionView { return [section._stylingRows(style)] }
|
|
136
136
|
if let group = content as? any _GroupView, group._spreads {
|
|
137
|
-
|
|
137
|
+
let target = _spreadTarget(group)
|
|
138
|
+
var children = target._children
|
|
139
|
+
// A view of the group that is absent (`if locked { badge }` when not
|
|
140
|
+
// locked) gets no styled element: an empty one would take a stack's
|
|
141
|
+
// spacing. Keyed children keep their places.
|
|
142
|
+
if target._keys == nil { children.removeAll(where: _rendersNothing) }
|
|
143
|
+
return children.map { _StyledView(content: AnyView($0), style: style) }
|
|
138
144
|
}
|
|
139
145
|
return [_StyledView(content: content, style: style)]
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
var _keys: [AnyHashable]? {
|
|
143
149
|
if let group = content as? any _GroupView, group._spreads {
|
|
144
|
-
return group._keys
|
|
150
|
+
return _spreadTarget(group)._keys
|
|
145
151
|
}
|
|
146
152
|
return nil
|
|
147
153
|
}
|
|
@@ -172,7 +172,8 @@ func _systemImage(of view: any View) -> String? {
|
|
|
172
172
|
|
|
173
173
|
// MARK: - Form
|
|
174
174
|
|
|
175
|
-
/// A container for grouped controls, drawn as
|
|
175
|
+
/// A container for grouped controls, drawn as an inset grouped list (what
|
|
176
|
+
/// iOS's automatic form style is on iPhone: cards inset from the edges).
|
|
176
177
|
public struct Form<Content: View> {
|
|
177
178
|
let content: Content
|
|
178
179
|
public init(@ViewBuilder content: () -> Content) { self.content = content() }
|
|
@@ -180,6 +181,6 @@ public struct Form<Content: View> {
|
|
|
180
181
|
extension Form: _PrimitiveView {}
|
|
181
182
|
extension Form: _HostView {
|
|
182
183
|
var _kind: String { "list" }
|
|
183
|
-
func _props(in environment: EnvironmentValues) -> _JSON { .object(["style": .string("
|
|
184
|
+
func _props(in environment: EnvironmentValues) -> _JSON { .object(["style": .string("insetGrouped")]) }
|
|
184
185
|
var _content: [any View] { [content] }
|
|
185
186
|
}
|
|
@@ -454,11 +454,13 @@ extension Button: _PrimitiveView {}
|
|
|
454
454
|
extension Button: _HostView {
|
|
455
455
|
var _kind: String { "button" }
|
|
456
456
|
func _props(in environment: EnvironmentValues) -> _JSON {
|
|
457
|
-
|
|
457
|
+
var props: [String: _JSON] = [
|
|
458
458
|
// A custom `ButtonStyle` draws its own look: `plain` (ButtonStyles.swift).
|
|
459
459
|
"style": .string(environment._buttonStyle == _CustomButtonStyle.marker ? "plain" : environment._buttonStyle),
|
|
460
460
|
"role": role.map { .string($0.name) } ?? .null,
|
|
461
|
-
]
|
|
461
|
+
]
|
|
462
|
+
if environment.controlSize != .regular { props["controlSize"] = .string(environment.controlSize._name) }
|
|
463
|
+
return .object(props)
|
|
462
464
|
}
|
|
463
465
|
var _content: [any View] { [label] }
|
|
464
466
|
var _handler: ((_JSON) -> Void)? {
|
|
@@ -5,11 +5,10 @@
|
|
|
5
5
|
// other view becomes a child of the `section`: a `sectionheader` before the
|
|
6
6
|
// rows and a `sectionfooter` after them, each with the view inside (props
|
|
7
7
|
// `{}`). In a list the page lays the header view out above the section's card
|
|
8
|
-
// and the footer view below it, in the content width
|
|
9
|
-
//
|
|
10
|
-
// stack with the rows. As
|
|
11
|
-
//
|
|
12
|
-
// inside the header undoes).
|
|
8
|
+
// and the footer view below it, in the content width and the secondary
|
|
9
|
+
// color (headers in the headline font, footers in the footnote font); outside a list (a `Section` in a `LazyVStack`) they
|
|
10
|
+
// stack with the rows. As in iOS 26, headers keep their case (iOS 18 and
|
|
11
|
+
// earlier upper-cased them).
|
|
13
12
|
|
|
14
13
|
/// A section's view header or footer: kind `sectionheader` or `sectionfooter`.
|
|
15
14
|
struct _SectionSupplement {
|
|
@@ -20,16 +19,7 @@ extension _SectionSupplement: _PrimitiveView {}
|
|
|
20
19
|
extension _SectionSupplement: _HostView {
|
|
21
20
|
var _kind: String { kind }
|
|
22
21
|
func _props(in environment: EnvironmentValues) -> _JSON { .object([:]) }
|
|
23
|
-
var _content: [any View] {
|
|
24
|
-
guard kind == "sectionheader" else { return [content] }
|
|
25
|
-
return [_withEnvironment(content) { $0.textCase = .uppercase }]
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/// `content.environment(…)` for an existential view.
|
|
30
|
-
private func _withEnvironment(_ view: any View, _ apply: @escaping (inout EnvironmentValues) -> Void) -> any View {
|
|
31
|
-
func open<V: View>(_ content: V) -> any View { _EnvironmentView(content: content, apply: apply) }
|
|
32
|
-
return open(view)
|
|
22
|
+
var _content: [any View] { [content] }
|
|
33
23
|
}
|
|
34
24
|
|
|
35
25
|
/// A header or footer view as the section stores it: a `Text` (or an
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/// A style serializes to the JSON the renderer resolves (see
|
|
3
3
|
/// docs/ops-protocol.md, "Phase 6 additions"): a `Color`'s own forms, or
|
|
4
4
|
/// `{"hierarchical": 1...4}`, `{"linearGradient": {...}}`,
|
|
5
|
-
/// `{"radialGradient": {...}}`, `{"
|
|
5
|
+
/// `{"radialGradient": {...}}`, `{"angularGradient": {...}}`,
|
|
6
|
+
/// `{"colorGradient": color}`, `{"material": name}`.
|
|
6
7
|
///
|
|
7
8
|
/// A style of an app's own (iOS 17) implements `resolve(in:)`, returning a
|
|
8
9
|
/// style it is made of; it is encoded as that style, resolved in the page's
|
|
@@ -240,6 +241,80 @@ public struct RadialGradient: ShapeStyle, Equatable {
|
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
/// A gradient swept around a center, clockwise from `startAngle` (0° points
|
|
245
|
+
/// right) to `endAngle`; equal angles sweep the whole turn. The renderer draws
|
|
246
|
+
/// a CSS `conic-gradient`.
|
|
247
|
+
public struct AngularGradient: ShapeStyle, Equatable {
|
|
248
|
+
public var gradient: Gradient
|
|
249
|
+
public var center: UnitPoint
|
|
250
|
+
public var startAngle: Angle
|
|
251
|
+
public var endAngle: Angle
|
|
252
|
+
|
|
253
|
+
public init(gradient: Gradient, center: UnitPoint, startAngle: Angle = .zero, endAngle: Angle = .zero) {
|
|
254
|
+
self.gradient = gradient
|
|
255
|
+
self.center = center
|
|
256
|
+
self.startAngle = startAngle
|
|
257
|
+
self.endAngle = endAngle
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public init(colors: [Color], center: UnitPoint, startAngle: Angle = .zero, endAngle: Angle = .zero) {
|
|
261
|
+
self.init(gradient: Gradient(colors: colors), center: center, startAngle: startAngle, endAngle: endAngle)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public init(stops: [Gradient.Stop], center: UnitPoint, startAngle: Angle = .zero, endAngle: Angle = .zero) {
|
|
265
|
+
self.init(gradient: Gradient(stops: stops), center: center, startAngle: startAngle, endAngle: endAngle)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// A conic gradient: the whole turn, starting at `angle`.
|
|
269
|
+
public init(gradient: Gradient, center: UnitPoint, angle: Angle = .zero) {
|
|
270
|
+
self.init(gradient: gradient, center: center, startAngle: angle, endAngle: .degrees(angle.degrees + 360))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
public init(colors: [Color], center: UnitPoint, angle: Angle = .zero) {
|
|
274
|
+
self.init(gradient: Gradient(colors: colors), center: center, angle: angle)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public init(stops: [Gradient.Stop], center: UnitPoint, angle: Angle = .zero) {
|
|
278
|
+
self.init(gradient: Gradient(stops: stops), center: center, angle: angle)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
public var _json: _JSON {
|
|
282
|
+
let end = endAngle == startAngle ? startAngle.degrees + 360 : endAngle.degrees
|
|
283
|
+
return .object(["angularGradient": .object([
|
|
284
|
+
"stops": gradient._stopsJSON,
|
|
285
|
+
"center": center._json,
|
|
286
|
+
"startAngle": .number(startAngle.degrees),
|
|
287
|
+
"endAngle": .number(end),
|
|
288
|
+
])])
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
extension ShapeStyle where Self == AngularGradient {
|
|
293
|
+
public static func angularGradient(_ gradient: Gradient, center: UnitPoint, startAngle: Angle, endAngle: Angle) -> AngularGradient {
|
|
294
|
+
AngularGradient(gradient: gradient, center: center, startAngle: startAngle, endAngle: endAngle)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
public static func angularGradient(colors: [Color], center: UnitPoint, startAngle: Angle, endAngle: Angle) -> AngularGradient {
|
|
298
|
+
AngularGradient(colors: colors, center: center, startAngle: startAngle, endAngle: endAngle)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
public static func angularGradient(stops: [Gradient.Stop], center: UnitPoint, startAngle: Angle, endAngle: Angle) -> AngularGradient {
|
|
302
|
+
AngularGradient(stops: stops, center: center, startAngle: startAngle, endAngle: endAngle)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public static func conicGradient(_ gradient: Gradient, center: UnitPoint, angle: Angle = .zero) -> AngularGradient {
|
|
306
|
+
AngularGradient(gradient: gradient, center: center, angle: angle)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
public static func conicGradient(colors: [Color], center: UnitPoint, angle: Angle = .zero) -> AngularGradient {
|
|
310
|
+
AngularGradient(colors: colors, center: center, angle: angle)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public static func conicGradient(stops: [Gradient.Stop], center: UnitPoint, angle: Angle = .zero) -> AngularGradient {
|
|
314
|
+
AngularGradient(stops: stops, center: center, angle: angle)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
243
318
|
extension ShapeStyle where Self == LinearGradient {
|
|
244
319
|
public static func linearGradient(_ gradient: Gradient, startPoint: UnitPoint, endPoint: UnitPoint) -> LinearGradient {
|
|
245
320
|
LinearGradient(gradient: gradient, startPoint: startPoint, endPoint: endPoint)
|
|
@@ -292,6 +367,12 @@ extension RadialGradient: _HostView {
|
|
|
292
367
|
func _props(in environment: EnvironmentValues) -> _JSON { _gradientViewProps(self) }
|
|
293
368
|
}
|
|
294
369
|
|
|
370
|
+
extension AngularGradient: _PrimitiveView {}
|
|
371
|
+
extension AngularGradient: _HostView {
|
|
372
|
+
var _kind: String { "shape" }
|
|
373
|
+
func _props(in environment: EnvironmentValues) -> _JSON { _gradientViewProps(self) }
|
|
374
|
+
}
|
|
375
|
+
|
|
295
376
|
extension AnyGradient: _PrimitiveView {}
|
|
296
377
|
extension AnyGradient: _HostView {
|
|
297
378
|
var _kind: String { "shape" }
|
|
@@ -37,7 +37,37 @@ protocol _GroupView: _TraitBearing {
|
|
|
37
37
|
|
|
38
38
|
extension _GroupView {
|
|
39
39
|
var _keys: [AnyHashable]? { nil }
|
|
40
|
-
var _spreads: Bool {
|
|
40
|
+
var _spreads: Bool {
|
|
41
|
+
if _keys != nil { return true }
|
|
42
|
+
let children = _children
|
|
43
|
+
if children.count != 1 { return true }
|
|
44
|
+
// A wrapper that adds nothing (`Group { a; b }`, `if c { a; b }`)
|
|
45
|
+
// spreads as the group it holds does.
|
|
46
|
+
guard self is any _TransparentGroup, let inner = children[0] as? any _GroupView else { return false }
|
|
47
|
+
return inner._spreads
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// A group that only holds what it wraps (`Group`, `AnyView`, an optional, a
|
|
52
|
+
/// conditional, a tuple, `EmptyView`): a modifier applied to it applies to the
|
|
53
|
+
/// views of the group it holds, as if written on that group (`_spreadTarget`).
|
|
54
|
+
protocol _TransparentGroup: _GroupView {}
|
|
55
|
+
|
|
56
|
+
/// Whether `view` contributes no view at all: `EmptyView`, an absent
|
|
57
|
+
/// optional, or a wrapper or tuple of nothing but those.
|
|
58
|
+
func _rendersNothing(_ view: any View) -> Bool {
|
|
59
|
+
guard let group = view as? any _TransparentGroup else { return false }
|
|
60
|
+
return group._children.allSatisfy(_rendersNothing)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// The group whose children a modifier applied to `group` styles one by one:
|
|
64
|
+
/// `group`, or the group a transparent wrapper of one child holds
|
|
65
|
+
/// (`Group { a; b }.font(…)` styles `a` and `b`).
|
|
66
|
+
func _spreadTarget(_ group: any _GroupView) -> any _GroupView {
|
|
67
|
+
guard group is any _TransparentGroup, group._keys == nil else { return group }
|
|
68
|
+
let children = group._children
|
|
69
|
+
guard children.count == 1, let inner = children[0] as? any _GroupView, inner._spreads else { return group }
|
|
70
|
+
return _spreadTarget(inner)
|
|
41
71
|
}
|
|
42
72
|
|
|
43
73
|
/// A view that becomes an element in the render tree (see docs/ops-protocol.md).
|
|
@@ -97,6 +127,7 @@ public struct EmptyView: Sendable {
|
|
|
97
127
|
public init() {}
|
|
98
128
|
}
|
|
99
129
|
extension EmptyView: _PrimitiveView {}
|
|
130
|
+
extension EmptyView: _TransparentGroup {}
|
|
100
131
|
extension EmptyView: _GroupView {
|
|
101
132
|
var _children: [any View] { [] }
|
|
102
133
|
}
|
|
@@ -111,6 +142,7 @@ public struct TupleView<T> {
|
|
|
111
142
|
}
|
|
112
143
|
}
|
|
113
144
|
extension TupleView: _PrimitiveView {}
|
|
145
|
+
extension TupleView: _TransparentGroup {}
|
|
114
146
|
extension TupleView: _GroupView {
|
|
115
147
|
var _children: [any View] { children }
|
|
116
148
|
}
|
|
@@ -123,6 +155,7 @@ public struct _ConditionalContent<TrueContent: View, FalseContent: View> {
|
|
|
123
155
|
let storage: Storage
|
|
124
156
|
}
|
|
125
157
|
extension _ConditionalContent: _PrimitiveView {}
|
|
158
|
+
extension _ConditionalContent: _TransparentGroup {}
|
|
126
159
|
extension _ConditionalContent: _GroupView {
|
|
127
160
|
var _children: [any View] {
|
|
128
161
|
switch storage {
|
|
@@ -138,6 +171,7 @@ extension Optional: View where Wrapped: View {
|
|
|
138
171
|
}
|
|
139
172
|
// Unconditional: the marker only lets the trait lookups run (ViewTraits.swift).
|
|
140
173
|
extension Optional: _TraitBearing {}
|
|
174
|
+
extension Optional: _TransparentGroup where Wrapped: View {}
|
|
141
175
|
extension Optional: _GroupView where Wrapped: View {
|
|
142
176
|
var _children: [any View] {
|
|
143
177
|
switch self {
|
|
@@ -155,6 +189,7 @@ public struct AnyView {
|
|
|
155
189
|
public init<V: View>(erasing view: V) { storage = view }
|
|
156
190
|
}
|
|
157
191
|
extension AnyView: _PrimitiveView {}
|
|
192
|
+
extension AnyView: _TransparentGroup {}
|
|
158
193
|
extension AnyView: _GroupView {
|
|
159
194
|
var _children: [any View] { [storage] }
|
|
160
195
|
}
|
|
@@ -165,6 +200,7 @@ public struct Group<Content: View> {
|
|
|
165
200
|
public init(@ViewBuilder content: () -> Content) { self.content = content() }
|
|
166
201
|
}
|
|
167
202
|
extension Group: _PrimitiveView {}
|
|
203
|
+
extension Group: _TransparentGroup {}
|
|
168
204
|
extension Group: _GroupView {
|
|
169
205
|
var _children: [any View] { [content] }
|
|
170
206
|
}
|
|
@@ -456,6 +456,28 @@ enum _LocalizedFormat {
|
|
|
456
456
|
return pieces
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
/// A formatted number's integer digits grouped with the locale's separator
|
|
460
|
+
/// and its decimal point the locale's, as Apple's localized formatting
|
|
461
|
+
/// (`String.localizedStringWithFormat`, which `Text` and
|
|
462
|
+
/// `String(localized:)` use) writes `%d`, `%u` and `%f`: `"\(1024)"` is
|
|
463
|
+
/// "1,024" in English (and a year "2,026", as on iOS).
|
|
464
|
+
static func localizedDigits(_ text: String) -> String {
|
|
465
|
+
let locale = Locale.current
|
|
466
|
+
let grouping = locale.groupingSeparator ?? ","
|
|
467
|
+
let decimal = locale.decimalSeparator ?? "."
|
|
468
|
+
guard let start = text.firstIndex(where: \.isNumber) else { return text }
|
|
469
|
+
let end = text[start...].firstIndex(where: { !$0.isNumber }) ?? text.endIndex
|
|
470
|
+
let digits = text[start..<end]
|
|
471
|
+
var grouped = ""
|
|
472
|
+
for (offset, digit) in digits.enumerated() {
|
|
473
|
+
if offset > 0 && (digits.count - offset) % 3 == 0 { grouped += grouping }
|
|
474
|
+
grouped.append(digit)
|
|
475
|
+
}
|
|
476
|
+
var rest = String(text[end...])
|
|
477
|
+
if rest.hasPrefix(".") { rest = decimal + rest.dropFirst() }
|
|
478
|
+
return String(text[..<start]) + grouped + rest
|
|
479
|
+
}
|
|
480
|
+
|
|
459
481
|
/// One argument as its specifier formats it.
|
|
460
482
|
static func render(_ argument: _LocalizationArgument, spec: String, conversion: Character) -> String {
|
|
461
483
|
let flags = spec.filter { !"hlqLztj".contains($0) }
|
|
@@ -469,7 +491,7 @@ enum _LocalizedFormat {
|
|
|
469
491
|
case .double(let v): v.isFinite ? Int64(v) : 0
|
|
470
492
|
case .string(let v): Int64(v) ?? 0
|
|
471
493
|
}
|
|
472
|
-
return flags.isEmpty ? String(value) : String(format: "%\(flags)lld", value)
|
|
494
|
+
return flags.isEmpty ? localizedDigits(String(value)) : String(format: "%\(flags)lld", value)
|
|
473
495
|
case "u", "U", "x", "X", "o", "O":
|
|
474
496
|
let value: UInt64 = switch argument {
|
|
475
497
|
case .int(let v): UInt64(bitPattern: v)
|
|
@@ -479,6 +501,7 @@ enum _LocalizedFormat {
|
|
|
479
501
|
}
|
|
480
502
|
let lower = Character(conversion.lowercased())
|
|
481
503
|
let code = lower == "u" ? "u" : String(conversion)
|
|
504
|
+
if lower == "u" && flags.isEmpty { return localizedDigits(String(value)) }
|
|
482
505
|
return String(format: "%\(flags)ll\(code)", value)
|
|
483
506
|
case "f", "F", "e", "E", "g", "G", "a", "A":
|
|
484
507
|
let value: Double = switch argument {
|
|
@@ -487,7 +510,10 @@ enum _LocalizedFormat {
|
|
|
487
510
|
case .double(let v): v
|
|
488
511
|
case .string(let v): Double(v) ?? 0
|
|
489
512
|
}
|
|
490
|
-
|
|
513
|
+
let text = String(format: "%\(flags)\(conversion)", value)
|
|
514
|
+
// A precision alone (`%.1f`) keeps the locale's digits; a width or a flag pads as given.
|
|
515
|
+
let plain = flags.isEmpty || flags.first == "." && flags.dropFirst().allSatisfy(\.isNumber)
|
|
516
|
+
return plain && (conversion == "f" || conversion == "F") ? localizedDigits(text) : text
|
|
491
517
|
case "c", "C":
|
|
492
518
|
if case .int(let v) = argument, let scalar = Unicode.Scalar(UInt32(truncatingIfNeeded: v)) { return String(Character(scalar)) }
|
|
493
519
|
return argument.text
|
|
@@ -36,7 +36,7 @@ extension UIColor {
|
|
|
36
36
|
static let semanticColors: [String: UIColor] = {
|
|
37
37
|
var colors: [String: UIColor] = ["primary": .label, "secondary": .secondaryLabel, "clear": .clear,
|
|
38
38
|
// The app's accent color is the page's; system blue is its default.
|
|
39
|
-
"accent": UIColor(red: 0, green: 0.
|
|
39
|
+
"accent": UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)]
|
|
40
40
|
let named: [UIColor] = [
|
|
41
41
|
.systemRed, .systemOrange, .systemYellow, .systemGreen, .systemMint, .systemTeal, .systemCyan, .systemBlue,
|
|
42
42
|
.systemIndigo, .systemPurple, .systemPink, .systemBrown, .systemGray,
|
|
@@ -145,8 +145,8 @@ public final class UIColor: Hashable, @unchecked Sendable {
|
|
|
145
145
|
public static let secondaryLabel = named("secondaryLabel", 0.235, 0.235, 0.263, 0.6)
|
|
146
146
|
public static let separator = named("separator", 0.235, 0.235, 0.263, 0.29)
|
|
147
147
|
public static let systemGray = named("gray", 0.557, 0.557, 0.576)
|
|
148
|
-
public static let systemBlue = named("blue", 0, 0.
|
|
149
|
-
public static let systemRed = named("red", 1, 0.
|
|
148
|
+
public static let systemBlue = named("blue", 0, 0.533, 1)
|
|
149
|
+
public static let systemRed = named("red", 1, 0.22, 0.235)
|
|
150
150
|
public static let systemGreen = named("green", 0.204, 0.78, 0.349)
|
|
151
151
|
public static let tintColor = UIColor.systemBlue
|
|
152
152
|
|
|
@@ -167,15 +167,15 @@ public final class UIColor: Hashable, @unchecked Sendable {
|
|
|
167
167
|
public static let systemGray4 = named("systemGray4", 0.82, 0.82, 0.839)
|
|
168
168
|
public static let systemGray5 = named("systemGray5", 0.898, 0.898, 0.918)
|
|
169
169
|
public static let systemGray6 = named("systemGray6", 0.949, 0.949, 0.969)
|
|
170
|
-
public static let systemOrange = named("orange", 1, 0.
|
|
170
|
+
public static let systemOrange = named("orange", 1, 0.553, 0.157)
|
|
171
171
|
public static let systemYellow = named("yellow", 1, 0.8, 0)
|
|
172
172
|
public static let systemPink = named("pink", 1, 0.176, 0.333)
|
|
173
|
-
public static let systemPurple = named("purple", 0.
|
|
174
|
-
public static let systemTeal = named("teal", 0
|
|
175
|
-
public static let systemIndigo = named("indigo", 0.
|
|
176
|
-
public static let systemMint = named("mint", 0, 0.
|
|
177
|
-
public static let systemCyan = named("cyan", 0
|
|
178
|
-
public static let systemBrown = named("brown", 0.
|
|
173
|
+
public static let systemPurple = named("purple", 0.796, 0.188, 0.878)
|
|
174
|
+
public static let systemTeal = named("teal", 0, 0.765, 0.816)
|
|
175
|
+
public static let systemIndigo = named("indigo", 0.38, 0.333, 0.961)
|
|
176
|
+
public static let systemMint = named("mint", 0, 0.784, 0.702)
|
|
177
|
+
public static let systemCyan = named("cyan", 0, 0.753, 0.91)
|
|
178
|
+
public static let systemBrown = named("brown", 0.675, 0.498, 0.369)
|
|
179
179
|
public static let lightText = UIColor(white: 1, alpha: 0.6)
|
|
180
180
|
public static let darkText = UIColor.black
|
|
181
181
|
}
|
package/manifest.mjs
CHANGED
|
@@ -17,7 +17,9 @@ export function publishedManifest(rootManifest) {
|
|
|
17
17
|
const lines = rootManifest.split('\n');
|
|
18
18
|
const start = lines.findIndex((line) => line.startsWith('let package = Package('));
|
|
19
19
|
if (start < 0) throw new Error('manifest.mjs: no `let package = Package(` in the root manifest');
|
|
20
|
-
const imports = lines
|
|
20
|
+
const imports = lines
|
|
21
|
+
.slice(0, start)
|
|
22
|
+
.filter((line) => (line.startsWith('// swift-tools-version') || line.startsWith('import ')) && !line.trim().endsWith('// repository only'));
|
|
21
23
|
const body = [];
|
|
22
24
|
for (const line of lines.slice(start)) {
|
|
23
25
|
const trimmed = line.trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swiftbrowser/swift",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The SwiftBrowser Swift package (the SwiftUI shim, Charts, SwiftData and FoundationBridge) as an npm package, so @swiftbrowser/cli builds apps against the exact version it ships with.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|