@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 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), so in its
106
- // Package.swift the macro also links os, whichever of the two
107
- // builds first (a line manifest.mjs leaves out of the published one).
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 separators.
30
- public static let listRowSeparatorLeading = HorizontalAlignment(name: "leading")
31
- public static let listRowSeparatorTrailing = HorizontalAlignment(name: "trailing")
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.039, 0.518, 1, a) : resolved(0, 0.478, 1, a)
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, 59, 48), rgb(255, 69, 58)),
117
- "orange": (rgb(255, 149, 0), rgb(255, 159, 10)),
118
- "yellow": (rgb(255, 204, 0), rgb(255, 214, 10)),
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, 199, 190), rgb(99, 230, 226)),
121
- "teal": (rgb(48, 176, 199), rgb(64, 200, 224)),
122
- "cyan": (rgb(50, 173, 230), rgb(100, 210, 255)),
123
- "blue": (rgb(0, 122, 255), rgb(10, 132, 255)),
124
- "indigo": (rgb(88, 86, 214), rgb(94, 92, 230)),
125
- "purple": (rgb(175, 82, 222), rgb(191, 90, 242)),
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(162, 132, 94), rgb(172, 142, 104)),
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
- public func controlSize(_ size: ControlSize) -> some View { self }
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`: accepted for source compatibility; controls keep one size.
236
- public enum ControlSize: Hashable, CaseIterable, Sendable { case mini, small, regular, large, extraLarge }
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
- /// `linearCapacity` gauge with its label; an indeterminate one (no value)
168
- /// has no spinner in the protocol and shows its label, or an hourglass
169
- /// symbol when it has none.
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(.linearCapacity)
190
+ .gaugeStyle(_ProgressGaugeStyle())
184
191
  } else if let label {
185
- label.foregroundStyle(.secondary)
192
+ VStack(spacing: 8) {
193
+ activityIndicator
194
+ label.foregroundStyle(.secondary)
195
+ }
186
196
  } else {
187
- Image(systemName: "hourglass").foregroundStyle(.secondary)
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
- return group._children.map { _StyledView(content: AnyView($0), style: style) }
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 a grouped list.
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("grouped")]) }
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
- .object([
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, in the footnote font
9
- // and the secondary color; outside a list (a `Section` in a `LazyVStack`) they
10
- // stack with the rows. As for a string header, the texts in a header view
11
- // are upper-cased (the environment's `textCase`, which `.textCase(nil)`
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": {...}}`, `{"colorGradient": color}`, `{"material": name}`.
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 { _keys != nil || _children.count != 1 }
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
- return String(format: "%\(flags)\(conversion)", value)
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.478, blue: 1, alpha: 1)]
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.478, 1)
149
- public static let systemRed = named("red", 1, 0.231, 0.188)
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.584, 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.686, 0.322, 0.871)
174
- public static let systemTeal = named("teal", 0.188, 0.69, 0.78)
175
- public static let systemIndigo = named("indigo", 0.345, 0.337, 0.839)
176
- public static let systemMint = named("mint", 0, 0.78, 0.745)
177
- public static let systemCyan = named("cyan", 0.196, 0.678, 0.902)
178
- public static let systemBrown = named("brown", 0.635, 0.518, 0.369)
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.slice(0, start).filter((line) => line.startsWith('// swift-tools-version') || line.startsWith('import '));
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.2.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",