diff --git a/Package.swift b/Package.swift index 6d2afd1f8..b5b9c1cbd 100644 --- a/Package.swift +++ b/Package.swift @@ -257,6 +257,8 @@ let package = Package( name: "BroadwayUI", dependencies: [ .target(name: "BroadwayCore"), + .target(name: "ViewDocumentCore"), + .target(name: "ViewDocumentUI"), ], path: "Shared/Broadway/BroadwayUI/Sources", ), diff --git a/Shared/Broadway/AGENTS.md b/Shared/Broadway/AGENTS.md index 61c3827f5..fe2168ad1 100644 --- a/Shared/Broadway/AGENTS.md +++ b/Shared/Broadway/AGENTS.md @@ -7,7 +7,7 @@ Read the root [`AGENTS.md`](../../AGENTS.md) first. That file owns build, format ## Modules & dependencies - **BroadwayCore** — foundation types (Foundation and UIKit). No sibling deps. -- **BroadwayUI** — components (SwiftUI and UIKit). Depends on BroadwayCore. +- **BroadwayUI** — components (SwiftUI and UIKit). Depends on BroadwayCore, ViewDocumentCore, and ViewDocumentUI. - **BroadwayCatalog** — showcase app. Depends on BroadwayUI and SFSafeSymbols. UIKit hosting helpers for Broadway's hosted test bundles live in the shared [`TestHostSupport`](../TestHostSupport) module, not in a Broadway module. @@ -21,3 +21,5 @@ Libraries live in [`Package.swift`](../../Package.swift). The app and hosted tes ## Testing Run `./test BroadwayCoreTests`, `./test BroadwayUITests`, or `./test BroadwayCatalogTests`. The Catalog bundle is currently hosted by the **BroadwayCatalog** app itself. That deviates from the shared-host convention. Track it in [`TODOs.md`](TODOs.md). Use 1:1 test files per the root rules. + +- Document decorations use validated, prepared content. Resolve scales and accessibility from the Broadway slice; inject host activity and snapshot motion policy. Keep networking and product attachment names in consumers. diff --git a/Shared/Broadway/BroadwayCore/README.md b/Shared/Broadway/BroadwayCore/README.md index 136904a3e..9863eeff4 100644 --- a/Shared/Broadway/BroadwayCore/README.md +++ b/Shared/Broadway/BroadwayCore/README.md @@ -37,3 +37,11 @@ The cache is `@EquatableIgnored`, so two contexts compare equal on their inputs, Local SPM library declared in the root [`Package.swift`](../../../Package.swift). Depend on it with `.package(product: "BroadwayCore")`. Run tests with `./test BroadwayCoreTests`. + +## Portable theme values + +`BModeVarying` supports Codable when its value does. `BThemeValue` encodes either a constant or paired light/dark values. Call `resolve(mode:)` with the stylesheet slice's mode. Product modules own theme schemas and fallback rules. + +`BTraits()` supplies deterministic defaults without live observers. Use it for document validation and off-tree defaults; use `BTraits.system` at a live root. + +`CopyOnWrite` encodes its wrapped value directly. Optional wrapped properties retain missing-key and null decoding, and omit nil values when encoding. diff --git a/Shared/Broadway/BroadwayCore/Sources/BModeVarying.swift b/Shared/Broadway/BroadwayCore/Sources/BModeVarying.swift index 0672d736f..148e9c4cc 100644 --- a/Shared/Broadway/BroadwayCore/Sources/BModeVarying.swift +++ b/Shared/Broadway/BroadwayCore/Sources/BModeVarying.swift @@ -19,3 +19,5 @@ public struct BModeVarying { extension BModeVarying: Equatable where Value: Equatable {} extension BModeVarying: Hashable where Value: Hashable {} extension BModeVarying: Sendable where Value: Sendable {} + +extension BModeVarying: Codable where Value: Codable {} diff --git a/Shared/Broadway/BroadwayCore/Sources/BThemeValue.swift b/Shared/Broadway/BroadwayCore/Sources/BThemeValue.swift new file mode 100644 index 000000000..607b02531 --- /dev/null +++ b/Shared/Broadway/BroadwayCore/Sources/BThemeValue.swift @@ -0,0 +1,12 @@ +/// A portable authored value, either constant or selected by the explicit slice mode. +public enum BThemeValue: Codable, Hashable, Sendable { + case constant(Value) + case modes(BModeVarying) + + public func resolve(mode: BMode) -> Value { + switch self { + case let .constant(value): value + case let .modes(values): values[mode] + } + } +} diff --git a/Shared/Broadway/BroadwayCore/Sources/CopyOnWrite.swift b/Shared/Broadway/BroadwayCore/Sources/CopyOnWrite.swift index fb2729578..2a2714770 100644 --- a/Shared/Broadway/BroadwayCore/Sources/CopyOnWrite.swift +++ b/Shared/Broadway/BroadwayCore/Sources/CopyOnWrite.swift @@ -85,6 +85,38 @@ extension CopyOnWrite: Equatable where Value: Equatable {} extension CopyOnWrite: Hashable where Value: Hashable {} extension CopyOnWrite: @unchecked Sendable where Value: Sendable {} +extension CopyOnWrite: Decodable where Value: Decodable { + public init(from decoder: any Decoder) throws { + try self.init(wrappedValue: decoder.singleValueContainer().decode(Value.self)) + } +} + +extension CopyOnWrite: Encodable where Value: Encodable { + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(wrappedValue) + } +} + +extension KeyedDecodingContainer { + /// Preserve optional-field semantics when synthesis sees the non-optional wrapper. + public func decode( + _: CopyOnWrite.Type, + forKey key: Key, + ) throws -> CopyOnWrite { + try .init(wrappedValue: decodeIfPresent(Value.self, forKey: key)) + } +} + +extension KeyedEncodingContainer { + public mutating func encode( + _ value: CopyOnWrite<(some Encodable)?>, + forKey key: Key, + ) throws { + try encodeIfPresent(value.wrappedValue, forKey: key) + } +} + extension CopyOnWrite.Box: Equatable where Value: Equatable { static func == (lhs: CopyOnWrite.Box, rhs: CopyOnWrite.Box) -> Bool { lhs === rhs || lhs.value == rhs.value diff --git a/Shared/Broadway/BroadwayCore/Sources/Traits/BTraits.swift b/Shared/Broadway/BroadwayCore/Sources/Traits/BTraits.swift index df2502bda..d706430b6 100644 --- a/Shared/Broadway/BroadwayCore/Sources/Traits/BTraits.swift +++ b/Shared/Broadway/BroadwayCore/Sources/Traits/BTraits.swift @@ -18,7 +18,8 @@ import UIKit /// Accessing a type that hasn't been explicitly set returns /// its ``BTraitsValue/defaultValue``. public struct BTraits: Equatable, Hashable, @unchecked Sendable { - init() {} + /// Deterministic defaults without live trait observers. Roots use `system`. + public init() {} /// A traits container pre-registered with all built-in system trait types. @MainActor public static var system: BTraits { diff --git a/Shared/Broadway/BroadwayCore/Tests/BThemeValueTests.swift b/Shared/Broadway/BroadwayCore/Tests/BThemeValueTests.swift new file mode 100644 index 000000000..e339249b6 --- /dev/null +++ b/Shared/Broadway/BroadwayCore/Tests/BThemeValueTests.swift @@ -0,0 +1,17 @@ +import BroadwayCore +import Foundation +import Testing + +struct BThemeValueTests { + @Test func roundTripsAndResolvesExplicitMode() throws { + let values: [BThemeValue] = [.constant(0), .modes(.init(light: 1, dark: 2))] + let decoded = try JSONDecoder().decode( + [BThemeValue].self, + from: JSONEncoder().encode(values), + ) + #expect(decoded == values) + #expect(decoded[0].resolve(mode: .dark) == 0) + #expect(decoded[1].resolve(mode: .light) == 1) + #expect(decoded[1].resolve(mode: .dark) == 2) + } +} diff --git a/Shared/Broadway/BroadwayCore/Tests/CopyOnWriteTests.swift b/Shared/Broadway/BroadwayCore/Tests/CopyOnWriteTests.swift index c405bb27e..6a57e826c 100644 --- a/Shared/Broadway/BroadwayCore/Tests/CopyOnWriteTests.swift +++ b/Shared/Broadway/BroadwayCore/Tests/CopyOnWriteTests.swift @@ -1,7 +1,52 @@ +import Foundation import Testing @_spi(CopyOnWrite) @testable import BroadwayCore struct CopyOnWriteTests { + private struct CodableModel: Codable, Equatable { + @CopyOnWrite var values: [Int] + @CopyOnWrite var optional: String? + } + + @Test func codablePreservesTheWrappedWireFormat() throws { + let decoder = JSONDecoder() + let absent = try decoder.decode(CodableModel.self, from: Data(#"{"values":[1,2]}"#.utf8)) + let null = try decoder.decode( + CodableModel.self, + from: Data(#"{"values":[1,2],"optional":null}"#.utf8), + ) + #expect(absent == null) + let encoded = try JSONEncoder().encode(absent) + let object = try #require(JSONSerialization.jsonObject(with: encoded) as? [String: Any]) + #expect(object["values"] as? [Int] == [1, 2]) + #expect(object["optional"] == nil) + var changed = absent + changed.values.append(3) + changed.optional = "present" + #expect(absent.values == [1, 2]) + #expect(absent.optional == nil) + #expect(try decoder + .decode(CodableModel.self, from: JSONEncoder().encode(changed)) == changed) + #expect(throws: DecodingError.self) { + try decoder.decode(CodableModel.self, from: Data(#"{"values":[],"optional":42}"#.utf8)) + } + } + + @Test func codableHonorsEncoderAndDecoderStrategies() throws { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let date = Date(timeIntervalSince1970: 0) + #expect(try encoder.encode(CopyOnWrite(wrappedValue: date)) == encoder.encode(date)) + #expect(try decoder.decode(CopyOnWrite.self, from: encoder.encode(date)) + .wrappedValue == date) + let data = Data([1, 2, 3]) + #expect(try encoder.encode(CopyOnWrite(wrappedValue: data)) == encoder.encode(data)) + #expect(try decoder.decode(CopyOnWrite.self, from: encoder.encode(data)) + .wrappedValue == data) + } + // MARK: - Initialization @Test("Stores the initial value") diff --git a/Shared/Broadway/BroadwayUI/AGENTS.md b/Shared/Broadway/BroadwayUI/AGENTS.md index 0c9efb30d..a07fc0e1c 100644 --- a/Shared/Broadway/BroadwayUI/AGENTS.md +++ b/Shared/Broadway/BroadwayUI/AGENTS.md @@ -1,6 +1,6 @@ # BroadwayUI – Module Shape -BroadwayUI provides UIKit and SwiftUI components that own and propagate a `BContext` down the view hierarchy. Key types: `BRootViewController` (UIKit root container and trait observation), `BRootView` / `.broadwayRoot(themes:)` (SwiftUI-native root), and `BTraitOverridesViewController` (scoped overrides). It depends on **BroadwayCore**. See [`README.md`](README.md). +BroadwayUI provides UIKit and SwiftUI components that own and propagate a `BContext` down the view hierarchy. Key types: `BRootViewController` (UIKit root container and trait observation), `BRootView` / `.broadwayRoot(themes:)` (SwiftUI-native root), and `BTraitOverridesViewController` (scoped overrides). It depends on **BroadwayCore**, **ViewDocumentCore**, and **ViewDocumentUI**. See [`README.md`](README.md). Read the root [`AGENTS.md`](../../../AGENTS.md) and the group [`../AGENTS.md`](../AGENTS.md) first. @@ -12,6 +12,7 @@ Read the root [`AGENTS.md`](../../../AGENTS.md) and the group [`../AGENTS.md`](. - **Do not add `BTraitsObserver` to `BRootView`.** SwiftUI re-evaluates `body` on color-scheme and Dynamic Type changes. - **A `.task` mirrors `BAccessibility.changes()` into state.** Both rebuild the injected `BContext`. - **Context-building lives in `BRootContext.make(...)`.** That makes the trait mapping testable without a host. +- Merge effective SwiftUI contrast into the root accessibility slice. A local override must not weaken the system setting. Guard: `BRootViewTests`. - **Make `\.bContext` prefer a synchronous SwiftUI value, and mirror to UIKit.** - **`BContext+SwiftUI` stores a SwiftUI-set context in a pure-SwiftUI `EnvironmentKey`.** That covers `BRootView`, `broadwayRoot`, and `bTraitOverrides`. - **Read it synchronously.** Do not round-trip through `UITraitCollection`. Do not accept first-frame lag. @@ -21,3 +22,8 @@ Read the root [`AGENTS.md`](../../../AGENTS.md) and the group [`../AGENTS.md`](. ## Testing Run `BroadwayUITests` in `StuffTestHost`, linking `TestHostSupport` (`./test BroadwayUITests`). + +- Document decorations use validated, prepared content. Resolve scales and accessibility from the Broadway slice; inject host activity and snapshot motion policy. Keep networking and product attachment names in consumers. +- Compose native content with `BSurface`. Its finish owns the inner material or transition source; exterior artwork stays outside that finish. Preserve its explicit accessibility content shape after exterior composition. +- Host artwork through validated `BArtworkPlacement` and `BArtworkLayer`. Keep paint bounds separate from layout and interaction. The parent reserves gaps and retains viewport clipping. +- Essential layers remain visible under accessibility simplification. Exterior artwork may paint only in its bounded edge band and exterior zone; native content remains protected. diff --git a/Shared/Broadway/BroadwayUI/README.md b/Shared/Broadway/BroadwayUI/README.md index 7b2972508..efc60ba10 100644 --- a/Shared/Broadway/BroadwayUI/README.md +++ b/Shared/Broadway/BroadwayUI/README.md @@ -1,11 +1,28 @@ # BroadwayUI +## Composed surfaces + +`BSurface` orders the base, interior artwork, native content, highlights, and exterior artwork. +Only native content determines layout. Supply a `finish` modifier for material treatment or the inner zoom-transition source. +The exterior layer follows that modifier and does not become part of its clipping or transition source. +An explicit accessibility content shape keeps combined focus bounds on the native surface. + +Validate a `ViewArtworkPlacement` with `BArtworkPlacement` before constructing `BArtworkLayer`. +The layer hosts either a prepared document or native artwork. It clips all transforms to the declared paint bounds. +Pass `maximumOutsets` when the host reserves smaller gutters. The layer scales artwork to fit without changing native content or interaction bounds. +Exterior artwork also excludes the content center. Its edge band is at most 16 points and shrinks for small hosts. +The containing layout must reserve gaps and gutters. The layer does not disable scroll viewport clipping. + +`BSurfaceLayerRole.essential` preserves required surface content when accessibility settings simplify ornament. +`BViewDocumentLayer` accepts that role and always retains the Reduce Motion policy. +Both artwork paths remain hidden from accessibility and ignore hit testing. + UIKit + SwiftUI components that carry a `BContext` (from BroadwayCore) through the view hierarchy. ## Public API - **`BRootViewController`** — a container view controller that owns the root `BContext`, observes system trait changes via `BTraitsObserver`, and republishes the context to descendants through `traitOverrides`. The designated initializer wraps any `UIViewController`. A convenience initializer hosts SwiftUI content directly. -- **`BRootView` / `.broadwayRoot(themes:)`** — the SwiftUI-native root. Seeds a root `BContext` from the live system traits (`@Environment(\.colorScheme)`, `\.dynamicTypeSize`, and `BAccessibility.changes()`) plus the given `themes`, and injects it so descendants read `@Environment(\.bContext)`. Unlike the UIKit root it takes `themes`, letting an app seed palette/typography at the root. +- **`BRootView` / `.broadwayRoot(themes:)`** — the SwiftUI-native root. Seeds a root `BContext` from the live system traits (`@Environment(\.colorScheme)`, `\.colorSchemeContrast`, `\.dynamicTypeSize`, and `BAccessibility.changes()`) plus the given `themes`, and injects it so descendants read `@Environment(\.bContext)`. Unlike the UIKit root it takes `themes`, letting an app seed palette/typography at the root. - **`BTraitOverridesViewController`** — scopes trait overrides to a subtree while preserving inherited base traits and themes. - **SwiftUI bridges** — `BContext+SwiftUI` resolves `@Environment(\.bContext)`, preferring a synchronous pure-SwiftUI value (set by `BRootView` / `broadwayRoot` / `bTraitOverrides`, and mirrored into UIKit traits so it also reaches nested UIKit) and falling back to the UIKit trait-bridged value when none is set. Plus `BTraitOverrides+SwiftUI` and the `BMode` / `BContentSizeCategory` initializers from `ColorScheme` / `DynamicTypeSize` (`BTraitValues+SwiftUI`). @@ -34,3 +51,13 @@ WindowGroup { Local SPM library declared in the root [`Package.swift`](../../../Package.swift) (depends on BroadwayCore): `.package(product: "BroadwayUI")`. Run tests with `./test BroadwayUITests`. + +## Portable documents + +BroadwayUI consumes the independent ViewDocument libraries. Prepare and validate a document before rendering it. Attach it with `bViewDocumentBackground` or `bViewDocumentOverlay`, passing host activity and static-motion policy. These decorations stay within the host bounds and do not receive input or accessibility focus. + +`BViewDocumentStylesheet` resolves semantic dimension scales from the current content-size category. Reduce Motion selects the document's static pose. Reduce Transparency and Increased Contrast hide decorations. The renderer does not load assets or fetch documents. + +The adapter supplies native background levels resolved from explicit mode and contrast traits. +Background elevation remains dynamic so sheets and popovers retain native raised surfaces. +The SwiftUI root includes effective contrast overrides without weakening the system's Increased Contrast setting. diff --git a/Shared/Broadway/BroadwayUI/Sources/BArtworkLayer.swift b/Shared/Broadway/BroadwayUI/Sources/BArtworkLayer.swift new file mode 100644 index 000000000..2733b8d08 --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Sources/BArtworkLayer.swift @@ -0,0 +1,124 @@ +import SwiftUI +import ViewDocumentCore + +/// Hosts prepared document or native artwork in paint bounds that never change the host's size. +public struct BArtworkLayer: View { + private let placement: BArtworkPlacement + private let shape: SurfaceShape + private let role: BSurfaceLayerRole + private let maximumOutsets: ViewArtworkPlacement.Outsets? + private let content: Content + @Environment(\.bViewDocumentStylesheet) private var sheet + /// Layout direction is local container geometry and can differ beneath the Broadway root. + @Environment(\.layoutDirection) private var layoutDirection + + public init( + placement: BArtworkPlacement, + shape: SurfaceShape, + role: BSurfaceLayerRole, + maximumOutsets: ViewArtworkPlacement.Outsets? = nil, + @ViewBuilder content: () -> Content, + ) { + self.placement = placement + self.shape = shape + self.role = role + self.maximumOutsets = maximumOutsets + self.content = content() + } + + public var body: some View { + if sheet.shows(role) { + GeometryReader { host in + let geometry = placement.value.geometry( + in: .init(width: host.size.width, height: host.size.height), + rightToLeft: layoutDirection == .rightToLeft, + maximumOutsets: maximumOutsets, + ) + let frame = geometry.frame + let bounds = geometry.paintBounds + if frame.width > 0, frame.height > 0 { + content + .frame(width: frame.width, height: frame.height) + .position( + x: frame.x + frame.width / 2 - bounds.x, + y: frame.y + frame.height / 2 - bounds.y, + ) + .frame(width: bounds.width, height: bounds.height) + .mask { + BArtworkClip( + shape: shape, + host: host.size, + paintBounds: bounds, + plane: placement.value.plane, + ) + .fill(style: FillStyle(eoFill: true)) + } + .offset(x: bounds.x, y: bounds.y) + } + } + .allowsHitTesting(false) + .accessibilityHidden(true) + } + } +} + +/// Foreground paint is restricted to a narrow edge band and the declared exterior bounds. +struct BArtworkClip: Shape { + let shape: SurfaceShape + let host: CGSize + let paintBounds: ViewArtworkPlacement.Frame + let plane: ViewArtworkPlacement.Plane + + func path(in rect: CGRect) -> Path { + let surface = CGRect( + x: -paintBounds.x, + y: -paintBounds.y, + width: host.width, + height: host.height, + ) + switch plane { + case .interior: return shape.path(in: surface) + case .exterior: + // Keep a protected center even when a host is smaller than the ordinary edge band. + let edge = min(16, min(host.width, host.height) / 4) + var path = Path(rect) + path.addPath(shape.inset(by: edge).path(in: surface)) + return path + } + } +} + +#if DEBUG + #Preview("Bounded exterior artwork") { BArtworkPreview.make() } + + @MainActor + private enum BArtworkPreview { + static func make() -> some View { + do { + let placement = try BArtworkPlacement(.init( + anchor: .bottomTrailing, + size: .fixed(width: 48, height: 48), + offset: .init(x: 8, y: 24), + plane: .exterior, + outsets: .init(top: 0, leading: 0, bottom: 24, trailing: 8), + )) + return Text("Native card").padding(40).background( + .orange.opacity(0.15), + in: .rect(cornerRadius: 20), + ) + .overlay { + BArtworkLayer( + placement: placement, + shape: RoundedRectangle(cornerRadius: 20), + role: .ornament, + ) { + Circle().fill(.orange) + } + } + .padding(40).broadwayRoot() + } catch { + preconditionFailure("Invalid artwork preview: \(error)") + } + } + } +#endif diff --git a/Shared/Broadway/BroadwayUI/Sources/BArtworkPlacement.swift b/Shared/Broadway/BroadwayUI/Sources/BArtworkPlacement.swift new file mode 100644 index 000000000..f9774b2f6 --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Sources/BArtworkPlacement.swift @@ -0,0 +1,16 @@ +import ViewDocumentCore + +/// Immutable proof that a host's artwork geometry passed the portable bounds checks. +public struct BArtworkPlacement: Hashable, Sendable { + public let value: ViewArtworkPlacement + + public init(_ value: ViewArtworkPlacement) throws { + try value.validate() + self.value = value + } +} + +/// Essential surface content remains visible when accessibility settings simplify ornament. +public enum BSurfaceLayerRole: String, Codable, Hashable, Sendable { + case essential, ornament +} diff --git a/Shared/Broadway/BroadwayUI/Sources/BRootView.swift b/Shared/Broadway/BroadwayUI/Sources/BRootView.swift index 41f5146d3..c6a729a1c 100644 --- a/Shared/Broadway/BroadwayUI/Sources/BRootView.swift +++ b/Shared/Broadway/BroadwayUI/Sources/BRootView.swift @@ -22,6 +22,7 @@ public struct BRootView: View { private let content: Content @Environment(\.colorScheme) private var colorScheme + @Environment(\.colorSchemeContrast) private var contrast @Environment(\.dynamicTypeSize) private var dynamicTypeSize @State private var accessibility: BAccessibility = .current() @@ -34,6 +35,7 @@ public struct BRootView: View { content .environment(\.bContext, BRootContext.make( colorScheme: colorScheme, + contrast: contrast, dynamicTypeSize: dynamicTypeSize, accessibility: accessibility, themes: themes, @@ -64,6 +66,7 @@ enum BRootContext { @MainActor static func make( colorScheme: ColorScheme, + contrast: ColorSchemeContrast, dynamicTypeSize: DynamicTypeSize, accessibility: BAccessibility, themes: BThemes, @@ -79,7 +82,11 @@ enum BRootContext { // there instead when a token needs to follow a scoped scheme override. traits.mode = BMode(colorScheme) traits.contentSizeCategory = BContentSizeCategory(dynamicTypeSize) - traits.accessibility = accessibility + var effectiveAccessibility = accessibility + effectiveAccessibility.isDarkerSystemColorsEnabled = accessibility + .isDarkerSystemColorsEnabled + || contrast == .increased + traits.accessibility = effectiveAccessibility return BContext(traits: traits, themes: themes) } } diff --git a/Shared/Broadway/BroadwayUI/Sources/BSurface.swift b/Shared/Broadway/BroadwayUI/Sources/BSurface.swift new file mode 100644 index 000000000..c32dd28be --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Sources/BSurface.swift @@ -0,0 +1,73 @@ +import SwiftUI + +/// Composes a surface without letting artwork participate in content layout or interaction. +public struct BSurface< + SurfaceShape: Shape, + Base: View, + Interior: View, + Content: View, + Highlights: View, + Exterior: View, + Finish: ViewModifier, +>: View { + private let shape: SurfaceShape + private let base: Base + private let interior: Interior + private let content: Content + private let highlights: Highlights + private let exterior: Exterior + private let finish: Finish + + /// Finish applies to the inner surface only. It can own material treatment or a zoom source. + public init( + shape: SurfaceShape, + finish: Finish, + @ViewBuilder base: () -> Base, + @ViewBuilder interior: () -> Interior, + @ViewBuilder content: () -> Content, + @ViewBuilder highlights: () -> Highlights, + @ViewBuilder exterior: () -> Exterior, + ) { + self.shape = shape + self.base = base() + self.interior = interior() + self.content = content() + self.highlights = highlights() + self.exterior = exterior() + self.finish = finish + } + + public var body: some View { + content + .background { + ZStack { base; interior } + .allowsHitTesting(false) + .accessibilityHidden(true) + } + .clipShape(shape) + .overlay { highlights.allowsHitTesting(false).accessibilityHidden(true) } + .contentShape(shape) + .modifier(finish) + .overlay { exterior.allowsHitTesting(false).accessibilityHidden(true) } + // SwiftUI can include hidden overlay paint in a combined element's frame. + // Keep its focus contour on the native surface after exterior composition. + .contentShape(.accessibility, shape) + } +} + +#if DEBUG + #Preview("Surface composition") { + BSurface(shape: RoundedRectangle(cornerRadius: 20), finish: EmptyModifier()) { + Color.orange.opacity(0.12) + } interior: { + Circle().fill(.orange.opacity(0.15)).padding(10) + } content: { + Text("Native content").font(.headline).padding(40) + } highlights: { + RoundedRectangle(cornerRadius: 20).stroke(.orange, lineWidth: 1) + } exterior: { + EmptyView() + } + .padding(40) + } +#endif diff --git a/Shared/Broadway/BroadwayUI/Sources/BViewDocumentLayer.swift b/Shared/Broadway/BroadwayUI/Sources/BViewDocumentLayer.swift new file mode 100644 index 000000000..7fa64aec6 --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Sources/BViewDocumentLayer.swift @@ -0,0 +1,128 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +/// A decoration resolves through the nearest Broadway context and stays inside its host bounds. +public struct BViewDocumentLayer: View { + let document: PreparedViewDocument + let isActive: Bool + let motionIsStatic: Bool + let role: BSurfaceLayerRole + @Environment(\.bViewDocumentStylesheet) private var sheet + + public init( + document: PreparedViewDocument, + isActive: Bool, + motionIsStatic: Bool, + role: BSurfaceLayerRole = .ornament, + ) { + self.document = document + self.isActive = isActive + self.motionIsStatic = motionIsStatic + self.role = role + } + + public var body: some View { + if sheet.shows(role) { + ViewDocumentView( + prepared: document, + appearance: sheet.appearance, + policy: .init( + purpose: .decoration, + isActive: isActive, + motionIsStatic: motionIsStatic || sheet.motionIsStatic, + ), + ) + } + } +} + +extension View { + /// Adds a bounded background without changing the host's layout or hit area. + public func bViewDocumentBackground( + _ document: PreparedViewDocument, + isActive: Bool, + motionIsStatic: Bool, + ) -> some View { + background { + GeometryReader { geometry in + BViewDocumentLayer( + document: document, + isActive: isActive, + motionIsStatic: motionIsStatic, + ) + .frame(width: geometry.size.width, height: geometry.size.height) + .clipped() + } + .allowsHitTesting(false) + .accessibilityHidden(true) + } + } + + /// Adds a bounded overlay without changing the host's layout or hit area. + public func bViewDocumentOverlay( + _ document: PreparedViewDocument, + isActive: Bool, + motionIsStatic: Bool, + ) -> some View { + overlay { + GeometryReader { geometry in + BViewDocumentLayer( + document: document, + isActive: isActive, + motionIsStatic: motionIsStatic, + ) + .frame(width: geometry.size.width, height: geometry.size.height) + .clipped() + } + .allowsHitTesting(false) + .accessibilityHidden(true) + } + } +} + +extension EnvironmentValues { + public var bViewDocumentStylesheet: BViewDocumentStylesheet { + bContext.stylesheet(BViewDocumentStylesheet.self, fallback: .default) + } +} + +#if DEBUG + #Preview("Broadway document decoration") { BViewDocumentPreview.make() } + + @MainActor + private enum BViewDocumentPreview { + static func make() -> some View { + do { + let validated = try ValidatedViewDocument( + document: .init(schemaVersion: 1, root: .init( + id: .init(rawValue: "artwork"), + content: .shape(.circle), + modifiers: [ + .foreground(.color(.semantic(.accent))), + .frame( + width: .scaled(points: 40, relativeTo: .body), + height: .scaled(points: 40, relativeTo: .body), + alignment: .center, + ), + ], + animation: nil, + accessibility: .hidden, + )), + resources: .init(images: [], symbols: []), + limits: .standard, + ) + let prepared = try PreparedViewDocument( + document: validated, + assets: .init(images: [:], symbols: [:]), + ) + return Color.clear.frame(width: 200, height: 120) + .bViewDocumentBackground(prepared, isActive: true, motionIsStatic: true) + .broadwayRoot() + } catch { + preconditionFailure("Invalid Broadway preview: \(error)") + } + } + } +#endif diff --git a/Shared/Broadway/BroadwayUI/Sources/BViewDocumentStylesheet.swift b/Shared/Broadway/BroadwayUI/Sources/BViewDocumentStylesheet.swift new file mode 100644 index 000000000..16c666d8f --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Sources/BViewDocumentStylesheet.swift @@ -0,0 +1,128 @@ +import BroadwayCore +import SwiftUI +import UIKit +import ViewDocumentCore +import ViewDocumentUI + +/// Resolves document dimensions and decoration policy once per Broadway slice. +public struct BViewDocumentStylesheet: BStylesheet, Sendable { + public let background: Color + public let backgrounds: ViewDocumentAppearance.Backgrounds + public let dimensionScales: [ViewFontDescriptor.TextStyle: Double] + public let motionIsStatic: Bool + public let showsDecoration: Bool + + public init(context: SlicingContext) { + self.init(traits: context.traits) + } + + public init(traits: BTraits) { + switch (traits.mode, traits.accessibility.isDarkerSystemColorsEnabled) { + case (.light, false): backgrounds = Self.lightBackgrounds + case (.dark, false): backgrounds = Self.darkBackgrounds + case (.light, true): backgrounds = Self.lightContrastBackgrounds + case (.dark, true): backgrounds = Self.darkContrastBackgrounds + } + background = backgrounds.primary + var scales: [ViewFontDescriptor.TextStyle: Double] = [:] + for style in ViewFontDescriptor.TextStyle.allCases { + scales[style] = Double(BScaledDimension.value( + 1, + relativeTo: style.uiTextStyle, + category: traits.contentSizeCategory, + )) + } + dimensionScales = scales + motionIsStatic = traits.accessibility.isReduceMotionEnabled + showsDecoration = !traits.accessibility.isReduceTransparencyEnabled && !traits.accessibility + .isDarkerSystemColorsEnabled + } + + public static let `default` = Self() + + private init() { + background = .white + backgrounds = .init( + primary: .white, + secondary: .white, + grouped: .white, + secondaryGrouped: .white, + ) + dimensionScales = [:] + motionIsStatic = true + showsDecoration = false + } + + // Cache the dynamic providers so equal trait slices keep equal Color identities. + private static let lightBackgrounds = makeBackgrounds(isDark: false, increasedContrast: false) + private static let darkBackgrounds = makeBackgrounds(isDark: true, increasedContrast: false) + private static let lightContrastBackgrounds = makeBackgrounds( + isDark: false, + increasedContrast: true, + ) + private static let darkContrastBackgrounds = makeBackgrounds( + isDark: true, + increasedContrast: true, + ) + + private enum BackgroundLevel { case primary, secondary, grouped, secondaryGrouped } + + private static func makeBackgrounds( + isDark: Bool, + increasedContrast: Bool, + ) -> ViewDocumentAppearance.Backgrounds { + func color(_ level: BackgroundLevel) -> Color { + Color(uiColor: UIColor { hostTraits in + // Mode and contrast belong to Broadway. Elevation belongs to the actual + // sheet/popover host and must remain dynamic until the color is drawn. + let traits = UITraitCollection(traitsFrom: [ + hostTraits, + UITraitCollection(userInterfaceStyle: isDark ? .dark : .light), + UITraitCollection(accessibilityContrast: increasedContrast ? .high : .normal), + ]) + let native: UIColor = switch level { + case .primary: .systemBackground + case .secondary: .secondarySystemBackground + case .grouped: .systemGroupedBackground + case .secondaryGrouped: .secondarySystemGroupedBackground + } + return native.resolvedColor(with: traits) + }) + } + return .init( + primary: color(.primary), + secondary: color(.secondary), + grouped: color(.grouped), + secondaryGrouped: color(.secondaryGrouped), + ) + } + + public var appearance: ViewDocumentAppearance { + .init(backgrounds: backgrounds, dimensionScales: dimensionScales) + } + + public func shows(_ role: BSurfaceLayerRole) -> Bool { + switch role { + case .essential: true + case .ornament: showsDecoration + } + } +} + +extension ViewFontDescriptor.TextStyle { + fileprivate var uiTextStyle: UIFont.TextStyle { + switch self { + case .largeTitle: .largeTitle + case .title: .title1 + case .title2: .title2 + case .title3: .title3 + case .headline: .headline + case .subheadline: .subheadline + case .body: .body + case .callout: .callout + case .footnote: .footnote + case .caption: .caption1 + case .caption2: .caption2 + } + } +} diff --git a/Shared/Broadway/BroadwayUI/Tests/BArtworkLayerTests.swift b/Shared/Broadway/BroadwayUI/Tests/BArtworkLayerTests.swift new file mode 100644 index 000000000..6aeba4e2f --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Tests/BArtworkLayerTests.swift @@ -0,0 +1,63 @@ +@testable import BroadwayUI +import SwiftUI +import Testing +import ViewDocumentCore + +@MainActor +struct BArtworkLayerTests { + @Test func exteriorMaskProtectsNativeContentAndStopsAtPaintBounds() { + let clip = BArtworkClip( + shape: Rectangle(), + host: CGSize(width: 100, height: 80), + paintBounds: .init(x: -10, y: -10, width: 120, height: 100), + plane: .exterior, + ) + let path = clip.path(in: CGRect(x: 0, y: 0, width: 120, height: 100)) + #expect(!path.contains(CGPoint(x: 60, y: 50), eoFill: true)) + #expect(path.contains(CGPoint(x: 12, y: 50), eoFill: true)) + #expect(path.contains(CGPoint(x: 5, y: 50), eoFill: true)) + #expect(!path.contains(CGPoint(x: -1, y: 50), eoFill: true)) + #expect(!path.contains(CGPoint(x: 121, y: 50), eoFill: true)) + } + + @Test func interiorMaskFollowsSurfaceCorners() { + let clip = BArtworkClip( + shape: RoundedRectangle(cornerRadius: 20), + host: CGSize(width: 100, height: 80), + paintBounds: .init(x: 0, y: 0, width: 100, height: 80), + plane: .interior, + ) + let path = clip.path(in: CGRect(x: 0, y: 0, width: 100, height: 80)) + #expect(!path.contains(CGPoint(x: 1, y: 1))) + #expect(path.contains(CGPoint(x: 50, y: 40))) + } + + @Test func exteriorBandInsetsTheRoundedContourWithoutRescalingIt() { + let clip = BArtworkClip( + shape: RoundedRectangle(cornerRadius: 20), + host: CGSize(width: 100, height: 80), + paintBounds: .init(x: 0, y: 0, width: 100, height: 80), + plane: .exterior, + ) + let path = clip.path(in: CGRect(x: 0, y: 0, width: 100, height: 80)) + #expect(!path.contains(CGPoint(x: 20, y: 20), eoFill: true)) + #expect(path.contains(CGPoint(x: 10, y: 20), eoFill: true)) + } + + @Test func overflowDoesNotChangeLayoutSize() throws { + let placement = try BArtworkPlacement(.init( + anchor: .bottomTrailing, + size: .fixed(width: 100, height: 100), + offset: .init(x: 20, y: 20), + plane: .exterior, + outsets: .init(top: 0, leading: 0, bottom: 20, trailing: 20), + )) + let view = Color.white.frame(width: 80, height: 60).overlay { + BArtworkLayer(placement: placement, shape: Rectangle(), role: .essential) { + Color.orange.scaleEffect(100) + } + } + let image = try #require(ImageRenderer(content: view).uiImage) + #expect(image.size == CGSize(width: 80, height: 60)) + } +} diff --git a/Shared/Broadway/BroadwayUI/Tests/BArtworkPlacementTests.swift b/Shared/Broadway/BroadwayUI/Tests/BArtworkPlacementTests.swift new file mode 100644 index 000000000..9901556fd --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Tests/BArtworkPlacementTests.swift @@ -0,0 +1,22 @@ +import BroadwayUI +import Testing +import ViewDocumentCore + +struct BArtworkPlacementTests { + @Test func rejectsUnboundedPaintBeforeHosting() { + #expect(throws: ViewDocumentValidationError.self) { + try BArtworkPlacement(.init( + anchor: .center, + size: .fixed(width: 80, height: 80), + offset: .zero, + plane: .exterior, + outsets: .init( + top: .infinity, + leading: 0, + bottom: 0, + trailing: 0, + ), + )) + } + } +} diff --git a/Shared/Broadway/BroadwayUI/Tests/BRootViewTests.swift b/Shared/Broadway/BroadwayUI/Tests/BRootViewTests.swift index 24a5f5886..8ac00d1d6 100644 --- a/Shared/Broadway/BroadwayUI/Tests/BRootViewTests.swift +++ b/Shared/Broadway/BroadwayUI/Tests/BRootViewTests.swift @@ -12,6 +12,17 @@ private enum Marker: String, BTheme { @MainActor struct BRootContextTests { + @Test func anEnvironmentOverrideCannotWeakenSystemContrast() { + let context = BRootContext.make( + colorScheme: .light, + contrast: .standard, + dynamicTypeSize: .large, + accessibility: .init(isDarkerSystemColorsEnabled: true), + themes: .init(), + ) + #expect(context.traits.accessibility.isDarkerSystemColorsEnabled) + } + @Test("make maps SwiftUI system inputs into the context traits and themes") func mapsSystemInputs() { var themes = BThemes() @@ -19,6 +30,7 @@ struct BRootContextTests { let context = BRootContext.make( colorScheme: .dark, + contrast: .standard, dynamicTypeSize: .accessibility3, accessibility: BAccessibility(isVoiceOverRunning: true), themes: themes, @@ -33,6 +45,15 @@ struct BRootContextTests { @MainActor struct BRootViewHostingTests { + @Test func effectiveContrastReachesTheRootStylesheet() throws { + let box = ContextBox() + let host = UIHostingController(rootView: BRootView { ContextProbe(box: box) }) + host.traitOverrides.accessibilityContrast = .high + try show(host) { _ in + try waitFor { box.value?.traits.accessibility.isDarkerSystemColorsEnabled == true } + } + } + @Test("Root injects its themes into the descendant environment") func injectsThemes() throws { var themes = BThemes() diff --git a/Shared/Broadway/BroadwayUI/Tests/BSurfaceTests.swift b/Shared/Broadway/BroadwayUI/Tests/BSurfaceTests.swift new file mode 100644 index 000000000..eba80907d --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Tests/BSurfaceTests.swift @@ -0,0 +1,22 @@ +import BroadwayUI +import SwiftUI +import Testing + +@MainActor +struct BSurfaceTests { + @Test func onlyNativeContentDeterminesLayout() throws { + let view = BSurface(shape: Rectangle(), finish: EmptyModifier()) { + Color.white + } interior: { + Color.red.frame(width: 1000, height: 1000) + } content: { + Color.clear.frame(width: 80, height: 60) + } highlights: { + Rectangle().stroke(.black, lineWidth: 1) + } exterior: { + Color.blue.frame(width: 1000, height: 1000) + } + let image = try #require(ImageRenderer(content: view).uiImage) + #expect(image.size == CGSize(width: 80, height: 60)) + } +} diff --git a/Shared/Broadway/BroadwayUI/Tests/BViewDocumentLayerTests.swift b/Shared/Broadway/BroadwayUI/Tests/BViewDocumentLayerTests.swift new file mode 100644 index 000000000..175b65666 --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Tests/BViewDocumentLayerTests.swift @@ -0,0 +1,74 @@ +import BroadwayCore +import BroadwayUI +import SwiftUI +import Testing +import ViewDocumentCore +import ViewDocumentUI + +@MainActor +struct BViewDocumentLayerTests { + @Test func contentChangeWithSameNodeIdentityInvalidatesSlice() { + var context = BContext(traits: .system) + let first = context.stylesheet( + DocumentSheet.self, + fallback: .init(document: DocumentTheme.defaultValue.document), + ) + var changed = first.document + changed.root.content = .text("Updated") + context.themes[DocumentTheme.self] = .init(document: changed) + let second = context.stylesheet(DocumentSheet.self, fallback: first) + #expect(second.document.root.id == first.document.root.id) + #expect(second.document.root.content == .text("Updated")) + #expect(second != first) + } + + @Test func decorationsCannotExpandHostLayout() throws { + let validated = try ValidatedViewDocument( + document: .init(schemaVersion: 1, root: .init( + id: .init(rawValue: "oversized"), + content: .shape(.rectangle), + modifiers: [.frame( + width: .points(1000), + height: .points(1000), + alignment: .center, + )], + animation: nil, + accessibility: .label("Must stay decorative"), + )), + resources: .init(images: [], symbols: []), + limits: .standard, + ) + let document = try PreparedViewDocument( + document: validated, + assets: .init(images: [:], symbols: [:]), + ) + let view = Color.white.frame(width: 40, height: 30) + .bViewDocumentBackground(document, isActive: true, motionIsStatic: true) + .bViewDocumentOverlay(document, isActive: true, motionIsStatic: true) + .broadwayRoot() + let image = try #require(ImageRenderer(content: view).uiImage) + #expect(image.size == CGSize(width: 40, height: 30)) + } +} + +private struct DocumentTheme: BTheme { + var document: ViewDocument + static let defaultValue = Self(document: .init(schemaVersion: 1, root: .init( + id: .init(rawValue: "stable"), + content: .text("Original"), + modifiers: [], + animation: nil, + accessibility: .automatic, + ))) +} + +private struct DocumentSheet: BStylesheet { + let document: ViewDocument + init(document: ViewDocument) { + self.document = document + } + + init(context: SlicingContext) { + document = context.themes[DocumentTheme.self].document + } +} diff --git a/Shared/Broadway/BroadwayUI/Tests/BViewDocumentStylesheetTests.swift b/Shared/Broadway/BroadwayUI/Tests/BViewDocumentStylesheetTests.swift new file mode 100644 index 000000000..2baa0ca08 --- /dev/null +++ b/Shared/Broadway/BroadwayUI/Tests/BViewDocumentStylesheetTests.swift @@ -0,0 +1,68 @@ +import BroadwayCore +import BroadwayUI +import SwiftUI +import Testing +import UIKit +import ViewDocumentCore + +@MainActor +struct BViewDocumentStylesheetTests { + @Test(arguments: [UIUserInterfaceLevel.base, .elevated]) + func backgroundLevelsPinModeAndContrastButRetainHostElevation(level: UIUserInterfaceLevel) { + var traits = BTraits() + traits.mode = .dark + traits.accessibility = .init(isDarkerSystemColorsEnabled: true) + let appearance = BViewDocumentStylesheet(traits: traits).appearance + let host = UITraitCollection(traitsFrom: [ + UITraitCollection(userInterfaceStyle: .light), + UITraitCollection(accessibilityContrast: .normal), + UITraitCollection(userInterfaceLevel: level), + ]) + let expected = UITraitCollection(traitsFrom: [ + UITraitCollection(userInterfaceStyle: .dark), + UITraitCollection(accessibilityContrast: .high), + UITraitCollection(userInterfaceLevel: level), + ]) + #expect(UIColor(appearance.color(.semantic(.background))).resolvedColor(with: host) + == UIColor.systemBackground.resolvedColor(with: expected)) + #expect(UIColor(appearance.color(.semantic(.secondaryBackground))).resolvedColor(with: host) + == UIColor.secondarySystemBackground.resolvedColor(with: expected)) + #expect(UIColor(appearance.color(.semantic(.secondaryGroupedBackground))) + .resolvedColor(with: host) + == UIColor.secondarySystemGroupedBackground.resolvedColor(with: expected)) + #expect(appearance.background == BViewDocumentStylesheet(traits: traits).appearance + .background) + } + + @Test func resolvesAccessibilityAndExplicitScaling() { + var traits = BTraits.system + traits.contentSizeCategory = .large + let standard = BViewDocumentStylesheet(traits: traits) + #expect(standard.showsDecoration) + #expect(!standard.motionIsStatic) + traits.contentSizeCategory = .accessibilityExtraExtraExtraLarge + traits.accessibility = .init(isReduceMotionEnabled: true) + let accessible = BViewDocumentStylesheet(traits: traits) + #expect(accessible.motionIsStatic) + #expect(accessible.dimensionScales[.body, default: 0] > standard.dimensionScales[ + .body, + default: 0, + ]) + traits.accessibility = .init(isReduceTransparencyEnabled: true) + #expect(!BViewDocumentStylesheet(traits: traits).showsDecoration) + #expect(BViewDocumentStylesheet(traits: traits).shows(.essential)) + #expect(!BViewDocumentStylesheet(traits: traits).shows(.ornament)) + traits.accessibility = .init(isDarkerSystemColorsEnabled: true) + #expect(!BViewDocumentStylesheet(traits: traits).showsDecoration) + #expect(BViewDocumentStylesheet(traits: traits).shows(.essential)) + } + + @Test func traitChangeResolvesNewCachedSlice() { + var context = BContext(traits: .system) + let initial = context.stylesheet(BViewDocumentStylesheet.self, fallback: .default) + context.traitOverrides.accessibility = .init(isReduceMotionEnabled: true) + let changed = context.stylesheet(BViewDocumentStylesheet.self, fallback: .default) + #expect(!initial.motionIsStatic) + #expect(changed.motionIsStatic) + } +} diff --git a/Shared/Broadway/README.md b/Shared/Broadway/README.md index 8d5289dc5..fba98bd64 100644 --- a/Shared/Broadway/README.md +++ b/Shared/Broadway/README.md @@ -19,3 +19,9 @@ UIKit hosting helpers (`show`, `waitFor`, …) for Broadway's hosted Swift Testi Libraries are declared in the root [`Package.swift`](../../Package.swift). The Catalog app and hosted test bundles are in [`Project.swift`](../../Project.swift) (bundle IDs `com.stuff.broadway.*`). Run `./test BroadwayCoreTests`, `./test BroadwayUITests`, or `./test BroadwayCatalogTests`. + +## Portable documents + +BroadwayUI consumes the independent ViewDocument libraries. Prepare and validate a document before rendering it. Attach it with `bViewDocumentBackground` or `bViewDocumentOverlay`, passing host activity and static-motion policy. These decorations stay within the host bounds and do not receive input or accessibility focus. + +`BViewDocumentStylesheet` resolves semantic dimension scales from the current content-size category. Reduce Motion selects the document's static pose. Reduce Transparency and Increased Contrast hide decorations. The renderer does not load assets or fetch documents. diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.WithData_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.WithData_iPad_contrast.png index d420d5263..7ee7b9056 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.WithData_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.WithData_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b50313c53053e275bd139f5d05699ebe0a5d13cc309036ebdc243c6d39b47a60 -size 3622838 +oid sha256:b0aae6a1a9919d40d8bc6a43e6e0c4a9e6590126003801f2f4e559913a54f04a +size 3984209 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.WithData_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.WithData_iPhone_contrast.png index 693c477e6..406d53897 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.WithData_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.WithData_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4069506f65fc227e39103173a44d195be2e07d48437e25b315b62f38d588956 -size 2108164 +oid sha256:7df4938662a7e82958d01ac6ca77d56eea75ef16fe61ec8eb95bf838425662b1 +size 2289161 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.YearOverview_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.YearOverview_iPad_contrast.png index 54f105909..34d8aff7f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.YearOverview_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.YearOverview_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:657bab045358cf07cfe8f1b1407b939c50001c7b8f7ce34bdc75c13d165969f0 -size 2655544 +oid sha256:a0f515c7ed0aaa37247bccf10814199c649f78de50e788b7699ea1ea9b247438 +size 2971996 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.YearOverview_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.YearOverview_iPhone_contrast.png index 847efd6bb..d5d074f36 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.YearOverview_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/other/calendarContent.YearOverview_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbf989908ab77e870bdb4ba3f87be646aefbd10260eba54d8fc8c75fd34c49ba -size 1458440 +oid sha256:8a70623b8a1183efce9e004dc4229619314a1dd160cdd82c8965059ab8067327 +size 1616923 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/other/componentThemes.Expanded_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/other/componentThemes.Expanded_contrast.png index 8be7001b7..374a5e4f6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/other/componentThemes.Expanded_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/other/componentThemes.Expanded_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac79feaf20d819a639b3c277d767e6a7ade377ad3b61c8ef711f994f9168c658 -size 2751837 +oid sha256:c90f8366df42ccb86d9432dec81cae4b47a9c03495f914b7d2dbee9e5133bf29 +size 3092228 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.Collapsed_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.Collapsed_contrast.png index a8983ae3b..60fd532d3 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.Collapsed_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.Collapsed_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:790c62ea2a76ab64256548c21acd6a86ac8311e35d83f159240b1ebbf3346a7c -size 462810 +oid sha256:e23e068a4e97aeac0adf6b874102c871dc8e33a87c466fded938c811fbd5dc5e +size 471099 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.SummaryExpanded_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.SummaryExpanded_contrast.png index 73369b7a7..a072822d5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.SummaryExpanded_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.SummaryExpanded_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26ac4601433a37ac80042a48efb39f2885e9c0faccf645db2a540fae9790084e -size 1901895 +oid sha256:651559b96a31eccd1e94cac1025ebeb9b48e5126883df1af7cca44bbdd59570d +size 1923005