@swiftbrowser/native 0.10.4 → 0.12.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.
@@ -15,10 +15,19 @@ struct NavigationStackElement: View {
15
15
  @Environment(\.nativeApp) private var app
16
16
 
17
17
  var body: some View {
18
- let screens = node.children.filter { $0.kind == "navscreen" }
18
+ Self.stack(for: node, app: app)
19
+ }
20
+
21
+ /// The stack itself, with its root screen and the pushed ones as its
22
+ /// path. A split view's detail column is one of these directly (not a
23
+ /// view around one): the shim wraps a detail that is not a
24
+ /// `NavigationStack` in a stack of its own, and a stack nested in a
25
+ /// stack is flattened, its path with it.
26
+ static func stack(for node: ElementNode?, app: NativeApp?) -> NavigationStack<[Int], AnyView> {
27
+ let screens = node?.children.filter { $0.kind == "navscreen" } ?? []
19
28
  let pushed = screens.dropFirst().map(\.id)
20
29
  let path = Binding<[Int]>(
21
- get: { node.children.filter { $0.kind == "navscreen" }.dropFirst().map(\.id) },
30
+ get: { node?.children.filter { $0.kind == "navscreen" }.dropFirst().map(\.id) ?? [] },
22
31
  set: { next in
23
32
  // Only pops come from here (pushes come from the app).
24
33
  for id in pushed where !next.contains(id) {
@@ -26,21 +35,69 @@ struct NavigationStackElement: View {
26
35
  }
27
36
  }
28
37
  )
29
- NavigationStack(path: path) {
30
- Group {
31
- if let root = screens.first {
32
- NavigationScreenElement(node: root)
38
+ return NavigationStack(path: path) {
39
+ AnyView(
40
+ Group {
41
+ if let root = screens.first {
42
+ NavigationScreenElement(node: root)
43
+ }
33
44
  }
34
- }
35
- .navigationDestination(for: Int.self) { id in
36
- if let screen = node.children.first(where: { $0.id == id }) {
37
- NavigationScreenElement(node: screen)
38
- } else {
39
- // Popped by the app already; SwiftUI is on its way out.
40
- Color.clear
45
+ .navigationDestination(for: Int.self) { id in
46
+ if let screen = node?.children.first(where: { $0.id == id }) {
47
+ NavigationScreenElement(node: screen)
48
+ } else {
49
+ // Popped by the app already; SwiftUI is on its way out.
50
+ Color.clear
51
+ }
52
+ }
53
+ )
54
+ }
55
+ }
56
+ }
57
+
58
+ /// `navsplit`: the sidebar's and the detail's stacks (its two `navstack`
59
+ /// children, in that order) as the columns of a split view. The app emits
60
+ /// it in a regular width only; in a compact width it emits one stack rooted
61
+ /// at the sidebar, as iOS shows. `sidebarVisible` is the column visibility
62
+ /// (`detailOnly` hides the sidebar); the sidebar toggle sends `tap` with the
63
+ /// split's id, and the app flips the visibility, as the protocol says.
64
+ struct NavigationSplitElement: View {
65
+ let node: ElementNode
66
+ @Environment(\.nativeApp) private var app
67
+
68
+ var body: some View {
69
+ let columns = node.children.filter { $0.kind == "navstack" }
70
+ let sidebar = columns.first
71
+ let detail = columns.count > 1 ? columns[1] : nil
72
+ let visibility = Binding<NavigationSplitViewVisibility>(
73
+ get: { NavSplitProps(props: node.props).sidebarVisible ? .all : .detailOnly },
74
+ set: { next in
75
+ if (next == .detailOnly) == NavSplitProps(props: node.props).sidebarVisible {
76
+ app?.dispatch(.tap(node.id))
41
77
  }
42
78
  }
79
+ )
80
+ NavigationSplitView(columnVisibility: visibility) {
81
+ sidebarColumn(sidebar)
82
+ } detail: {
83
+ NavigationStackElement.stack(for: detail, app: app)
84
+ }
85
+ }
86
+
87
+ /// The sidebar column: natively its stack (the column pushes within
88
+ /// itself); against the shim its root screen alone, since the shim gives
89
+ /// the sidebar a stack of its own and would flatten this one into it.
90
+ @ViewBuilder
91
+ private func sidebarColumn(_ sidebar: ElementNode?) -> some View {
92
+ #if canImport(FoundationBridge)
93
+ if let root = sidebar?.children.first(where: { $0.kind == "navscreen" }) {
94
+ NavigationScreenElement(node: root)
43
95
  }
96
+ #else
97
+ if let sidebar {
98
+ NavigationStackElement(node: sidebar)
99
+ }
100
+ #endif
44
101
  }
45
102
  }
46
103
 
@@ -98,7 +155,14 @@ struct NavigationScreenElement: View {
98
155
  case "principal": return .principal
99
156
  case "secondary": return .secondaryAction
100
157
  case "bottom": return bottomPlacement
101
- default: return trailingPlacement // `trailing`, and `largeTitle` beside it
158
+ case "largeTitle":
159
+ // Beside the large title: the shim's own placement; SwiftUI has none, so the bar's trailing edge.
160
+ #if canImport(FoundationBridge)
161
+ return .largeTitle
162
+ #else
163
+ return trailingPlacement
164
+ #endif
165
+ default: return trailingPlacement
102
166
  }
103
167
  }
104
168
  }
@@ -11,6 +11,7 @@ struct VStackElement: View {
11
11
  VStack(alignment: HorizontalAlignment(name: props.alignment), spacing: props.spacing.map { CGFloat($0) }) {
12
12
  ChildrenView(node: node)
13
13
  }
14
+ .scrollTargetLayout(isEnabled: props.scrollTarget)
14
15
  }
15
16
  }
16
17
 
@@ -32,6 +33,7 @@ struct HStackElement: View {
32
33
  HStack(alignment: VerticalAlignment(name: props.alignment), spacing: props.spacing.map { CGFloat($0) }) {
33
34
  ChildrenView(node: node)
34
35
  }
36
+ .scrollTargetLayout(isEnabled: props.scrollTarget)
35
37
  }
36
38
  }
37
39
  }
@@ -106,21 +108,71 @@ struct GeometryElement: View {
106
108
  }
107
109
  }
108
110
 
111
+ /// `scrollview`: its axes and indicators; `refreshable` sends `refresh`;
112
+ /// `snap` is its scroll target behavior (the stack inside marked
113
+ /// `scrollTarget` is the layout whose children snap); `reportsPosition`
114
+ /// is `.scrollPosition(id:)`, which sends `scrollposition` with the marked
115
+ /// element at the anchor once the view settles. A `ScrollViewReader`'s
116
+ /// `proxy.scrollTo(x)` arrives as a `scrollTo` hint, with a new serial each
117
+ /// time, on the marked element it names (`scrollID`, `.id(x)`): the view
118
+ /// finds the latest in its content and scrolls to it.
109
119
  struct ScrollViewElement: View {
110
120
  let node: ElementNode
111
121
  @Environment(\.nativeApp) private var app
122
+ @State private var position: Int?
123
+ @State private var served: Double = -1
112
124
 
113
125
  var body: some View {
114
126
  let props = ScrollViewProps(props: node.props)
115
127
  let axes: Axis.Set = props.axes == "horizontal" ? .horizontal : props.axes == "both" ? [.horizontal, .vertical] : .vertical
116
- let scroll = ScrollView(axes, showsIndicators: props.showsIndicators) {
117
- ChildrenView(node: node)
128
+ let request = Self.scrollRequest(in: node)
129
+ ScrollViewReader { proxy in
130
+ let scroll = ScrollView(axes, showsIndicators: props.showsIndicators) {
131
+ ChildrenView(node: node)
132
+ }
133
+ let refreshed: AnyView = props.refreshable ? AnyView(scroll.refreshable { app?.dispatch(.refresh(node.id)) }) : AnyView(scroll)
134
+ let snapped: AnyView = props.snap == "paging" ? AnyView(refreshed.scrollTargetBehavior(.paging))
135
+ : props.snap == "viewAligned" ? AnyView(refreshed.scrollTargetBehavior(.viewAligned)) : refreshed
136
+ let positioned: AnyView = props.reportsPosition
137
+ ? AnyView(snapped.scrollPosition(id: Binding(get: { position }, set: { next in
138
+ guard next != position else { return }
139
+ position = next
140
+ if let next { app?.dispatch(.scrollPosition(next)) }
141
+ }), anchor: props.positionAnchor.map(UnitPoint.init(spec:))))
142
+ : snapped
143
+ positioned
144
+ .onAppear { perform(request, with: proxy) }
145
+ .onChange(of: request?.serial) { _, _ in perform(request, with: proxy) }
118
146
  }
119
- if props.refreshable {
120
- scroll.refreshable { app?.dispatch(.refresh(node.id)) }
121
- } else {
122
- scroll
147
+ }
148
+
149
+ private func perform(_ request: ScrollRequest?, with proxy: ScrollViewProxy) {
150
+ guard let request, request.serial > served else { return }
151
+ served = request.serial
152
+ position = request.id
153
+ proxy.scrollTo(request.id, anchor: request.anchor)
154
+ }
155
+
156
+ struct ScrollRequest: Equatable {
157
+ var serial: Double
158
+ var id: Int
159
+ var anchor: UnitPoint?
160
+ }
161
+
162
+ /// The newest `scrollTo` among the content's marked elements (not those of a scroll view nested inside).
163
+ static func scrollRequest(in node: ElementNode) -> ScrollRequest? {
164
+ var best: ScrollRequest?
165
+ func visit(_ element: ElementNode) {
166
+ for child in element.children {
167
+ if child.kind == "scrollview" || child.kind == "list" { continue }
168
+ if child.kind == "styled", let request = child.style.scrollTo, best == nil || request.serial > best!.serial {
169
+ best = ScrollRequest(serial: request.serial, id: child.id, anchor: request.anchor.map(UnitPoint.init(spec:)))
170
+ }
171
+ visit(child)
172
+ }
123
173
  }
174
+ visit(node)
175
+ return best
124
176
  }
125
177
  }
126
178
 
@@ -11,7 +11,7 @@ import SwiftUI
11
11
  /// Type-erased step by step: a chain of sixty optional modifiers would
12
12
  /// otherwise be one type the compiler has to name.
13
13
  extension View {
14
- func styled(_ style: Style) -> AnyView {
14
+ func styled(_ style: Style, id: Int? = nil) -> AnyView {
15
15
  var view = AnyView(self)
16
16
 
17
17
  // Text attributes, which flow down the environment. A font with only a
@@ -71,16 +71,31 @@ extension View {
71
71
  }
72
72
  }
73
73
  if let frame = style.frame {
74
- if frame.isFlexible {
74
+ // Per axis, a fixed length wins over a min/max, as the web renderer
75
+ // lays it out: `{ maxWidth: "infinity", height: 1 }` is
76
+ // `.frame(height: 1).frame(maxWidth: .infinity)`, a rule across
77
+ // the width.
78
+ let alignment = Alignment(name: frame.alignment)
79
+ if frame.width != nil || frame.height != nil {
80
+ view = AnyView(view.frame(width: frame.width.map { CGFloat($0) }, height: frame.height.map { CGFloat($0) }, alignment: alignment))
81
+ }
82
+ let flexibleWidth = frame.width == nil && (frame.minWidth != nil || frame.maxWidth != nil)
83
+ let flexibleHeight = frame.height == nil && (frame.minHeight != nil || frame.maxHeight != nil)
84
+ if flexibleWidth || flexibleHeight {
75
85
  view = AnyView(view.frame(
76
- minWidth: CGFloat(dimension: frame.minWidth), maxWidth: CGFloat(dimension: frame.maxWidth),
77
- minHeight: CGFloat(dimension: frame.minHeight), maxHeight: CGFloat(dimension: frame.maxHeight),
78
- alignment: Alignment(name: frame.alignment)
86
+ minWidth: flexibleWidth ? CGFloat(dimension: frame.minWidth) : nil, maxWidth: flexibleWidth ? CGFloat(dimension: frame.maxWidth) : nil,
87
+ minHeight: flexibleHeight ? CGFloat(dimension: frame.minHeight) : nil, maxHeight: flexibleHeight ? CGFloat(dimension: frame.maxHeight) : nil,
88
+ alignment: alignment
79
89
  ))
80
- } else {
81
- view = AnyView(view.frame(width: frame.width.map { CGFloat($0) }, height: frame.height.map { CGFloat($0) }, alignment: Alignment(name: frame.alignment)))
82
90
  }
83
91
  }
92
+ if let relative = style.containerRelativeFrame {
93
+ var axes: Axis.Set = []
94
+ if relative.horizontal { axes.insert(.horizontal) }
95
+ if relative.vertical { axes.insert(.vertical) }
96
+ let a = relative.a, b = relative.b
97
+ view = AnyView(view.containerRelativeFrame(axes, alignment: Alignment(name: relative.alignment)) { length, _ in a * length + b })
98
+ }
84
99
  if let ratio = style.aspectRatio {
85
100
  let mode: ContentMode = style.aspectRatioContentMode == "fill" ? .fill : .fit
86
101
  view = AnyView(view.aspectRatio(ratio.map { CGFloat($0) }, contentMode: mode))
@@ -127,6 +142,9 @@ extension View {
127
142
  if style.hitTesting == false { view = AnyView(view.allowsHitTesting(false)) }
128
143
  if style.hidden { view = AnyView(view.hidden()) }
129
144
  if let help = style.help { view = AnyView(view.help(help)) }
145
+ // A `ScrollViewReader`'s `.id(x)`: the element's own id names it to a `scrollTo` and a scroll position.
146
+ if style.scrollID, let id { view = AnyView(view.id(id)) }
147
+ if let width = style.raw.double("inspectorColumnWidth") { view = AnyView(view.inspectorColumnWidth(width)) }
130
148
 
131
149
  // Lists.
132
150
  if let badge = style.badge { view = AnyView(view.badge(Text(badge))) }
@@ -54,9 +54,13 @@ public struct StackProps: Equatable, Sendable {
54
54
  /// A `VStack`'s horizontal alignment, an `HStack`'s vertical one, a `ZStack`'s frame alignment.
55
55
  public var alignment: String
56
56
 
57
+ /// `.scrollTargetLayout()`: the stack's children are the snap points of the scroll view around it.
58
+ public var scrollTarget: Bool
59
+
57
60
  public init(props: JSONObject) {
58
61
  spacing = props.double("spacing")
59
62
  alignment = props.string("alignment") ?? "center"
63
+ scrollTarget = props.bool("scrollTarget") ?? false
60
64
  }
61
65
  }
62
66
 
@@ -111,13 +115,27 @@ public struct ScrollViewProps: Equatable, Sendable {
111
115
  public var axes: String
112
116
  public var showsIndicators: Bool
113
117
  public var refreshable: Bool
118
+ /// `.scrollTargetBehavior`: `viewAligned` or `paging`.
119
+ public var snap: String?
120
+ /// `.scrollPosition(id:)`: the view reports the marked element at its anchor as it settles.
121
+ public var reportsPosition: Bool
122
+ public var positionAnchor: UnitPointSpec?
114
123
  public init(props: JSONObject) {
115
124
  axes = props.string("axes") ?? "vertical"
116
125
  showsIndicators = props.bool("showsIndicators") ?? true
117
126
  refreshable = props.bool("refreshable") ?? false
127
+ snap = props.string("snap")
128
+ reportsPosition = props.bool("reportsPosition") ?? false
129
+ positionAnchor = UnitPointSpec(json: props.value("positionAnchor"))
118
130
  }
119
131
  }
120
132
 
133
+ /// `inspector`: the modified view's elements, then an `inspectorpanel`.
134
+ public struct InspectorProps: Equatable, Sendable {
135
+ public var presented: Bool
136
+ public init(props: JSONObject) { presented = props.bool("presented") ?? false }
137
+ }
138
+
121
139
  public struct ListProps: Equatable, Sendable {
122
140
  /// `insetGrouped` (the default), `grouped`, `plain`, `sidebar`.
123
141
  public var style: String
@@ -183,6 +201,12 @@ public struct NavScreenProps: Equatable, Sendable {
183
201
  }
184
202
  }
185
203
 
204
+ public struct NavSplitProps: Equatable, Sendable {
205
+ /// Whether the sidebar column shows (`false` hides it).
206
+ public var sidebarVisible: Bool
207
+ public init(props: JSONObject) { sidebarVisible = props.bool("sidebarVisible") ?? true }
208
+ }
209
+
186
210
  public struct NavLinkProps: Equatable, Sendable {
187
211
  public var selected: Bool
188
212
  public var plain: Bool
@@ -467,3 +491,155 @@ public struct SizeReportProps: Equatable, Sendable {
467
491
  reader = props.int("reader")
468
492
  }
469
493
  }
494
+
495
+ // MARK: - Charts
496
+
497
+ /// `chart`: the marks in unit coordinates of the plot (x 0…1 left to right,
498
+ /// y 0…1 top to bottom), the axes with their grid lines, ticks and labels
499
+ /// already placed and formatted, and the legend (docs/ops-protocol.md,
500
+ /// "Phase 7"). A renderer measures the labels, lays the plot out and draws.
501
+ public struct ChartProps: Equatable, Sendable {
502
+ public var marks: [ChartMarkSpec]
503
+ public var xAxis: ChartAxisSpec?
504
+ public var yAxis: ChartAxisSpec?
505
+ public var legend: ChartLegendSpec?
506
+ public init(props: JSONObject) {
507
+ marks = (props.array("marks") ?? []).compactMap(ChartMarkSpec.init(json:))
508
+ xAxis = ChartAxisSpec(json: props.value("xAxis"))
509
+ yAxis = ChartAxisSpec(json: props.value("yAxis"))
510
+ legend = ChartLegendSpec(json: props.value("legend"))
511
+ }
512
+ }
513
+
514
+ /// A point of a line (`y`) or an area (`y0 ≤ y1`) mark.
515
+ public struct ChartPointSpec: Equatable, Sendable {
516
+ public var x: Double
517
+ public var y: Double
518
+ public var y0: Double
519
+ public var y1: Double
520
+ init?(json: JSONValue) {
521
+ guard let x = json["x"].double else { return nil }
522
+ self.x = x
523
+ y = json["y"].double ?? json["y0"].double ?? 0
524
+ y0 = json["y0"].double ?? y
525
+ y1 = json["y1"].double ?? y
526
+ }
527
+ }
528
+
529
+ /// One mark: `rect` (bars), `line`, `area`, `point` or `rule`.
530
+ public struct ChartMarkSpec: Equatable, Sendable {
531
+ public var kind: String
532
+ public var style: ShapeStyleSpec?
533
+ public var opacity: Double
534
+ /// `rect` and `rule`: the unit rect (a rule's two ends); `point`: `x0`/`y0`.
535
+ public var x0: Double
536
+ public var x1: Double
537
+ public var y0: Double
538
+ public var y1: Double
539
+ public var cornerRadius: Double
540
+ /// A rect this wide in points, centered on the unit rect, instead of the unit rect's width.
541
+ public var fixedWidth: Double?
542
+ /// `line` and `area`.
543
+ public var points: [ChartPointSpec]
544
+ public var lineWidth: Double
545
+ public var interpolation: String
546
+ /// `circle`, `square`, `triangle`, `diamond`, `cross`, `plus`, `asterisk`; nil for none.
547
+ public var symbol: String?
548
+ public var symbolSize: Double
549
+ public init?(json: JSONValue) {
550
+ guard let kind = json["kind"].string else { return nil }
551
+ self.kind = kind
552
+ style = ShapeStyleSpec(json: json["style"])
553
+ opacity = json["opacity"].double ?? 1
554
+ x0 = json["x0"].double ?? json["x"].double ?? 0
555
+ x1 = json["x1"].double ?? x0
556
+ y0 = json["y0"].double ?? json["y"].double ?? 0
557
+ y1 = json["y1"].double ?? y0
558
+ cornerRadius = json["cornerRadius"].double ?? 0
559
+ fixedWidth = json["fixedWidth"].double
560
+ points = (json["points"].array ?? []).compactMap(ChartPointSpec.init(json:))
561
+ lineWidth = json["lineWidth"].double ?? (kind == "rule" ? 1 : 2)
562
+ interpolation = json["interpolation"].string ?? "linear"
563
+ symbol = json["symbol"].string ?? (kind == "point" ? "circle" : nil)
564
+ symbolSize = json["symbolSize"].double ?? 8
565
+ }
566
+ }
567
+
568
+ public struct ChartAxisLabelSpec: Equatable, Sendable {
569
+ /// Unit position along the axis (y: 0 is the top).
570
+ public var at: Double
571
+ public var text: String
572
+ /// `leading`: an x label that starts at its tick instead of centering on it.
573
+ public var anchor: String?
574
+ init?(json: JSONValue) {
575
+ guard let at = json["at"].double else { return nil }
576
+ self.at = at
577
+ text = json["text"].string ?? ""
578
+ anchor = json["anchor"].string
579
+ }
580
+ }
581
+
582
+ public struct ChartAxisSpec: Equatable, Sendable {
583
+ /// x: `bottom` or `top`; y: `trailing` or `leading`.
584
+ public var position: String
585
+ public var gridLines: [Double]
586
+ public var dashed: Bool
587
+ public var ticks: [Double]
588
+ public var labels: [ChartAxisLabelSpec]
589
+ public init?(json: JSONValue) {
590
+ guard let fields = json.object else { return nil }
591
+ position = fields.string("position") ?? "bottom"
592
+ gridLines = (fields.array("gridLines") ?? []).compactMap(\.double)
593
+ dashed = fields.bool("dashed") ?? false
594
+ ticks = (fields.array("ticks") ?? []).compactMap(\.double)
595
+ labels = (fields.array("labels") ?? []).compactMap(ChartAxisLabelSpec.init(json:))
596
+ }
597
+ }
598
+
599
+ public struct ChartLegendItemSpec: Equatable, Sendable {
600
+ public var label: String
601
+ public var style: ShapeStyleSpec?
602
+ /// Nil: a short line instead of a symbol.
603
+ public var symbol: String?
604
+ init(json: JSONValue) {
605
+ label = json["label"].string ?? ""
606
+ style = ShapeStyleSpec(json: json["style"])
607
+ symbol = json["symbol"].string
608
+ }
609
+ }
610
+
611
+ public struct ChartLegendSpec: Equatable, Sendable {
612
+ /// `top`, `bottom`, `leading` or `trailing`.
613
+ public var position: String
614
+ public var items: [ChartLegendItemSpec]
615
+ public init?(json: JSONValue) {
616
+ guard let fields = json.object else { return nil }
617
+ position = fields.string("position") ?? "bottom"
618
+ items = (fields.array("items") ?? []).map(ChartLegendItemSpec.init(json:))
619
+ }
620
+ }
621
+
622
+ /// `chartitem`: an annotation against its mark's unit rect, or an axis label
623
+ /// view at its tick (`x0 == x1` for an x label, `y0 == y1` for a y label).
624
+ public struct ChartItemProps: Equatable, Sendable {
625
+ /// `annotation`, `xLabel` or `yLabel`.
626
+ public var role: String
627
+ public var x0: Double
628
+ public var x1: Double
629
+ public var y0: Double
630
+ public var y1: Double
631
+ /// `top`, `bottom`, `leading`, `trailing` or `overlay`.
632
+ public var position: String
633
+ public var alignment: String
634
+ public var spacing: Double
635
+ public init(props: JSONObject) {
636
+ role = props.string("role") ?? "annotation"
637
+ x0 = props.double("x0") ?? 0
638
+ x1 = props.double("x1") ?? x0
639
+ y0 = props.double("y0") ?? 0
640
+ y1 = props.double("y1") ?? y0
641
+ position = props.string("position") ?? "overlay"
642
+ alignment = props.string("alignment") ?? "center"
643
+ spacing = props.double("spacing") ?? 0
644
+ }
645
+ }
@@ -88,6 +88,8 @@ public struct RendererEvent: Equatable, Sendable {
88
88
  public static func longPress(_ id: Int) -> RendererEvent { with("longpress", id: id) }
89
89
  /// A `list` or `scrollview` with `refreshable` was pulled.
90
90
  public static func refresh(_ id: Int) -> RendererEvent { with("refresh", id: id) }
91
+ /// `.scrollPosition(id:)`: the marked element at the scroll view's anchor once it settled.
92
+ public static func scrollPosition(_ id: Int) -> RendererEvent { with("scrollposition", id: id) }
91
93
  /// A `menu` opened or closed (lazy menus build their items on `open`).
92
94
  public static func menu(_ id: Int, open: Bool) -> RendererEvent { with("menu", id: id, ["open": .bool(open)]) }
93
95
  /// The host's answer to a `native` element's own event (docs/native.md, the escape hatch).
@@ -67,6 +67,12 @@ public struct Style: Equatable, Sendable {
67
67
  public var help: String?
68
68
  public var matchedGeometry: String?
69
69
  public var contentTransition: String?
70
+ /// `.containerRelativeFrame`: the length along each named axis is `a × container + b`.
71
+ public var containerRelativeFrame: ContainerRelativeFrameSpec?
72
+ /// `.id(x)` inside a `ScrollViewReader`: the element a `scrollTo` or a scroll position names.
73
+ public var scrollID: Bool
74
+ /// `proxy.scrollTo(x)`: the request (its serial is new each time) on the element it names.
75
+ public var scrollTo: ScrollToSpec?
70
76
 
71
77
  public init(json: JSONValue) {
72
78
  let fields = json.object ?? [:]
@@ -137,6 +143,9 @@ public struct Style: Equatable, Sendable {
137
143
  help = fields.string("help")
138
144
  matchedGeometry = fields.string("matchedGeometry")
139
145
  contentTransition = fields.object("contentTransition")?.string("kind")
146
+ containerRelativeFrame = ContainerRelativeFrameSpec(json: fields.value("containerRelativeFrame"))
147
+ scrollID = fields.bool("scrollID") ?? false
148
+ scrollTo = ScrollToSpec(json: fields.value("scrollTo"))
140
149
  }
141
150
 
142
151
  public init() {
@@ -146,3 +155,35 @@ public struct Style: Equatable, Sendable {
146
155
  /// The style's field names, for the unsupported-field diagnostics and tests.
147
156
  public var fieldNames: [String] { raw.keys.sorted() }
148
157
  }
158
+
159
+ /// `.containerRelativeFrame(axes)` as the style hint carries it: `length = a × container + b`.
160
+ public struct ContainerRelativeFrameSpec: Equatable, Sendable {
161
+ public var horizontal: Bool
162
+ public var vertical: Bool
163
+ public var a: Double
164
+ public var b: Double
165
+ public var alignment: String
166
+ public init?(json: JSONValue) {
167
+ guard let fields = json.object else { return nil }
168
+ horizontal = fields.bool("horizontal") ?? false
169
+ vertical = fields.bool("vertical") ?? false
170
+ a = fields.double("a") ?? 1
171
+ b = fields.double("b") ?? 0
172
+ alignment = fields.string("alignment") ?? "center"
173
+ }
174
+ }
175
+
176
+ /// A `ScrollViewReader` proxy's `scrollTo` on the element it names.
177
+ public struct ScrollToSpec: Equatable, Sendable {
178
+ /// Increases with every request, across all readers.
179
+ public var serial: Double
180
+ public var anchor: UnitPointSpec?
181
+ /// `horizontal`, `vertical` or `both` (the default).
182
+ public var axes: String
183
+ public init?(json: JSONValue) {
184
+ guard let fields = json.object, let serial = fields.double("serial") else { return nil }
185
+ self.serial = serial
186
+ anchor = UnitPointSpec(json: fields.value("anchor"))
187
+ axes = fields.string("axes") ?? "both"
188
+ }
189
+ }
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: package/wasm/SwiftBrowserNative.wasm
3
+ size: 49299172 bytes
4
+ sha256: 4b0ae4bc428d21234784a4c806f021482aa294045f94c5fc80eeea0ad40b5405