diff --git a/.bumper/RULES.md b/.bumper/RULES.md index 07a79a581..7d79b7678 100644 --- a/.bumper/RULES.md +++ b/.bumper/RULES.md @@ -185,3 +185,5 @@ is needed. ViewDocumentCore uses Foundation only. ViewDocumentUI depends on Core and system UI frameworks. Both reject Broadway, Where, and SnapshotKit imports. The `ViewDocument rejects consumer dependencies` mutation test covers these boundaries. Asset providers supply prepared images. The import rules do not inspect image contents or enforce resource limits; document validation owns those checks. + +WhereUI may consume ViewDocumentCore and ViewDocumentUI for validated theme layers. The domain and adapter targets cannot import those modules directly. The downward-dependency fixture covers this presentation boundary. diff --git a/.bumper/Tests/WhereArchitectureTests.swift b/.bumper/Tests/WhereArchitectureTests.swift index 207ddb39d..c034c22d3 100644 --- a/.bumper/Tests/WhereArchitectureTests.swift +++ b/.bumper/Tests/WhereArchitectureTests.swift @@ -78,7 +78,7 @@ func `Where architecture accepts downward dependencies`() throws { SourceInput( path: "Where/WhereUI/Sources/Screen.swift", component: ComponentID(WhereComponent.whereUI.rawValue), - source: "import WhereCore\nimport SwiftUI\nstruct Screen {}", + source: "import WhereCore\nimport SwiftUI\nimport ViewDocumentCore\nimport ViewDocumentUI\nstruct Screen {}", ), ], ), diff --git a/BumperBowling.swift b/BumperBowling.swift index dc5f3d490..405dcf400 100644 --- a/BumperBowling.swift +++ b/BumperBowling.swift @@ -104,6 +104,8 @@ let bumper = BumperProject { Owns("Where/WhereIntents/Tests") Owns("Where/Where/Tests") MayDependOn( + .viewDocumentCore, + .viewDocumentUI, .periscope, .installerKit, .installerKitUI, @@ -136,7 +138,15 @@ let bumper = BumperProject { Component(.whereUI) { Owns("Where/WhereUI/Sources") Modules("WhereUI") - MayDependOn(.periscope, .installerKit, .installerKitUI, .regionKit, .whereCore) + MayDependOn( + .periscope, + .installerKit, + .installerKitUI, + .regionKit, + .whereCore, + .viewDocumentCore, + .viewDocumentUI, + ) Applies(.wherePresentationLayer) } diff --git a/Package.swift b/Package.swift index b5b9c1cbd..925bab557 100644 --- a/Package.swift +++ b/Package.swift @@ -214,6 +214,8 @@ let package = Package( .target( name: "WhereUI", dependencies: [ + .target(name: "ViewDocumentCore"), + .target(name: "ViewDocumentUI"), .target(name: "InstallerKitUI"), .target(name: "InstallerKit"), .target(name: "WhereCore"), diff --git a/Project.swift b/Project.swift index 82546c9b5..7f3ad0dce 100644 --- a/Project.swift +++ b/Project.swift @@ -715,6 +715,7 @@ let project = Project( bundleIdSuffix: "whereui", productDependency: "WhereUI", sources: ["Where/WhereUI/Tests/**"], + resources: ["Where/WhereUI/Tests/Fixtures/Themes/*.json"], // The parser initializes SwiftUI accessibility for hosted interaction tests, // including on fresh CI simulators with no accessibility tooling attached. extraPackageProducts: ["AccessibilitySnapshotParser", "LifecycleKit", "SFSafeSymbols"], diff --git a/Tools/AGENTS.md b/Tools/AGENTS.md index fb8743766..5f05b05e2 100644 --- a/Tools/AGENTS.md +++ b/Tools/AGENTS.md @@ -33,6 +33,7 @@ These rules also apply when changing a root command or installer that calls this ## Snapshot shard plans +- Exclude helper-only Swift files from suite discovery. Files named `*Tests.swift` or containing test declarations must declare one matching top-level suite. Guard: `test_snapshot_shards.py`. - Keep measured timings in balance reports, not the persisted execution plan. - Preserve assignments unless intake, the improvement threshold, or an explicit shard-count change requires an update. Guard: [test_snapshot_shards.py](Tests/test_snapshot_shards.py). diff --git a/Tools/README.md b/Tools/README.md index c041fd910..6a514a9b1 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -93,6 +93,8 @@ The [Astro project](InstallerKit/Site/README.md) owns routes, components, and st ## Snapshot shard policy +Discovery accepts shared Swift helpers without test declarations. Each test file must contain one matching top-level suite; helper names cannot hide tests. + `snapshot_shards.py` keeps weekly assignments stable. Its bounded greedy search places intake first, then stops existing-suite moves at a 5% reduction in the slowest worker. If single-suite moves cannot reach that target, it discards them. diff --git a/Tools/Tests/test_snapshot_shards.py b/Tools/Tests/test_snapshot_shards.py index 6e9aeaa8b..c864b1254 100755 --- a/Tools/Tests/test_snapshot_shards.py +++ b/Tools/Tests/test_snapshot_shards.py @@ -169,6 +169,53 @@ def test_rejects_a_test_file_with_a_different_suite_name(self): with self.assertRaisesRegex(ValueError, "must declare one top-level suite"): MODULE.source_inventory(repo) + def test_shared_snapshot_helpers_are_not_suites(self): + with tempfile.TemporaryDirectory() as directory: + repo = self.make_repo(directory) + helper = repo / "Module" / "SnapshotTests" / "WhereThemeSnapshotGate.swift" + helper.write_text( + "protocol WhereThemeSnapshotGate {}\n" + "enum LightDarkSnapshots {}\n" + ) + plan = repo / "plan.json" + plan.write_text(json.dumps(self.make_plan())) + + self.assertEqual(len(MODULE.source_inventory(repo)), 3) + result = subprocess.run( + [sys.executable, SCRIPT, "--plan", plan, "check", "--repo", repo], + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, "") + self.assertIn("3 suites", result.stdout) + + def test_helper_names_cannot_hide_test_declarations(self): + for source in ( + "struct HiddenTests { @Test func example() {} }\n", + "enum Helpers {\n struct Nested { @Test func example() {} }\n}\n", + "enum Helpers {\n struct Nested { @Testing.Test func example() {} }\n}\n", + ): + with ( + self.subTest(source=source), + tempfile.TemporaryDirectory() as directory, + ): + repo = self.make_repo(directory) + helper = repo / "Module" / "SnapshotTests" / "Helpers.swift" + helper.write_text(source) + with self.assertRaisesRegex( + ValueError, "must declare one top-level suite" + ): + MODULE.source_inventory(repo) + + def test_empty_test_files_are_rejected(self): + with tempfile.TemporaryDirectory() as directory: + repo = self.make_repo(directory) + source = repo / "Module" / "SnapshotTests" / "EmptyTests.swift" + source.write_text("import Testing\n") + with self.assertRaisesRegex(ValueError, "must declare one top-level suite"): + MODULE.source_inventory(repo) + def test_compares_the_plan_with_xcode_enumeration(self): with tempfile.TemporaryDirectory() as directory: path = pathlib.Path(directory) / "suites.txt" diff --git a/Tools/snapshot_shards.py b/Tools/snapshot_shards.py index a5a2a6cae..f1ff5fde9 100755 --- a/Tools/snapshot_shards.py +++ b/Tools/snapshot_shards.py @@ -75,6 +75,12 @@ def source_inventory(repo): text, re.MULTILINE, ) + if ( + not suite.endswith("Tests") + and not declarations + and not re.search(r"@(?:Testing\.)?Test\b", text) + ): + continue if declarations != [suite]: relative = path.relative_to(repo) found = ", ".join(declarations) if declarations else "none" diff --git a/Where/Where/README.md b/Where/Where/README.md index 6608c4211..d0a8b756c 100644 --- a/Where/Where/README.md +++ b/Where/Where/README.md @@ -119,3 +119,5 @@ Before shipping a schema change: On a fresh install, onboarding recommends automatic recording On for an iPhone only when no other device recently reported recording, and Off for an iPad/other device or explicit rejoin, then requires the user to confirm. Existing installations created before that choice was introduced revisit only the final recording page once. Enabling is the only path that asks for location access. + +Standard, Flat, and Halloween share the saved theme preference and root rendering path in every audience. Appearance Settings saves the selection. Onboarding previews it through `WhereModel` and commits it on completion. The normal theme handoff supplies widgets and App Intents. diff --git a/Where/Where/Sources/RegularApplicationRuntime.swift b/Where/Where/Sources/RegularApplicationRuntime.swift index 8e9b0b884..76c2d5865 100644 --- a/Where/Where/Sources/RegularApplicationRuntime.swift +++ b/Where/Where/Sources/RegularApplicationRuntime.swift @@ -164,15 +164,16 @@ final class RegularApplicationRuntime: WhereApplicationRuntime { func makeRootView() -> AnyView { #if DEBUG - AnyView(RootView( + let root = RootView( model: model, launcher: launcher, primaryAppIconName: buildEnvironment.primaryAppIconName, updates: updates, developerLaunchController: developerLaunchController, - )) + ) + return AnyView(root) #else - AnyView(RootView( + return AnyView(RootView( model: model, launcher: launcher, primaryAppIconName: buildEnvironment.primaryAppIconName, diff --git a/Where/WhereCore/README.md b/Where/WhereCore/README.md index 423383348..6c369f662 100644 --- a/Where/WhereCore/README.md +++ b/Where/WhereCore/README.md @@ -279,6 +279,7 @@ one it belongs to rather than to a god-object: read, with a freshness policy for the independently aggregated data. - **`WidgetPresentationPublisher`** — atomically writes the device-local `WhereTheme` to its own App Group file and reloads WidgetKit without reading or rebuilding widget data. + Bundled seasonal themes use this same preference and publication path. - **`BackupCoordinator`** — ZIP export/import via `ZIPFoundation`. Export pins tables, planned-stay and home-region revisions, sample-attribution revisions, and evidence blobs to one generation-consistent snapshot. Merge preserves queued locations diff --git a/Where/WhereCore/Sources/Preferences/WhereTheme.swift b/Where/WhereCore/Sources/Preferences/WhereTheme.swift index 33da68f3d..57648332f 100644 --- a/Where/WhereCore/Sources/Preferences/WhereTheme.swift +++ b/Where/WhereCore/Sources/Preferences/WhereTheme.swift @@ -9,4 +9,7 @@ public enum WhereTheme: String, CaseIterable, Codable, Hashable, Sendable { /// The Flat presentation theme. Its original raw value remains permanent. case alternate + + /// Illustrated pumpkins and plum paper surfaces. + case halloween } diff --git a/Where/WhereCore/Tests/WherePreferencesTests.swift b/Where/WhereCore/Tests/WherePreferencesTests.swift index 4c05417d2..2840370aa 100644 --- a/Where/WhereCore/Tests/WherePreferencesTests.swift +++ b/Where/WhereCore/Tests/WherePreferencesTests.swift @@ -128,12 +128,13 @@ struct WherePreferencesTests { } } - @Test func themeRoundTripsAndUnknownValuesFallBackToStandard() { + @Test(arguments: WhereTheme.allCases) + func themeRoundTripsAndUnknownValuesFallBackToStandard(theme: WhereTheme) { let store = InMemoryKeyValueStore() let preferences = WherePreferences(store: store) - preferences.theme = .alternate - #expect(preferences.theme == .alternate) + preferences.theme = theme + #expect(WherePreferences(store: store).theme == theme) store.set("future-theme", forKey: "where.theme") #expect(preferences.theme == .standard) diff --git a/Where/WhereCore/Tests/WhereThemeTests.swift b/Where/WhereCore/Tests/WhereThemeTests.swift index c5c3bb636..0420ff8cf 100644 --- a/Where/WhereCore/Tests/WhereThemeTests.swift +++ b/Where/WhereCore/Tests/WhereThemeTests.swift @@ -6,6 +6,7 @@ struct WhereThemeTests { @Test func rawValuesAreStable() { #expect(WhereTheme.standard.rawValue == "standard") #expect(WhereTheme.alternate.rawValue == "alternate") + #expect(WhereTheme.halloween.rawValue == "halloween") } @Test(arguments: WhereTheme.allCases) diff --git a/Where/WhereCore/Tests/WidgetPresentationPublisherTests.swift b/Where/WhereCore/Tests/WidgetPresentationPublisherTests.swift index bcf2c8255..8b342f759 100644 --- a/Where/WhereCore/Tests/WidgetPresentationPublisherTests.swift +++ b/Where/WhereCore/Tests/WidgetPresentationPublisherTests.swift @@ -16,7 +16,8 @@ struct WidgetPresentationPublisherTests { } } - @Test func publishesWithoutTouchingWidgetData() async throws { + @Test(arguments: WhereTheme.allCases) + func publishesWithoutTouchingWidgetData(theme: WhereTheme) async throws { let directory = FileManager.default.temporaryDirectory .appending(path: "WidgetPresentationPublisherTests-\(UUID().uuidString)") try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) @@ -25,9 +26,9 @@ struct WidgetPresentationPublisherTests { let reloads = ReloadSpy() let publisher = WidgetPresentationPublisher(store: store, reloadTimelines: reloads.reload) - await publisher.publish(.alternate) + await publisher.publish(theme) - #expect(store.readTheme() == .alternate) + #expect(store.readTheme() == theme) #expect(reloads.reloadCount == 1) #expect(!FileManager.default.fileExists( atPath: directory.appending(path: "widget-snapshot.json").path, diff --git a/Where/WhereCore/Tests/WidgetPresentationStoreTests.swift b/Where/WhereCore/Tests/WidgetPresentationStoreTests.swift index 23dcb3035..4b10b9078 100644 --- a/Where/WhereCore/Tests/WidgetPresentationStoreTests.swift +++ b/Where/WhereCore/Tests/WidgetPresentationStoreTests.swift @@ -11,13 +11,14 @@ struct WidgetPresentationStoreTests { try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) } - @Test func writeThenReadRoundTrips() throws { + @Test(arguments: WhereTheme.allCases) + func writeThenReadRoundTrips(theme: WhereTheme) throws { defer { try? FileManager.default.removeItem(at: directory) } let store = WidgetPresentationStore(directory: directory) - try store.write(theme: .alternate) + try store.write(theme: theme) - #expect(store.readTheme() == .alternate) + #expect(store.readTheme() == theme) } @Test func missingOrUnknownPresentationDefaultsToStandard() throws { diff --git a/Where/WhereIntents/Tests/IntentServicesTests.swift b/Where/WhereIntents/Tests/IntentServicesTests.swift index b0c1c5b62..1943ce33b 100644 --- a/Where/WhereIntents/Tests/IntentServicesTests.swift +++ b/Where/WhereIntents/Tests/IntentServicesTests.swift @@ -82,15 +82,16 @@ struct IntentServicesTests { #expect(resolved.journal === replacement.journal) } - @Test func themeUpdatesAtomicallyWithoutReplacingTheStack() async throws { + @Test(arguments: WhereTheme.allCases) + func themeUpdatesAtomicallyWithoutReplacingTheStack(theme: WhereTheme) async throws { let handoff = IntentServices(appGroupIdentifier: appGroupIdentifier) let stack = try makeStack() await handoff.install(stack, theme: .standard) - await handoff.updateTheme(.alternate) + await handoff.updateTheme(theme) let context = try await handoff.currentContext() #expect(context.services.journal === stack.journal) - #expect(context.theme == .alternate) + #expect(context.theme == theme) } } diff --git a/Where/WhereUI/AGENTS.md b/Where/WhereUI/AGENTS.md index 4ec9177c3..ce5effac6 100644 --- a/Where/WhereUI/AGENTS.md +++ b/Where/WhereUI/AGENTS.md @@ -245,12 +245,14 @@ worked examples. Guard: `RegionAppearanceCatalogTests.idDerivedDefaultsHaveStableAssignments`. - **Seed `WhereTheme` through `whereBroadwayRoot(theme:regionStyles:)`.** Standard and Flat use the stable `standard` and `alternate` identities. -- Keep Standard as the base stylesheet. Resolve shared traits before applying `WhereThemeOverrides`. - Put theme values and theme-specific trait decisions in their definition, such as `WhereFlatTheme`. - Keep override application independent of theme selection and trait resolution. - Omitted overrides inherit the adjusted base. Explicit zero values disable effects. - Use `BModeVarying` for paired light/dark values. Keep runtime region colors outside theme definitions. - Guards: `WhereThemeOverridesTests`, `WhereFlatThemeTests`. +- Bundled JSON is authoritative. Standard is a complete root; Flat inherits Standard; Halloween inherits Flat. Resolve documents through `WhereThemeCatalog` before rendering. Do not add a native theme override path. +- Keep portable appearance in `Sources/Themes/WhereThemeAppearance*`. Add each authored stylesheet field to its typed patch, inheritance, canonical Standard document, and mapping tests. Keep runtime geometry, region identity, data reduction, and lifecycle scheduling native. +- Omitted fields inherit. Explicit values replace. Artwork arrays replace as a unit, including empty arrays. Optional visuals use `WhereThemeOptional.clear`. A literal value replaces an inherited named reference; do not supply both in one document. +- Validate complete roots, parents, named references, complexity, and placement before root injection. Prepare assets once. Cache identity includes complete resolved content, not only its ID. Failed imports retain the last accepted theme. +- Apply native accessibility constraints after document resolution. Keep ornament separate from essential surfaces. Inject static-motion policy and actual host activity into Broadway's renderer. +- Use component surfaces for interior and exterior artwork. Keep paint bounds finite, reserve gaps and gutters in parent layouts, and preserve viewport clipping. Exterior artwork must not expand interaction or accessibility bounds. Keep the inner card as the zoom source. +- Validate host capabilities before injection. Keep List rows and joined timeline segments interior-only. Apply artwork spacing outside buttons and accessibility groups in other hosts. Guard: `WhereThemeComponentsCapabilitiesTests`. +- Route every bundled theme through the saved `WhereTheme` selection and `whereBroadwayRoot`. Use the same selection flow in every app audience, including onboarding and relaunch. Guards: `WhereModelTests`, `WhereStylesheetEnvironmentTests`. - **Design new and changed custom components for every supported theme.** Resolve theme and accessibility choices in the owning `WhereStylesheet` component style. Views consume that style without checking the selected theme. @@ -334,3 +336,5 @@ lightweight widget/snippet surfaces do not need duplicate theme cases. Other themes use stable consumer-owned groups (`flat/` for `.alternate`). Mixed-theme sheets use `themes/`. Special trait images use `other/` inside the selected group. Never derive the folder from a localized label or rename a persisted theme identity. + +- Keep composite theme appearance groups boxed with `CopyOnWrite`. Background JSON decoding must fit a worker thread stack; preserve the unboxed JSON shape. Guard: `WhereThemeAppearanceTests`. diff --git a/Where/WhereUI/Artwork.md b/Where/WhereUI/Artwork.md new file mode 100644 index 000000000..fd8b6bf23 --- /dev/null +++ b/Where/WhereUI/Artwork.md @@ -0,0 +1,11 @@ +# Halloween artwork + +The Halloween theme uses three original illustrations generated for Where with OpenAI ImageGen on 2026-10-29. + +- `HalloweenPumpkins.png`: apricot and cream pumpkins, sage vines, and lavender mushrooms. +- `HalloweenMoon.png`: a crescent moon, bat, and sparse stars. +- `HalloweenVine.png`: a sage vine with an apricot flower and lavender star. + +Each PNG has a transparent background. The images contain no text or controls. Their shared palette uses cream, lavender, sage, apricot, and plum outlines. + +`Sources/Resources/Themes/HalloweenTheme.json` owns placement, opacity, paint bounds, and motion. `HalloweenThemeAssets` supplies only these approved bundled resources. Native SwiftUI owns text, interaction, borders, and layout. diff --git a/Where/WhereUI/README.md b/Where/WhereUI/README.md index 83bca762c..325cb830e 100644 --- a/Where/WhereUI/README.md +++ b/Where/WhereUI/README.md @@ -449,20 +449,14 @@ User-picked appearances and the original bespoke defaults take precedence. Glass and reflective artwork. Flat uses opaque, strongly colored paper with printed edges and neutral drop shadows. It keeps the passport artwork and omits sheen, luminous glows, and GPS-dot halos. The welcome, Elsewhere, privacy, -and estimate surfaces share this finish. System chrome stays native. +and estimate surfaces share this finish. Halloween adds illustrated edges and coordinated paper and ink colors. System chrome stays native. -Theme assembly starts with Standard defaults, resolves shared traits, then applies -`WhereThemeOverrides`. Standard supplies empty overrides. `WhereFlatTheme` owns -Flat's values and resolves them against the adjusted base. Its `BModeVarying` -values keep light and dark choices together. Accessibility decisions stay explicit -in the resolver. +Theme assembly starts with the bundled JSON catalog. Standard defines every authored value. +Flat inherits Standard and replaces its finishes. Halloween inherits Flat. Every theme uses the same complete resolved document path. +The stylesheet selects light or dark values, then applies native accessibility constraints. +Runtime region colors and live Card Designer drafts retain their existing ownership. -Overrides are `Equatable` values grouped by component. Omitted fields inherit the -base, while explicit zero values disable effects. Application assigns only the -specified fields. Views continue to consume complete component styles, and live -Card Designer drafts compose over those styles. - -New custom components support both themes through their resolved styles. +New custom components support every bundled theme through their resolved styles. The [module rules](AGENTS.md) define surface treatments and required snapshot coverage. Flat adjusts region text colors for contrast against its opaque fills. @@ -470,8 +464,8 @@ The palette test covers every selectable region color in light and dark modes. Flat retains the persisted `alternate` raw value. Onboarding previews before commit. Appearance Settings persists immediately. Widgets and snippets -receive the same identity through their cross-process snapshots. Their existing -lightweight content and system backgrounds stay unchanged. +receive the same identity through the widget presentation file and App Intents handoff. +They retain their lightweight native content and system backgrounds. Both `RegionSummaryCard` sizes ask the root-owned `RegionOutlinePathCache` for region artwork. The watermark uses a medium path, and the stamp seal uses a @@ -485,8 +479,8 @@ paper in dark mode. Silhouettes, stamps, and borders use the original region col with the shared decorative opacities. Text uses a separate contrast adjustment. Its cards omit annual progress bars. Compact primary specimens in theme pickers and galleries retain the selected Locations theme. Elsewhere uses the shared -matte renderer in both themes. Flat supplies its outline and shadow and -preserves the white or neutral-dark fill. +matte renderer in Standard and Flat. Flat supplies its outline and shadow and +preserves the white or neutral-dark fill. Halloween supplies its own paper, ink, and artwork. Regions without geometry retain the symbol treatment. On both tabs, recorded fixes for the selected year are projected through that same geometry and reduced to a clipped, static constellation of glowing pinpricks. Manually logged days add no invented @@ -532,7 +526,7 @@ as dates pass. Appearance settings can hide estimates without removing plans or The Estimated Time settings page also opens the planner. On Locations, the collapsible estimate uses soft glass with faint security print, primary-region watermarks, and a translucent black outer stroke. -Flat appearance and Reduce Transparency use opaque paper. Calendar, Timeline, and discovery panels retain their passport-visa appearance. +Flat and Halloween use opaque paper. Reduce Transparency also enforces opaque paper. Calendar, Timeline, and discovery panels retain their passport-visa appearance. The estimate's outline sits outside the card in both Standard and Flat, blending with the surrounding background. `LocationForecastSummary` owns the Locations disclosure and expansion state. It shares `LocationForecastContent` (forecast rows and planning controls) with the always-expanded @@ -691,3 +685,62 @@ for changes confined to a child component. Standard references keep existing pat Flat screen references use `flat/`, and comparison sheets use `themes/`. Special traits nest under the group's `other/` directory. Review sheet PNGs before recording. Their embedded panel maps are part of the reference; preserve PNG metadata when copying them. + +## Portable themes + +`WhereThemeDocument` uses schema version 2. Each document defines a complete root or names a parent with a typed identity. +Standard is the canonical root. Flat inherits Standard. Halloween inherits Flat. +The persisted `WhereTheme` identities remain `standard` and `alternate`. + +`WhereThemeAppearance` contains typed component geometry, colors, typography, and motion. +`WhereThemeValues` names colors, fonts, and surfaces. `WhereThemeComponents` references these values through typed roles. +Omitted fields inherit. Explicit values replace. Named values merge by identity. +Artwork collections replace as a unit; an empty collection removes inherited artwork. +`WhereThemeOptional.clear` removes an optional visual part or named reference. +A direct appearance value replaces an inherited reference. Supplying both in one document is an error. + +Prepare a theme before passing it to a view: + +```swift +let catalog = try WhereThemeCatalog(documents: documents) +let prepared = try catalog.prepare(themeID, assets: assets) +content.whereBroadwayRoot(themeDocument: prepared) +``` + +`whereBroadwayRoot(theme:)` selects prepared bundled documents through the same path. +Rootless stylesheet defaults come from canonical Standard. Native property initializers provide allocation storage and a logged packaging-failure fallback. +They do not supply omitted values in accepted documents. + +The catalog rejects incomplete roots, missing parents, cycles, invalid named references, and invalid appearance values. +Preparation checks artwork resources, placement, and shared rendering budgets before injection. +A catalog contains at most 64 documents and four MiB of encoded content. Inheritance has a depth limit of 16. +Resolution walks the parent chain without recursion. Composite appearance groups use copy-on-write storage so background decoding fits a worker thread stack. +Each document retains ViewDocument's byte, node, animation, modifier, and transform limits. +Version 1 imports convert to version 2. The old seasonal payloads remain test fixtures. + +Component surfaces compose base paint, interior artwork, native content, highlights, and exterior artwork. +Native passport marks have document-owned parameters and explicit selection. +Names, counts, geographic data, progress, controls, localization, and accessibility remain native. +Artwork declares an anchor, size, offset, drawing plane, and finite per-edge outsets. +Interior artwork clips to the surface. Exterior artwork clips to its paint bounds and cannot cover the protected content center. +Parent layouts reserve gaps and gutters. Overflow does not change hit targets or accessibility bounds. +Apply `whereThemeArtworkSpace` outside buttons and accessibility groups in additional card hosts. +Native navigation owns page headings. Pages and List rows use `background`, not `surface`. +List rows and joined timeline segments accept interior artwork only, preserving native clipping and connected rails. +The catalog rejects unsupported fields and native artwork sources; see `WhereThemeComponents.Capabilities.swift` for the role contract. +Privacy paper, text, and artwork share a dark appearance. Its elevation responds to the surrounding page. + +Native accessibility constraints apply after theme resolution. Essential backgrounds remain visible when optional ornament is hidden. +Native component fonts scale with Dynamic Type, including authored point-size baselines. +Supporting text uses stronger document ink when a component supplies a foreground color. Built-in secondary text retains its existing system style. +Reduce Motion and snapshots use authored static poses. Animation also depends on visibility, the selected tab, scene activity, and covering presentations. + +Standard, Flat, and Halloween are saved `WhereTheme` choices in every app audience. +Appearance Settings and onboarding share the adaptive picker. `RootView` applies the selection through `whereBroadwayRoot`, including launch and ready content. +The existing widget and App Intents handoffs carry the same selection to their lightweight native surfaces. +Imported documents use `WhereThemeCatalog.replacing` followed by `prepare`; callers accept the replacement only after both succeed. +This iteration includes no downloader or additional widget artwork. + +The fast visual gate uses one iPhone size, normal Dynamic Type, and standard light/dark captures. +Card and Locations comparisons pass before the remaining tabs. Extended entry points retain broader CI coverage and existing reference identities. +The full integration suite and extended visual matrix run after the light/dark gate passes. diff --git a/Where/WhereUI/SnapshotTests/AppearanceSettingsViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/AppearanceSettingsViewSnapshotTests.swift index 411b44e68..0c63fbbb6 100644 --- a/Where/WhereUI/SnapshotTests/AppearanceSettingsViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/AppearanceSettingsViewSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct AppearanceSettingsViewSnapshotTests { - @Test func appearance() async { - await assertSnapshots(of: AppearanceSettingsView.self) + @Test func appearanceLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "appearance()", + ) + } + + @Test func appearanceExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "appearance()", + ) + } +} + +extension AppearanceSettingsView: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Default", "FlatSelected"] } } diff --git a/Where/WhereUI/SnapshotTests/CalendarContentViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/CalendarContentViewSnapshotTests.swift index 775f76ede..ee2beed26 100644 --- a/Where/WhereUI/SnapshotTests/CalendarContentViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/CalendarContentViewSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct CalendarContentViewSnapshotTests { - @Test func calendarContent() async { - await assertSnapshots(of: CalendarContentView.self) + @Test func calendarContentLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "calendarContent()", + ) + } + + @Test func calendarContentExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "calendarContent()", + ) + } +} + +extension CalendarContentView: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["WithData", "YearOverview", "YearOverviewPlannedStays"] } } diff --git a/Where/WhereUI/SnapshotTests/ElsewhereViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/ElsewhereViewSnapshotTests.swift index a09a24015..99650212a 100644 --- a/Where/WhereUI/SnapshotTests/ElsewhereViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/ElsewhereViewSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct ElsewhereViewSnapshotTests { - @Test func elsewhere() async { - await assertSnapshots(of: ElsewhereView.self) + @Test func elsewhereLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "elsewhere()", + ) + } + + @Test func elsewhereExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "elsewhere()", + ) + } +} + +extension ElsewhereView: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Loaded", "FlatLoaded"] } } diff --git a/Where/WhereUI/SnapshotTests/LocationForecastPanelSnapshotTests.swift b/Where/WhereUI/SnapshotTests/LocationForecastPanelSnapshotTests.swift index 2465f6d32..f40c459f0 100644 --- a/Where/WhereUI/SnapshotTests/LocationForecastPanelSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/LocationForecastPanelSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct LocationForecastPanelSnapshotTests { - @Test func componentThemes() async { - await assertSnapshots(of: LocationForecastPanel.self) + @Test func componentThemesLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "componentThemes()", + ) + } + + @Test func componentThemesExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "componentThemes()", + ) + } +} + +extension LocationForecastPanel: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Expanded"] } } diff --git a/Where/WhereUI/SnapshotTests/LocationForecastSummarySnapshotTests.swift b/Where/WhereUI/SnapshotTests/LocationForecastSummarySnapshotTests.swift index 3967e67dd..834fefe3d 100644 --- a/Where/WhereUI/SnapshotTests/LocationForecastSummarySnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/LocationForecastSummarySnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct LocationForecastSummarySnapshotTests { - @Test func componentThemes() async { - await assertSnapshots(of: LocationForecastSummary.self) + @Test func componentThemesLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "componentThemes()", + ) + } + + @Test func componentThemesExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "componentThemes()", + ) + } +} + +extension LocationForecastSummary: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Collapsed"] } } diff --git a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift index 16bd0120e..b92c7b4c4 100644 --- a/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/LocationsViewSnapshotTests.swift @@ -4,7 +4,27 @@ import Testing @MainActor struct LocationsViewSnapshotTests { - @Test func locations() async { - await assertSnapshots(of: LocationsView.self) + @Test func seasonalDocuments() async { + await assertSnapshots(of: LocationsView.SeasonalThemes.self) + } + + @Test func locationsLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "locations()", + ) + } + + @Test func locationsExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "locations()", + ) + } +} + +extension LocationsView: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Loaded", "FlatLoaded"] } } diff --git a/Where/WhereUI/SnapshotTests/MainTabsSnapshotTests.swift b/Where/WhereUI/SnapshotTests/MainTabsSnapshotTests.swift index 81957685f..74146411d 100644 --- a/Where/WhereUI/SnapshotTests/MainTabsSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/MainTabsSnapshotTests.swift @@ -1,9 +1,41 @@ +@_spi(Capture) import SnapshotKit import SnapshotKitTesting import Testing @testable import WhereUI @MainActor struct MainTabsSnapshotTests { + @Test func halloweenLocationsLightDark() async throws { + try await assertThemeSnapshot(MainTabs.halloweenSnapshot(selection: .locations)) + } + + @Test func halloweenOtherTabsLightDark() async throws { + for selection: MainTabSelection.TabID in [.elsewhere, .year, .settings] { + try await assertThemeSnapshot(MainTabs.halloweenSnapshot(selection: selection)) + } + } + + @Test func halloweenLocationsExtended() async throws { + let snapshot = try await MainTabs.halloweenSnapshot(selection: .locations) + await assertThemeSnapshot(snapshot.withThemeConfigurations([ + .init(dynamicType: .accessibility5, device: .iPhoneFullContent), + .init(contrast: .increased, device: .iPhoneFullContent), + .init(device: .iPhoneFullContent, snapshotType: .accessibility), + .init(device: .iPadFullContent), + .init(colorScheme: .dark, device: .iPadFullContent), + ])) + } + + @Test func halloweenOtherTabsExtended() async throws { + for selection: MainTabSelection.TabID in [.elsewhere, .year, .settings] { + let snapshot = try await MainTabs.halloweenSnapshot(selection: selection) + await assertThemeSnapshot(snapshot.withThemeConfigurations([ + .init(dynamicType: .accessibility5, device: .iPhoneFullContent), + .init(device: .iPadFullContent), + ])) + } + } + @Test func mainTabs() async { await assertSnapshots(of: MainTabs.self) } diff --git a/Where/WhereUI/SnapshotTests/OnboardingThemeSelectionViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/OnboardingThemeSelectionViewSnapshotTests.swift index d8d6aeb13..60c988738 100644 --- a/Where/WhereUI/SnapshotTests/OnboardingThemeSelectionViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/OnboardingThemeSelectionViewSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct OnboardingThemeSelectionViewSnapshotTests { - @Test func themeSelection() async { - await assertSnapshots(of: OnboardingThemeSelectionView.self) + @Test func themeSelectionLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "themeSelection()", + ) + } + + @Test func themeSelectionExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "themeSelection()", + ) + } +} + +extension OnboardingThemeSelectionView: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Standard", "Alternate", "Halloween"] } } diff --git a/Where/WhereUI/SnapshotTests/PlannedStaysViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/PlannedStaysViewSnapshotTests.swift index 94d05fbe8..2acfd6999 100644 --- a/Where/WhereUI/SnapshotTests/PlannedStaysViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/PlannedStaysViewSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct PlannedStaysViewSnapshotTests { - @Test func plannedStays() async { - await assertSnapshots(of: PlannedStaysView.self) + @Test func plannedStaysLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "plannedStays()", + ) + } + + @Test func plannedStaysExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "plannedStays()", + ) + } +} + +extension PlannedStaysView: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Itinerary"] } } diff --git a/Where/WhereUI/SnapshotTests/PresenceTimelineListSnapshotTests.swift b/Where/WhereUI/SnapshotTests/PresenceTimelineListSnapshotTests.swift index 1d0be2661..d85937be0 100644 --- a/Where/WhereUI/SnapshotTests/PresenceTimelineListSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/PresenceTimelineListSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct PresenceTimelineListSnapshotTests { - @Test func presenceTimeline() async { - await assertSnapshots(of: PresenceTimelineList.self) + @Test func presenceTimelineLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "presenceTimeline()", + ) + } + + @Test func presenceTimelineExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "presenceTimeline()", + ) + } +} + +extension PresenceTimelineList: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["WithData"] } } diff --git a/Where/WhereUI/SnapshotTests/PrivacyPassportCardSnapshotTests.swift b/Where/WhereUI/SnapshotTests/PrivacyPassportCardSnapshotTests.swift index 4331679f1..2233aad81 100644 --- a/Where/WhereUI/SnapshotTests/PrivacyPassportCardSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/PrivacyPassportCardSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct PrivacyPassportCardSnapshotTests { - @Test func componentThemes() async { - await assertSnapshots(of: PrivacyPassportCard.self) + @Test func componentThemesLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "componentThemes()", + ) + } + + @Test func componentThemesExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "componentThemes()", + ) + } +} + +extension PrivacyPassportCard: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Privacy"] } } diff --git a/Where/WhereUI/SnapshotTests/RegionSummaryCardSnapshotTests.swift b/Where/WhereUI/SnapshotTests/RegionSummaryCardSnapshotTests.swift index 089136098..ee3cdc25b 100644 --- a/Where/WhereUI/SnapshotTests/RegionSummaryCardSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/RegionSummaryCardSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct RegionSummaryCardSnapshotTests { - @Test func componentThemes() async { - await assertSnapshots(of: RegionSummaryCard.self) + @Test func componentThemesLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "componentThemes()", + ) + } + + @Test func componentThemesExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "componentThemes()", + ) + } +} + +extension RegionSummaryCard: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Regular", "Compact", "Elsewhere"] } } diff --git a/Where/WhereUI/SnapshotTests/RegionWelcomeCardSnapshotTests.swift b/Where/WhereUI/SnapshotTests/RegionWelcomeCardSnapshotTests.swift index 0bcbea69b..c295a625e 100644 --- a/Where/WhereUI/SnapshotTests/RegionWelcomeCardSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/RegionWelcomeCardSnapshotTests.swift @@ -4,7 +4,23 @@ import Testing @MainActor struct RegionWelcomeCardSnapshotTests { - @Test func regionWelcomeCard() async { - await assertSnapshots(of: RegionWelcomeCard.self) + @Test func regionWelcomeCardLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "regionWelcomeCard()", + ) + } + + @Test func regionWelcomeCardExtended() async { + await assertSnapshots( + of: WhereExtendedSnapshots.self, + testName: "regionWelcomeCard()", + ) + } +} + +extension RegionWelcomeCard: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Welcome"] } } diff --git a/Where/WhereUI/SnapshotTests/SettingsViewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/SettingsViewSnapshotTests.swift index 67f65ca90..9ea86c7b9 100644 --- a/Where/WhereUI/SnapshotTests/SettingsViewSnapshotTests.swift +++ b/Where/WhereUI/SnapshotTests/SettingsViewSnapshotTests.swift @@ -4,7 +4,20 @@ import Testing @MainActor struct SettingsViewSnapshotTests { - @Test func settings() async { - await assertSnapshots(of: SettingsView.self) + @Test func settingsLightDark() async { + await assertSnapshots( + of: WhereLightDarkSnapshots.self, + testName: "settings()", + ) + } + + @Test func settingsExtended() async { + await assertSnapshots(of: WhereExtendedSnapshots.self, testName: "settings()") + } +} + +extension SettingsView: WhereThemeSnapshotGate { + static var lightDarkCases: Set { + ["Default"] } } diff --git a/Where/WhereUI/SnapshotTests/WhereThemePreviewSnapshotTests.swift b/Where/WhereUI/SnapshotTests/WhereThemePreviewSnapshotTests.swift new file mode 100644 index 000000000..712b945d0 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/WhereThemePreviewSnapshotTests.swift @@ -0,0 +1,82 @@ +import SnapshotKit +import SnapshotKitTesting +import SwiftUI +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemePreviewSnapshotTests { + @Test func seasonalDocumentsLightDark() async throws { + try await capture(types: [.standard]) + } + + @Test func sharedSurfacesLightDark() async throws { + for subject in WhereThemePreview.Subject.allCases where subject != .appearance { + try await assertThemeSnapshot(WhereThemePreview.snapshot(subject)) + } + } + + @Test func appearanceLightDark() async throws { + try await assertThemeSnapshot( + WhereThemePreview.snapshot(.appearance), + testName: "sharedSurfacesLightDark()", + ) + } + + @Test func seasonalDocumentsExtended() async throws { + try await capture(types: [.accessibility]) + } + + @Test func sharedSurfacesExtended() async throws { + for subject in WhereThemePreview.Subject.allCases where subject != .appearance { + let snapshot = try WhereThemePreview.snapshot(subject) + let configurations: [SnapshotConfiguration] = switch subject { + case .forecast, .welcome, .privacy: + [ + .init(dynamicType: .accessibility5), + .init(contrast: .increased), + .init(snapshotType: .accessibility), + ] + case .timeline, .appearance, .planning: + [ + .init(dynamicType: .accessibility5, device: .iPhoneFullContent), + .init(device: .iPhoneFullContent, snapshotType: .accessibility), + ] + } + await assertThemeSnapshot(snapshot.withThemeConfigurations(configurations)) + } + } + + @Test func appearanceExtended() async throws { + let snapshot = try WhereThemePreview.snapshot(.appearance) + await assertThemeSnapshot( + snapshot.withThemeConfigurations([ + .init(dynamicType: .accessibility5, device: .iPhoneFullContent), + .init(device: .iPhoneFullContent, snapshotType: .accessibility), + ]), + testName: "sharedSurfacesExtended()", + ) + } + + @Test func accessibilityPolicyExtended() async throws { + for policy in WhereThemePreview.AccessibilityPolicy.allCases { + try await assertThemeSnapshot(WhereThemePreview.snapshot(policy)) + } + } + + private func capture(types: [SnapshotConfiguration.SnapshotType]) async throws { + for example in WhereThemeExamples.allCases { + let prepared = try example.prepare() + await assertSnapshots( + of: WhereThemePreview(theme: prepared), + named: example.rawValue, + configurations: SnapshotConfiguration.combinations( + devices: [.init(name: "iPhone", size: .intrinsic(maxWidth: 402))], + colorSchemes: [.light, .dark], + snapshotTypes: types, + ), + testName: "seasonalDocuments()", + ) + } + } +} diff --git a/Where/WhereUI/SnapshotTests/WhereThemeSnapshotGate.swift b/Where/WhereUI/SnapshotTests/WhereThemeSnapshotGate.swift new file mode 100644 index 000000000..e501fbd5f --- /dev/null +++ b/Where/WhereUI/SnapshotTests/WhereThemeSnapshotGate.swift @@ -0,0 +1,109 @@ +@_spi(Capture) import SnapshotKit +import SnapshotKitTesting +import SwiftUI +import Testing + +/// The fast gate keeps existing reference identities and partitions, rather than reduces, coverage. +@MainActor +protocol WhereThemeSnapshotGate: SnapshotProviding { + static var lightDarkCases: Set { get } +} + +enum WhereLightDarkSnapshots: SnapshotProviding { + static var snapshots: [SnapshotCase] { + Source.partitionedSnapshots(lightDark: true) + } +} + +enum WhereExtendedSnapshots: SnapshotProviding { + static var snapshots: [SnapshotCase] { + Source.partitionedSnapshots(lightDark: false) + } +} + +extension WhereThemeSnapshotGate { + fileprivate static func partitionedSnapshots(lightDark: Bool) -> [SnapshotCase] { + snapshots.compactMap { snapshot in + let configurations = snapshot.configurations.filter { configuration in + let baseline = lightDarkCases.contains(snapshot.name) + && configuration.snapshotType == .standard + && configuration.dynamicType == .large + && configuration.contrast == .standard + && configuration.layoutDirection == .leftToRight + && configuration.legibilityWeight == .regular + && configuration.layoutTraits == nil + && configuration.name == nil + && ["", "iPhone"].contains(configuration.device.name) + return baseline == lightDark + } + guard !configurations.isEmpty else { return nil } + return snapshot.withThemeConfigurations(configurations) + } + } +} + +@MainActor +func assertThemeSnapshot( + _ snapshot: SnapshotCase, + fileID: StaticString = #fileID, + file: StaticString = #filePath, + testName: String = #function, + line: UInt = #line, + column: UInt = #column, +) async { + if case let .single(content) = snapshot.capture { + await assertSnapshots( + of: content(), + named: snapshot.name, + configurations: snapshot.configurations, + referenceGroup: snapshot.referenceGroup, + measurementReadiness: snapshot.measurementReadiness, + onReadyToMeasure: snapshot.onReadyToMeasure, + settle: snapshot.settle, + onReadyToSnapshot: snapshot.onReadyToSnapshot, + fileID: fileID, + file: file, + testName: testName, + line: line, + column: column, + ) + } else { Issue.record( + "Expected one production screen", + sourceLocation: .init( + fileID: String(describing: fileID), + filePath: String(describing: file), + line: Int(line), + column: Int(column), + ), + ) } +} + +extension SnapshotCase { + /// Shares the production fixture and capture hooks across the two validation gates. + @MainActor func withThemeConfigurations(_ configurations: [SnapshotConfiguration]) -> Self { + switch capture { + case let .single(content): + SnapshotCase( + name: name, + configurations: configurations, + referenceGroup: referenceGroup, + measurementReadiness: measurementReadiness, + onReadyToMeasure: onReadyToMeasure, + settle: settle, + onReadyToSnapshot: onReadyToSnapshot, + ) { content() } + case let .cutsheet(collection): + .cutsheet( + name: name, + configurations: configurations, + referenceGroup: referenceGroup, + measurementReadiness: measurementReadiness, + onReadyToMeasure: onReadyToMeasure, + settle: settle, + onReadyToSnapshot: onReadyToSnapshot, + ) { for panel in collection.panels { + panel + } } + } + } +} diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png index a09fb77a1..0dc3e161d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37b8145152f9d438ea4fddbfccb0546bfd0e0629d0bf5dcad601c3b766da1924 -size 1231383 +oid sha256:c329b1966dd9dd07b46ccfff9316b81c16c5908933e70215d51179bca74bb78f +size 2121204 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png index c1af75a0f..a78bad981 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57b5fdcce962ccd820228b15510eca541555f5a8576addce03ca25d97ee1720d -size 1368348 +oid sha256:e5b498453aa35ffddff881ef70d8f17e216eb70ae4af44539eb48d8cde757f78 +size 2106027 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png index 8ac09a58c..e5f9398cb 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a62d657df150888664cf469ce1205d0656a72ca27cf2b168619d97807329903e -size 1291234 +oid sha256:c781e9beafc98690de75fab2a7172b4d7ec785a0e81e636e565b890b1afd0762 +size 2104772 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png index 17bb62f65..488034a1f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63f2e104ba950c4b157270f8d7286c8206ffc9e801e2915772fde99d9e18a798 -size 796857 +oid sha256:ba12f46ae585e79c1839cf4b56cde65585ada239cc31faa68db9f569f31f5697 +size 1314053 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png index 7664f2577..ac06fa3e3 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ceb135061198960cecb5df50157d86db80fcce12fa96d093f4a632893c8b9c03 -size 1082703 +oid sha256:9de640ecc7da1c43fa2bfa7e5f92a52fa99f854d143cd459bfecb977eb3db09a +size 1538144 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png index 06e799f92..d8447cbc5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ad1aef7910122e156eb15dd91c2d4b70d7b4e6eae68044fa4efd782bc7a2f83 -size 802658 +oid sha256:9157c184d582e2145d985ce7a151bd23bf1608849d03c9a25c66cda4d012b7f4 +size 1263115 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/flat/appearance.FlatSelected_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/flat/appearance.FlatSelected_iPhone.png index e7b701e77..506b6e7fd 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/flat/appearance.FlatSelected_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/flat/appearance.FlatSelected_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3eebe7b03431af1c324f2df82d91f1ab2cd52a6dd29465577ae720940c07b61e -size 782005 +oid sha256:2c41c98d61ecdee426fc5b233c93b7d9e4ef206acd8b8153e7377432bff46f76 +size 1278972 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/flat/appearance.FlatSelected_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/flat/appearance.FlatSelected_iPhone_dark.png index b6416cf97..5bf4b8b88 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/flat/appearance.FlatSelected_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/flat/appearance.FlatSelected_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eaced3ab0c531e3473056cb05e880456224e9ca0b7a79dbb2041c053f2db1ed7 -size 798453 +oid sha256:83c32e19f6a2d297cf8850eeec1dd85094f256e4f8a8e9b7d3f78b5bc9a6d30b +size 1239765 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPad_ax5.png index 4f9e5c214..504aa68ff 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPad_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPad_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6d835fdaafdaad2df1838550a170ea9bd603418965af369906933eece96dff3 -size 3333393 +oid sha256:019e19919ed2974fc494b31d0c487193d9f0da872ef7f7cded961fdbc93c0b67 +size 3827339 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPad_contrast.png index ad1ef88b4..51d09c4bc 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:744495f3c2ba47c7bb61b72bc50c8c6857e5310ecd034715e668aac63e12f51c -size 1264020 +oid sha256:08e5434544c1f6f955ae04cbfeead1cfa30e4b7ee2b8e0bd71028ff2837c6b0c +size 2149037 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPhone_ax5.png index 12c184bab..a4230a5ad 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71fced2174d26ab04da688a26b3a3b6a1ec7e657cf9bac2accef6c51c1cde76f -size 2388481 +oid sha256:eac3ad072ae1d31e01b98c0b7a79cc2da19a5cc93b8c5c1dde6f133c2a702349 +size 2789133 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPhone_contrast.png index b49fa1946..5ed4abd74 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/other/appearance.Default_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c9033d65bd7377a5e9cf58a00df466a09595f0dec0c668a8e0b8d7128fcaf67 -size 822355 +oid sha256:710260e6ddc5f8d68a4a9aa798ada1abccfe02b92169be499de7ff662201959c +size 1328406 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/themes/calendarContent.YearOverviewPlannedStays_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/themes/calendarContent.YearOverviewPlannedStays_iPhone.png index a53b0a447..cbf68c38a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/themes/calendarContent.YearOverviewPlannedStays_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/themes/calendarContent.YearOverviewPlannedStays_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8704d4fb7f993448ab53c287314b17dd2728d1c16a0219eb04d72de8d1a6cccb -size 3400117 +oid sha256:201d2ef81b552a3c7d2dc3db6c47f293adec536dd8d6318be64dc6c2bdfe3743 +size 5323460 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/themes/calendarContent.YearOverviewPlannedStays_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/themes/calendarContent.YearOverviewPlannedStays_iPhone_dark.png index c52554ded..b533e3d7d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/themes/calendarContent.YearOverviewPlannedStays_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/themes/calendarContent.YearOverviewPlannedStays_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:800c9513b00a48f1dba3a49a4e4140f8869c87ae68a77e948bb6b42b213f740f -size 1797043 +oid sha256:9657ba3b16ebe14d793aef7f7e8776943ad352446eb7f0e76caa03ae3587aa76 +size 2933080 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded.png index e69497dd6..1c38a7bf2 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff81d3c469a7dea25b479d34fa832007cab958ad9d759e6518d3c2321c9ce2c7 -size 2730068 +oid sha256:c4e318b9ee37ab982e77037308011dcbe72e19cac56cac74486abef44f6e1ab9 +size 3303910 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded_accessibility.png index ce6fb5070..6cd9be6aa 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29fc63319a6c4918df7f59df66a5f0458ad0c3149f90355871731c8afd2c748e -size 2841715 +oid sha256:1efde41436a1b49fac83527e4eb6913cf506e1f9ea80bf1b74fffaa4996546ae +size 3548757 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded_dark.png index 6bda0d0ee..586f86b64 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/componentThemes.Expanded_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ddd8275162de9880f513f3cda36c02b3c70c28e3ef966e6d5dfb0a7b6a7d65a -size 1153374 +oid sha256:0046380c2ed74da95fed605dbee6638fa4fe49c22ad2b8afa0063771b241f700 +size 1392339 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/other/componentThemes.Expanded_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/other/componentThemes.Expanded_ax5.png index cb86de115..b890f8edf 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/other/componentThemes.Expanded_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastPanelSnapshotTests/themes/other/componentThemes.Expanded_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec750af0b1c94ac8eb243f13b1e5f665aec541e0a68ff307bf03b5a63e4fdd4f -size 8684191 +oid sha256:98b852a829c894a4bd1dc1bc2c36151057a5f8db49b8960e31f86878b51ca2f6 +size 10606599 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 374a5e4f6..50ecd022e 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:c90f8366df42ccb86d9432dec81cae4b47a9c03495f914b7d2dbee9e5133bf29 -size 3092228 +oid sha256:1ab32207ad7e158da78d916ee0d72f61e43e25ee3091408b9aa6349893111239 +size 3710774 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed.png index 1795fd039..69cc88f22 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:122eae0e6a0792485fd0eb981a446cc7f52047bafaee7458474ce9326f8326d8 -size 522733 +oid sha256:12b795763b62279cb66b51648acfa17439a8ea1d16a56b4d30595c3d2413a740 +size 696418 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed_accessibility.png index e20c40eba..f15b2e420 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea0354d0afa521e06a320479ae5987796e976c4d872ee419a88f4bd50ed1c238 -size 496073 +oid sha256:c5ec59043b5be473668868df737cd6fa3feb594179ff9ebb4ccf891416d31b5b +size 701452 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed_dark.png index 3b3724dac..c8f3c1eb1 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.Collapsed_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a02ce38cbfeebae32ecb8a2b48f15f013fda27e75d52a2fbe2e53bf6848f9ea -size 250158 +oid sha256:10f1fad1845bf12aee35a7fffe61fd645867af9150db984e8d5d8403967bf7a5 +size 319918 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.ReduceTransparency.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.ReduceTransparency.png index f23a6bf2a..8a412533f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.ReduceTransparency.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.ReduceTransparency.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56916c228ae8aeddfaec446fc05ad6b51fe68a7cd7e7103bd210c9b2908b8126 -size 316833 +oid sha256:13358a045cc954d1807356a00a185c6a0f349ee41711682520de4b23fb2379f1 +size 489920 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.ReduceTransparency_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.ReduceTransparency_dark.png index 80805cabe..bcb4ce364 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.ReduceTransparency_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.ReduceTransparency_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7ff4747fc9067df9b00c5e677a83a257399aeae36d6e572d7e05e70734c5dd8 -size 197894 +oid sha256:e72cb56a74837051eb39ba69bb6ddb51fb7e2134f3ee2ca16e6a120ff0019d29 +size 267759 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded.png index 5ee1c27d4..78f77309b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2955819c8e0d9ef2a1b12a4e826b5c3ebf28e26f0a370bf9864c9b6f68524a11 -size 1749266 +oid sha256:31790075b719077c448826ede7fffe04d5b9aa07c35be198511e32f9ad6e6a15 +size 2284200 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded_accessibility.png index cfa42da4f..baa5e1212 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b90744e0edc71beee79026674998ef50ccae0c79152069fcc55ced1b038fc38 -size 1711066 +oid sha256:914c37bed3b344c2620cbeaaffe4811ccf6efefb050fffa4384c79ca1b671175 +size 2386061 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded_dark.png index 0445875c9..f5b521b18 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/componentThemes.SummaryExpanded_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fea6f3bd19770982744ba36119267e3f051e5e227bb9342c3f6199e391878f20 -size 936180 +oid sha256:b74db06c824b2571e7562d0fe329e21ecd2a9dee63449fed068de648fbd9fd86 +size 1174578 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.Collapsed_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.Collapsed_ax5.png index 4febd04ef..83f03ec13 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.Collapsed_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.Collapsed_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b78414144f5fe69e2649b3398b255cb7f2f6dfb95dfe3d2869f8e838d4087197 -size 1327067 +oid sha256:f45189b493068cd6ca4bbb69db95a3479693dc795c8b1d669a693cccdddd6817 +size 1754301 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 60fd532d3..a4df97e98 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:e23e068a4e97aeac0adf6b874102c871dc8e33a87c466fded938c811fbd5dc5e -size 471099 +oid sha256:ec0b4dd1b81e38a3750e9a9b301e694011226cd8697e8520bcbfb0166619bf2a +size 659952 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.SummaryExpanded_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.SummaryExpanded_ax5.png index 6588d1246..2eac1c5e0 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.SummaryExpanded_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationForecastSummarySnapshotTests/themes/other/componentThemes.SummaryExpanded_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b223b00c006125adebc39f17f2571bcd1439badadce1d1c46ad1168c9adfb7e -size 5243527 +oid sha256:e128bf785b4e101485fa18ae1301588303fe3db72b56743ac27a1d7d820a7370 +size 6970169 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 a072822d5..537f949ba 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:651559b96a31eccd1e94cac1025ebeb9b48e5126883df1af7cca44bbdd59570d -size 1923005 +oid sha256:40bb7cabee6a789507d13f03bd9f2860e7707c44363184a344e47e1835720484 +size 2512539 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone.png new file mode 100644 index 000000000..e8ecddbee --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05440147a99d75e88820f50a1418b835efa409c0f1c525fe1763125dcddbc372 +size 1261388 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_dark.png new file mode 100644 index 000000000..f45acc9a7 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fda9025189251fa9ba3ce0e43c8f9d255721b0bca0516c912ca6b6dd0165fd4 +size 958900 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsExtended.Locations_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsExtended.Locations_iPad.png new file mode 100644 index 000000000..634dcf738 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsExtended.Locations_iPad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b8c78da3c00f99e3634a925166ef1212156050ec980d66075a3e8761d25272e +size 1672581 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsExtended.Locations_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsExtended.Locations_iPad_dark.png new file mode 100644 index 000000000..90fbd2b04 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsExtended.Locations_iPad_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b97f4393f394523a50e5864e7c60a0d25ab4e0b35f725ea3b3be3e633d9c5fc +size 1262062 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsExtended.Locations_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsExtended.Locations_iPhone_accessibility.png new file mode 100644 index 000000000..4a7f4b22e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsExtended.Locations_iPhone_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16d8cf87efdb346c931f17853cd74b3cedc3193eb421a6bcf333f928c49c6b70 +size 1203298 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsLightDark.Locations_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsLightDark.Locations_iPhone.png new file mode 100644 index 000000000..951329372 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsLightDark.Locations_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06f5dde8294ba1ac2c1f826b162538ed94f83282ede97b7bed85261ca41a4b57 +size 1299591 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsLightDark.Locations_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsLightDark.Locations_iPhone_dark.png new file mode 100644 index 000000000..e8ddb3af0 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenLocationsLightDark.Locations_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a48c4b4935723e0da32072c2f1e5ec96a1442ed3304787d313ed88139e0a8fc +size 1009646 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsExtended.Elsewhere_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsExtended.Elsewhere_iPad.png new file mode 100644 index 000000000..70da78447 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsExtended.Elsewhere_iPad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2150f1a46d3588872d4b7e4686b0247b65139b39f881ba0a2b3aa3beb33ffd02 +size 921398 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsExtended.Settings_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsExtended.Settings_iPad.png new file mode 100644 index 000000000..cd7268f75 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsExtended.Settings_iPad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae6c8d0e32c4c8db4cceccb5375751c61721a839ea45d947dcf2a7fa09520a91 +size 604758 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsExtended.Year_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsExtended.Year_iPad.png new file mode 100644 index 000000000..3f9e3233b --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsExtended.Year_iPad.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf8a349b12c7658f1c4ad0974ea3f966a9e1cc08269ac5792789a32b7a483035 +size 3666411 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Elsewhere_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Elsewhere_iPhone.png new file mode 100644 index 000000000..b9911a304 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Elsewhere_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:877a05b3604e3815d63abcc22026b06a8b13f68af5cf2e747b39bea66c613c3f +size 726565 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Elsewhere_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Elsewhere_iPhone_dark.png new file mode 100644 index 000000000..2a595937b --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Elsewhere_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5061389bd5931a7cd816f260c0214bb810f0fd8ab75137d3e4df8c49ed49862c +size 499012 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Settings_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Settings_iPhone.png new file mode 100644 index 000000000..6bd37fb69 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Settings_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20a491293a81565a5a8e6af23834fa73875f7f7af45ea062666ee349d2271b6a +size 470943 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Settings_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Settings_iPhone_dark.png new file mode 100644 index 000000000..e21e2c65e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Settings_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6be48048953cd7d3f8fa5026766d82f5b8ac10e69ae9046ec37a22473aa2ad6 +size 479626 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Year_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Year_iPhone.png new file mode 100644 index 000000000..0eee79f14 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Year_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa3267a0638b443a0012b2f0bab58354b20f774d7a235f5d97dc750a709a96a0 +size 2917039 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Year_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Year_iPhone_dark.png new file mode 100644 index 000000000..9bb0f7365 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/halloweenOtherTabsLightDark.Year_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5b477a82d7bc468affc3716d30f622aa3a5f668181949b24e5998accfb67494 +size 1247700 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenLocationsExtended.Locations_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenLocationsExtended.Locations_iPhone_ax5.png new file mode 100644 index 000000000..1ec7b7271 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenLocationsExtended.Locations_iPhone_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ab809ad67d835aef6d4fdda2b8c6a9b2cb4547195838f8ff6eec35ec7e7e141 +size 1719060 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenLocationsExtended.Locations_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenLocationsExtended.Locations_iPhone_contrast.png new file mode 100644 index 000000000..7b487fdb5 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenLocationsExtended.Locations_iPhone_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9bca8cd5c3511a6d915a7cacd60196964e85b9a920b2710fa22e7229f0726e6 +size 754344 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenOtherTabsExtended.Elsewhere_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenOtherTabsExtended.Elsewhere_iPhone_ax5.png new file mode 100644 index 000000000..00d511589 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenOtherTabsExtended.Elsewhere_iPhone_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16a2a76a82a6a2304c47c0f8911ea487571deff1f3b27dc4bbcbb63e70a736e6 +size 881647 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenOtherTabsExtended.Settings_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenOtherTabsExtended.Settings_iPhone_ax5.png new file mode 100644 index 000000000..9f4a7dbaa --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenOtherTabsExtended.Settings_iPhone_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b00d599c1ff20647003af7c2e0d751662cc689d62c5be3289bd7769645a49dff +size 1179873 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenOtherTabsExtended.Year_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenOtherTabsExtended.Year_iPhone_ax5.png new file mode 100644 index 000000000..7f2fd6d51 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/halloween/other/halloweenOtherTabsExtended.Year_iPhone_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4edbb4b8b4e4add7b0f9dec1498581e83202c963bf2288c535e8ed834c36cb43 +size 5404581 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Elsewhere_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Elsewhere_iPad_contrast.png index dcd4d52a3..7d18929dd 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Elsewhere_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Elsewhere_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a733448e6e31b207488a96aa27e7362464fa39fcd4a337e7ed7c9dbb0c552acb -size 2157718 +oid sha256:e9ae165288bf8559f02a468563c56cb695a73809232feffb79b33f5d11721a41 +size 2164748 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Elsewhere_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Elsewhere_iPhone_contrast.png index da9792dc8..82f0128e7 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Elsewhere_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/MainTabsSnapshotTests/other/mainTabs.Elsewhere_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0b00c9715f179fde6094840a86793c2495dc30a76560fcf0f82e01f15899188 -size 952626 +oid sha256:39b1dda117b5bb1965e9b45075f98ca05e95df6df05c278afa48fd251223423c +size 958236 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/flat/themeSelection.Alternate_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/flat/themeSelection.Alternate_iPhone.png index d5cf17d25..fc4a89497 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/flat/themeSelection.Alternate_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/flat/themeSelection.Alternate_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e53c1d9d8a165e7b732ba32fca79a2701d41ee0620769787d9dbca9aa3d94a2 -size 1257483 +oid sha256:b8a664d07058bdc9f26ee021a988a122eb9f6c528ea708993f8ad34d9e7771c4 +size 1881893 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/flat/themeSelection.Alternate_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/flat/themeSelection.Alternate_iPhone_dark.png index eb0a7f9b0..08bcea5e6 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/flat/themeSelection.Alternate_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/flat/themeSelection.Alternate_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16eb8c0115d3db8ca890c30ba3ac5020aceffb07254b82ea7e8b3bfcc75ebdb8 -size 1194224 +oid sha256:493683eddfdf0d8c9690f4a647322bc0cc63ad83815801baaaa59eb9a07a3537 +size 1834467 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/halloween/themeSelection.Halloween_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/halloween/themeSelection.Halloween_iPhone.png new file mode 100644 index 000000000..2911fd88f --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/halloween/themeSelection.Halloween_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9dc36fa5673344a8af91cafe492f4115e364716b538e1ac0a7afcb1b6da7da8d +size 1884078 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/halloween/themeSelection.Halloween_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/halloween/themeSelection.Halloween_iPhone_dark.png new file mode 100644 index 000000000..4adb9e9db --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/halloween/themeSelection.Halloween_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:edff9b9293ec80c30f0c97796cf39b9a76679cb92cf0df82a4948ccbcc0c4772 +size 1829489 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_ax5.png index c81eb9d53..e6fa3aa73 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82f306d5a4e375dcafe167212d798d115b09d4508af0adaf7a546cfd1ffb028b -size 3796206 +oid sha256:600d62c3cefe01c3f8d722aa712eb854fe70ccca9f4320e789a41bc8a14cf54b +size 4590277 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_contrast.png index 089191df3..9f85eff41 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:369766412e3701637b4fcb722179ecf715f842dfc335b20dae3daf52a58e0558 -size 2068368 +oid sha256:03e8477352f11f57b9a546f6f1abf4a2c56ffba0ee0f36a2f4ad164458ce1852 +size 2801649 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_rtl.png index 49b79bd20..4c5e5d335 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_rtl.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPad_rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e373d89225342351356b5488b0666f279b8719088c163b3ab1b00f8ecd07c7f0 -size 2028575 +oid sha256:b6aa7011a574dd05748e530f03d4034113250e3a5da7103b47a38cb359932461 +size 2833852 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_ax5.png index d89edc292..7381701a8 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c5c3f9bdb7e60d0a518a5f046172e8403267dfc3d0e575745b42ee95e220c68 -size 2596076 +oid sha256:ed266914aa2c36e9fc72eb7611dd008e0f699a9564208a539d618b57c619f43f +size 3484608 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_contrast.png index abb2c6f6a..df2d4b24c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec3ee47b8f3464f804674064f591b5222e9ea9b183ef0797ce41cbfa65a92dbd -size 1232004 +oid sha256:9d0d565cf4568ae46db507c646e43322112756bbd472107991bc8eb6904eac3c +size 1831089 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_rtl.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_rtl.png index 845b2fb23..6c5aa8caf 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_rtl.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/other/themeSelection.Standard_iPhone_rtl.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:226d3a16a1698d1f72c641736e65e42d09c08b04eafd41fdf2142a93eceadfb3 -size 1251216 +oid sha256:3d011d31322fe031b58b48436d26883c167dde5c9f43fa80096495de8e354d62 +size 1871065 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad.png index 48d3e8b66..e61269bf9 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:523f531072a010cd59d1287e275d934a2ed77193097e816ebd5e1be70017a543 -size 2131979 +oid sha256:0eaba27e6484f4fb40fe398d538a8fbdf234f1de1e93a56bd40c69fb1a69f1b0 +size 2838294 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad_accessibility.png index 0ae8a48ab..ace301e4f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fef12bfed6f4cdd35e80a8b4df1a140d664afe82d5533f6c78d1a944d928bbd -size 2431858 +oid sha256:70f7cffd009467bf268fccd3fcecf8178849dfd9129b38c14fba453c6edb86f9 +size 2709763 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad_dark.png index a1b5d946f..e417a1414 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPad_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:460faa835343aed1d651444f101cafbcc3c422a7277ee15b57d8ae1f90e4cf34 -size 2099696 +oid sha256:2a92820decb2af4e8e4f815ecdb93a7d79686b203e80d89c2338fc6c5af25296 +size 2899633 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone.png index f4161eb49..24d263102 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2a043e3f4e9525ae5436b7b05b4bc693fb26a5f22c8d236e4ec16ccff290082 -size 1254229 +oid sha256:ea4dfd0a8c75b5347f48e29d0b2448d3c35118c1705d413ed6c0b0a6a8c3cdc8 +size 1874542 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone_accessibility.png index c9bb002dd..c0e8fa5f1 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cf8de30e219baa221371c092054e1bc1d6964a5aadde915bf4d878a92373919 -size 1168208 +oid sha256:0a8b54d3ad69d7e60fcc1a02a9b26a06270765537912666e169611e545b8137e +size 1566222 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone_dark.png index 0957cc960..0dbaf7f4d 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/OnboardingThemeSelectionViewSnapshotTests/themeSelection.Standard_iPhone_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9ad0a23d532b8d004fedbbf17cc22615166a2b5a1cf84972eedfe70eab3b603 -size 1189696 +oid sha256:6db4d4b2209c4b3fbf580c67dfbd18f154b1b6f45e1a96db8ae198e1a0dab77d +size 1883683 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy.png index 92ffe6b16..373d16e5a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf9a4557897eacfa16eb0ec80e88b48c886964ff2dfc698b184d0c6ad78a25a7 -size 1806053 +oid sha256:03bfbf3443f152779db47d0e21796e434410738e5b59a7e13deae325250c5419 +size 2058676 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy_accessibility.png index 3c51a9131..f7c481236 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:660f3eedbd11eb25c98599d0a40b3e7526152b9d3976d9ee1e7fbc5507c1ba2a -size 1076132 +oid sha256:93e322c745d35fe9bf3939e60129978803314eaab40ad5e2bcbf22d433bb4c4f +size 1375949 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy_dark.png index 32a2fd586..5497bc968 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/componentThemes.Privacy_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd0a347ade7be1a29159ccd3bf2c626129eb1d03b0d4387d6e40199543ec0cc6 -size 1740191 +oid sha256:00f228146043d6beae12ff3c5cb268e4535c4c6f57881db8242a8c2b94683600 +size 1885944 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/other/componentThemes.Privacy_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/other/componentThemes.Privacy_ax5.png index 0a2abd33e..b0da16294 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/other/componentThemes.Privacy_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/other/componentThemes.Privacy_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eaa7aa2eecb411193d4d41562d79e6b3d4f45db8b57963df44fe0849c2f214a1 -size 8271258 +oid sha256:5ef24bfe7b1d3a790930a005f1954a1a6c83cf3994755a25b8b69f530f83fe85 +size 9329671 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/other/componentThemes.Privacy_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/other/componentThemes.Privacy_contrast.png index 236838033..e499d2f24 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/other/componentThemes.Privacy_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/PrivacyPassportCardSnapshotTests/themes/other/componentThemes.Privacy_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ccc977b666dcf75a5624ee1c13ef2321fbfdf26bbf28ea8d469ec7d8447ce2a -size 1810923 +oid sha256:d13ed30344e3e31f21d02edf9e59658e2624e469a9f4b10e96327f9472a7e950 +size 2114573 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact.png index f4e462ae2..051237799 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4fa185b3ff72fe9409caf62819414e87cffba6e31c20b8c0fa3698e8a59b741 -size 960759 +oid sha256:08c7133425216903f19cd0e05366721800d5b374d06f507fdc49815441e172d8 +size 1245842 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.CompactReduceTransparency.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.CompactReduceTransparency.png index 06f13830a..25ea3e37e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.CompactReduceTransparency.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.CompactReduceTransparency.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2395c026d46ee5b6ab000802aa7c32469d0df84ed50dd7a33407475796cc4e97 -size 950437 +oid sha256:0cce6b7e23314ed949409888d2b6541b68808a3267517d0ec25c7e560951ea7d +size 1185342 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.CompactReduceTransparency_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.CompactReduceTransparency_dark.png index e42eccbd2..7c89c68fb 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.CompactReduceTransparency_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.CompactReduceTransparency_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34e394dbbf2264bf9c3e59320b8bec0156aaba60c36ea377bc4cec3bedf001ed -size 1039762 +oid sha256:8ff156ff6d3f7646083dac94efe6ac22881a11537b8c04142137c12f5156c809 +size 1162163 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact_accessibility.png index acbe88fd0..8205715bd 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:108137c9392e672d5ec991d3e2d969d13ac7dfa276e43d2480f1ef2197426cbf -size 671135 +oid sha256:fde8af934fb52ef9ebda2758d72221c46e2e716b40c25d7cad1bb72c874c80ce +size 961302 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact_dark.png index 6266e8fde..511a076de 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Compact_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:293981e8cfe87c945c31c0b2b0f742c3fa9ff22b7971dfd41f1e8b506ffd55d9 -size 1050813 +oid sha256:d1930ea25809cdb42d38576e2e8cc4b9c9620f371535a10f7a0eb2ff044fd305 +size 1218245 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere.png index bfe412edf..f79030a5a 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e2a9bea44b3a6445e700cc29d86b157a3be1951c04b36136087491c3e464a53 -size 574735 +oid sha256:52d3d2afd652077e66a2a89e11ffc0afdd6b38d6d8a96ed491a0e46c9df82b57 +size 781887 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.ElsewhereReduceTransparency.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.ElsewhereReduceTransparency.png index 31c2396d3..9a021b2bb 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.ElsewhereReduceTransparency.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.ElsewhereReduceTransparency.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9c409d8bb44cc205fef32f193eb0b28bbe5a8199839dc82cca267449c05afac -size 574735 +oid sha256:c3952533fe1cda584c0287d628a7201e669b8f5f905c657705a62899f3a6aad7 +size 768431 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.ElsewhereReduceTransparency_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.ElsewhereReduceTransparency_dark.png index 2fd061967..c23b1bf79 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.ElsewhereReduceTransparency_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.ElsewhereReduceTransparency_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5565fd58ec64f2d8149525f856951657198b2690283104cf88179549e86c75bd -size 338331 +oid sha256:10b2773be96c80acabab5579e4903ffff722ff090ca9f8837de5b8377e90dde9 +size 422189 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere_accessibility.png index 744a1c410..bed0653ee 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99de259fe005f462dd44c84987d1b4b1572b1f9eea1a377c91c8ccbd377fe867 -size 543253 +oid sha256:24d0ca74c2a85ce78ed4629109fb3e575f85050da8d5be155bb7318013dcc32c +size 777958 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere_dark.png index 2fd061967..81b313ba3 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Elsewhere_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5565fd58ec64f2d8149525f856951657198b2690283104cf88179549e86c75bd -size 338331 +oid sha256:02d5c32c3285d2fe5422274a0723f929aad3a8b6c1a16be3e30502be99758bb3 +size 434321 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular.png index 38cc478b0..d2c9b35cf 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0fedc39b8222df00e5019fa818a6ee31c01dad3699531299f24792e84e339dd -size 1522000 +oid sha256:be994383bba8639172e5702975d4c0786cb2a8870a224dd13758d7b91adf3fe1 +size 1974383 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.RegularReduceTransparency.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.RegularReduceTransparency.png index 59b849b3b..0f44e907f 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.RegularReduceTransparency.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.RegularReduceTransparency.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:720321f30cf4964d386fe9f67a388a22308e6171fdc2e21676bb1ed8eedb8181 -size 1498655 +oid sha256:ececb65848a5f0be8ed1a1bc159adcf5c84cd5cc80f5afac853ad79902a94d9d +size 1791650 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.RegularReduceTransparency_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.RegularReduceTransparency_dark.png index bf7df28e3..08e64bc7c 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.RegularReduceTransparency_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.RegularReduceTransparency_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c000d56f63c8096cf35179ffaa77bff38fbd684bbffa464ea26434d7376064c -size 1542257 +oid sha256:b91f0cdeddeb02b81f2f0648dc27b7a9539f8af727ef3a7ee264f11dc3597b42 +size 1691327 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular_accessibility.png index 543ef0a56..0abc7a5b5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c36eb7c9993919b7cb39fc9b4114aa637875d8183bb42bac7c753b50a16d2927 -size 1063121 +oid sha256:fa9ab34edf76652bde21d6d030954dcd13df3cb255ca8908cbafdf95477b8147 +size 1471850 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular_dark.png index 288b30b71..474a627f5 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/componentThemes.Regular_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ea92091ad231900f48c569ed5f31c13299d9d6e92f439d5e402999785a755a9 -size 1568384 +oid sha256:71c4c45366ebbf22a70455541ca5119b86b168bfa5c0c953a329e5463c976e6d +size 1868795 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Compact_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Compact_ax5.png index 20077529e..a935c08d8 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Compact_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Compact_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d591fc2d7bf3db9992c33a3d7df74a68f27a0b6fc2332c5842fa54a04f234899 -size 1838381 +oid sha256:fcdf50dadc86fc723d3c8ebc58d66c0e69c0c75ce852d861933fd2bab5e8b5c7 +size 2367431 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Compact_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Compact_contrast.png index bb64389b5..4533423ca 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Compact_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Compact_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32db9b363e4553ecd6b24b5e5798413481ffbeb43a7ecd561b9d69bf7bfdde8f -size 1083660 +oid sha256:e94f2b890b1c907c8a69e769b45d1103fbc5aa52e0cc162f53723ded7df2dcf4 +size 1355042 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Elsewhere_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Elsewhere_ax5.png index ac850515e..b6935ae59 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Elsewhere_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Elsewhere_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b8faa6df64d6b832483ee5744a8e2df554136f859faee9cd8c14a278b9ad114 -size 1270100 +oid sha256:e6e6b4e2d61e185931fbc212b4e34b7cc07303da56384cb049b2955404e544b0 +size 1697649 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Elsewhere_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Elsewhere_contrast.png index 99a196abd..a91d41962 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Elsewhere_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Elsewhere_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5c3e43a78eab9c7820695aa1d28e9796b00e60a81871e46ca3db4fde2e120e6 -size 585771 +oid sha256:159936de19383f95acdd4fa105254cccb49ef1e54085ebfb4a0e07cdf1e19e88 +size 855054 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Regular_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Regular_ax5.png index 33a5c0dae..54868e770 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Regular_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Regular_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c872768d5bbc93f27eaea478205e122dad5924b3f5f93f0e1c0079d893b58b8 -size 2234050 +oid sha256:ceaa970631427baaf419b9d7d98929b610c971ebeb679aec02570fe953ca805b +size 2864166 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Regular_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Regular_contrast.png index 55585ed38..84548ffcd 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Regular_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionSummaryCardSnapshotTests/themes/other/componentThemes.Regular_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d93605bbcec195a57a7986d8df2a571e4f50579d6f1281801bbd17c7d9ab947c -size 1699314 +oid sha256:6b811c9c8bdf56bd3f5d7944c3c3b55a45a99c344884002f3607ed368eac4743 +size 2049019 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/other/regionWelcomeCard.Welcome_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/other/regionWelcomeCard.Welcome_ax5.png index 0655f39c1..f3aac2a15 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/other/regionWelcomeCard.Welcome_ax5.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/other/regionWelcomeCard.Welcome_ax5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dd64d9f5d1d867302415a11adc5a6e927686c68e7490df29f643b6daad7ea3d -size 3920872 +oid sha256:ee0f87f3579045fa502b0b7cfd0d6b15f032aef8b106fb7435d84d7889143fc8 +size 4934418 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/other/regionWelcomeCard.Welcome_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/other/regionWelcomeCard.Welcome_contrast.png index fedd7515d..9432eec64 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/other/regionWelcomeCard.Welcome_contrast.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/other/regionWelcomeCard.Welcome_contrast.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80092f295b6091849e8cd8ae84fef652ba7e9c0e094762bd67cb2b3147577096 -size 1490419 +oid sha256:df7bfbd66e800785da933ab7cdcf4ad0b5de622d079b1185a91967f66914657f +size 1909668 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome.png index 20fe6a165..070cefd24 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:acca0ad7b9c470bf6363320781a3cc23c02364d61278b8577a31c7fc4c8342ef -size 1497669 +oid sha256:1392dd51a5637516bd3a3c8b4f4c4788726293199a61467ae647ec76c4fb3655 +size 1835413 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome_accessibility.png index 668206fa6..288ef591b 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome_accessibility.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome_accessibility.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8649b3332cc8e0fd2d36614d833eeed12c46c43506e750443aafbacc105bb445 -size 1306879 +oid sha256:bcb5bc566219068db7f3de7215846484c8df314f77a6375aa394186585b8f4f2 +size 1686636 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome_dark.png index 979029dcc..6ff6f765e 100644 --- a/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome_dark.png +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/RegionWelcomeCardSnapshotTests/themes/regionWelcomeCard.Welcome_dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:52d2f3b6cbf99746e324c835657f7ce17ead9291f7c197e90e9eb6fc6e33bfee -size 1181066 +oid sha256:393a3e70b76dd681a5c535b6291b67627af2e73dcf6183484d9b62533d56ca2a +size 1335708 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceMotion_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceMotion_iPhone.png new file mode 100644 index 000000000..abcd15aff --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceMotion_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08274f2c8da033cbad31ae5d67f947e719fdb50497fe14be806ccf8ab04c4739 +size 603238 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceMotion_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceMotion_iPhone_dark.png new file mode 100644 index 000000000..c8b233751 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceMotion_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c1a1f1812146cf49bd01271739b9de717b0687d5c9ccb3179ef8db6578de1ed +size 365225 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceTransparency_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceTransparency_iPhone.png new file mode 100644 index 000000000..bc1be9132 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceTransparency_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff600eb38807bc0a9dc829c28e3efad003c2c3ef7926ded8808e92ca798e31a0 +size 409110 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceTransparency_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceTransparency_iPhone_dark.png new file mode 100644 index 000000000..27747cd1a --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/accessibilityPolicyExtended.ReduceTransparency_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6fd673c23c39a099bea656b6fcda5be46faa54ab7cd1990cdc9f372e8433dbc +size 174460 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Appearance_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Appearance_iPhone_ax5.png new file mode 100644 index 000000000..745a774fc --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Appearance_iPhone_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e157fab05f14f5ce103a701fabd11ab6884267d9a333270b6a0dce26e6f52c6b +size 2895595 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Forecast_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Forecast_ax5.png new file mode 100644 index 000000000..275721cea --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Forecast_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7d80fd9a560b557f5e239c190de9721e5c81028aa7da3e2b1420331d709cf03 +size 2383004 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Forecast_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Forecast_contrast.png new file mode 100644 index 000000000..48746d105 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Forecast_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:735613aff2ee68ed1cef9fc12f7d12ad8387f3d5e43348ba4a83fc6edbd999a9 +size 643730 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Planning_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Planning_iPhone_ax5.png new file mode 100644 index 000000000..d6ce19c55 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Planning_iPhone_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:939544ba15103f36a36448260294ec68a16a5a8630d23593fad37c4a1a1b53f9 +size 1556857 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Privacy_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Privacy_ax5.png new file mode 100644 index 000000000..c9355e183 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Privacy_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53689c037aea2d075ace94ba4fb9fa5018f88ed72aab944b9e2ab3840a829c2d +size 966152 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Privacy_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Privacy_contrast.png new file mode 100644 index 000000000..006c93c32 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Privacy_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9cfcf9f49db727daa44fc4b80781bf464968e9aa4f303e676caf3547102ceda9 +size 223405 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Timeline_iPhone_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Timeline_iPhone_ax5.png new file mode 100644 index 000000000..4b22c7c99 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Timeline_iPhone_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:998cf6141f5418820d6ac837bace229a9de2c97892df4a7429fde61ae93b3fde +size 5813482 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Welcome_ax5.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Welcome_ax5.png new file mode 100644 index 000000000..0bfd63b47 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Welcome_ax5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:834f31b0f1b27b9e8a3497d02c38194c59465ba5e841b104033df7a023d4f548 +size 1026908 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Welcome_contrast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Welcome_contrast.png new file mode 100644 index 000000000..a829f771f --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/other/sharedSurfacesExtended.Welcome_contrast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a50e62ae04287cd06fcdcf21ca27bc5787550572902c014e37bc94e341ee508d +size 321956 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Appearance_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Appearance_iPhone_accessibility.png new file mode 100644 index 000000000..07bda339b --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Appearance_iPhone_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0dafb76ed0c67c5697c3f8dcdce36b2ed2df133a6eae4fddf107e0caec4fdb6 +size 1498370 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Forecast_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Forecast_accessibility.png new file mode 100644 index 000000000..cb58e0c69 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Forecast_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4640100cc4abdcd409937e3befd74f5b6097a9a688de6b38947e8c02bf38fe23 +size 738895 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Planning_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Planning_iPhone_accessibility.png new file mode 100644 index 000000000..adb612372 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Planning_iPhone_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ae1581c57d2ca4f00448c35046b47ad3bf7cad96aaec582259c604bb7fe4813 +size 737709 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Privacy_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Privacy_accessibility.png new file mode 100644 index 000000000..719783c9f --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Privacy_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df332545a9d06301b746ac1dbed6ce9681b0e2ddfcd1cbb4cce8460a10b4c608 +size 244615 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Timeline_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Timeline_iPhone_accessibility.png new file mode 100644 index 000000000..c451f3955 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Timeline_iPhone_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bc99c9126f7922a92abc00c9eb6ede1d48e05c8e25e30d1af033c7142679e5b +size 2317335 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Welcome_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Welcome_accessibility.png new file mode 100644 index 000000000..49053c8dc --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesExtended.Welcome_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:235651cd860c6d3b4ee2124bc41bfa74b1b217f859bf9a8fd681b32cf03b4bde +size 312851 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Appearance_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Appearance_iPhone.png new file mode 100644 index 000000000..25575d51d --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Appearance_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2cc2208c0d4e11e83f77fcdcd7174786ed1dbc4df4ab0b3e68ad7e82dff8c59 +size 1359896 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Appearance_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Appearance_iPhone_dark.png new file mode 100644 index 000000000..fca4261c6 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Appearance_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e45c26dbf2c872e981680ed04d4f27ea34694f08210e31e7ec5518af41bf5a8e +size 1294408 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Forecast.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Forecast.png new file mode 100644 index 000000000..536f4501e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Forecast.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f8b1bd3fde229815be3c04e1d0bd5541f3084962e5d50566d81460e541b8293 +size 598811 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Forecast_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Forecast_dark.png new file mode 100644 index 000000000..16f11829d --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Forecast_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:002ecd57e33db6935434def19fdeb22483361784ac10d5275eb398156b641c46 +size 349560 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Planning_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Planning_iPhone.png new file mode 100644 index 000000000..8473b794e --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Planning_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a4e66cbafce2325e81e7aa67c3867fe198d2aaf6b07dcb675f5f42278de8c84 +size 435596 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Planning_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Planning_iPhone_dark.png new file mode 100644 index 000000000..dbca1f8a3 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Planning_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f879cea655050719162d75a9728dbdb79d9b924b62f670355794a43260185b8 +size 444249 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Privacy.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Privacy.png new file mode 100644 index 000000000..c51ce033a --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Privacy.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d964efec47fd94df5be8c17543f3d136a261760997c495fa75ef3b0f4701d4f6 +size 216004 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Privacy_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Privacy_dark.png new file mode 100644 index 000000000..93d95a651 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Privacy_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa2f3f390673837937ed699fc5541cc3e0a70d6d157fa423dcb92b9fb6c116ca +size 110489 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Timeline_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Timeline_iPhone.png new file mode 100644 index 000000000..06a0712cf --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Timeline_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3410240e8d4b56990401f77ea418fc98c78e0fe634ab2033f9b02289882b4f1d +size 2058818 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Timeline_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Timeline_iPhone_dark.png new file mode 100644 index 000000000..65daa6395 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Timeline_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a988a491297f11672cf7e81376b2ffba51c793fa0aeb07ce266cb3348f9099b9 +size 1194490 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Welcome.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Welcome.png new file mode 100644 index 000000000..88d57c277 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Welcome.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c01470fae2684b01850e84bad19fce2d8ad49d33b590e4a53e0197a1b7f1a93 +size 294713 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Welcome_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Welcome_dark.png new file mode 100644 index 000000000..3a7e5c15c --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/halloween/sharedSurfacesLightDark.Welcome_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f62ac9831ccd4449f74e58456d353e5be824587c4ea6ff5395380b904207b6c +size 128989 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone.png new file mode 100644 index 000000000..aee8d1445 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aad97a3ac2da1288d4c136474c9ef228efab15e814273801294a058e0600fc34 +size 603057 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_accessibility.png new file mode 100644 index 000000000..e4473718f --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b23926c6012d1b2c70df13b5aa1b318b29886c0a167a80c12c627462724e0ff5 +size 589954 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_dark.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_dark.png new file mode 100644 index 000000000..f5009f83a --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6521e583567867c63109c550763e67b76c4ade04f574802b87b25646ed0ef647 +size 365423 diff --git a/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_dark_accessibility.png b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_dark_accessibility.png new file mode 100644 index 000000000..6ffa06693 --- /dev/null +++ b/Where/WhereUI/SnapshotTests/__Snapshots__/WhereThemePreviewSnapshotTests/seasonalDocuments.HalloweenTheme_iPhone_dark_accessibility.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b051f88e4bd92b327749d798e6bf2fe915af84860b56ba7d1cb5c0f054c2592 +size 334132 diff --git a/Where/WhereUI/Sources/Developer/CardDesigner/CardDesignerPreview.swift b/Where/WhereUI/Sources/Developer/CardDesigner/CardDesignerPreview.swift index 3abebd133..89e9aaa46 100644 --- a/Where/WhereUI/Sources/Developer/CardDesigner/CardDesignerPreview.swift +++ b/Where/WhereUI/Sources/Developer/CardDesigner/CardDesignerPreview.swift @@ -29,6 +29,7 @@ import BroadwayUI ) } .buttonStyle(cardStyle.buttonStyle(tint: previewStyle.tint)) + .whereThemeArtworkSpace(variant.style == .regular ? .regularCard : .compactCard) } .environment(\.cardDesignerConfiguration, configuration) .environment(\.colorScheme, colorScheme) diff --git a/Where/WhereUI/Sources/Developer/RankingAnimation/RankingAnimationPreview.swift b/Where/WhereUI/Sources/Developer/RankingAnimation/RankingAnimationPreview.swift index b77a8ee9a..7a651da7d 100644 --- a/Where/WhereUI/Sources/Developer/RankingAnimation/RankingAnimationPreview.swift +++ b/Where/WhereUI/Sources/Developer/RankingAnimation/RankingAnimationPreview.swift @@ -25,6 +25,7 @@ yearLength: 365, year: RankingAnimationLabModel.year, ) + .whereThemeArtworkSpace(.regularCard) .locationCardOvertakeEffect( region: item.region, presentation: model.presentation, diff --git a/Where/WhereUI/Sources/Elsewhere/ElsewhereView.swift b/Where/WhereUI/Sources/Elsewhere/ElsewhereView.swift index d48ae3c3c..6e5977980 100644 --- a/Where/WhereUI/Sources/Elsewhere/ElsewhereView.swift +++ b/Where/WhereUI/Sources/Elsewhere/ElsewhereView.swift @@ -13,6 +13,7 @@ struct ElsewhereView: View { @State private var retrySequence = 0 @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components @Environment(\.scenePhase) private var scenePhase @Environment(\.regionStyles) private var regionStyles @@ -50,9 +51,10 @@ struct ElsewhereView: View { var body: some View { NavigationStack { screen + .modifier(WhereComponentTextStyle(role: .elsewhere)) .frame(maxWidth: .infinity, maxHeight: .infinity) .background { - LocationsBackground(regions: backgroundRegions) + LocationsBackground(regions: backgroundRegions, role: .elsewhere) .ignoresSafeArea() } .navigationBarTitleDisplayMode(.inline) @@ -81,7 +83,9 @@ struct ElsewhereView: View { emptyState } else { GlassEffectContainer(spacing: stylesheet.spacing.large) { - VStack(spacing: stylesheet.spacing.large) { + VStack(spacing: stylesheet.spacing.large + components[.elsewhereCard] + .outsets.top + components[.elsewhereCard].outsets.bottom) + { ForEach(content.regions) { item in NavigationLink { RegionDaysView( @@ -100,7 +104,8 @@ struct ElsewhereView: View { year: report.selectedYear, recordedPoints: content.cardLocations .pointsByRegion[item.region] ?? [], - showsRecordedPoints: report.showsRecordedLocationDots, + showsRecordedPoints: report + .showsRecordedLocationDots, recordedPointsID: content.cardLocations.id, ) } @@ -109,10 +114,23 @@ struct ElsewhereView: View { )) } } + .padding(.top, components[.elsewhereCard].outsets.top) + .padding(.bottom, components[.elsewhereCard].outsets.bottom) + .padding( + .horizontal, + max( + 0, + max( + components[.elsewhereCard].outsets.leading, + components[.elsewhereCard].outsets.trailing, + ) - stylesheet + .spacing.xxLarge, + ), + ) } } } - .padding() + .padding(stylesheet.spacing.xxLarge) } .defaultScrollAnchor(.center, for: .alignment) .scrollBounceBehavior(.basedOnSize) diff --git a/Where/WhereUI/Sources/Elsewhere/RegionDaysView.swift b/Where/WhereUI/Sources/Elsewhere/RegionDaysView.swift index 4ad026274..85177f0ba 100644 --- a/Where/WhereUI/Sources/Elsewhere/RegionDaysView.swift +++ b/Where/WhereUI/Sources/Elsewhere/RegionDaysView.swift @@ -128,7 +128,7 @@ private struct DayRow: View { var body: some View { HStack(spacing: stylesheet.spacing.large) { Image(systemSymbol: .calendar) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .accessibilityHidden(true) VStack(alignment: .leading, spacing: stylesheet.spacing.xxSmall) { Text(day.displayDate.formatted(.dateTime.weekday(.wide).month(.wide).day().year())) @@ -141,7 +141,7 @@ private struct DayRow: View { .secondaryRegionCurrent(regions: Region.inCanonicalOrder(day.regions) .map(\.localizedName).joined(separator: ", "))) .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } } .padding(.vertical, stylesheet.spacing.xSmall) diff --git a/Where/WhereUI/Sources/Forecasting/LocationForecastContent.swift b/Where/WhereUI/Sources/Forecasting/LocationForecastContent.swift index 8434eab28..6a34f5f7c 100644 --- a/Where/WhereUI/Sources/Forecasting/LocationForecastContent.swift +++ b/Where/WhereUI/Sources/Forecasting/LocationForecastContent.swift @@ -19,7 +19,7 @@ struct LocationForecastContent: View { if forecasts.contains(where: { !$0.estimatedTotalDays.isExact }) { Text(String(localized: .forecastDateRangeExplanation)) .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } if let planningAction { LocationForecastControls(planningAction: planningAction) diff --git a/Where/WhereUI/Sources/Forecasting/LocationForecastEndorsementButtonStyle.swift b/Where/WhereUI/Sources/Forecasting/LocationForecastEndorsementButtonStyle.swift index 810559489..afd794b85 100644 --- a/Where/WhereUI/Sources/Forecasting/LocationForecastEndorsementButtonStyle.swift +++ b/Where/WhereUI/Sources/Forecasting/LocationForecastEndorsementButtonStyle.swift @@ -11,9 +11,11 @@ struct LocationForecastEndorsementButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .font(controls.font) + .lineLimit(nil) .foregroundStyle(tint) .padding(.horizontal, controls.horizontalPadding) .frame(maxWidth: expands ? .infinity : nil, minHeight: controls.minimumHeight) + .fixedSize(horizontal: false, vertical: true) .background { RoundedRectangle(cornerRadius: controls.cornerRadius) .fill(tint.opacity(ink.controlFillOpacity)) diff --git a/Where/WhereUI/Sources/Forecasting/LocationForecastHeader.swift b/Where/WhereUI/Sources/Forecasting/LocationForecastHeader.swift index 2e0342097..d4a687f2e 100644 --- a/Where/WhereUI/Sources/Forecasting/LocationForecastHeader.swift +++ b/Where/WhereUI/Sources/Forecasting/LocationForecastHeader.swift @@ -5,6 +5,7 @@ struct LocationForecastHeader: View { let elapsedDays: Int? let isExpanded: Bool var expansionAction: (() -> Void)? + var role: WhereThemeComponents.Role = .summary @Environment(\.stylesheet) private var stylesheet @@ -15,6 +16,7 @@ struct LocationForecastHeader: View { elapsedDays: elapsedDays, isExpanded: isExpanded, showsDisclosure: true, + role: role, ) .contentShape(Rectangle()) } @@ -31,6 +33,7 @@ struct LocationForecastHeader: View { elapsedDays: elapsedDays, isExpanded: true, showsDisclosure: false, + role: role, ) } } diff --git a/Where/WhereUI/Sources/Forecasting/LocationForecastHeaderLabel.swift b/Where/WhereUI/Sources/Forecasting/LocationForecastHeaderLabel.swift index df4c16b63..c88a20fd7 100644 --- a/Where/WhereUI/Sources/Forecasting/LocationForecastHeaderLabel.swift +++ b/Where/WhereUI/Sources/Forecasting/LocationForecastHeaderLabel.swift @@ -6,6 +6,8 @@ struct LocationForecastHeaderLabel: View { let elapsedDays: Int? let isExpanded: Bool let showsDisclosure: Bool + var role: WhereThemeComponents.Role = .summary + @Environment(\.whereComponentStylesheet) private var components @Environment(\.stylesheet) private var stylesheet @@ -16,37 +18,44 @@ struct LocationForecastHeaderLabel: View { if header.layout == .stacked { VStack(alignment: .leading, spacing: header.contentSpacing) { HStack { - PassportSeal( - systemSymbol: .chartLineUptrendXyaxis, - tint: Color.primary.opacity(style.ink.sealOpacity), - ) + emblem Spacer(minLength: 0) if showsDisclosure { Image(systemSymbol: .chevronRight) .rotationEffect(.degrees(isExpanded ? 90 : 0)) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .accessibilityHidden(true) } } - LocationForecastHeaderText(elapsedDays: elapsedDays) + LocationForecastHeaderText(elapsedDays: elapsedDays, role: role) } } else { HStack(spacing: header.contentSpacing) { - PassportSeal( - systemSymbol: .chartLineUptrendXyaxis, - tint: Color.primary.opacity(style.ink.sealOpacity), - ) - LocationForecastHeaderText(elapsedDays: elapsedDays) + emblem + LocationForecastHeaderText(elapsedDays: elapsedDays, role: role) Spacer(minLength: 0) if showsDisclosure { Image(systemSymbol: .chevronRight) .rotationEffect(.degrees(isExpanded ? 90 : 0)) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .accessibilityHidden(true) } } } } + + @ViewBuilder private var emblem: some View { + if components[role].nativeArtwork.contains(.stamp) { + PassportSeal( + systemSymbol: .chartLineUptrendXyaxis, + tint: Color.primary.opacity(stylesheet.locationForecast.ink.sealOpacity), + ) + } else { + Image(systemSymbol: .chartLineUptrendXyaxis) + .font(components[role].heading) + .accessibilityHidden(true) + } + } } #if DEBUG diff --git a/Where/WhereUI/Sources/Forecasting/LocationForecastHeaderText.swift b/Where/WhereUI/Sources/Forecasting/LocationForecastHeaderText.swift index 9a0a9c756..23329d1cb 100644 --- a/Where/WhereUI/Sources/Forecasting/LocationForecastHeaderText.swift +++ b/Where/WhereUI/Sources/Forecasting/LocationForecastHeaderText.swift @@ -3,20 +3,22 @@ import SwiftUI /// The localized title and elapsed-day block beside the forecast seal. struct LocationForecastHeaderText: View { let elapsedDays: Int? + var role: WhereThemeComponents.Role = .summary @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components var body: some View { let header = stylesheet.locationForecast.header VStack(alignment: .leading, spacing: header.textSpacing) { Text(String(localized: .locationForecastTitle)) - .font(header.titleFont) + .font(components[role].heading ?? header.titleFont) .bold() if let elapsedDays { Text(WhereFormat.locationForecastElapsed(days: elapsedDays)) - .font(header.elapsedFont) - .foregroundStyle(.secondary) + .font(components[role].body ?? header.elapsedFont) + .foregroundStyle(.whereSecondary) .monospacedDigit() } } diff --git a/Where/WhereUI/Sources/Forecasting/LocationForecastPanel.swift b/Where/WhereUI/Sources/Forecasting/LocationForecastPanel.swift index a436dcb4a..dbe2227f5 100644 --- a/Where/WhereUI/Sources/Forecasting/LocationForecastPanel.swift +++ b/Where/WhereUI/Sources/Forecasting/LocationForecastPanel.swift @@ -16,6 +16,7 @@ struct LocationForecastPanel: View { } @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components private var style: WhereStylesheet.LocationForecastStyle { stylesheet.locationForecast @@ -25,7 +26,11 @@ struct LocationForecastPanel: View { let shape = RoundedRectangle(cornerRadius: style.cornerRadius) VStack(alignment: .leading, spacing: style.rowSpacing) { - LocationForecastHeader(elapsedDays: forecasts.first?.elapsedDays, isExpanded: true) + LocationForecastHeader( + elapsedDays: forecasts.first?.elapsedDays, + isExpanded: true, + role: .forecastPanel, + ) LocationForecastContent( forecasts: forecasts, homeRegion: homeRegion, @@ -34,9 +39,14 @@ struct LocationForecastPanel: View { } .padding(style.padding) .frame(maxWidth: .infinity, alignment: .leading) - .background { + .whereThemePanel( + .forecastPanel, + in: shape, + tint: components[.forecastPanel].foreground ?? .primary, + ) { ZStack { - shape.fill(.background) + if components[.forecastPanel].surface == nil, + components[.forecastPanel].background == nil { shape.fill(.background) } if style.ink.surfaceWashOpacity > 0 { LinearGradient( colors: [ @@ -48,15 +58,17 @@ struct LocationForecastPanel: View { endPoint: .bottomTrailing, ) } - SecurityPrintRosette( - tint: .primary, - wobble: style.surface.rosetteWobble, - lineWidth: style.surface.rosetteLineWidth, - primaryRingSpacing: style.surface.primaryRingSpacing, - secondaryRingSpacing: style.surface.secondaryRingSpacing, - primaryOpacity: style.ink.rosettePrimaryOpacity, - secondaryOpacity: style.ink.rosetteSecondaryOpacity, - ) + if components[.forecastPanel].nativeArtwork.contains(.rosette) { + SecurityPrintRosette( + tint: .primary, + wobble: style.surface.rosetteWobble, + lineWidth: style.surface.rosetteLineWidth, + primaryRingSpacing: style.surface.primaryRingSpacing, + secondaryRingSpacing: style.surface.secondaryRingSpacing, + primaryOpacity: style.ink.rosettePrimaryOpacity, + secondaryOpacity: style.ink.rosetteSecondaryOpacity, + ) + } } .clipShape(shape) .allowsHitTesting(false) @@ -67,15 +79,17 @@ struct LocationForecastPanel: View { Color.primary.opacity(style.ink.surfaceOutlineOpacity), lineWidth: style.surface.outlineWidth, ) - RegionOutlineSecurityBorder( - paths: regionBorderPaths, - tint: .primary, - cornerRadius: style.cornerRadius, - inset: style.surface.inset, - glyphSize: style.surface.microprintGlyphSize, - spacing: style.surface.microprintSpacing, - opacity: style.ink.microprintOpacity, - ) + if components[.forecastPanel].nativeArtwork.contains(.microprint) { + RegionOutlineSecurityBorder( + paths: regionBorderPaths, + tint: .primary, + cornerRadius: style.cornerRadius, + inset: style.surface.inset, + glyphSize: style.surface.microprintGlyphSize, + spacing: style.surface.microprintSpacing, + opacity: style.ink.microprintOpacity, + ) + } } .allowsHitTesting(false) } diff --git a/Where/WhereUI/Sources/Forecasting/LocationForecastRow.swift b/Where/WhereUI/Sources/Forecasting/LocationForecastRow.swift index 4c5f2b957..aa666297f 100644 --- a/Where/WhereUI/Sources/Forecasting/LocationForecastRow.swift +++ b/Where/WhereUI/Sources/Forecasting/LocationForecastRow.swift @@ -21,7 +21,7 @@ struct LocationForecastRow: View { Text(WhereFormat.forecastPercentage(forecast.estimatedTotalDays, year: forecast.year)) .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) LocationForecastProgress(forecast: forecast, tint: tint) VStack(alignment: .leading, spacing: row.estimateSpacing) { @@ -29,21 +29,22 @@ struct LocationForecastRow: View { yearToDateDays: forecast.yearToDateDays, )) .font(row.detailFont) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) if forecast.plannedDays.upper > 0 { Text(String(localized: .forecastPlannedContribution(WhereFormat .dayCount(forecast.plannedDays)))) .font(row.detailFont) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } if forecast.projectedRemainingDays.upper > 0 { Text(gapDescription) .font(row.detailFont) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } } } + .fixedSize(horizontal: false, vertical: true) .padding(row.padding) .frame(maxWidth: .infinity, alignment: .leading) .background { diff --git a/Where/WhereUI/Sources/Forecasting/LocationForecastSummary.swift b/Where/WhereUI/Sources/Forecasting/LocationForecastSummary.swift index 3ec5cbcb8..680c616fb 100644 --- a/Where/WhereUI/Sources/Forecasting/LocationForecastSummary.swift +++ b/Where/WhereUI/Sources/Forecasting/LocationForecastSummary.swift @@ -39,9 +39,16 @@ struct LocationForecastSummary: View { LocationForecastSummarySurface(artwork: artworkModel .artwork(for: primaryRegions) ?? []) } + .overlay { WhereComponentArtwork( + role: .summary, + plane: .exterior, + shape: RoundedRectangle(cornerRadius: style.summary.cornerRadius), + ) } .regionArtworkTask(id: primaryRegions, model: artworkModel) { cache in await LocationForecastArtworkLoader.loadSummary(regions: primaryRegions, cache: cache) } + .modifier(WhereComponentTextStyle(role: .summary)) + .whereThemeArtworkSpace(.summary) } private func toggleExpansion() { diff --git a/Where/WhereUI/Sources/Forecasting/LocationForecastSummarySurface.swift b/Where/WhereUI/Sources/Forecasting/LocationForecastSummarySurface.swift index 9065234b4..0317df0c8 100644 --- a/Where/WhereUI/Sources/Forecasting/LocationForecastSummarySurface.swift +++ b/Where/WhereUI/Sources/Forecasting/LocationForecastSummarySurface.swift @@ -5,31 +5,41 @@ struct LocationForecastSummarySurface: View { let artwork: [LocationForecastArtworkLoader.Item] @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components var body: some View { let style = stylesheet.locationForecast.summary let shape = RoundedRectangle(cornerRadius: style.cornerRadius) ZStack { - SecurityPrintRosette( - tint: style.ink, - wobble: style.rosette.wobble, - lineWidth: style.rosette.lineWidth, - primaryRingSpacing: style.rosette.primaryRingSpacing, - secondaryRingSpacing: style.rosette.secondaryRingSpacing, - primaryOpacity: style.rosetteOpacity, - secondaryOpacity: style.rosetteOpacity, - ) - RegionOutlineSecurityBorder( - paths: artwork.map(\.microprint), - tint: style.ink, - cornerRadius: style.cornerRadius, - inset: style.border.inset, - glyphSize: style.border.glyphSize, - spacing: style.border.spacing, - opacity: style.border.opacity, - ) - LocationForecastSilhouettes(artwork: artwork) + // The surface keeps its host's bounds when every optional mark is removed. + Color.clear + if let background = components[.summary].background { shape.fill(background) } + if components[.summary].nativeArtwork.contains(.rosette) { + SecurityPrintRosette( + tint: style.ink, + wobble: style.rosette.wobble, + lineWidth: style.rosette.lineWidth, + primaryRingSpacing: style.rosette.primaryRingSpacing, + secondaryRingSpacing: style.rosette.secondaryRingSpacing, + primaryOpacity: style.rosetteOpacity, + secondaryOpacity: style.rosetteOpacity, + ) + } + if components[.summary].nativeArtwork.contains(.microprint) { + RegionOutlineSecurityBorder( + paths: artwork.map(\.microprint), + tint: style.ink, + cornerRadius: style.cornerRadius, + inset: style.border.inset, + glyphSize: style.border.glyphSize, + spacing: style.border.spacing, + opacity: style.border.opacity, + ) + } + if components[.summary].nativeArtwork + .contains(.watermark) { LocationForecastSilhouettes(artwork: artwork) } + WhereComponentArtwork(role: .summary, plane: .interior, shape: shape) } .background { if style.treatment.usesReflection { diff --git a/Where/WhereUI/Sources/Forecasting/PlannedStayEditor.swift b/Where/WhereUI/Sources/Forecasting/PlannedStayEditor.swift index 23e3d4620..fcdce377a 100644 --- a/Where/WhereUI/Sources/Forecasting/PlannedStayEditor.swift +++ b/Where/WhereUI/Sources/Forecasting/PlannedStayEditor.swift @@ -30,75 +30,81 @@ struct PlannedStayEditor: View { NavigationStack { Form { - if let error = model.report.forecasts.loadFailure { - Section { - Label( - error.localizedDescription, - systemSymbol: .exclamationmarkTriangleFill, - ) - .foregroundStyle(.red) - Button(String(localized: .commonRetry)) { - Task { await model.report.forecasts.refresh() } + Group { + if let error = model.report.forecasts.loadFailure { + Section { + Label( + error.localizedDescription, + systemSymbol: .exclamationmarkTriangleFill, + ) + .foregroundStyle(.red) + Button(String(localized: .commonRetry)) { + Task { await model.report.forecasts.refresh() } + } } } - } - destinationSection - PlannedStayBoundarySection( - title: String(localized: .plannedStayEditorArrival), - boundary: $model.arrival, - ) - PlannedStayBoundarySection( - title: String(localized: .plannedStayEditorLastDay), - boundary: $model.departure, - ) - Section { - if let message = model.validationMessage { - Label(message, systemSymbol: .exclamationmarkCircle) - .foregroundStyle(.secondary) - } - } footer: { - Text(String(localized: .plannedStayEditorDatesFooter)) - } - if model.hasDefiniteOverlap || model.hasPossibleOverlap { + destinationSection + PlannedStayBoundarySection( + title: String(localized: .plannedStayEditorArrival), + boundary: $model.arrival, + ) + PlannedStayBoundarySection( + title: String(localized: .plannedStayEditorLastDay), + boundary: $model.departure, + ) Section { - if model.hasDefiniteOverlap { - Label( - String(localized: .plannedStaysDefiniteOverlap), - systemSymbol: .rectangleOnRectangle, - ) + if let message = model.validationMessage { + Label(message, systemSymbol: .exclamationmarkCircle) + .foregroundStyle(.whereSecondary) } - if model.hasPossibleOverlap { + } footer: { + Text(String(localized: .plannedStayEditorDatesFooter)) + } + if model.hasDefiniteOverlap || model.hasPossibleOverlap { + Section { + if model.hasDefiniteOverlap { + Label( + String(localized: .plannedStaysDefiniteOverlap), + systemSymbol: .rectangleOnRectangle, + ) + } + if model.hasPossibleOverlap { + Label( + String(localized: .plannedStaysPossibleOverlap), + systemSymbol: .rectangleDashed, + ) + } + } footer: { + Text(String(localized: .plannedStaysOverlapFooter)) + } + } + if case let .failed(error) = model.saveState { + Section { Label( - String(localized: .plannedStaysPossibleOverlap), - systemSymbol: .rectangleDashed, + error.localizedDescription, + systemSymbol: .exclamationmarkTriangleFill, ) + .foregroundStyle(.red) } - } footer: { - Text(String(localized: .plannedStaysOverlapFooter)) - } - } - if case let .failed(error) = model.saveState { - Section { - Label( - error.localizedDescription, - systemSymbol: .exclamationmarkTriangleFill, - ) - .foregroundStyle(.red) } - } - if model.isEditing { - Section { - Button(String(localized: .plannedStayEditorDelete), role: .destructive) { - Task { - if await model.delete() { dismiss() } + if model.isEditing { + Section { + Button( + String(localized: .plannedStayEditorDelete), + role: .destructive, + ) { + Task { + if await model.delete() { dismiss() } + } } } } - } + }.whereThemeListRow() } .environment(\.calendar, model.report.calendar) .environment(\.timeZone, model.report.calendar.timeZone) .disabled(model.isSaving) + .whereThemeScreen(.sheet) .navigationTitle(String(localized: model.isEditing ? .plannedStayEditorEditTitle : .plannedStayEditorNewTitle)) @@ -124,6 +130,7 @@ struct PlannedStayEditor: View { } } } + .whereThemeActivity(!showsManageRegions) .sheet(isPresented: $showsManageRegions, onDismiss: { Task { await model.regionSelection.load() } }) { @@ -155,14 +162,14 @@ struct PlannedStayEditor: View { !tracked.contains(region) { Text(String(localized: .plannedStayEditorUntracked)) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) Button(String(localized: .settingsRegionsManage), systemSymbol: .map) { showsManageRegions = true } } if case let .failed(error) = model.regionSelection.loadState { Label(error.localizedDescription, systemSymbol: .exclamationmarkTriangle) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) Button(String(localized: .commonRetry)) { Task { await model.regionSelection.load() } } diff --git a/Where/WhereUI/Sources/Forecasting/PlannedStaySummaryRow.swift b/Where/WhereUI/Sources/Forecasting/PlannedStaySummaryRow.swift index 4e54725e4..142884e6e 100644 --- a/Where/WhereUI/Sources/Forecasting/PlannedStaySummaryRow.swift +++ b/Where/WhereUI/Sources/Forecasting/PlannedStaySummaryRow.swift @@ -23,7 +23,7 @@ struct PlannedStaySummaryRow: View { .font(.headline) Spacer(minLength: 0) Image(systemSymbol: .pencil) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .accessibilityHidden(true) } LabeledContent(String(localized: .plannedStayEditorArrival)) { @@ -38,7 +38,7 @@ struct PlannedStaySummaryRow: View { systemSymbol: .rectangleOnRectangle, ) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } if hasPossibleOverlap { Label( @@ -46,7 +46,7 @@ struct PlannedStaySummaryRow: View { systemSymbol: .rectangleDashed, ) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } } .foregroundStyle(.primary) diff --git a/Where/WhereUI/Sources/Forecasting/PlannedStaysView.swift b/Where/WhereUI/Sources/Forecasting/PlannedStaysView.swift index 3ef1b60be..584f83e62 100644 --- a/Where/WhereUI/Sources/Forecasting/PlannedStaysView.swift +++ b/Where/WhereUI/Sources/Forecasting/PlannedStaysView.swift @@ -25,62 +25,65 @@ struct PlannedStaysView: View { NavigationStack { List { - if let error = model.report.forecasts.loadFailure { - Section { - Label( - error.localizedDescription, - systemSymbol: .exclamationmarkTriangleFill, - ) - .foregroundStyle(.red) - Button(String(localized: .commonRetry)) { - Task { await model.report.forecasts.refresh() } + Group { + if let error = model.report.forecasts.loadFailure { + Section { + Label( + error.localizedDescription, + systemSymbol: .exclamationmarkTriangleFill, + ) + .foregroundStyle(.red) + Button(String(localized: .commonRetry)) { + Task { await model.report.forecasts.refresh() } + } } } - } - Section { - if !model.report.forecasts.hasLoaded, - model.report.forecasts.loadFailure == nil - { - ProgressView() - } else if model.report.forecasts.hasLoaded, model.upcoming.isEmpty { - Text(String(localized: .plannedStaysEmpty)) - .foregroundStyle(.secondary) - } else { - ForEach(model.upcoming) { stay in - stayRow(stay) + Section { + if !model.report.forecasts.hasLoaded, + model.report.forecasts.loadFailure == nil + { + ProgressView() + } else if model.report.forecasts.hasLoaded, model.upcoming.isEmpty { + Text(String(localized: .plannedStaysEmpty)) + .foregroundStyle(.whereSecondary) + } else { + ForEach(model.upcoming) { stay in + stayRow(stay) + } } + Button( + String(localized: .plannedStaysAdd), + systemSymbol: .plus, + action: model.add, + ) + } header: { + Text(String(localized: .plannedStaysUpcoming)) + } footer: { + Text(String(localized: .plannedStaysFooter)) } - Button( - String(localized: .plannedStaysAdd), - systemSymbol: .plus, - action: model.add, - ) - } header: { - Text(String(localized: .plannedStaysUpcoming)) - } footer: { - Text(String(localized: .plannedStaysFooter)) - } - if model.report.forecasts.hasLoaded { - gapSection - } + if model.report.forecasts.hasLoaded { + gapSection + } - if !model.past.isEmpty { - Section { - DisclosureGroup( - String(localized: .plannedStaysPast), - isExpanded: $model.showsPast, - ) { - ForEach(model.past) { stay in - stayRow(stay) + if !model.past.isEmpty { + Section { + DisclosureGroup( + String(localized: .plannedStaysPast), + isExpanded: $model.showsPast, + ) { + ForEach(model.past) { stay in + stayRow(stay) + } } + } footer: { + Text(String(localized: .plannedStaysPastFooter)) } - } footer: { - Text(String(localized: .plannedStaysPastFooter)) } - } + }.whereThemeListRow() } + .whereThemeScreen(.sheet) .navigationTitle(String(localized: .plannedStaysTitle)) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -89,6 +92,7 @@ struct PlannedStaysView: View { } } } + .whereThemeActivity(model.editor == nil) .sheet(item: $model.editor) { editor in PlannedStayEditor(model: editor) } diff --git a/Where/WhereUI/Sources/Forecasting/PlanningRegionPickerView.swift b/Where/WhereUI/Sources/Forecasting/PlanningRegionPickerView.swift index 461ae093c..8bffdd5de 100644 --- a/Where/WhereUI/Sources/Forecasting/PlanningRegionPickerView.swift +++ b/Where/WhereUI/Sources/Forecasting/PlanningRegionPickerView.swift @@ -19,7 +19,7 @@ struct PlanningRegionPickerView: View { if case let .failed(error) = model.loadState { Section { Label(error.localizedDescription, systemSymbol: .exclamationmarkTriangle) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) Button(String(localized: .commonRetry)) { Task { await model.load() } } diff --git a/Where/WhereUI/Sources/Logging/WhereThemeLog.swift b/Where/WhereUI/Sources/Logging/WhereThemeLog.swift new file mode 100644 index 000000000..1ee75133b --- /dev/null +++ b/Where/WhereUI/Sources/Logging/WhereThemeLog.swift @@ -0,0 +1,13 @@ +import PeriscopeCore + +@LogScope("WhereTheme") +enum WhereThemeLog { + @LogEvent("bundle-unreadable", level: .fault, version: 1) + struct BundleUnreadable { + @LogField(exposure: .restricted, kind: .errorDetails) + var error: LogError + var message: String { + "Failed to load a bundled theme: \(error.description)" + } + } +} diff --git a/Where/WhereUI/Sources/MainTabs.swift b/Where/WhereUI/Sources/MainTabs.swift index fde47cbfb..c42ea6a9e 100644 --- a/Where/WhereUI/Sources/MainTabs.swift +++ b/Where/WhereUI/Sources/MainTabs.swift @@ -38,6 +38,7 @@ struct MainTabs: View { @Environment(\.isInDemoMode) private var isInDemoMode @Environment(\.scenePhase) private var scenePhase @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var componentStylesheet private let recordingWarningSource: RecordingConfigurationWarningModel.Source private let allowsWelcomeLookup: Bool @@ -73,6 +74,9 @@ struct MainTabs: View { value: TabID.locations, ) { LocationsView(report: report, openElsewhere: { selection.selected = .elsewhere }) + .whereThemeActivity(selection + .selected == .locations && welcomePresentation == nil && + planningDestination == nil) .reportingDeveloperTabBarInset() } @@ -83,17 +87,27 @@ struct MainTabs: View { value: TabID.elsewhere, ) { ElsewhereView(report: report, isSelected: selection.selected == .elsewhere) + .whereThemeActivity(selection + .selected == .elsewhere && welcomePresentation == nil && + planningDestination == nil) .reportingDeveloperTabBarInset() } } Tab(String(localized: .tabYear), systemSymbol: .calendar, value: TabID.year) { YearView(report: report) + .whereThemeActivity(selection + .selected == .year && welcomePresentation == nil && planningDestination == + nil) .reportingDeveloperTabBarInset() } Tab(value: TabID.settings) { SettingsView(report: report, recordingWarning: recordingWarning) + .whereThemeActivity(selection + .selected == .settings && welcomePresentation == nil && + planningDestination == + nil) .reportingDeveloperTabBarInset() } label: { RecordingConfigurationWarningTabLabel( @@ -105,6 +119,7 @@ struct MainTabs: View { .badge((recordingWarning.isPresented ? 1 : 0) + ((!isInDemoMode && updates?.needsAttention == true) ? 1 : 0)) } + .tint(componentStylesheet[.locations].foreground) .accessibilityHidden(welcomePresentation != nil) .onChange(of: report.hasElsewhere, initial: true) { _, hasElsewhere in selection.reconcile(hasElsewhere: hasElsewhere) @@ -436,3 +451,66 @@ struct MainTabs: View { MainTabsPreview() } #endif + +#if DEBUG + extension MainTabs { + static func halloweenSnapshot(selection: MainTabSelection + .TabID) async throws -> SnapshotCase + { + let session = PreviewSupport.loadedSession() + session.preferences.showsLocationWelcome = false + let report = YearReportModel( + services: session.services, + selectedYear: PreviewSupport.year, + preferences: session.preferences, + now: session.now, + ) + let welcome = LocationWelcomeModel( + services: session.services, + preferences: session.preferences, + now: session.now, + ) + let cache = RegionOutlinePathCache() + let model = PreviewSupport.loadedModel() + model.previewTheme(.halloween) + try await session.services.setPrimaryRegions([ + PrimaryRegion(region: .california, appearance: nil, order: 0), + PrimaryRegion(region: .newYork, appearance: nil, order: 1), + ]) + for day in PreviewSupport.sampleReport().days { + try await session.services.journal.addManualDay( + date: day.day.startOfDay(in: report.calendar), + regions: day.regions, + audit: nil, + ) + } + await report.refresh() + if let input = report.elsewhereInput { await report.elsewhere.refresh(input) } + let name = switch selection { + case .locations: "Locations" + case .elsewhere: "Elsewhere" + case .year: "Year" + case .settings: "Settings" + } + return SnapshotCase( + name: name, + configurations: .fullContentPhoneLightDark, + referenceGroup: .init(rawValue: "halloween"), + onReadyToSnapshot: { + await PreviewSupport.loadCardArtwork( + for: [.california, .newYork] + + (report.elsewhere.content(for: report.selectedYear)?.regions + .map(\.region) ?? []), + cache: cache, + ) + }, + ) { + MainTabs(session: session, report: report, welcome: welcome, selection: selection) + .environment(session) + .environment(model) + .environment(\.regionOutlinePathCache, cache) + .whereBroadwayRoot(theme: .halloween) + } + } + } +#endif diff --git a/Where/WhereUI/Sources/Onboarding/OnboardingThemeSelectionView.swift b/Where/WhereUI/Sources/Onboarding/OnboardingThemeSelectionView.swift index dd53432f1..c2596de22 100644 --- a/Where/WhereUI/Sources/Onboarding/OnboardingThemeSelectionView.swift +++ b/Where/WhereUI/Sources/Onboarding/OnboardingThemeSelectionView.swift @@ -3,7 +3,7 @@ import SnapshotKit import SwiftUI import WhereCore -/// First-run choice between Where's two presentation identities. +/// First-run choice among Where's bundled themes. struct OnboardingThemeSelectionView: View { let selection: WhereTheme let onSelect: (WhereTheme) -> Void @@ -109,6 +109,17 @@ struct OnboardingThemeSelectionView: View { OnboardingThemeSelectionSnapshotRoot(selection: .alternate) .environment(\.regionOutlinePathCache, cache) }, + whereSnapshot( + name: "Halloween", + configurations: .fullContentPhoneLightDark, + theme: .halloween, + onReadyToSnapshot: { + await PreviewSupport.loadCardArtwork(for: [.california], cache: cache) + }, + ) { + OnboardingThemeSelectionSnapshotRoot(selection: .halloween) + .environment(\.regionOutlinePathCache, cache) + }, ] } } diff --git a/Where/WhereUI/Sources/Preview/WhereSnapshot.swift b/Where/WhereUI/Sources/Preview/WhereSnapshot.swift index 229f93bcc..22e3fd371 100644 --- a/Where/WhereUI/Sources/Preview/WhereSnapshot.swift +++ b/Where/WhereUI/Sources/Preview/WhereSnapshot.swift @@ -43,6 +43,7 @@ switch self { case .standard: nil case .alternate: SnapshotReferenceGroup(rawValue: "flat")! + case .halloween: SnapshotReferenceGroup(rawValue: "halloween")! } } } diff --git a/Where/WhereUI/Sources/Primary/CalendarContentView.swift b/Where/WhereUI/Sources/Primary/CalendarContentView.swift index bf0f8bb1a..db3f46d4d 100644 --- a/Where/WhereUI/Sources/Primary/CalendarContentView.swift +++ b/Where/WhereUI/Sources/Primary/CalendarContentView.swift @@ -109,6 +109,8 @@ struct CalendarContentView: View { // Log View Mode: reveal an inspect badge for this calendar's events. A // no-op in release. .debugLogInspectable(WhereLog.session(CalendarViewLog.self)) + .whereThemeScreen(.year) + .whereThemeActivity(planningDestination == nil) .sheet(item: $planningDestination) { destination in PlannedStaysDestinationView(destination: destination, report: report) } @@ -221,6 +223,7 @@ struct CalendarContentView: View { } .id(target(for: month)) .accessibilityFocused($focusedTarget, equals: target(for: month)) + .whereThemeArtworkSpace(.calendarMonth) if showsForecast(after: month) { LocationForecastPanel( forecasts: calendarForecasts, @@ -236,6 +239,7 @@ struct CalendarContentView: View { $focusedTarget, equals: .estimate(monthKey(month)), ) + .whereThemeArtworkSpace(.forecastPanel) } } } @@ -456,6 +460,7 @@ struct CalendarContentView: View { rejected: presentation.rejectedTarget == destination, trigger: presentation.rejectionCount, )) + .whereThemeArtworkSpace(.calendarMonth) } private func overviewEstimate(_ month: CalendarMonth) -> some View { @@ -472,6 +477,7 @@ struct CalendarContentView: View { [.overview: [.tile(destination): $0]] } .accessibilityHint(String(localized: .calendarExpandEstimate)) + .whereThemeArtworkSpace(.forecastPanel) } private func showsForecast(after month: CalendarMonth) -> Bool { @@ -507,6 +513,7 @@ private struct MonthGridView: View { var isAvailable = true @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components private var calendar: WhereStylesheet.CalendarStyle { compact ? stylesheet.calendar.compact : stylesheet.calendar @@ -551,7 +558,7 @@ private struct MonthGridView: View { VStack(alignment: .leading, spacing: calendar.month.sectionSpacing) { Text(monthName) .font(compact ? .system(size: calendar.overview.titleFontSize, weight: .semibold) : - .title.weight(.semibold)) + (components[.calendarMonth].heading ?? .title.weight(.semibold))) .frame(maxWidth: .infinity, alignment: .leading) .foregroundStyle(isAvailable ? card.foreground : calendar.overview .unavailableForeground) @@ -586,20 +593,20 @@ private struct MonthGridView: View { } } .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } } .padding(calendar.month.padding) .foregroundStyle(card.foreground) - .background { - // Transparent month cards share the screen background. The current - // month stands out through its accent text and heavier border. - cardShape - .fill(card.fill) - .overlay { - cardShape.strokeBorder(card.border, lineWidth: card.borderWidth) - } + .whereThemePanel( + .calendarMonth, + in: cardShape, + tint: components[.calendarMonth].foreground ?? .primary, + ) { + if components[.calendarMonth].surface == nil, + components[.calendarMonth].background == nil { cardShape.fill(card.fill) } } + .overlay { cardShape.strokeBorder(card.border, lineWidth: card.borderWidth) } .contentShape(cardShape) .accessibilityElement(children: .ignore) .accessibilityLabel(monthAccessibility.label) @@ -621,7 +628,7 @@ private struct MonthGridView: View { Text(symbol) .font(.system(size: calendar.month.weekdayFontSize)) .lineLimit(1) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .frame(maxWidth: .infinity) } } @@ -811,7 +818,7 @@ private struct MonthFooter: View { Text(WhereFormat.dayCount(tally.days)) .font(.subheadline) .monospacedDigit() - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } .frame(maxWidth: .infinity, alignment: .leading) .opacity(focusedRegion == nil || isFocused ? 1 : calendar.month.unfocusedRowOpacity) diff --git a/Where/WhereUI/Sources/Primary/FlightJourneyRow.swift b/Where/WhereUI/Sources/Primary/FlightJourneyRow.swift index b7cfd134f..425db7631 100644 --- a/Where/WhereUI/Sources/Primary/FlightJourneyRow.swift +++ b/Where/WhereUI/Sources/Primary/FlightJourneyRow.swift @@ -25,14 +25,14 @@ struct FlightJourneyRow: View { end: period.end, calendar: calendar, )) - .font(.caption).foregroundStyle(.secondary) + .font(.caption).foregroundStyle(.whereSecondary) } if arrival.isTimingUncertain { Text(.flightStatusUncertainTitle) - .font(.caption).foregroundStyle(.secondary) + .font(.caption).foregroundStyle(.whereSecondary) Text(String(localized: .flightStatusGroundObserved(arrival.firstGroundAt .formatted(.dateTime.month(.abbreviated).day().hour().minute())))) - .font(.caption).foregroundStyle(.secondary) + .font(.caption).foregroundStyle(.whereSecondary) } } .frame(maxWidth: .infinity, alignment: .leading) diff --git a/Where/WhereUI/Sources/Primary/LocationsBackground.swift b/Where/WhereUI/Sources/Primary/LocationsBackground.swift index 844abcf1d..4e34f2855 100644 --- a/Where/WhereUI/Sources/Primary/LocationsBackground.swift +++ b/Where/WhereUI/Sources/Primary/LocationsBackground.swift @@ -5,8 +5,10 @@ import SwiftUI /// Stationary, noninteractive paper texture shared by Locations and Elsewhere. struct LocationsBackground: View { let regions: [Region] + var role: WhereThemeComponents.Role = .locations @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var componentStylesheet @State private var artworkModel = RegionArtworkModel<[Region], LocationsBackgroundArtwork>() private var style: WhereStylesheet.LocationsBackgroundStyle { @@ -14,24 +16,29 @@ struct LocationsBackground: View { } private var requestedRegions: [Region] { - style.showsInk ? regions.filter { $0 != .other } : [] + style.showsInk && componentStylesheet[role].nativeArtwork.contains(.regionPattern) ? regions + .filter { $0 != .other } : [] } var body: some View { ZStack { - style.paper + componentStylesheet[role].background ?? style.paper if style.showsInk { - SecurityPrintRosette( - tint: style.ink, - wobble: style.rosette.wobble, - lineWidth: style.rosette.lineWidth, - primaryRingSpacing: style.rosette.primaryRingSpacing, - secondaryRingSpacing: style.rosette.secondaryRingSpacing, - primaryOpacity: style.rosetteOpacity, - secondaryOpacity: style.rosetteOpacity, - ) - silhouettes + if componentStylesheet[role].nativeArtwork.contains(.rosette) { + SecurityPrintRosette( + tint: style.ink, + wobble: style.rosette.wobble, + lineWidth: style.rosette.lineWidth, + primaryRingSpacing: style.rosette.primaryRingSpacing, + secondaryRingSpacing: style.rosette.secondaryRingSpacing, + primaryOpacity: style.rosetteOpacity, + secondaryOpacity: style.rosetteOpacity, + ) + } + if componentStylesheet[role].nativeArtwork.contains(.regionPattern) { silhouettes } } + WhereComponentArtwork(role: role, plane: .interior, shape: Rectangle()) + WhereComponentArtwork(role: role, plane: .exterior, shape: Rectangle()) } .clipped() .allowsHitTesting(false) diff --git a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift index fab7e938d..ea68aadd3 100644 --- a/Where/WhereUI/Sources/Primary/LocationsCardsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsCardsView.swift @@ -14,8 +14,19 @@ struct LocationsCardsView: View { let planningAction: () -> Void @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var componentStylesheet @Environment(\.regionStyles) private var regionStyles + private var artworkSpacing: CGFloat { + let bounds = componentStylesheet[.regularCard].outsets + return stylesheet.spacing.xxLarge + bounds.top + bounds.bottom + } + + private var additionalGutter: CGFloat { + let bounds = componentStylesheet[.regularCard].outsets + return max(0, max(bounds.leading, bounds.trailing) - stylesheet.spacing.xxLarge) + } + var body: some View { let presentedCards = cardPresentation.presented(report.locationRegions) @@ -26,7 +37,7 @@ struct LocationsCardsView: View { LocationsFlightNotice(report: report) LocationCardRankingStack( - spacing: stylesheet.spacing.xxLarge, + spacing: artworkSpacing, presentation: cardPresentation, motion: stylesheet.locationCardStack.overtake, ) { @@ -45,41 +56,12 @@ struct LocationsCardsView: View { .pointsByRegion[item.region] ?? [], showsRecordedPoints: report.showsRecordedLocationDots, recordedPointsID: report.primaryRegionLocations?.id, + transitionNamespace: calendarTransition, ) } .buttonStyle(stylesheet.card.regular.buttonStyle( tint: regionStyles.style(for: item.region).tint, )) - // The card is the zoom source: tapping it expands the - // card into the pushed calendar (matched geometry). The - // configuration re-states the card's rounded shape and - // its glow/lift shadows so the transition interpolates - // them — without it, the zoom clips the source to a bare - // rectangle and the card's soft shadow pops on/off. - .matchedTransitionSource( - id: item.region, - in: calendarTransition, - ) { source in - let card = stylesheet.card.regular - let tint = regionStyles.style(for: item.region).tint - let elevation = card.transitionElevation(tint: tint) - return source - .clipShape( - RoundedRectangle( - cornerRadius: card.cornerRadius, - style: .continuous, - ), - ) - .shadow( - color: elevation.glow.color, - radius: elevation.glow.radius, - ) - .shadow( - color: elevation.lift.color, - radius: elevation.lift.radius, - y: elevation.lift.offsetY, - ) - } .accessibilityHint(String(localized: .primaryCardCalendarHint)) .locationCardOvertakeEffect( region: item.region, @@ -90,16 +72,18 @@ struct LocationsCardsView: View { } } + .padding(.top, componentStylesheet[.regularCard].outsets.top) + .padding(.bottom, componentStylesheet[.regularCard].outsets.bottom) + LocationsEstimatePanel(report: report, planningAction: planningAction) } - .padding() + .padding(stylesheet.spacing.xxLarge) + .padding(.horizontal, additionalGutter) .frame(maxWidth: .infinity) } .defaultScrollAnchor(report.liveFlightReview == nil ? .center : .top) .scrollBounceBehavior(.basedOnSize) - // The card normally stays clipped to the scrolling viewport. Reveal - // overflow only while the authored arc, scale, and rotation need it. - .scrollClipDisabled(cardPresentation.isSpatialOvertakeActive) + // Theme artwork remains clipped to the scrolling viewport during ranking changes. .accessibilityIdentifier("where_root_title") .onAppear { isCardSurfaceVisible = true } .onDisappear { isCardSurfaceVisible = false } diff --git a/Where/WhereUI/Sources/Primary/LocationsView.swift b/Where/WhereUI/Sources/Primary/LocationsView.swift index cdd2aae5b..6b0ed9f2b 100644 --- a/Where/WhereUI/Sources/Primary/LocationsView.swift +++ b/Where/WhereUI/Sources/Primary/LocationsView.swift @@ -46,6 +46,7 @@ struct LocationsView: View { var body: some View { NavigationStack { screen + .modifier(WhereComponentTextStyle(role: .locations)) .frame(maxWidth: .infinity, maxHeight: .infinity) .background { LocationsBackground(regions: backgroundRegions) @@ -77,6 +78,8 @@ struct LocationsView: View { } } } + // Each layer tracks its own visibility, including backgrounds in empty/loading states. + .whereThemeActivity(!showingResolution && planningDestination == nil) .observesTilt( tilt, enabled: isCardSurfaceUncovered && cardSurface.usesReflection, @@ -170,7 +173,8 @@ private struct ResolveToolbarLabel: View { extension LocationsView: SnapshotProviding { /// Allow native glass adaptation and the background artwork to settle before capture. static var snapshots: [SnapshotCase] { - for theme in WhereTheme.allCases where theme != .standard { + // Halloween has full-screen and accessibility coverage in MainTabs snapshots. + for theme in [WhereTheme.alternate] { artworkSnapshot( name: "\(theme.snapshotReferenceGroup!.rawValue.capitalized)Loaded", report: PreviewSupport.loadedYearReportModel(), @@ -361,3 +365,44 @@ private struct ResolveToolbarLabel: View { } } #endif + +#if DEBUG + extension LocationsView { + @MainActor + enum SeasonalThemes: SnapshotProviding { + static var snapshots: [SnapshotCase] { + for example in WhereThemeExamples.allCases { + snapshot(example) + } + } + + private static func snapshot(_ example: WhereThemeExamples) -> SnapshotCase { + let theme: PreparedWhereThemeDocument + do { theme = try example.prepare() } + catch { preconditionFailure("Invalid bundled theme: \(error)") } + let report = PreviewSupport.loadedYearReportModel() + let cache = RegionOutlinePathCache() + let regions = LocationsBackgroundArtwork.regions(in: report.ranking) + return SnapshotCase( + name: example.rawValue, + configurations: .fullContentPhoneLightDark, + measurementReadiness: .immediate, + settle: .settledAtLeast(minDuration: 1), + onReadyToSnapshot: { + for region in regions { + _ = await cache.path(for: region, resolution: .small) + _ = await cache.path(for: region, resolution: .medium) + _ = await cache.path(for: region, resolution: .micro) + } + }, + ) { + LocationsView(report: report) + .environment(\.regionOutlinePathCache, cache) + .whereBroadwayRoot(themeDocument: theme) + } + } + } + } + + #Preview("Seasonal theme documents") { LocationsView.SeasonalThemes.snapshotPreviews } +#endif diff --git a/Where/WhereUI/Sources/Primary/PlannedPresenceJourneyCardContent.swift b/Where/WhereUI/Sources/Primary/PlannedPresenceJourneyCardContent.swift index ce4f5593d..c5d66efea 100644 --- a/Where/WhereUI/Sources/Primary/PlannedPresenceJourneyCardContent.swift +++ b/Where/WhereUI/Sources/Primary/PlannedPresenceJourneyCardContent.swift @@ -12,6 +12,7 @@ struct PlannedPresenceJourneyCardContent: View { let tint: Color @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components var body: some View { let timeline = stylesheet.timeline @@ -31,20 +32,20 @@ struct PlannedPresenceJourneyCardContent: View { if row.stacksDayCount { VStack(alignment: .leading, spacing: row.labelSpacing) { Text(regionName) - .font(.subheadline.bold()) + .font(components[.timelineEntry].heading ?? .subheadline.bold()) Text(dateRange) - .font(.subheadline) - .foregroundStyle(.secondary) + .font(components[.timelineEntry].body ?? .subheadline) + .foregroundStyle(.whereSecondary) } .frame(maxWidth: .infinity, alignment: .leading) } else { HStack(spacing: planned.joinedLabelSpacing) { Text(regionName) - .font(.subheadline.bold()) + .font(components[.timelineEntry].heading ?? .subheadline.bold()) .layoutPriority(1) Text(dateRange) - .font(.subheadline) - .foregroundStyle(.secondary) + .font(components[.timelineEntry].body ?? .subheadline) + .foregroundStyle(.whereSecondary) .lineLimit(1) } .frame(maxWidth: .infinity, alignment: .leading) @@ -62,13 +63,13 @@ struct PlannedPresenceJourneyCardContent: View { } else { VStack(alignment: .leading, spacing: row.labelSpacing) { Text(regionName) - .font(.headline) + .font(components[.timelineEntry].heading ?? .headline) Text(dateRange) - .font(.subheadline) - .foregroundStyle(.secondary) + .font(components[.timelineEntry].body ?? .subheadline) + .foregroundStyle(.whereSecondary) Text(detailLabel) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .opacity(planned.labelOpacity) } .frame(maxWidth: .infinity, alignment: .leading) diff --git a/Where/WhereUI/Sources/Primary/PlannedPresenceJourneyRow.swift b/Where/WhereUI/Sources/Primary/PlannedPresenceJourneyRow.swift index bcb4cb84c..acb34d4f4 100644 --- a/Where/WhereUI/Sources/Primary/PlannedPresenceJourneyRow.swift +++ b/Where/WhereUI/Sources/Primary/PlannedPresenceJourneyRow.swift @@ -40,11 +40,12 @@ struct PlannedPresenceJourneyRow: View { position: cardPosition, tint: style.tint, ) - .background { - PlannedPresenceJourneyBackground( - position: cardPosition, - tint: style.tint, - ) + .whereThemePanel( + .timelineEntry, + in: cardPosition.shape(cornerRadius: row.cornerRadius), + tint: style.tint, + ) { + PlannedPresenceJourneyBackground(position: cardPosition, tint: style.tint) } .overlay { PresenceJourneyCardBorder( diff --git a/Where/WhereUI/Sources/Primary/PresenceJourneyRow.swift b/Where/WhereUI/Sources/Primary/PresenceJourneyRow.swift index ec27783af..6fb493446 100644 --- a/Where/WhereUI/Sources/Primary/PresenceJourneyRow.swift +++ b/Where/WhereUI/Sources/Primary/PresenceJourneyRow.swift @@ -11,6 +11,7 @@ struct PresenceJourneyRow: View { let cardPosition: PresenceJourneyCardPosition @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components @Environment(\.regionStyles) private var regionStyles var body: some View { @@ -38,10 +39,10 @@ struct PresenceJourneyRow: View { countLayout { VStack(alignment: .leading, spacing: row.labelSpacing) { Text(stint.region.localizedName) - .font(.headline) + .font(components[.timelineEntry].heading ?? .headline) Text(dateRange) - .font(.subheadline) - .foregroundStyle(.secondary) + .font(components[.timelineEntry].body ?? .subheadline) + .foregroundStyle(.whereSecondary) } .frame(maxWidth: .infinity, alignment: .leading) @@ -58,9 +59,11 @@ struct PresenceJourneyRow: View { .frame(minHeight: proportionalHeight) .padding(.horizontal, row.horizontalPadding) .padding(.vertical, sizing.verticalPadding) - .background { - cardShape.fill(style.tint.opacity(row.fillOpacity)) - } + .whereThemePanel( + .timelineEntry, + in: cardPosition.shape(cornerRadius: row.cornerRadius), + tint: style.tint, + ) { cardShape.fill(style.tint.opacity(row.fillOpacity)) } .overlay { PresenceJourneyCardBorder( position: cardPosition, diff --git a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift index e23737343..2ac981837 100644 --- a/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift +++ b/Where/WhereUI/Sources/Primary/PresenceTimelineList.swift @@ -97,6 +97,7 @@ struct PresenceTimelineList: View { homeRegion: report.forecasts.planning.homeRegion, planningAction: { planningDestination = .list }, ) + .whereThemeArtworkSpace(.forecastPanel) } } .padding(.horizontal, stylesheet.spacing.xxLarge) @@ -122,6 +123,8 @@ struct PresenceTimelineList: View { .id(report.selectedYear) } } + .whereThemeScreen(.year) + .whereThemeActivity(planningDestination == nil) .sheet(item: $planningDestination) { destination in PlannedStaysDestinationView(destination: destination, report: report) } diff --git a/Where/WhereUI/Sources/Primary/RegionArtworkTransform.swift b/Where/WhereUI/Sources/Primary/RegionArtworkTransform.swift index 8ae872c05..cdb23908a 100644 --- a/Where/WhereUI/Sources/Primary/RegionArtworkTransform.swift +++ b/Where/WhereUI/Sources/Primary/RegionArtworkTransform.swift @@ -17,10 +17,13 @@ struct RegionArtworkTransform { size.width * style.extent.width / bounds.width, size.height * style.extent.height / bounds.height, ) * style.scale + // Zero-sized authored artwork and collapsed layout proposals have no drawable transform. + guard scale.isFinite, scale > 0 else { return nil } translation = CGPoint( x: size.width * style.center.x / scale - bounds.midX, y: size.height * style.center.y / scale - bounds.midY, ) + guard translation.x.isFinite, translation.y.isFinite else { return nil } } func apply(to context: inout GraphicsContext) { diff --git a/Where/WhereUI/Sources/Primary/RegionCardTransition.swift b/Where/WhereUI/Sources/Primary/RegionCardTransition.swift new file mode 100644 index 000000000..587151fe0 --- /dev/null +++ b/Where/WhereUI/Sources/Primary/RegionCardTransition.swift @@ -0,0 +1,29 @@ +import RegionKit +import SwiftUI + +/// The native card is the zoom source; exterior ornaments remain outside its rounded clip. +struct RegionCardTransition: ViewModifier { + let region: Region + let namespace: Namespace.ID? + let card: WhereStylesheet.CardStyle + let tint: Color + + func body(content: Content) -> some View { + if let namespace { + content.matchedTransitionSource(id: region, in: namespace) { source in + let elevation = card.transitionElevation(tint: tint) + return source + .clipShape(RoundedRectangle( + cornerRadius: card.cornerRadius, + style: .continuous, + )) + .shadow(color: elevation.glow.color, radius: elevation.glow.radius) + .shadow( + color: elevation.lift.color, + radius: elevation.lift.radius, + y: elevation.lift.offsetY, + ) + } + } else { content } + } +} diff --git a/Where/WhereUI/Sources/Primary/RegionSummaryCard.swift b/Where/WhereUI/Sources/Primary/RegionSummaryCard.swift index 180c4a554..acbf6e885 100644 --- a/Where/WhereUI/Sources/Primary/RegionSummaryCard.swift +++ b/Where/WhereUI/Sources/Primary/RegionSummaryCard.swift @@ -1,3 +1,4 @@ +import BroadwayUI import Foundation import RegionKit import SFSafeSymbols @@ -67,6 +68,8 @@ struct RegionSummaryCard: View { /// Identity of the loaded recorded-point snapshot. Callers supply it to /// restart projection only when the underlying point content changes. var recordedPointsID: RegionCardLocations.ID? + var transitionNamespace: Namespace.ID? + @State private var artworkIsVisible = true /// Loaded from the root-owned UI path cache. The large watermark uses /// medium fidelity, the stamp uses small, and the repeated border uses @@ -82,6 +85,7 @@ struct RegionSummaryCard: View { } @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var componentStylesheet @Environment(\.regionStyles) private var regionStyles #if DEBUG // The live designer draft is a runtime override, outside the cached base slice. @@ -203,17 +207,20 @@ struct RegionSummaryCard: View { let rosette = card.rosette let rosetteFill = cardStyles.rosetteFill return ZStack { - SecurityPrintRosette( - tint: tint, - wobble: rosette.wobble, - lineWidth: rosette.lineWidth, - primaryRingSpacing: rosette.primaryRingSpacing, - secondaryRingSpacing: rosette.secondaryRingSpacing, - primaryOpacity: rosetteFill.primary, - secondaryOpacity: rosetteFill.secondary, - ) + if nativeArtwork.contains(.rosette) { + SecurityPrintRosette( + tint: tint, + wobble: rosette.wobble, + lineWidth: rosette.lineWidth, + primaryRingSpacing: rosette.primaryRingSpacing, + secondaryRingSpacing: rosette.secondaryRingSpacing, + primaryOpacity: rosetteFill.primary, + secondaryOpacity: rosetteFill.secondary, + ) + } if + nativeArtwork.contains(.microprint), let regionShape = card.regionShape, let regionPath = regionPaths?.microprint, !regionPath.isEmpty @@ -237,13 +244,9 @@ struct RegionSummaryCard: View { RegionOutlineArtwork( path: regionPath, tint: tint, - style: regionShape.watermark, - constellationPoints: showsRecordedPoints - ? regionPaths?.constellation ?? [] - : [], - constellationStyle: cardStyles.constellation, + style: watermarkStyle(regionShape.watermark), ) - } else { + } else if nativeArtwork.contains(.watermark) { Image(systemSymbol: style.symbol) .font(.system(size: card.watermarkFontSize)) .foregroundStyle(tint.opacity(cardStyles.watermarkOpacity)) @@ -258,6 +261,26 @@ struct RegionSummaryCard: View { .accessibilityHidden(true) } + @ViewBuilder private var recordedConstellation: some View { + if showsRecordedPoints, + let artwork = card.regionShape?.watermark, + let paths = regionPaths, + !paths.constellation.isEmpty + { + RegionLocationConstellation( + path: paths.watermark, + points: paths.constellation, + tint: securityPrintTint, + artworkStyle: artwork, + style: cardStyles.constellation, + ) + .blendMode(card.paper == nil ? cardStyles.securityPrint.backgroundBlendMode : .normal) + .clipShape(cardShape) + .allowsHitTesting(false) + .accessibilityHidden(true) + } + } + private func loadRegionOutlines(_ regionOutlinePathCache: RegionOutlinePathCache) async -> RegionArtworkPaths? { @@ -302,7 +325,7 @@ struct RegionSummaryCard: View { private var content: some View { let regionName = Text(regionDays.region.localizedName) - .font(card.regionNameTypography.font) + .font(componentStylesheet[componentRole].heading ?? card.regionNameTypography.font) .tracking(card.regionNameTracking) .lineLimit(1) .allowsTightening(true) @@ -333,7 +356,7 @@ struct RegionSummaryCard: View { Spacer(minLength: 0) - if renderPurpose == .content { + if renderPurpose == .content, nativeArtwork.contains(.stamp) { entryStamp } } @@ -403,18 +426,55 @@ struct RegionSummaryCard: View { .animation(dayCount.animation, value: estimatedDays) .padding(card.padding) .frame(maxWidth: .infinity, alignment: .leading) - .background { stampPaper } + // Recorded locations are native information and stay above decorative interior art. + .background { recordedConstellation } + } + + private var componentRole: WhereThemeComponents.Role { + switch variant { + case .regular: .regularCard; case .compact: .compactCard; case .elsewhere: .elsewhereCard + } + } + + private var nativeArtwork: Set { + componentStylesheet[componentRole].nativeArtwork + } + + private func watermarkStyle(_ authored: WhereStylesheet.CardStyle.RegionShape + .Artwork) -> WhereStylesheet.CardStyle.RegionShape.Artwork + { + guard !nativeArtwork.contains(.watermark) else { return authored } + var style = authored + style.fillOpacity = 0 + style.stroke?.opacity = 0 + return style } var body: some View { content - .modifier(RegionCardSurface( - card: card, - glassTintOpacity: cardStyles.glassTintOpacity, - tint: style.tint, - interactive: interactive, - tilt: tilt, - )) + .modifier(WhereComponentSurface( + role: componentRole, + shape: cardShape, + finish: WhereCardFinish( + surface: RegionCardSurface( + card: card, + glassTintOpacity: cardStyles.glassTintOpacity, + tint: style.tint, + interactive: interactive, + tilt: tilt, + ), + transition: RegionCardTransition( + region: regionDays.region, + namespace: transitionNamespace, + card: card, + tint: style.tint, + ), + ), + ) { stampPaper }) + .whereThemeActivity(artworkIsVisible) + .onScrollVisibilityChange(threshold: 0.01) { artworkIsVisible = $0 } + .onAppear { artworkIsVisible = true } + .onDisappear { artworkIsVisible = false } .accessibilityElement(children: .combine) .accessibilityLabel(accessibilityLabel) .accessibilityValue(places ?? "") @@ -629,6 +689,11 @@ private struct ArcText: View { case .compact: "Compact" case .elsewhere: "Elsewhere" } + let role: WhereThemeComponents.Role = switch variant { + case .regular: .regularCard + case .compact: .compactCard + case .elsewhere: .elsewhereCard + } return whereThemeSnapshot( name: name + (reduceTransparency ? "ReduceTransparency" : ""), configurations: reduceTransparency ? .componentLightDark : .componentRepresentative, @@ -648,6 +713,7 @@ private struct ArcText: View { estimatedDays: variant == .elsewhere ? nil : DayBounds(lower: 180, upper: 220), year: 2026, ) + .whereThemeArtworkSpace(role) .padding(24) .background(.background) .environment(\.regionOutlinePathCache, cache) diff --git a/Where/WhereUI/Sources/Primary/RegionWelcomeCard.swift b/Where/WhereUI/Sources/Primary/RegionWelcomeCard.swift index b98e63ebc..ba3cfd558 100644 --- a/Where/WhereUI/Sources/Primary/RegionWelcomeCard.swift +++ b/Where/WhereUI/Sources/Primary/RegionWelcomeCard.swift @@ -15,6 +15,7 @@ struct RegionWelcomeCard: View { } @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components @Environment(\.regionStyles) private var regionStyles private var welcome: WhereStylesheet.LocationWelcomeStyle { @@ -42,20 +43,22 @@ struct RegionWelcomeCard: View { .font(.largeTitle) .accessibilityHidden(true) Spacer() - PassportSeal(systemSymbol: regionStyle.symbol, tint: regionStyle.tint) + if components[.welcome].nativeArtwork.contains(.stamp) { PassportSeal( + systemSymbol: regionStyle.symbol, + tint: regionStyle.tint, + ) } } VStack(alignment: .leading, spacing: stylesheet.spacing.medium) { Text(title) - .font(.title2.bold()) - .fontDesign(.serif) + .font(components[.welcome].heading ?? .system(.title2, design: .serif).bold()) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) Text(String(localized: .locationWelcomeMessage( presentation.region.localizedName, ))) - .font(.body) - .foregroundStyle(.secondary) + .font(components[.welcome].body ?? .body) + .foregroundStyle(.whereSecondary) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) } @@ -91,17 +94,22 @@ struct RegionWelcomeCard: View { } ZStack { - SecurityPrintRosette( - tint: regionStyle.tint, - wobble: stylesheet.card.regular.rosette.wobble, - lineWidth: stylesheet.card.regular.rosette.lineWidth, - primaryRingSpacing: stylesheet.card.regular.rosette.primaryRingSpacing, - secondaryRingSpacing: stylesheet.card.regular.rosette.secondaryRingSpacing, - primaryOpacity: stylesheet.card.rosetteFill.primary, - secondaryOpacity: stylesheet.card.rosetteFill.secondary, - ) + if let background = components[.welcome].background { shape.fill(background) } + if components[.welcome].nativeArtwork.contains(.rosette) { + SecurityPrintRosette( + tint: regionStyle.tint, + wobble: stylesheet.card.regular.rosette.wobble, + lineWidth: stylesheet.card.regular.rosette.lineWidth, + primaryRingSpacing: stylesheet.card.regular.rosette.primaryRingSpacing, + secondaryRingSpacing: stylesheet.card.regular.rosette + .secondaryRingSpacing, + primaryOpacity: stylesheet.card.rosetteFill.primary, + secondaryOpacity: stylesheet.card.rosetteFill.secondary, + ) + } if + components[.welcome].nativeArtwork.contains(.microprint), let regionShape = stylesheet.card.regular.regionShape, let microprint = regionPaths?.microprint, !microprint.isEmpty @@ -118,6 +126,7 @@ struct RegionWelcomeCard: View { } if + components[.welcome].nativeArtwork.contains(.watermark), let artwork = stylesheet.card.regular.regionShape, let watermark = regionPaths?.watermark, !watermark.isEmpty @@ -130,11 +139,13 @@ struct RegionWelcomeCard: View { } } .blendMode(stylesheet.card.securityPrint.backgroundBlendMode) + WhereComponentArtwork(role: .welcome, plane: .interior, shape: shape) } .clipShape(shape) .allowsHitTesting(false) } .matteSurface(welcome.surface, tint: regionStyle.tint, in: shape) + .overlay { WhereComponentArtwork(role: .welcome, plane: .exterior, shape: shape) } .overlay(alignment: .topTrailing) { Button(action: dismissAction) { Label(String(localized: .locationWelcomeDismiss), systemSymbol: .xmark) @@ -191,6 +202,8 @@ struct RegionWelcomeCard: View { microprint: microprintPath, ) } + .modifier(WhereComponentTextStyle(role: .welcome)) + .whereThemeArtworkSpace(.welcome) } private func planStay() { diff --git a/Where/WhereUI/Sources/Primary/WhereCardFinish.swift b/Where/WhereUI/Sources/Primary/WhereCardFinish.swift new file mode 100644 index 000000000..be9b7b624 --- /dev/null +++ b/Where/WhereUI/Sources/Primary/WhereCardFinish.swift @@ -0,0 +1,10 @@ +import RegionKit +import SwiftUI + +struct WhereCardFinish: ViewModifier { + let surface: RegionCardSurface + let transition: RegionCardTransition + func body(content: Content) -> some View { + content.modifier(surface).modifier(transition) + } +} diff --git a/Where/WhereUI/Sources/Primary/YearRibbon.swift b/Where/WhereUI/Sources/Primary/YearRibbon.swift index e3483a9e1..7acace8c4 100644 --- a/Where/WhereUI/Sources/Primary/YearRibbon.swift +++ b/Where/WhereUI/Sources/Primary/YearRibbon.swift @@ -31,7 +31,7 @@ struct YearRibbon: View { ForEach(monthStarts, id: \.self) { month in Text(month, format: .dateTime.month(.narrow)) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .frame(maxWidth: .infinity) } } diff --git a/Where/WhereUI/Sources/Regions/RegionCustomizeView.swift b/Where/WhereUI/Sources/Regions/RegionCustomizeView.swift index 0cc552456..3967f49f4 100644 --- a/Where/WhereUI/Sources/Regions/RegionCustomizeView.swift +++ b/Where/WhereUI/Sources/Regions/RegionCustomizeView.swift @@ -52,6 +52,7 @@ struct RegionAppearanceEditor: View { styleOverride: RegionStyle(appearance), ) } + .whereThemeArtworkSpace(.regularCard) .animation(stylesheet.motion.captionFade, value: appearance) } diff --git a/Where/WhereUI/Sources/Resources/Localizable.xcstrings b/Where/WhereUI/Sources/Resources/Localizable.xcstrings index f8a44eeba..246c86735 100644 --- a/Where/WhereUI/Sources/Resources/Localizable.xcstrings +++ b/Where/WhereUI/Sources/Resources/Localizable.xcstrings @@ -11320,6 +11320,28 @@ } } }, + "theme.halloween.detail" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Illustrated pumpkins, warm paper, and deep plum colors." + } + } + } + }, + "theme.halloween.title" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Halloween" + } + } + } + }, "theme.notSelected.value" : { "extractionState" : "manual", "localizations" : { diff --git a/Where/WhereUI/Sources/Resources/Themes/FlatTheme.json b/Where/WhereUI/Sources/Resources/Themes/FlatTheme.json new file mode 100644 index 000000000..61ea9eb0a --- /dev/null +++ b/Where/WhereUI/Sources/Resources/Themes/FlatTheme.json @@ -0,0 +1,760 @@ +{ + "appearance": { + "card": { + "compact": { + "contentTint": { + "modes": { + "_0": { + "dark": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + }, + "fraction": 0.6 + }, + "light": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + }, + "fraction": 0.6 + } + } + } + } + }, + "constellation": { + "haloOpacity": { + "constant": { + "_0": 0 + } + } + }, + "regular": { + "contentTint": { + "modes": { + "_0": { + "dark": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + }, + "fraction": 0.6 + }, + "light": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + }, + "fraction": 0.6 + } + } + } + } + }, + "securityPrint": { + "backgroundBlendMode": { + "constant": { + "_0": "normal" + } + } + } + }, + "locationForecast": { + "ink": { + "surfaceWashOpacity": { + "constant": { + "_0": 0 + } + } + }, + "summary": { + "shadowOpacity": { + "constant": { + "_0": 0 + } + } + } + }, + "locationWelcome": { + "close": { + "glow": { + "opacity": { + "constant": { + "_0": 0 + } + } + }, + "lift": { + "opacity": { + "constant": { + "_0": 0 + } + } + }, + "surface": { + "modes": { + "_0": { + "dark": { + "matte": { + "_0": { + "fill": { + "base": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0.11999999731779099, + "green": 0.11999999731779099, + "red": 0.11999999731779099 + } + } + } + }, + "tintMix": 0 + }, + "outline": { + "opacity": 0.4, + "width": 1 + }, + "pressedTintOpacity": 0.1, + "shadow": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 0.3, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + }, + "offsetY": 3, + "radius": 5 + } + } + } + }, + "light": { + "matte": { + "_0": { + "fill": { + "base": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + }, + "tintMix": 0 + }, + "outline": { + "opacity": 0.32, + "width": 1 + }, + "pressedTintOpacity": 0.1, + "shadow": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 0.14, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + }, + "offsetY": 3, + "radius": 5 + } + } + } + } + } + } + } + }, + "glow": { + "opacity": { + "constant": { + "_0": 0 + } + } + }, + "lift": { + "opacity": { + "constant": { + "_0": 0 + } + } + } + } + }, + "components": {}, + "id": "flat", + "parent": "standard", + "schemaVersion": 2, + "values": { + "colors": [ + { + "id": "regularCard.fill", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0, + "green": 0, + "red": 0 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + } + } + }, + { + "id": "regularCard.shadow", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 0.3, + "blue": 0, + "green": 0, + "red": 0 + } + }, + "light": { + "rgba": { + "alpha": 0.14, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + } + } + }, + { + "id": "compactCard.fill", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0, + "green": 0, + "red": 0 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + } + } + }, + { + "id": "compactCard.shadow", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 0.3, + "blue": 0, + "green": 0, + "red": 0 + } + }, + "light": { + "rgba": { + "alpha": 0.14, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + } + } + }, + { + "id": "elsewhereCard.shadow", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 0.3, + "blue": 0, + "green": 0, + "red": 0 + } + }, + "light": { + "rgba": { + "alpha": 0.14, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + } + } + }, + { + "id": "welcome.fill", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.11999999731779099, + "green": 0.11999999731779099, + "red": 0.11999999731779099 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + } + } + }, + { + "id": "welcome.shadow", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 0.3, + "blue": 0, + "green": 0, + "red": 0 + } + }, + "light": { + "rgba": { + "alpha": 0.14, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + } + } + }, + { + "id": "privacy.fill", + "value": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0.25, + "green": 0.1249999925494194, + "red": 0.04999999701976776 + } + } + } + } + }, + { + "id": "privacy.shadow", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 0.3, + "blue": 0, + "green": 0, + "red": 0 + } + }, + "light": { + "rgba": { + "alpha": 0.14, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + } + } + }, + { + "id": "summary.fill", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.1599999964237213, + "green": 0.1599999964237213, + "red": 0.1599999964237213 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.9399999976158142, + "green": 0.9399999976158142, + "red": 0.9399999976158142 + } + } + } + } + } + }, + { + "id": "summary.shadow", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 0.3, + "blue": 0, + "green": 0, + "red": 0 + } + }, + "light": { + "rgba": { + "alpha": 0.14, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + } + } + } + ], + "fonts": [], + "surfaces": [ + { + "id": "regularCard", + "value": { + "modes": { + "_0": { + "dark": { + "paper": { + "_0": { + "fill": "regularCard.fill", + "outline": { + "opacity": 0.4, + "width": 1 + }, + "pressedTintOpacity": 0.1, + "shadow": { + "color": "regularCard.shadow", + "offsetY": 4, + "radius": 10 + }, + "tintMix": 0.58 + } + } + }, + "light": { + "paper": { + "_0": { + "fill": "regularCard.fill", + "outline": { + "opacity": 0.32, + "width": 1 + }, + "pressedTintOpacity": 0.1, + "shadow": { + "color": "regularCard.shadow", + "offsetY": 4, + "radius": 10 + }, + "tintMix": 0.52 + } + } + } + } + } + } + }, + { + "id": "compactCard", + "value": { + "modes": { + "_0": { + "dark": { + "paper": { + "_0": { + "fill": "compactCard.fill", + "outline": { + "opacity": 0.4, + "width": 1 + }, + "pressedTintOpacity": 0.1, + "shadow": { + "color": "compactCard.shadow", + "offsetY": 3, + "radius": 6 + }, + "tintMix": 0.58 + } + } + }, + "light": { + "paper": { + "_0": { + "fill": "compactCard.fill", + "outline": { + "opacity": 0.32, + "width": 1 + }, + "pressedTintOpacity": 0.1, + "shadow": { + "color": "compactCard.shadow", + "offsetY": 3, + "radius": 6 + }, + "tintMix": 0.52 + } + } + } + } + } + } + }, + { + "id": "elsewhereCard", + "value": { + "modes": { + "_0": { + "dark": { + "paper": { + "_0": { + "fill": "elsewhereCard.fill", + "outline": { + "opacity": 0.4, + "width": 1 + }, + "pressedTintOpacity": 0.08, + "shadow": { + "color": "elsewhereCard.shadow", + "offsetY": 3, + "radius": 8 + }, + "tintMix": 0 + } + } + }, + "light": { + "paper": { + "_0": { + "fill": "elsewhereCard.fill", + "outline": { + "opacity": 0.32, + "width": 1 + }, + "pressedTintOpacity": 0.08, + "shadow": { + "color": "elsewhereCard.shadow", + "offsetY": 3, + "radius": 8 + }, + "tintMix": 0 + } + } + } + } + } + } + }, + { + "id": "welcome", + "value": { + "modes": { + "_0": { + "dark": { + "paper": { + "_0": { + "fill": "welcome.fill", + "outline": { + "opacity": 0.4, + "width": 1 + }, + "pressedTintOpacity": 0, + "shadow": { + "color": "welcome.shadow", + "offsetY": 4, + "radius": 10 + }, + "tintMix": 0 + } + } + }, + "light": { + "paper": { + "_0": { + "fill": "welcome.fill", + "outline": { + "opacity": 0.32, + "width": 1 + }, + "pressedTintOpacity": 0, + "shadow": { + "color": "welcome.shadow", + "offsetY": 4, + "radius": 10 + }, + "tintMix": 0 + } + } + } + } + } + } + }, + { + "id": "privacy", + "value": { + "constant": { + "_0": { + "paper": { + "_0": { + "fill": "privacy.fill", + "outline": { + "opacity": 0.32, + "width": 1 + }, + "pressedTintOpacity": 0, + "shadow": { + "color": "privacy.shadow", + "offsetY": 3, + "radius": 6 + }, + "tintMix": 0 + } + } + } + } + } + }, + { + "id": "summary", + "value": { + "modes": { + "_0": { + "dark": { + "paper": { + "_0": { + "fill": "summary.fill", + "outline": { + "opacity": 0.4, + "width": 1 + }, + "pressedTintOpacity": 0, + "shadow": { + "color": "summary.shadow", + "offsetY": 3, + "radius": 8 + }, + "tintMix": 0 + } + } + }, + "light": { + "paper": { + "_0": { + "fill": "summary.fill", + "outline": { + "opacity": 0.32, + "width": 1 + }, + "pressedTintOpacity": 0, + "shadow": { + "color": "summary.shadow", + "offsetY": 3, + "radius": 8 + }, + "tintMix": 0 + } + } + } + } + } + } + } + ] + } +} diff --git a/Where/WhereUI/Sources/Resources/Themes/HalloweenMoon.png b/Where/WhereUI/Sources/Resources/Themes/HalloweenMoon.png new file mode 100644 index 000000000..338f2c149 Binary files /dev/null and b/Where/WhereUI/Sources/Resources/Themes/HalloweenMoon.png differ diff --git a/Where/WhereUI/Sources/Resources/Themes/HalloweenPumpkins.png b/Where/WhereUI/Sources/Resources/Themes/HalloweenPumpkins.png new file mode 100644 index 000000000..98365e0c8 Binary files /dev/null and b/Where/WhereUI/Sources/Resources/Themes/HalloweenPumpkins.png differ diff --git a/Where/WhereUI/Sources/Resources/Themes/HalloweenTheme.json b/Where/WhereUI/Sources/Resources/Themes/HalloweenTheme.json new file mode 100644 index 000000000..8f6e2222d --- /dev/null +++ b/Where/WhereUI/Sources/Resources/Themes/HalloweenTheme.json @@ -0,0 +1,1718 @@ +{ + "appearance": { + "card": { + "compact": { + "contentTint": { + "constant": { + "_0": { + "color": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 1.0, + "green": 1.0, + "red": 1.0 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.2196078431372549, + "green": 0.12549019607843137, + "red": 0.1843137254901961 + } + } + } + } + }, + "fraction": 0.65 + } + } + }, + "cornerRadius": { + "constant": { + "_0": 20 + } + }, + "dayUnitTypography": { + "design": { + "constant": { + "_0": { + "rounded": {} + } + } + } + }, + "heroNumberTypography": { + "design": { + "constant": { + "_0": { + "rounded": {} + } + } + } + }, + "padding": { + "constant": { + "_0": 18 + } + }, + "progressBarHeight": { + "constant": { + "_0": 7 + } + }, + "regionNameTracking": { + "constant": { + "_0": 0 + } + }, + "regionNameTypography": { + "design": { + "constant": { + "_0": { + "rounded": {} + } + } + }, + "weight": { + "constant": { + "_0": { + "bold": {} + } + } + } + } + }, + "nameOpacity": { + "constant": { + "_0": 1 + } + }, + "regular": { + "contentTint": { + "constant": { + "_0": { + "color": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 1.0, + "green": 1.0, + "red": 1.0 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.2196078431372549, + "green": 0.12549019607843137, + "red": 0.1843137254901961 + } + } + } + } + }, + "fraction": 0.65 + } + } + }, + "cornerRadius": { + "constant": { + "_0": 26 + } + }, + "dayUnitTypography": { + "design": { + "constant": { + "_0": { + "rounded": {} + } + } + } + }, + "heroNumberTypography": { + "design": { + "constant": { + "_0": { + "rounded": {} + } + } + } + }, + "padding": { + "constant": { + "_0": 24 + } + }, + "progressBarHeight": { + "constant": { + "_0": 7 + } + }, + "regionNameTracking": { + "constant": { + "_0": 0 + } + }, + "regionNameTypography": { + "design": { + "constant": { + "_0": { + "rounded": {} + } + } + }, + "weight": { + "constant": { + "_0": { + "bold": {} + } + } + } + } + }, + "securityPrint": { + "backgroundBlendMode": { + "constant": { + "_0": "normal" + } + } + }, + "watermarkOpacity": { + "constant": { + "_0": 0.04 + } + } + }, + "locationForecast": { + "header": { + "elapsedFont": { + "constant": { + "_0": { + "design": "rounded", + "size": { + "semantic": { + "_0": "subheadline" + } + }, + "weight": "regular" + } + } + }, + "titleFont": { + "constant": { + "_0": { + "design": "rounded", + "size": { + "semantic": { + "_0": "title3" + } + }, + "weight": "bold" + } + } + } + } + }, + "locationsBackground": { + "ink": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.8, + "green": 0.6431372549019608, + "red": 0.7333333333333333 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.6039215686274509, + "green": 0.45098039215686275, + "red": 0.5568627450980392 + } + } + } + } + } + }, + "privacyPassportCard": { + "outlineOpacity": { + "constant": { + "_0": 0.3 + } + } + } + }, + "components": { + "calendarMonth": { + "artwork": [], + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "value": { + "_0": "heading" + } + }, + "nativeArtwork": [], + "surface": { + "value": { + "_0": "halloween.lavender" + } + } + }, + "compactCard": { + "artwork": [ + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "shape": { + "_0": { + "rectangle": {} + } + } + }, + "id": "edge", + "modifiers": [ + { + "foreground": { + "_0": { + "color": { + "_0": { + "rgba": { + "alpha": 0, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + } + } + }, + { + "border": { + "paint": { + "color": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "primary" + } + }, + "_1": 0.2 + } + } + } + }, + "shape": { + "roundedRectangle": { + "radius": 20 + } + }, + "width": 2.5 + } + } + ] + }, + "schemaVersion": 1 + }, + "id": "edge", + "opacity": { + "constant": { + "_0": 1 + } + }, + "placement": { + "anchor": "center", + "offset": { + "x": 0, + "y": 0 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "relative": { + "height": 1, + "maximumHeight": 2048, + "maximumWidth": 2048, + "width": 1 + } + } + }, + "role": "essential" + }, + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.pumpkins", + "sizing": { + "fit": {} + } + } + } + }, + "id": "pumpkins", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "pumpkins", + "opacity": { + "constant": { + "_0": 1 + } + }, + "placement": { + "anchor": "bottomTrailing", + "offset": { + "x": 6, + "y": 38 + }, + "outsets": { + "bottom": 38, + "leading": 0, + "top": 0, + "trailing": 6 + }, + "plane": "exterior", + "size": { + "fixed": { + "height": 51, + "width": 70 + } + } + }, + "role": "ornament" + } + ], + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "watermark" + ], + "surface": { + "value": { + "_0": "halloween.paper" + } + } + }, + "elsewhere": { + "artwork": [ + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.moon", + "sizing": { + "fit": {} + } + } + } + }, + "id": "moon", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "moon", + "opacity": { + "constant": { + "_0": 1 + } + }, + "placement": { + "anchor": "topLeading", + "offset": { + "x": 5, + "y": 15 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "fixed": { + "height": 96, + "width": 105 + } + } + }, + "role": "ornament" + }, + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.vine", + "sizing": { + "fit": {} + } + } + } + }, + "id": "garden", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "garden", + "opacity": { + "constant": { + "_0": 0.4 + } + }, + "placement": { + "anchor": "bottomTrailing", + "offset": { + "x": 20, + "y": 20 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "fixed": { + "height": 225, + "width": 150 + } + } + }, + "role": "ornament" + } + ], + "background": { + "value": { + "_0": "page" + } + }, + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [] + }, + "elsewhereCard": { + "artwork": [ + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "shape": { + "_0": { + "rectangle": {} + } + } + }, + "id": "edge", + "modifiers": [ + { + "foreground": { + "_0": { + "color": { + "_0": { + "rgba": { + "alpha": 0, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + } + } + }, + { + "border": { + "paint": { + "color": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "primary" + } + }, + "_1": 0.2 + } + } + } + }, + "shape": { + "roundedRectangle": { + "radius": 20 + } + }, + "width": 2.5 + } + } + ] + }, + "schemaVersion": 1 + }, + "id": "edge", + "opacity": { + "constant": { + "_0": 1 + } + }, + "placement": { + "anchor": "center", + "offset": { + "x": 0, + "y": 0 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "relative": { + "height": 1, + "maximumHeight": 2048, + "maximumWidth": 2048, + "width": 1 + } + } + }, + "role": "essential" + }, + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.vine", + "sizing": { + "fit": {} + } + } + } + }, + "id": "vine", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "vine", + "opacity": { + "constant": { + "_0": 0.25 + } + }, + "placement": { + "anchor": "topTrailing", + "offset": { + "x": 4, + "y": -4 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "fixed": { + "height": 64, + "width": 43 + } + } + }, + "role": "ornament" + } + ], + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [], + "surface": { + "value": { + "_0": "halloween.paper" + } + } + }, + "forecastPanel": { + "artwork": [], + "body": { + "value": { + "_0": "forecastBody" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "value": { + "_0": "forecastHeading" + } + }, + "nativeArtwork": [], + "surface": { + "value": { + "_0": "halloween.lavender" + } + } + }, + "locations": { + "artwork": [ + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "animation": { + "curve": "easeInOut", + "delay": 0, + "duration": 9, + "from": { + "offsetX": 0, + "offsetY": -2, + "opacity": 1, + "rotation": -1.4, + "scaleX": 1, + "scaleY": 1 + }, + "repetition": { + "forever": { + "autoreverses": true + } + }, + "staticPose": { + "offsetX": 0, + "offsetY": 0, + "opacity": 1, + "rotation": 0, + "scaleX": 1, + "scaleY": 1 + }, + "to": { + "offsetX": 0, + "offsetY": 2, + "opacity": 1, + "rotation": 1.4, + "scaleX": 1, + "scaleY": 1 + } + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.moon", + "sizing": { + "fit": {} + } + } + } + }, + "id": "moon", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "moon", + "opacity": { + "constant": { + "_0": 1 + } + }, + "placement": { + "anchor": "topLeading", + "offset": { + "x": 9, + "y": 36 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "fixed": { + "height": 158, + "width": 172 + } + } + }, + "role": "ornament" + }, + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.vine", + "sizing": { + "fit": {} + } + } + } + }, + "id": "garden", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "garden", + "opacity": { + "constant": { + "_0": 0.4 + } + }, + "placement": { + "anchor": "bottomTrailing", + "offset": { + "x": 20, + "y": 20 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "fixed": { + "height": 225, + "width": 150 + } + } + }, + "role": "ornament" + } + ], + "background": { + "value": { + "_0": "page" + } + }, + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [] + }, + "privacy": { + "artwork": [], + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "value": { + "_0": "heading" + } + }, + "nativeArtwork": [], + "surface": { + "value": { + "_0": "halloween.passport" + } + } + }, + "regularCard": { + "artwork": [ + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "shape": { + "_0": { + "rectangle": {} + } + } + }, + "id": "edge", + "modifiers": [ + { + "foreground": { + "_0": { + "color": { + "_0": { + "rgba": { + "alpha": 0, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + } + } + }, + { + "border": { + "paint": { + "color": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "primary" + } + }, + "_1": 0.2 + } + } + } + }, + "shape": { + "roundedRectangle": { + "radius": 26 + } + }, + "width": 2.5 + } + } + ] + }, + "schemaVersion": 1 + }, + "id": "edge", + "opacity": { + "constant": { + "_0": 1 + } + }, + "placement": { + "anchor": "center", + "offset": { + "x": 0, + "y": 0 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "relative": { + "height": 1, + "maximumHeight": 2048, + "maximumWidth": 2048, + "width": 1 + } + } + }, + "role": "essential" + }, + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.vine", + "sizing": { + "fit": {} + } + } + } + }, + "id": "vine", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "vine", + "opacity": { + "constant": { + "_0": 0.24 + } + }, + "placement": { + "anchor": "topTrailing", + "offset": { + "x": 7, + "y": -8 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "fixed": { + "height": 108, + "width": 72 + } + } + }, + "role": "ornament" + }, + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.pumpkins", + "sizing": { + "fit": {} + } + } + } + }, + "id": "pumpkins", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "pumpkins", + "opacity": { + "constant": { + "_0": 1 + } + }, + "placement": { + "anchor": "bottomTrailing", + "offset": { + "x": 12, + "y": 70 + }, + "outsets": { + "bottom": 70, + "leading": 0, + "top": 0, + "trailing": 12 + }, + "plane": "exterior", + "size": { + "fixed": { + "height": 92, + "width": 126 + } + } + }, + "role": "ornament" + } + ], + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "watermark" + ], + "surface": { + "value": { + "_0": "halloween.paper" + } + } + }, + "section": { + "artwork": [], + "background": { + "value": { + "_0": "lavender" + } + }, + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [], + "surface": { + "clear": {} + } + }, + "settings": { + "artwork": [ + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.moon", + "sizing": { + "fit": {} + } + } + } + }, + "id": "moon", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "moon", + "opacity": { + "constant": { + "_0": 0.7 + } + }, + "placement": { + "anchor": "topLeading", + "offset": { + "x": 14, + "y": 6 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "fixed": { + "height": 83, + "width": 90 + } + } + }, + "role": "ornament" + } + ], + "background": { + "value": { + "_0": "page" + } + }, + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [] + }, + "sheet": { + "artwork": [], + "background": { + "value": { + "_0": "page" + } + }, + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [] + }, + "summary": { + "artwork": [], + "body": { + "value": { + "_0": "forecastBody" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "value": { + "_0": "forecastHeading" + } + }, + "nativeArtwork": [], + "surface": { + "value": { + "_0": "halloween.lavender" + } + } + }, + "timelineEntry": { + "artwork": [], + "body": { + "value": { + "_0": "forecastBody" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "value": { + "_0": "heading" + } + }, + "nativeArtwork": [], + "surface": { + "value": { + "_0": "halloween.paper" + } + } + }, + "welcome": { + "artwork": [], + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "value": { + "_0": "heading" + } + }, + "nativeArtwork": [], + "surface": { + "value": { + "_0": "halloween.paper" + } + } + }, + "year": { + "artwork": [ + { + "document": { + "root": { + "accessibility": { + "hidden": {} + }, + "content": { + "raster": { + "_0": { + "asset": "halloween.vine", + "sizing": { + "fit": {} + } + } + } + }, + "id": "vine", + "modifiers": [] + }, + "schemaVersion": 1 + }, + "id": "vine", + "opacity": { + "constant": { + "_0": 0.35 + } + }, + "placement": { + "anchor": "bottomTrailing", + "offset": { + "x": 10, + "y": 16 + }, + "outsets": { + "bottom": 0, + "leading": 0, + "top": 0, + "trailing": 0 + }, + "plane": "interior", + "size": { + "fixed": { + "height": 147, + "width": 98 + } + } + }, + "role": "ornament" + } + ], + "background": { + "value": { + "_0": "page" + } + }, + "body": { + "value": { + "_0": "body" + } + }, + "foreground": { + "value": { + "_0": "ink" + } + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [] + } + }, + "id": "halloween", + "parent": "flat", + "schemaVersion": 2, + "values": { + "colors": [ + { + "id": "page", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.19215686274509805, + "green": 0.11764705882352941, + "red": 0.1450980392156863 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.9098039215686274, + "green": 0.9411764705882353, + "red": 0.9647058823529412 + } + } + } + } + } + }, + { + "id": "paper", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.2627450980392157, + "green": 0.17254901960784313, + "red": 0.2196078431372549 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.9176470588235294, + "green": 0.9686274509803922, + "red": 0.9921568627450981 + } + } + } + } + } + }, + { + "id": "lavender", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.24313725490196078, + "green": 0.1450980392156863, + "red": 0.18823529411764706 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.9490196078431372, + "green": 0.8862745098039215, + "red": 0.9215686274509803 + } + } + } + } + } + }, + { + "id": "ink", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.9294117647058824, + "green": 0.8549019607843137, + "red": 0.9098039215686274 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.32941176470588235, + "green": 0.2235294117647059, + "red": 0.3137254901960784 + } + } + } + } + } + }, + { + "id": "accent", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.4745098039215686, + "green": 0.6980392156862745, + "red": 0.9294117647058824 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.21176470588235294, + "green": 0.3058823529411765, + "red": 0.5568627450980392 + } + } + } + } + } + }, + { + "id": "softShadow", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 0.32, + "blue": 0.09411764705882353, + "green": 0.043137254901960784, + "red": 0.06274509803921569 + } + }, + "light": { + "rgba": { + "alpha": 0.12, + "blue": 0.32941176470588235, + "green": 0.2235294117647059, + "red": 0.3137254901960784 + } + } + } + } + } + }, + { + "id": "passport", + "value": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0.24, + "green": 0.12, + "red": 0.18 + } + } + } + } + } + ], + "fonts": [ + { + "id": "heading", + "value": { + "constant": { + "_0": { + "design": "rounded", + "size": { + "semantic": { + "_0": "headline" + } + }, + "weight": "semibold" + } + } + } + }, + { + "id": "body", + "value": { + "constant": { + "_0": { + "design": "rounded", + "size": { + "semantic": { + "_0": "body" + } + }, + "weight": "regular" + } + } + } + }, + { + "id": "forecastHeading", + "value": { + "constant": { + "_0": { + "design": "rounded", + "size": { + "semantic": { + "_0": "title3" + } + }, + "weight": "bold" + } + } + } + }, + { + "id": "forecastBody", + "value": { + "constant": { + "_0": { + "design": "rounded", + "size": { + "semantic": { + "_0": "subheadline" + } + }, + "weight": "regular" + } + } + } + } + ], + "surfaces": [ + { + "id": "halloween.paper", + "value": { + "constant": { + "_0": { + "paper": { + "_0": { + "fill": "paper", + "outline": { + "opacity": 0.3, + "width": 1.25 + }, + "pressedTintOpacity": 0.1, + "shadow": { + "color": "softShadow", + "offsetY": 4, + "radius": 9 + }, + "tintMix": 0.06 + } + } + } + } + } + }, + { + "id": "halloween.lavender", + "value": { + "constant": { + "_0": { + "paper": { + "_0": { + "fill": "lavender", + "outline": { + "opacity": 0.3, + "width": 1.25 + }, + "pressedTintOpacity": 0.1, + "shadow": { + "color": "softShadow", + "offsetY": 4, + "radius": 9 + }, + "tintMix": 0 + } + } + } + } + } + }, + { + "id": "halloween.passport", + "value": { + "constant": { + "_0": { + "paper": { + "_0": { + "fill": "passport", + "outline": { + "opacity": 0.3, + "width": 1.25 + }, + "pressedTintOpacity": 0.1, + "shadow": { + "color": "softShadow", + "offsetY": 4, + "radius": 9 + }, + "tintMix": 0 + } + } + } + } + } + } + ] + } +} diff --git a/Where/WhereUI/Sources/Resources/Themes/HalloweenVine.png b/Where/WhereUI/Sources/Resources/Themes/HalloweenVine.png new file mode 100644 index 000000000..e4b105d7a Binary files /dev/null and b/Where/WhereUI/Sources/Resources/Themes/HalloweenVine.png differ diff --git a/Where/WhereUI/Sources/Resources/Themes/StandardTheme.json b/Where/WhereUI/Sources/Resources/Themes/StandardTheme.json new file mode 100644 index 000000000..3b3b073c5 --- /dev/null +++ b/Where/WhereUI/Sources/Resources/Themes/StandardTheme.json @@ -0,0 +1,5265 @@ +{ + "appearance": { + "appIcon": { + "backgroundedCellOpacity": { + "constant": { + "_0": 0.5 + } + }, + "cellLabelSpacing": { + "constant": { + "_0": 6 + } + }, + "cellSpacing": { + "constant": { + "_0": 12 + } + }, + "columnSpacing": { + "constant": { + "_0": 16 + } + }, + "gridMax": { + "constant": { + "_0": 180 + } + }, + "gridPadding": { + "constant": { + "_0": 16 + } + }, + "gridSpacing": { + "constant": { + "_0": 20 + } + }, + "panel": { + "background": { + "constant": { + "_0": { + "semantic": { + "_0": "background" + } + } + } + }, + "bottomPadding": { + "constant": { + "_0": 16 + } + }, + "cornerRadius": { + "constant": { + "_0": 28 + } + }, + "grabberOpacity": { + "constant": { + "_0": 0.5 + } + }, + "grabberSize": { + "constant": { + "_0": { + "height": 5, + "width": 40 + } + } + }, + "grabberTopPadding": { + "constant": { + "_0": 8 + } + }, + "horizontalPadding": { + "constant": { + "_0": 20 + } + }, + "shadowColor": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "black" + } + }, + "_1": 0.18 + } + } + } + }, + "shadowOffsetY": { + "constant": { + "_0": -4 + } + }, + "shadowRadius": { + "constant": { + "_0": 18 + } + }, + "spacing": { + "constant": { + "_0": 14 + } + }, + "textSpacing": { + "constant": { + "_0": 4 + } + } + }, + "previewMax": { + "constant": { + "_0": 280 + } + }, + "scrim": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "black" + } + }, + "_1": 0.25 + } + } + } + } + }, + "calendar": { + "day": { + "badge": { + "iconSize": { + "constant": { + "_0": 8 + } + }, + "iconSpacing": { + "constant": { + "_0": 2 + } + }, + "offset": { + "constant": { + "_0": { + "height": -2, + "width": 3 + } + } + }, + "padding": { + "constant": { + "_0": 2 + } + } + }, + "contentSpacing": { + "constant": { + "_0": 2 + } + }, + "dotOverlap": { + "constant": { + "_0": 2 + } + }, + "dotSize": { + "constant": { + "_0": 8 + } + }, + "dotStrokeWidth": { + "constant": { + "_0": 1.5 + } + }, + "minHeight": { + "constant": { + "_0": 44 + } + }, + "numberDotSpacing": { + "constant": { + "_0": 0 + } + }, + "numberFontSize": { + "constant": { + "_0": 17 + } + }, + "numberSize": { + "constant": { + "_0": 26 + } + }, + "todayMarker": { + "constant": { + "_0": { + "semantic": { + "_0": "accent" + } + } + } + }, + "todayNumberColor": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + }, + "unresolvedMarker": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "red" + } + }, + "_1": 0.15 + } + } + } + }, + "unresolvedNumberColor": { + "constant": { + "_0": { + "semantic": { + "_0": "red" + } + } + } + } + }, + "dotSize": { + "constant": { + "_0": 6 + } + }, + "month": { + "cornerRadius": { + "constant": { + "_0": 28 + } + }, + "current": { + "border": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "accent" + } + }, + "_1": 0.7 + } + } + } + }, + "borderWidth": { + "constant": { + "_0": 3 + } + }, + "fill": { + "constant": { + "_0": { + "rgba": { + "alpha": 0, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + }, + "foreground": { + "constant": { + "_0": { + "mix": { + "_0": { + "semantic": { + "_0": "primary" + } + }, + "_1": { + "semantic": { + "_0": "accent" + } + }, + "fraction": 0.25 + } + } + } + } + }, + "footerDividerSpacing": { + "constant": { + "_0": 8 + } + }, + "footerRowSpacing": { + "constant": { + "_0": 6 + } + }, + "footerSpacing": { + "constant": { + "_0": 4 + } + }, + "gridSpacing": { + "constant": { + "_0": 6 + } + }, + "padding": { + "constant": { + "_0": 16 + } + }, + "plain": { + "border": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "primary" + } + }, + "_1": 0.12 + } + } + } + }, + "borderWidth": { + "constant": { + "_0": 2 + } + }, + "fill": { + "constant": { + "_0": { + "rgba": { + "alpha": 0, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + }, + "foreground": { + "constant": { + "_0": { + "semantic": { + "_0": "primary" + } + } + } + } + }, + "sectionSpacing": { + "constant": { + "_0": 8 + } + }, + "stacksFooter": { + "constant": { + "_0": false + } + }, + "unfocusedRowOpacity": { + "constant": { + "_0": 0.55 + } + }, + "weekdayFontSize": { + "constant": { + "_0": 11 + } + } + }, + "monthSpacing": { + "constant": { + "_0": 16 + } + }, + "overview": { + "borderWidth": { + "constant": { + "_0": 0.5 + } + }, + "columns": { + "constant": { + "_0": 3 + } + }, + "cornerRadius": { + "constant": { + "_0": 12 + } + }, + "currentBorderWidth": { + "constant": { + "_0": 1 + } + }, + "dayFontSize": { + "constant": { + "_0": 8 + } + }, + "dayHeight": { + "constant": { + "_0": 12 + } + }, + "duration": { + "constant": { + "_0": 0.25 + } + }, + "padding": { + "constant": { + "_0": 6 + } + }, + "regionBand": { + "continuationRadius": { + "constant": { + "_0": 1 + } + }, + "cornerRadius": { + "constant": { + "_0": 3 + } + }, + "opacity": { + "modes": { + "_0": { + "dark": 0.55, + "light": 0.4 + } + } + }, + "planned": { + "fillOpacity": { + "modes": { + "_0": { + "dark": 0.24, + "light": 0.16 + } + } + }, + "hatchLineWidth": { + "constant": { + "_0": 1 + } + }, + "hatchOpacity": { + "modes": { + "_0": { + "dark": 0.85, + "light": 0.7 + } + } + }, + "hatchSpacing": { + "constant": { + "_0": 4 + } + } + }, + "verticalInset": { + "constant": { + "_0": 0 + } + } + }, + "sectionSpacing": { + "constant": { + "_0": 4 + } + }, + "shakeDistance": { + "constant": { + "_0": 4 + } + }, + "spacing": { + "constant": { + "_0": 8 + } + }, + "titleFontSize": { + "constant": { + "_0": 13 + } + }, + "unavailableForeground": { + "constant": { + "_0": { + "semantic": { + "_0": "secondary" + } + } + } + }, + "usesSpatialMotion": { + "constant": { + "_0": true + } + }, + "zoomBackground": { + "constant": { + "_0": { + "semantic": { + "_0": "background" + } + } + } + }, + "zoomDuration": { + "constant": { + "_0": 0.4 + } + } + }, + "regionBand": { + "continuationRadius": { + "constant": { + "_0": 3 + } + }, + "cornerRadius": { + "constant": { + "_0": 14 + } + }, + "opacity": { + "constant": { + "_0": 0.16 + } + }, + "planned": { + "fillOpacity": { + "constant": { + "_0": 0.07 + } + }, + "hatchLineWidth": { + "constant": { + "_0": 1 + } + }, + "hatchOpacity": { + "constant": { + "_0": 0.32 + } + }, + "hatchSpacing": { + "constant": { + "_0": 6 + } + } + }, + "verticalInset": { + "constant": { + "_0": 4 + } + } + } + }, + "card": { + "compact": { + "contentSpacing": { + "constant": { + "_0": 10 + } + }, + "contentTint": { + "constant": { + "_0": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 0, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + }, + "fraction": 0 + } + } + }, + "cornerRadius": { + "constant": { + "_0": 22 + } + }, + "dayUnitTypography": { + "design": { + "constant": { + "_0": { + "default": {} + } + } + }, + "size": { + "constant": { + "_0": { + "semantic": { + "_0": "subheadline" + } + } + } + }, + "weight": { + "constant": { + "_0": { + "medium": {} + } + } + } + }, + "entryStamp": { + "arc": { + "clear": {} + }, + "content": { + "artworkExtent": { + "constant": { + "_0": { + "height": 0.28, + "width": 0.42 + } + } + }, + "opacity": { + "constant": { + "_0": 0.85 + } + }, + "spacingFraction": { + "constant": { + "_0": 0.02 + } + }, + "symbolFont": { + "design": { + "constant": { + "_0": "standard" + } + }, + "sizeFraction": { + "constant": { + "_0": 0.26 + } + }, + "weight": { + "constant": { + "_0": "regular" + } + } + }, + "yearFont": { + "design": { + "constant": { + "_0": "serif" + } + }, + "sizeFraction": { + "constant": { + "_0": 0.15 + } + }, + "weight": { + "constant": { + "_0": "bold" + } + } + } + }, + "innerRing": { + "dash": { + "lengthFraction": { + "constant": { + "_0": 0.05 + } + }, + "spacingFraction": { + "constant": { + "_0": 0.035 + } + } + }, + "insetFraction": { + "constant": { + "_0": 0.13 + } + }, + "lineWidthFraction": { + "constant": { + "_0": 0.012 + } + }, + "opacity": { + "constant": { + "_0": 0.45 + } + } + }, + "outerRing": { + "lineWidthFraction": { + "constant": { + "_0": 0.035 + } + }, + "opacity": { + "constant": { + "_0": 0.7 + } + } + }, + "rotationDegrees": { + "constant": { + "_0": -8 + } + }, + "size": { + "constant": { + "_0": 52 + } + } + }, + "glow": { + "offsetY": { + "constant": { + "_0": 0 + } + }, + "opacity": { + "constant": { + "_0": 0.55 + } + }, + "radius": { + "constant": { + "_0": 6 + } + } + }, + "heroNumberTypography": { + "design": { + "constant": { + "_0": { + "rounded": {} + } + } + }, + "size": { + "constant": { + "_0": { + "semantic": { + "_0": "title" + } + } + } + }, + "weight": { + "constant": { + "_0": { + "bold": {} + } + } + } + }, + "lift": { + "offsetY": { + "constant": { + "_0": 9 + } + }, + "opacity": { + "constant": { + "_0": 0.4 + } + }, + "radius": { + "constant": { + "_0": 17 + } + } + }, + "padding": { + "constant": { + "_0": 16 + } + }, + "paper": { + "clear": {} + }, + "progressBarHeight": { + "constant": { + "_0": 6 + } + }, + "regionNameTracking": { + "constant": { + "_0": 0 + } + }, + "regionNameTypography": { + "design": { + "constant": { + "_0": { + "serif": {} + } + } + }, + "size": { + "constant": { + "_0": { + "semantic": { + "_0": "title3" + } + } + } + }, + "weight": { + "constant": { + "_0": { + "semibold": {} + } + } + } + }, + "regionShape": { + "value": { + "_0": { + "securityBorder": { + "glyphSize": { + "constant": { + "_0": 8 + } + }, + "inset": { + "constant": { + "_0": 9 + } + }, + "opacity": { + "constant": { + "_0": 0.22 + } + }, + "spacing": { + "constant": { + "_0": 11 + } + } + }, + "stamp": { + "center": { + "constant": { + "_0": { + "x": 0.5, + "y": 0.5 + } + } + }, + "extent": { + "constant": { + "_0": { + "height": 0.78, + "width": 0.78 + } + } + }, + "fillOpacity": { + "constant": { + "_0": 0.78 + } + }, + "scale": { + "constant": { + "_0": 0.88 + } + }, + "stroke": { + "clear": {} + } + }, + "watermark": { + "center": { + "constant": { + "_0": { + "x": 0.7, + "y": 0.57 + } + } + }, + "extent": { + "constant": { + "_0": { + "height": 0.78, + "width": 0.72 + } + } + }, + "fillOpacity": { + "constant": { + "_0": 0.13 + } + }, + "scale": { + "constant": { + "_0": 0.88 + } + }, + "stroke": { + "value": { + "_0": { + "opacity": { + "constant": { + "_0": 0.28 + } + }, + "width": { + "constant": { + "_0": 1.5 + } + } + } + } + } + } + } + } + }, + "rosette": { + "lineWidth": { + "constant": { + "_0": 1 + } + }, + "primaryRingSpacing": { + "constant": { + "_0": 13 + } + }, + "secondaryRingSpacing": { + "constant": { + "_0": 11 + } + }, + "wobble": { + "constant": { + "_0": 2 + } + } + }, + "sheen": { + "intensity": { + "constant": { + "_0": 0.5 + } + }, + "staticGlintIntensity": { + "constant": { + "_0": 0.5 + } + }, + "staticPose": { + "pitch": { + "constant": { + "_0": 0 + } + }, + "roll": { + "constant": { + "_0": 0 + } + } + } + }, + "watermarkFontSize": { + "constant": { + "_0": 96 + } + }, + "watermarkOffset": { + "constant": { + "_0": { + "height": 10, + "width": 12 + } + } + } + }, + "constellation": { + "coreDiameter": { + "constant": { + "_0": 2.5 + } + }, + "coreOpacity": { + "constant": { + "_0": 0.92 + } + }, + "coreWhiteMix": { + "constant": { + "_0": 0.72 + } + }, + "haloOpacity": { + "constant": { + "_0": 0.32 + } + }, + "haloRadius": { + "constant": { + "_0": 6 + } + } + }, + "dayCount": { + "animation": { + "constant": { + "_0": { + "easeOut": { + "duration": 0.3 + } + } + } + }, + "morph": { + "constant": { + "_0": { + "rollingDigits": {} + } + } + } + }, + "elsewherePaper": { + "contentTint": { + "modes": { + "_0": { + "dark": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + }, + "fraction": 0.4 + }, + "light": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + }, + "fraction": 0.4 + } + } + } + } + }, + "estimateSticker": { + "contentOpacity": { + "constant": { + "_0": 0.92 + } + }, + "cornerRadius": { + "constant": { + "_0": 8 + } + }, + "fillOpacity": { + "constant": { + "_0": 0.08 + } + }, + "horizontalPadding": { + "constant": { + "_0": 10 + } + }, + "innerDash": { + "constant": { + "_0": [ + 3, + 2 + ] + } + }, + "innerInset": { + "constant": { + "_0": 3 + } + }, + "innerOutlineWidth": { + "constant": { + "_0": 1 + } + }, + "labelTypography": { + "design": { + "constant": { + "_0": { + "default": {} + } + } + }, + "size": { + "constant": { + "_0": { + "semantic": { + "_0": "caption" + } + } + } + }, + "weight": { + "constant": { + "_0": { + "semibold": {} + } + } + } + }, + "outlineOpacity": { + "constant": { + "_0": 0.58 + } + }, + "outlineWidth": { + "constant": { + "_0": 1 + } + }, + "rotationDegrees": { + "constant": { + "_0": -2 + } + }, + "scale": { + "constant": { + "_0": 0.8 + } + }, + "valueTypography": { + "design": { + "constant": { + "_0": { + "default": {} + } + } + }, + "size": { + "constant": { + "_0": { + "semantic": { + "_0": "headline" + } + } + } + }, + "weight": { + "constant": { + "_0": { + "semibold": {} + } + } + } + }, + "verticalPadding": { + "constant": { + "_0": 7 + } + } + }, + "estimatedProgressOpacity": { + "constant": { + "_0": 0.3 + } + }, + "glassTintOpacity": { + "constant": { + "_0": 0.18 + } + }, + "nameOpacity": { + "constant": { + "_0": 0.8 + } + }, + "regular": { + "contentSpacing": { + "constant": { + "_0": 16 + } + }, + "contentTint": { + "constant": { + "_0": { + "color": { + "constant": { + "_0": { + "rgba": { + "alpha": 0, + "blue": 0, + "green": 0, + "red": 0 + } + } + } + }, + "fraction": 0 + } + } + }, + "cornerRadius": { + "constant": { + "_0": 28 + } + }, + "dayUnitTypography": { + "design": { + "constant": { + "_0": { + "default": {} + } + } + }, + "size": { + "constant": { + "_0": { + "semantic": { + "_0": "title3" + } + } + } + }, + "weight": { + "constant": { + "_0": { + "medium": {} + } + } + } + }, + "entryStamp": { + "arc": { + "value": { + "_0": { + "font": { + "design": { + "constant": { + "_0": "serif" + } + }, + "sizeFraction": { + "constant": { + "_0": 0.1 + } + }, + "weight": { + "constant": { + "_0": "semibold" + } + } + }, + "maximumSweepDegrees": { + "constant": { + "_0": 250 + } + }, + "opacity": { + "constant": { + "_0": 0.7 + } + }, + "radiusFraction": { + "constant": { + "_0": 0.37 + } + }, + "sweepDegreesPerCharacter": { + "constant": { + "_0": 17 + } + } + } + } + }, + "content": { + "artworkExtent": { + "constant": { + "_0": { + "height": 0.28, + "width": 0.42 + } + } + }, + "opacity": { + "constant": { + "_0": 0.85 + } + }, + "spacingFraction": { + "constant": { + "_0": 0.02 + } + }, + "symbolFont": { + "design": { + "constant": { + "_0": "standard" + } + }, + "sizeFraction": { + "constant": { + "_0": 0.26 + } + }, + "weight": { + "constant": { + "_0": "regular" + } + } + }, + "yearFont": { + "design": { + "constant": { + "_0": "serif" + } + }, + "sizeFraction": { + "constant": { + "_0": 0.15 + } + }, + "weight": { + "constant": { + "_0": "bold" + } + } + } + }, + "innerRing": { + "dash": { + "lengthFraction": { + "constant": { + "_0": 0.05 + } + }, + "spacingFraction": { + "constant": { + "_0": 0.035 + } + } + }, + "insetFraction": { + "constant": { + "_0": 0.13 + } + }, + "lineWidthFraction": { + "constant": { + "_0": 0.012 + } + }, + "opacity": { + "constant": { + "_0": 0.45 + } + } + }, + "outerRing": { + "lineWidthFraction": { + "constant": { + "_0": 0.035 + } + }, + "opacity": { + "constant": { + "_0": 0.7 + } + } + }, + "rotationDegrees": { + "constant": { + "_0": -8 + } + }, + "size": { + "constant": { + "_0": 88 + } + } + }, + "glow": { + "offsetY": { + "constant": { + "_0": 0 + } + }, + "opacity": { + "constant": { + "_0": 0.75 + } + }, + "radius": { + "constant": { + "_0": 12 + } + } + }, + "heroNumberTypography": { + "design": { + "constant": { + "_0": { + "rounded": {} + } + } + }, + "size": { + "constant": { + "_0": { + "fixed": { + "_0": 40 + } + } + } + }, + "weight": { + "constant": { + "_0": { + "bold": {} + } + } + } + }, + "lift": { + "offsetY": { + "constant": { + "_0": 18 + } + }, + "opacity": { + "constant": { + "_0": 0.6 + } + }, + "radius": { + "constant": { + "_0": 34 + } + } + }, + "padding": { + "constant": { + "_0": 22 + } + }, + "paper": { + "clear": {} + }, + "progressBarHeight": { + "constant": { + "_0": 10 + } + }, + "regionNameTracking": { + "constant": { + "_0": -0.5 + } + }, + "regionNameTypography": { + "design": { + "constant": { + "_0": { + "serif": {} + } + } + }, + "size": { + "constant": { + "_0": { + "fixed": { + "_0": 38 + } + } + } + }, + "weight": { + "constant": { + "_0": { + "semibold": {} + } + } + } + }, + "regionShape": { + "value": { + "_0": { + "securityBorder": { + "glyphSize": { + "constant": { + "_0": 8 + } + }, + "inset": { + "constant": { + "_0": 9 + } + }, + "opacity": { + "constant": { + "_0": 0.22 + } + }, + "spacing": { + "constant": { + "_0": 11 + } + } + }, + "stamp": { + "center": { + "constant": { + "_0": { + "x": 0.5, + "y": 0.5 + } + } + }, + "extent": { + "constant": { + "_0": { + "height": 0.78, + "width": 0.78 + } + } + }, + "fillOpacity": { + "constant": { + "_0": 0.78 + } + }, + "scale": { + "constant": { + "_0": 0.88 + } + }, + "stroke": { + "clear": {} + } + }, + "watermark": { + "center": { + "constant": { + "_0": { + "x": 0.7, + "y": 0.57 + } + } + }, + "extent": { + "constant": { + "_0": { + "height": 0.78, + "width": 0.72 + } + } + }, + "fillOpacity": { + "constant": { + "_0": 0.13 + } + }, + "scale": { + "constant": { + "_0": 0.88 + } + }, + "stroke": { + "value": { + "_0": { + "opacity": { + "constant": { + "_0": 0.28 + } + }, + "width": { + "constant": { + "_0": 1.5 + } + } + } + } + } + } + } + } + }, + "rosette": { + "lineWidth": { + "constant": { + "_0": 1 + } + }, + "primaryRingSpacing": { + "constant": { + "_0": 13.5 + } + }, + "secondaryRingSpacing": { + "constant": { + "_0": 9.5 + } + }, + "wobble": { + "constant": { + "_0": 2 + } + } + }, + "sheen": { + "intensity": { + "constant": { + "_0": 1 + } + }, + "staticGlintIntensity": { + "constant": { + "_0": 0.25 + } + }, + "staticPose": { + "pitch": { + "constant": { + "_0": -1 + } + }, + "roll": { + "constant": { + "_0": 0 + } + } + } + }, + "watermarkFontSize": { + "constant": { + "_0": 150 + } + }, + "watermarkOffset": { + "constant": { + "_0": { + "height": 12, + "width": 20 + } + } + } + }, + "rosetteFill": { + "primary": { + "constant": { + "_0": 0.12 + } + }, + "secondary": { + "constant": { + "_0": 0.08 + } + } + }, + "securityPrint": { + "backgroundBlendMode": { + "modes": { + "_0": { + "dark": "luminosity", + "light": "normal" + } + } + }, + "whiteMix": { + "modes": { + "_0": { + "dark": 0.65, + "light": 0 + } + } + } + }, + "watermarkOpacity": { + "constant": { + "_0": 0.08 + } + } + }, + "developerOverlay": { + "edgeInset": { + "constant": { + "_0": 16 + } + }, + "floatingWindow": { + "heightFraction": { + "constant": { + "_0": 0.62 + } + }, + "maxContentInsetFraction": { + "constant": { + "_0": 0.8 + } + }, + "maxHeight": { + "constant": { + "_0": 620 + } + }, + "maxWidth": { + "constant": { + "_0": 420 + } + }, + "minSize": { + "constant": { + "_0": { + "height": 320, + "width": 260 + } + } + } + }, + "launcher": { + "diameter": { + "constant": { + "_0": 52 + } + }, + "glyphRatio": { + "constant": { + "_0": 0.4 + } + }, + "shadowColor": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "black" + } + }, + "_1": 0.15 + } + } + } + }, + "shadowOffsetY": { + "constant": { + "_0": 1 + } + }, + "shadowRadius": { + "constant": { + "_0": 3 + } + } + }, + "menu": { + "cornerRadius": { + "constant": { + "_0": 18 + } + }, + "horizontalPadding": { + "constant": { + "_0": 14 + } + }, + "iconWidth": { + "constant": { + "_0": 24 + } + }, + "launcherSpacing": { + "constant": { + "_0": 10 + } + }, + "maxWidth": { + "constant": { + "_0": 310 + } + }, + "minRowHeight": { + "constant": { + "_0": 44 + } + }, + "motion": { + "animation": { + "constant": { + "_0": { + "spring": { + "bounce": 0.2, + "duration": 0.42 + } + } + } + }, + "scale": { + "constant": { + "_0": 0.86 + } + }, + "stagger": { + "constant": { + "_0": 0.04 + } + }, + "usesSpatialMotion": { + "constant": { + "_0": true + } + } + }, + "rowSpacing": { + "constant": { + "_0": 8 + } + }, + "subtitleSpacing": { + "constant": { + "_0": 2 + } + }, + "verticalPadding": { + "constant": { + "_0": 10 + } + } + }, + "panel": { + "controlHorizontalPadding": { + "constant": { + "_0": 16 + } + }, + "controlVerticalPadding": { + "constant": { + "_0": 10 + } + }, + "cornerRadius": { + "constant": { + "_0": 22 + } + }, + "dragHandleMinHeight": { + "constant": { + "_0": 28 + } + }, + "dragHandleSize": { + "constant": { + "_0": { + "height": 5, + "width": 40 + } + } + }, + "fullScreenInset": { + "constant": { + "_0": 12 + } + }, + "resizeGripClearance": { + "constant": { + "_0": 40 + } + }, + "resizeGripSize": { + "constant": { + "_0": 44 + } + }, + "resizeIconSize": { + "constant": { + "_0": 13 + } + }, + "shadowOffsetY": { + "constant": { + "_0": 6 + } + }, + "shadowOpacity": { + "constant": { + "_0": 0.3 + } + }, + "shadowRadius": { + "constant": { + "_0": 20 + } + } + }, + "presentationAnimation": { + "constant": { + "_0": { + "snappy": { + "duration": 0.3, + "extraBounce": 0 + } + } + } + } + }, + "evidence": { + "loadingMinHeight": { + "constant": { + "_0": 200 + } + }, + "pdfPreviewMinHeight": { + "constant": { + "_0": 420 + } + }, + "previewCornerRadius": { + "constant": { + "_0": 22 + } + } + }, + "featureDiscovery": { + "appIcon": { + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "spacing": { + "constant": { + "_0": 8 + } + } + }, + "backgroundPattern": { + "centerXRatio": { + "constant": { + "_0": 0.18 + } + }, + "centerYRatio": { + "constant": { + "_0": 0.46 + } + }, + "contourSpacing": { + "constant": { + "_0": 30 + } + }, + "horizontalScale": { + "constant": { + "_0": 1.22 + } + }, + "lineWidth": { + "constant": { + "_0": 0.9 + } + }, + "opacity": { + "constant": { + "_0": 0.12 + } + }, + "phaseStep": { + "constant": { + "_0": 0.31 + } + }, + "primaryDistortion": { + "constant": { + "_0": 13 + } + }, + "secondaryDistortion": { + "constant": { + "_0": 6 + } + } + }, + "estimatedTime": { + "calculationSpacing": { + "constant": { + "_0": 8 + } + }, + "legendDotSize": { + "constant": { + "_0": 10 + } + }, + "segmentCornerRadius": { + "constant": { + "_0": 5 + } + }, + "timelineHeight": { + "constant": { + "_0": 18 + } + }, + "timelineSpacing": { + "constant": { + "_0": 3 + } + } + }, + "evidenceArchive": { + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "spacing": { + "constant": { + "_0": 8 + } + } + }, + "marketingHeader": { + "badgeSize": { + "constant": { + "_0": 76 + } + }, + "badgeTintOpacity": { + "constant": { + "_0": 0.14 + } + }, + "contentMaxWidth": { + "constant": { + "_0": 560 + } + }, + "spacing": { + "constant": { + "_0": 14 + } + }, + "symbolPointSize": { + "constant": { + "_0": 34 + } + }, + "verticalPadding": { + "constant": { + "_0": 24 + } + } + }, + "marketingPanel": { + "contentSpacing": { + "constant": { + "_0": 12 + } + }, + "cornerRadius": { + "constant": { + "_0": 20 + } + }, + "maxWidth": { + "constant": { + "_0": 680 + } + }, + "padding": { + "constant": { + "_0": 16 + } + }, + "rowVerticalInset": { + "constant": { + "_0": 6 + } + } + }, + "regionStyle": { + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "spacing": { + "constant": { + "_0": 6 + } + } + }, + "shareSheet": { + "sourceLayout": { + "constant": { + "_0": { + "stacked": {} + } + } + }, + "sourcesLayout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "sourcesSpacing": { + "constant": { + "_0": 12 + } + }, + "titleAlignment": { + "constant": { + "_0": "center" + } + } + }, + "siri": { + "accent": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.42, + "green": 0.42, + "red": 0.42 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.28, + "green": 0.28, + "red": 0.28 + } + } + } + } + }, + "bubble": { + "cornerRadius": { + "constant": { + "_0": 16 + } + }, + "horizontalPadding": { + "constant": { + "_0": 12 + } + }, + "indent": { + "constant": { + "_0": 34 + } + }, + "verticalPadding": { + "constant": { + "_0": 10 + } + } + }, + "card": { + "cornerRadius": { + "constant": { + "_0": 20 + } + }, + "maxWidth": { + "constant": { + "_0": 680 + } + }, + "padding": { + "constant": { + "_0": 16 + } + }, + "rowVerticalInset": { + "constant": { + "_0": 6 + } + }, + "spacing": { + "constant": { + "_0": 12 + } + } + }, + "speakerIcon": { + "containerSize": { + "constant": { + "_0": 28 + } + }, + "symbolPointSize": { + "constant": { + "_0": 12 + } + } + } + }, + "widgets": { + "device": { + "contentMaxWidth": { + "constant": { + "_0": 560 + } + }, + "cornerRadius": { + "constant": { + "_0": 28 + } + }, + "padding": { + "constant": { + "_0": 14 + } + }, + "regularContentWidth": { + "constant": { + "_0": 320 + } + }, + "spacing": { + "constant": { + "_0": 12 + } + } + }, + "frame": { + "cornerRadius": { + "constant": { + "_0": 18 + } + }, + "padding": { + "constant": { + "_0": 12 + } + } + }, + "lockWidgetHeight": { + "constant": { + "_0": 76 + } + }, + "wallpapers": { + "home": { + "bottom": { + "constant": { + "_0": { + "semantic": { + "_0": "cyan" + } + } + } + }, + "top": { + "constant": { + "_0": { + "semantic": { + "_0": "indigo" + } + } + } + } + }, + "lock": { + "bottom": { + "constant": { + "_0": { + "semantic": { + "_0": "blue" + } + } + } + }, + "top": { + "constant": { + "_0": { + "semantic": { + "_0": "purple" + } + } + } + } + } + } + } + }, + "flightReviewPoint": { + "detailFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "caption" + } + } + } + } + }, + "mapCornerRadius": { + "constant": { + "_0": 12 + } + }, + "minimumActionHeight": { + "constant": { + "_0": 44 + } + }, + "titleFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "headline" + } + } + } + } + } + }, + "flightStatus": { + "background": { + "constant": { + "_0": { + "semantic": { + "_0": "secondaryGroupedBackground" + } + } + } + }, + "bodyFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "subheadline" + } + } + } + } + }, + "cornerRadius": { + "constant": { + "_0": 14 + } + }, + "padding": { + "constant": { + "_0": 14 + } + }, + "spacing": { + "constant": { + "_0": 8 + } + }, + "titleFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "headline" + } + } + } + } + } + }, + "launch": { + "captionAnimation": { + "value": { + "_0": { + "constant": { + "_0": { + "easeOut": { + "duration": 0.3 + } + } + } + } + } + }, + "reveal": { + "constant": { + "_0": { + "zoom": {} + } + } + }, + "revealAnimation": { + "constant": { + "_0": { + "easeIn": { + "duration": 0.16 + } + } + } + } + }, + "locationCardStack": { + "overtake": { + "bounce": { + "constant": { + "_0": 0.18 + } + }, + "duration": { + "constant": { + "_0": 0.72 + } + }, + "lateralArc": { + "constant": { + "_0": 18 + } + }, + "liftScale": { + "constant": { + "_0": 1.03 + } + }, + "minimumOpacity": { + "constant": { + "_0": 1 + } + }, + "rotationDegrees": { + "constant": { + "_0": 1.5 + } + }, + "settleScale": { + "constant": { + "_0": 0.975 + } + }, + "usesSpatialMotion": { + "constant": { + "_0": true + } + } + } + }, + "locationForecast": { + "controls": { + "cornerRadius": { + "constant": { + "_0": 12 + } + }, + "font": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "subheadline" + } + } + } + } + }, + "horizontalPadding": { + "constant": { + "_0": 10 + } + }, + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "layoutSpacing": { + "constant": { + "_0": 6 + } + }, + "minimumHeight": { + "constant": { + "_0": 44 + } + }, + "sectionSpacing": { + "constant": { + "_0": 8 + } + }, + "strokeWidth": { + "constant": { + "_0": 1 + } + } + }, + "cornerRadius": { + "constant": { + "_0": 22 + } + }, + "expansionAnimation": { + "constant": { + "_0": { + "easeInOut": { + "duration": 0.2 + } + } + } + }, + "header": { + "contentSpacing": { + "constant": { + "_0": 10 + } + }, + "elapsedFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "footnote" + } + } + } + } + }, + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "minimumHeight": { + "constant": { + "_0": 50 + } + }, + "textSpacing": { + "constant": { + "_0": 2 + } + }, + "titleFont": { + "constant": { + "_0": { + "design": "serif", + "size": { + "semantic": { + "_0": "headline" + } + } + } + } + } + }, + "ink": { + "controlFillOpacity": { + "constant": { + "_0": 0.06 + } + }, + "controlStrokeOpacity": { + "constant": { + "_0": 0.24 + } + }, + "microprintOpacity": { + "constant": { + "_0": 0.18 + } + }, + "progressEstimateFillOpacity": { + "constant": { + "_0": 0.2 + } + }, + "progressHatchOpacity": { + "constant": { + "_0": 0.42 + } + }, + "progressTrackOpacity": { + "constant": { + "_0": 0.1 + } + }, + "rosettePrimaryOpacity": { + "constant": { + "_0": 0.08 + } + }, + "rosetteSecondaryOpacity": { + "constant": { + "_0": 0.045 + } + }, + "rowFillOpacity": { + "constant": { + "_0": 0.07 + } + }, + "rowOutlineOpacity": { + "constant": { + "_0": 0.24 + } + }, + "sealOpacity": { + "constant": { + "_0": 0.72 + } + }, + "surfaceOutlineOpacity": { + "constant": { + "_0": 0.22 + } + }, + "surfaceWashOpacity": { + "constant": { + "_0": 0.035 + } + } + }, + "padding": { + "constant": { + "_0": 16 + } + }, + "progress": { + "hatchLineWidth": { + "constant": { + "_0": 1 + } + }, + "hatchSpacing": { + "constant": { + "_0": 6 + } + }, + "height": { + "constant": { + "_0": 8 + } + } + }, + "row": { + "contentSpacing": { + "constant": { + "_0": 6 + } + }, + "cornerRadius": { + "constant": { + "_0": 14 + } + }, + "detailFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "footnote" + } + } + } + } + }, + "estimateFont": { + "constant": { + "_0": { + "design": "rounded", + "size": { + "semantic": { + "_0": "headline" + } + } + } + } + }, + "estimateSpacing": { + "constant": { + "_0": 2 + } + }, + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "outlineWidth": { + "constant": { + "_0": 0.75 + } + }, + "padding": { + "constant": { + "_0": 10 + } + }, + "regionFont": { + "constant": { + "_0": { + "design": "serif", + "size": { + "semantic": { + "_0": "title3" + } + } + } + } + } + }, + "rowSpacing": { + "constant": { + "_0": 12 + } + }, + "summary": { + "artwork": { + "gap": { + "constant": { + "_0": 8 + } + }, + "inset": { + "constant": { + "_0": 16 + } + }, + "leadingOpacity": { + "constant": { + "_0": 0.18 + } + }, + "silhouette": { + "center": { + "constant": { + "_0": { + "x": 0.5, + "y": 0.5 + } + } + }, + "extent": { + "constant": { + "_0": { + "height": 0.9, + "width": 0.9 + } + } + }, + "fillOpacity": { + "constant": { + "_0": 0.11 + } + }, + "scale": { + "constant": { + "_0": 1 + } + }, + "stroke": { + "value": { + "_0": { + "opacity": { + "constant": { + "_0": 0.22 + } + }, + "width": { + "constant": { + "_0": 0.7 + } + } + } + } + } + }, + "widthFraction": { + "constant": { + "_0": 0.62 + } + } + }, + "border": { + "glyphSize": { + "constant": { + "_0": 7 + } + }, + "inset": { + "constant": { + "_0": 7 + } + }, + "opacity": { + "constant": { + "_0": 0.18 + } + }, + "spacing": { + "constant": { + "_0": 12 + } + } + }, + "cornerRadius": { + "constant": { + "_0": 28 + } + }, + "glassTintOpacity": { + "constant": { + "_0": 0.06 + } + }, + "ink": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.78, + "green": 0.78, + "red": 0.78 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.36, + "green": 0.36, + "red": 0.36 + } + } + } + } + }, + "outlineColor": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "black" + } + }, + "_1": 0.14 + } + } + } + }, + "outlineWidth": { + "constant": { + "_0": 1 + } + }, + "padding": { + "constant": { + "_0": 22 + } + }, + "paper": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.16, + "green": 0.16, + "red": 0.16 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 0.94, + "green": 0.94, + "red": 0.94 + } + } + } + } + }, + "rosette": { + "lineWidth": { + "constant": { + "_0": 0.5 + } + }, + "primaryRingSpacing": { + "constant": { + "_0": 12 + } + }, + "secondaryRingSpacing": { + "constant": { + "_0": 17 + } + }, + "wobble": { + "constant": { + "_0": 0.04 + } + } + }, + "rosetteOpacity": { + "constant": { + "_0": 0.035 + } + }, + "shadowOffsetY": { + "constant": { + "_0": 3 + } + }, + "shadowOpacity": { + "constant": { + "_0": 0.06 + } + }, + "shadowRadius": { + "constant": { + "_0": 8 + } + }, + "usesOpaquePaper": { + "constant": { + "_0": false + } + } + }, + "surface": { + "inset": { + "constant": { + "_0": 9 + } + }, + "microprintGlyphSize": { + "constant": { + "_0": 7 + } + }, + "microprintSpacing": { + "constant": { + "_0": 10 + } + }, + "outlineWidth": { + "constant": { + "_0": 1.25 + } + }, + "primaryRingSpacing": { + "constant": { + "_0": 10 + } + }, + "rosetteLineWidth": { + "constant": { + "_0": 0.75 + } + }, + "rosetteWobble": { + "constant": { + "_0": 5 + } + }, + "secondaryRingSpacing": { + "constant": { + "_0": 16 + } + }, + "shadowColor": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "black" + } + }, + "_1": 0.08 + } + } + } + }, + "shadowOffsetY": { + "constant": { + "_0": 3 + } + }, + "shadowRadius": { + "constant": { + "_0": 10 + } + } + } + }, + "locationWelcome": { + "accessory": { + "contentSpacing": { + "constant": { + "_0": 10 + } + }, + "copy": { + "constant": { + "_0": { + "full": {} + } + } + }, + "horizontalPadding": { + "constant": { + "_0": 12 + } + }, + "minimumActionHeight": { + "constant": { + "_0": 44 + } + }, + "symbolSize": { + "constant": { + "_0": 16 + } + }, + "titleFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "subheadline" + } + }, + "weight": "semibold" + } + } + }, + "verticalPadding": { + "constant": { + "_0": 8 + } + } + }, + "close": { + "glow": { + "offsetY": { + "constant": { + "_0": 0 + } + }, + "opacity": { + "constant": { + "_0": 0.28 + } + }, + "radius": { + "constant": { + "_0": 8 + } + } + }, + "lift": { + "offsetY": { + "constant": { + "_0": 3 + } + }, + "opacity": { + "constant": { + "_0": 0.22 + } + }, + "radius": { + "constant": { + "_0": 5 + } + } + }, + "offset": { + "constant": { + "_0": { + "height": -8, + "width": 8 + } + } + }, + "surface": { + "constant": { + "_0": { + "reflective": {} + } + } + }, + "tintOpacity": { + "constant": { + "_0": 0.24 + } + } + }, + "contentSpacing": { + "constant": { + "_0": 16 + } + }, + "cornerRadius": { + "constant": { + "_0": 30 + } + }, + "glassTintOpacity": { + "constant": { + "_0": 0.2 + } + }, + "glow": { + "offsetY": { + "constant": { + "_0": 0 + } + }, + "opacity": { + "constant": { + "_0": 0.16 + } + }, + "radius": { + "constant": { + "_0": 22 + } + } + }, + "lift": { + "offsetY": { + "constant": { + "_0": 6 + } + }, + "opacity": { + "constant": { + "_0": 0.18 + } + }, + "radius": { + "constant": { + "_0": 12 + } + } + }, + "maxWidth": { + "constant": { + "_0": 390 + } + }, + "motion": { + "arrival": { + "animation": { + "constant": { + "_0": { + "spring": { + "bounce": 0.28, + "duration": 0.3 + } + } + } + }, + "rotationDegrees": { + "constant": { + "_0": -9 + } + }, + "scale": { + "constant": { + "_0": 1.28 + } + }, + "verticalOffset": { + "constant": { + "_0": -24 + } + } + }, + "departure": { + "animation": { + "constant": { + "_0": { + "easeOut": { + "duration": 0.16 + } + } + } + }, + "rotationDegrees": { + "constant": { + "_0": 3 + } + }, + "scale": { + "constant": { + "_0": 1.045 + } + }, + "verticalOffset": { + "constant": { + "_0": -10 + } + } + }, + "scrimAnimation": { + "constant": { + "_0": { + "easeOut": { + "duration": 0.16 + } + } + } + }, + "usesSpatialMotion": { + "constant": { + "_0": true + } + } + }, + "padding": { + "constant": { + "_0": 24 + } + }, + "paperOpacity": { + "constant": { + "_0": 0.92 + } + }, + "scrimOpacity": { + "constant": { + "_0": 0.28 + } + } + }, + "locationsBackground": { + "artwork": { + "center": { + "constant": { + "_0": { + "x": 0.5, + "y": 0.5 + } + } + }, + "extent": { + "constant": { + "_0": { + "height": 0.545, + "width": 0.545 + } + } + }, + "fillOpacity": { + "constant": { + "_0": 0 + } + }, + "scale": { + "constant": { + "_0": 1 + } + }, + "stroke": { + "value": { + "_0": { + "opacity": { + "constant": { + "_0": 0.043 + } + }, + "width": { + "constant": { + "_0": 1.1 + } + } + } + } + } + }, + "ink": { + "constant": { + "_0": { + "semantic": { + "_0": "primary" + } + } + } + }, + "maximumAspectScale": { + "constant": { + "_0": 1.25 + } + }, + "preferredCellSize": { + "constant": { + "_0": 54 + } + }, + "rosette": { + "lineWidth": { + "constant": { + "_0": 0.5 + } + }, + "primaryRingSpacing": { + "constant": { + "_0": 18 + } + }, + "secondaryRingSpacing": { + "constant": { + "_0": 25 + } + }, + "wobble": { + "constant": { + "_0": 0.04 + } + } + }, + "rosetteOpacity": { + "constant": { + "_0": 0.015 + } + }, + "showsInk": { + "constant": { + "_0": true + } + } + }, + "motion": { + "captionFade": { + "constant": { + "_0": { + "easeOut": { + "duration": 0.3 + } + } + } + }, + "reducedReveal": { + "constant": { + "_0": { + "easeInOut": { + "duration": 0.2 + } + } + } + }, + "reveal": { + "constant": { + "_0": { + "easeIn": { + "duration": 0.16 + } + } + } + }, + "staggeredReveal": { + "animation": { + "constant": { + "_0": { + "easeOut": { + "duration": 0.35 + } + } + } + }, + "delay": { + "constant": { + "_0": 0.08 + } + }, + "verticalOffset": { + "constant": { + "_0": 16 + } + } + } + }, + "openSourceStamp": { + "contentSpacing": { + "constant": { + "_0": 12 + } + }, + "detailFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "subheadline" + } + } + } + } + }, + "ink": { + "accessoryOpacity": { + "constant": { + "_0": 0.86 + } + }, + "detailOpacity": { + "constant": { + "_0": 0.68 + } + }, + "outlineOpacity": { + "constant": { + "_0": 0.72 + } + }, + "primaryRosetteOpacity": { + "constant": { + "_0": 0.1 + } + }, + "sealOpacity": { + "constant": { + "_0": 0.9 + } + }, + "secondaryRosetteOpacity": { + "constant": { + "_0": 0.06 + } + }, + "titleOpacity": { + "constant": { + "_0": 1 + } + } + }, + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "outlineWidth": { + "constant": { + "_0": 1.5 + } + }, + "padding": { + "constant": { + "_0": 16 + } + }, + "rosette": { + "lineWidth": { + "constant": { + "_0": 0.75 + } + }, + "primaryRingSpacing": { + "constant": { + "_0": 10 + } + }, + "secondaryRingSpacing": { + "constant": { + "_0": 16 + } + }, + "wobble": { + "constant": { + "_0": 5 + } + } + }, + "tint": { + "constant": { + "_0": { + "semantic": { + "_0": "accent" + } + } + } + }, + "titleFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "headline" + } + } + } + } + } + }, + "palette": { + "onboarding": { + "backgroundBottom": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "accent" + } + }, + "_1": 0.12 + } + } + } + }, + "backgroundTop": { + "constant": { + "_0": { + "semantic": { + "_0": "background" + } + } + } + } + }, + "primary": { + "backgroundBottom": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0.05, + "green": 0.02, + "red": 0.02 + } + } + } + }, + "backgroundTop": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0.13, + "green": 0.08, + "red": 0.07 + } + } + } + } + }, + "splash": { + "background": { + "constant": { + "_0": { + "semantic": { + "_0": "background" + } + } + } + }, + "iconGlow": { + "constant": { + "_0": { + "semantic": { + "_0": "accent" + } + } + } + }, + "vignetteCenter": { + "constant": { + "_0": { + "semantic": { + "_0": "secondaryBackground" + } + } + } + }, + "vignetteEdge": { + "constant": { + "_0": { + "semantic": { + "_0": "background" + } + } + } + } + } + }, + "passportSeal": { + "dashLength": { + "constant": { + "_0": 3 + } + }, + "dashSpacing": { + "constant": { + "_0": 3 + } + }, + "innerInset": { + "constant": { + "_0": 7 + } + }, + "innerLineWidth": { + "constant": { + "_0": 1 + } + }, + "outerLineWidth": { + "constant": { + "_0": 2 + } + }, + "rotationDegrees": { + "constant": { + "_0": -8 + } + }, + "size": { + "constant": { + "_0": 52 + } + }, + "symbolFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "title3" + } + } + } + } + } + }, + "plannedStayWarningStamp": { + "contentSpacing": { + "constant": { + "_0": 12 + } + }, + "detailFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "subheadline" + } + } + } + } + }, + "ink": { + "accessoryOpacity": { + "constant": { + "_0": 0.86 + } + }, + "detailOpacity": { + "constant": { + "_0": 0.68 + } + }, + "outlineOpacity": { + "constant": { + "_0": 0.72 + } + }, + "primaryRosetteOpacity": { + "constant": { + "_0": 0.1 + } + }, + "sealOpacity": { + "constant": { + "_0": 0.9 + } + }, + "secondaryRosetteOpacity": { + "constant": { + "_0": 0.06 + } + }, + "titleOpacity": { + "constant": { + "_0": 1 + } + } + }, + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "outlineWidth": { + "constant": { + "_0": 1.5 + } + }, + "padding": { + "constant": { + "_0": 16 + } + }, + "rosette": { + "lineWidth": { + "constant": { + "_0": 0.75 + } + }, + "primaryRingSpacing": { + "constant": { + "_0": 10 + } + }, + "secondaryRingSpacing": { + "constant": { + "_0": 16 + } + }, + "wobble": { + "constant": { + "_0": 5 + } + } + }, + "tint": { + "constant": { + "_0": { + "semantic": { + "_0": "orange" + } + } + } + }, + "titleFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "headline" + } + } + } + } + } + }, + "privacyPassportCard": { + "cornerRadius": { + "constant": { + "_0": 20 + } + }, + "detailFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "subheadline" + } + } + } + } + }, + "disclosure": { + "contentSpacing": { + "constant": { + "_0": 10 + } + }, + "cornerRadius": { + "constant": { + "_0": 12 + } + }, + "detailFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "footnote" + } + } + } + } + }, + "fillOpacity": { + "constant": { + "_0": 0.08 + } + }, + "iconSize": { + "constant": { + "_0": 32 + } + }, + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "padding": { + "constant": { + "_0": 10 + } + }, + "rowSpacing": { + "constant": { + "_0": 8 + } + }, + "statusFillOpacity": { + "constant": { + "_0": 0.14 + } + }, + "statusFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "footnote" + } + } + } + } + }, + "statusHorizontalPadding": { + "constant": { + "_0": 8 + } + }, + "statusVerticalPadding": { + "constant": { + "_0": 4 + } + }, + "strokeOpacity": { + "constant": { + "_0": 0.18 + } + }, + "strokeWidth": { + "constant": { + "_0": 0.75 + } + }, + "symbolFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "subheadline" + } + } + } + } + }, + "textSpacing": { + "constant": { + "_0": 3 + } + }, + "titleFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "subheadline" + } + } + } + } + } + }, + "headerLayout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "headerSpacing": { + "constant": { + "_0": 12 + } + }, + "outlineOpacity": { + "constant": { + "_0": 0.32 + } + }, + "outlineWidth": { + "constant": { + "_0": 1 + } + }, + "padding": { + "constant": { + "_0": 16 + } + }, + "reflectiveSurface": { + "accent": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0.32, + "green": 0.72, + "red": 0.88 + } + } + } + }, + "backgroundBottom": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0.16, + "green": 0.07, + "red": 0.02 + } + } + } + }, + "backgroundTop": { + "constant": { + "_0": { + "rgba": { + "alpha": 1, + "blue": 0.34, + "green": 0.18, + "red": 0.08 + } + } + } + }, + "intensity": { + "constant": { + "_0": 0.28 + } + }, + "staticGlintIntensity": { + "constant": { + "_0": 0.28 + } + }, + "staticPose": { + "pitch": { + "constant": { + "_0": -0.15 + } + }, + "roll": { + "constant": { + "_0": 0.3 + } + } + } + }, + "rosette": { + "lineWidth": { + "constant": { + "_0": 0.75 + } + }, + "primaryOpacity": { + "constant": { + "_0": 0.1 + } + }, + "primaryRingSpacing": { + "constant": { + "_0": 10 + } + }, + "secondaryOpacity": { + "constant": { + "_0": 0.06 + } + }, + "secondaryRingSpacing": { + "constant": { + "_0": 16 + } + }, + "wobble": { + "constant": { + "_0": 5 + } + } + }, + "sectionSpacing": { + "constant": { + "_0": 12 + } + }, + "titleFont": { + "constant": { + "_0": { + "size": { + "semantic": { + "_0": "headline" + } + } + } + } + } + }, + "regionMap": { + "captureInset": { + "constant": { + "_0": 16 + } + }, + "capturePointDiameter": { + "constant": { + "_0": 8 + } + }, + "overview": { + "height": { + "constant": { + "_0": 220 + } + }, + "minimumSpanMeters": { + "constant": { + "_0": 0 + } + } + }, + "pointPreview": { + "height": { + "constant": { + "_0": 140 + } + }, + "minimumSpanMeters": { + "constant": { + "_0": 50000 + } + } + }, + "routeLineWidth": { + "constant": { + "_0": 2 + } + }, + "uncertaintyFillOpacity": { + "constant": { + "_0": 0.15 + } + }, + "uncertaintyStrokeOpacity": { + "constant": { + "_0": 0.6 + } + }, + "uncertaintyStrokeWidth": { + "constant": { + "_0": 1 + } + } + }, + "regionPicker": { + "colorSwatchMinWidth": { + "constant": { + "_0": 44 + } + }, + "colorSwatchSelectionRing": { + "constant": { + "_0": 3 + } + }, + "colorSwatchSize": { + "constant": { + "_0": 40 + } + }, + "glyphCornerRadius": { + "constant": { + "_0": 6 + } + }, + "glyphSelectedBackgroundOpacity": { + "constant": { + "_0": 0.2 + } + }, + "glyphSelectionStrokeWidth": { + "constant": { + "_0": 2 + } + }, + "glyphTileMinWidth": { + "constant": { + "_0": 52 + } + }, + "glyphTileSize": { + "constant": { + "_0": 48 + } + }, + "mapCenterLatitude": { + "constant": { + "_0": 39.5 + } + }, + "mapCenterLongitude": { + "constant": { + "_0": -98.35 + } + }, + "mapCornerRadius": { + "constant": { + "_0": 12 + } + }, + "mapSpanLatitude": { + "constant": { + "_0": 45 + } + }, + "mapSpanLongitude": { + "constant": { + "_0": 55 + } + }, + "selectedFillOpacity": { + "constant": { + "_0": 0.55 + } + }, + "selectedStrokeOpacity": { + "constant": { + "_0": 0.9 + } + }, + "selectedStrokeWidth": { + "constant": { + "_0": 2 + } + }, + "unselectedFillOpacity": { + "constant": { + "_0": 0.12 + } + }, + "unselectedStrokeOpacity": { + "constant": { + "_0": 0.35 + } + }, + "unselectedStrokeWidth": { + "constant": { + "_0": 1 + } + } + }, + "settings": { + "flashAnimation": { + "value": { + "_0": { + "constant": { + "_0": { + "easeInOut": { + "duration": 0.4 + } + } + } + } + } + }, + "flashDuration": { + "constant": { + "_0": 1 + } + }, + "iconCornerRadius": { + "constant": { + "_0": 7 + } + }, + "iconForeground": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0, + "green": 0, + "red": 0 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + } + }, + "iconSize": { + "constant": { + "_0": 29 + } + }, + "iconSymbolSize": { + "constant": { + "_0": 15 + } + } + }, + "size": { + "launchCaptionBottomInset": { + "constant": { + "_0": 72 + } + }, + "launchIcon": { + "constant": { + "_0": 120 + } + }, + "statusIconWidth": { + "constant": { + "_0": 28 + } + } + }, + "spacing": { + "large": { + "constant": { + "_0": 12 + } + }, + "medium": { + "constant": { + "_0": 8 + } + }, + "regular": { + "constant": { + "_0": 10 + } + }, + "small": { + "constant": { + "_0": 6 + } + }, + "xLarge": { + "constant": { + "_0": 14 + } + }, + "xSmall": { + "constant": { + "_0": 4 + } + }, + "xxLarge": { + "constant": { + "_0": 16 + } + }, + "xxSmall": { + "constant": { + "_0": 2 + } + }, + "xxxLarge": { + "constant": { + "_0": 20 + } + } + }, + "themePicker": { + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + }, + "minimumOptionWidth": { + "constant": { + "_0": 180 + } + }, + "spacing": { + "constant": { + "_0": 12 + } + } + }, + "timeline": { + "flight": { + "iconFont": { + "constant": { + "_0": { + "size": { + "fixed": { + "_0": 16 + } + }, + "weight": "semibold" + } + } + }, + "labelSpacing": { + "constant": { + "_0": 2 + } + }, + "tint": { + "constant": { + "_0": { + "semantic": { + "_0": "accent" + } + } + } + }, + "verticalPadding": { + "constant": { + "_0": 12 + } + } + }, + "overview": { + "background": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "primary" + } + }, + "_1": 0.035 + } + } + } + }, + "border": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "primary" + } + }, + "_1": 0.1 + } + } + } + }, + "borderWidth": { + "constant": { + "_0": 1 + } + }, + "cornerRadius": { + "constant": { + "_0": 24 + } + }, + "padding": { + "constant": { + "_0": 16 + } + }, + "pinsToViewport": { + "constant": { + "_0": true + } + }, + "spacing": { + "constant": { + "_0": 12 + } + }, + "yearFont": { + "constant": { + "_0": { + "design": "serif", + "size": { + "semantic": { + "_0": "title2" + } + }, + "weight": "bold" + } + } + } + }, + "planned": { + "borderOpacity": { + "constant": { + "_0": 0.14 + } + }, + "fillOpacity": { + "constant": { + "_0": 0.035 + } + }, + "hatchLineWidth": { + "constant": { + "_0": 1 + } + }, + "hatchOpacity": { + "constant": { + "_0": 0.16 + } + }, + "hatchSpacing": { + "constant": { + "_0": 8 + } + }, + "joinedBaseHeight": { + "constant": { + "_0": 32 + } + }, + "joinedCountHorizontalPadding": { + "constant": { + "_0": 8 + } + }, + "joinedCountVerticalPadding": { + "constant": { + "_0": 4 + } + }, + "joinedLabelSpacing": { + "constant": { + "_0": 5 + } + }, + "joinedVerticalPadding": { + "constant": { + "_0": 8 + } + }, + "labelOpacity": { + "constant": { + "_0": 0.7 + } + }, + "transitionHeight": { + "constant": { + "_0": 16 + } + } + }, + "rail": { + "lineWidth": { + "constant": { + "_0": 4 + } + }, + "nodeEmojiFont": { + "constant": { + "_0": { + "size": { + "fixed": { + "_0": 20 + } + } + } + } + }, + "nodeFillOpacity": { + "constant": { + "_0": 0.18 + } + }, + "nodeSize": { + "constant": { + "_0": 42 + } + }, + "nodeStrokeWidth": { + "constant": { + "_0": 2 + } + }, + "toCardSpacing": { + "constant": { + "_0": 10 + } + } + }, + "ribbon": { + "border": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "primary" + } + }, + "_1": 0.12 + } + } + } + }, + "borderWidth": { + "constant": { + "_0": 1 + } + }, + "height": { + "constant": { + "_0": 18 + } + }, + "monthLabelSpacing": { + "constant": { + "_0": 6 + } + }, + "regionLabelSpacing": { + "constant": { + "_0": 4 + } + }, + "regionSpacing": { + "constant": { + "_0": 8 + } + }, + "separatesRegions": { + "constant": { + "_0": false + } + }, + "track": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "primary" + } + }, + "_1": 0.07 + } + } + } + } + }, + "row": { + "baseHeight": { + "constant": { + "_0": 64 + } + }, + "borderOpacity": { + "constant": { + "_0": 0.24 + } + }, + "borderWidth": { + "constant": { + "_0": 1 + } + }, + "cornerRadius": { + "constant": { + "_0": 18 + } + }, + "countFillOpacity": { + "constant": { + "_0": 0.16 + } + }, + "countHorizontalPadding": { + "constant": { + "_0": 10 + } + }, + "countVerticalPadding": { + "constant": { + "_0": 6 + } + }, + "fillOpacity": { + "constant": { + "_0": 0.09 + } + }, + "gap": { + "constant": { + "_0": 8 + } + }, + "horizontalPadding": { + "constant": { + "_0": 14 + } + }, + "labelSpacing": { + "constant": { + "_0": 3 + } + }, + "singleDay": { + "baseHeight": { + "constant": { + "_0": 0 + } + }, + "verticalPadding": { + "constant": { + "_0": 8 + } + } + }, + "spacing": { + "constant": { + "_0": 12 + } + }, + "stacksDayCount": { + "constant": { + "_0": false + } + }, + "verticalPadding": { + "constant": { + "_0": 12 + } + }, + "yearScaleHeight": { + "constant": { + "_0": 320 + } + } + } + }, + "typography": { + "onboardingIcon": { + "constant": { + "_0": { + "size": { + "fixed": { + "_0": 72 + } + } + } + } + }, + "widgetHeroRegion": { + "constant": { + "_0": { + "design": "serif", + "size": { + "semantic": { + "_0": "headline" + } + }, + "weight": "semibold" + } + } + }, + "widgetTotalNumber": { + "constant": { + "_0": { + "design": "rounded", + "size": { + "semantic": { + "_0": "body" + } + }, + "weight": "bold" + } + } + } + }, + "yearModePicker": { + "layout": { + "constant": { + "_0": { + "inline": {} + } + } + } + } + }, + "components": { + "calendarMonth": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [], + "surface": { + "clear": {} + } + }, + "compactCard": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "rosette", + "microprint", + "watermark", + "stamp" + ], + "surface": { + "value": { + "_0": "compactCard" + } + } + }, + "elsewhere": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "rosette", + "regionPattern" + ], + "surface": { + "clear": {} + } + }, + "elsewhereCard": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "rosette", + "microprint", + "watermark", + "stamp" + ], + "surface": { + "value": { + "_0": "elsewhereCard" + } + } + }, + "forecastPanel": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "rosette", + "microprint", + "stamp" + ], + "surface": { + "clear": {} + } + }, + "locations": { + "artwork": [], + "background": { + "value": { + "_0": "page" + } + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "rosette", + "regionPattern" + ], + "surface": { + "clear": {} + } + }, + "privacy": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "rosette", + "stamp" + ], + "surface": { + "value": { + "_0": "privacy" + } + } + }, + "regularCard": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "rosette", + "microprint", + "watermark", + "stamp" + ], + "surface": { + "value": { + "_0": "regularCard" + } + } + }, + "section": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [], + "surface": { + "clear": {} + } + }, + "settings": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [], + "surface": { + "clear": {} + } + }, + "sheet": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [], + "surface": { + "clear": {} + } + }, + "summary": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "rosette", + "microprint", + "watermark", + "stamp" + ], + "surface": { + "value": { + "_0": "summary" + } + } + }, + "timelineEntry": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [], + "surface": { + "clear": {} + } + }, + "welcome": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [ + "rosette", + "stamp", + "watermark", + "microprint" + ], + "surface": { + "value": { + "_0": "welcome" + } + } + }, + "year": { + "artwork": [], + "background": { + "clear": {} + }, + "body": { + "clear": {} + }, + "foreground": { + "clear": {} + }, + "heading": { + "clear": {} + }, + "nativeArtwork": [], + "surface": { + "clear": {} + } + } + }, + "id": "standard", + "schemaVersion": 2, + "values": { + "colors": [ + { + "id": "elsewhereCard.fill", + "value": { + "modes": { + "_0": { + "dark": { + "rgba": { + "alpha": 1, + "blue": 0.1411764705882353, + "green": 0.1411764705882353, + "red": 0.1411764705882353 + } + }, + "light": { + "rgba": { + "alpha": 1, + "blue": 1, + "green": 1, + "red": 1 + } + } + } + } + } + }, + { + "id": "elsewhereCard.shadow", + "value": { + "constant": { + "_0": { + "opacity": { + "_0": { + "semantic": { + "_0": "black" + } + }, + "_1": 0.08 + } + } + } + } + }, + { + "id": "page", + "value": { + "constant": { + "_0": { + "semantic": { + "_0": "background" + } + } + } + } + } + ], + "fonts": [], + "surfaces": [ + { + "id": "regularCard", + "value": { + "constant": { + "_0": { + "reflective": {} + } + } + } + }, + { + "id": "compactCard", + "value": { + "constant": { + "_0": { + "reflective": {} + } + } + } + }, + { + "id": "elsewhereCard", + "value": { + "constant": { + "_0": { + "paper": { + "_0": { + "fill": "elsewhereCard.fill", + "outline": { + "opacity": 0.45, + "width": 1 + }, + "pressedTintOpacity": 0.08, + "shadow": { + "color": "elsewhereCard.shadow", + "offsetY": 3, + "radius": 6 + }, + "tintMix": 0 + } + } + } + } + } + }, + { + "id": "welcome", + "value": { + "constant": { + "_0": { + "reflective": {} + } + } + } + }, + { + "id": "privacy", + "value": { + "constant": { + "_0": { + "reflective": {} + } + } + } + }, + { + "id": "summary", + "value": { + "constant": { + "_0": { + "reflective": {} + } + } + } + } + ] + } +} diff --git a/Where/WhereUI/Sources/Settings/AppIconPreviewPanel.swift b/Where/WhereUI/Sources/Settings/AppIconPreviewPanel.swift new file mode 100644 index 000000000..2b8e469e6 --- /dev/null +++ b/Where/WhereUI/Sources/Settings/AppIconPreviewPanel.swift @@ -0,0 +1,119 @@ +import BroadwayUI +import SwiftUI + +/// Resolves the preview surface and geometry inside its selected appearance scope. +struct AppIconPreviewPanel: View { + let option: AppIconOption + let containerSize: CGSize + let previewMode: ColorScheme + let appearanceToggles: Int + let canApply: Bool + let toggleAppearance: () -> Void + let apply: () -> Void + @Environment(\.stylesheet) private var stylesheet + + private var appIcon: WhereStylesheet.AppIconStyle { + stylesheet.appIcon + } + + var body: some View { + let iconSize = AppIconLayout.previewIconSize(containerSize: containerSize, style: appIcon) + VStack(spacing: appIcon.panel.spacing) { + Capsule() + .fill(.secondary.opacity(appIcon.panel.grabberOpacity)) + .frame( + width: appIcon.panel.grabberSize.width, + height: appIcon.panel.grabberSize.height, + ) + .padding(.top, appIcon.panel.grabberTopPadding) + + Button { + toggleAppearance() + } label: { + previewIcon(size: iconSize) + } + .buttonStyle(.plain) + .accessibilityLabel(option.displayName) + .accessibilityValue( + previewMode == .dark + ? String(localized: .appIconAppearanceDark) + : String(localized: .appIconAppearanceLight), + ) + .accessibilityHint(String(localized: .appIconAppearanceHint)) + + VStack(spacing: appIcon.panel.textSpacing) { + Text(option.displayName) + .font(.title3.weight(.semibold)) + Text(String(localized: .appIconAppearanceHint)) + .font(.footnote) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + + Button { + apply() + } label: { + Text(String(localized: .appIconSet)) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .disabled(!canApply) + } + .padding(.horizontal, appIcon.panel.horizontalPadding) + .padding(.bottom, appIcon.panel.bottomPadding) + .frame(maxWidth: .infinity) + // The preview can invert the surrounding sheet's appearance. + .foregroundStyle(Color(uiColor: .label)) + .environment(\.whereThemeSecondaryInk, nil) + .background { + UnevenRoundedRectangle( + topLeadingRadius: appIcon.panel.cornerRadius, + topTrailingRadius: appIcon.panel.cornerRadius, + style: .continuous, + ) + .fill(appIcon.panel.background) + .shadow( + color: appIcon.panel.shadowColor, + radius: appIcon.panel.shadowRadius, + y: appIcon.panel.shadowOffsetY, + ) + .ignoresSafeArea(edges: .bottom) + } + } + + /// The tappable preview icon. Tapping toggles light/dark, which crossfades + /// the art and background while this plays a quick scale-down-then-bounce. + /// Keyed off `appearanceToggles` so the bounce only fires on a real tap, not + /// when the panel first opens. + private func previewIcon(size: CGFloat) -> some View { + AppIconImage(name: option.previewImageName, size: size) + .keyframeAnimator(initialValue: 1.0, trigger: appearanceToggles) { content, scale in + content.scaleEffect(scale) + } keyframes: { _ in + KeyframeTrack { + CubicKeyframe(0.82, duration: 0.12) + SpringKeyframe(1.0, duration: 0.42, spring: .bouncy) + } + } + } +} + +#if DEBUG + #Preview { + if let option = AppIconCatalog.loadedOptions().first { + AppIconPreviewPanel( + option: option, + containerSize: CGSize(width: 402, height: 874), + previewMode: .dark, + appearanceToggles: 0, + canApply: true, + toggleAppearance: {}, + apply: {}, + ) + .environment(\.colorScheme, .dark) + .bMode(.dark) + .whereBroadwayRoot() + } + } +#endif diff --git a/Where/WhereUI/Sources/Settings/AppIconView.swift b/Where/WhereUI/Sources/Settings/AppIconView.swift index bd1619d8d..6c8a150ae 100644 --- a/Where/WhereUI/Sources/Settings/AppIconView.swift +++ b/Where/WhereUI/Sources/Settings/AppIconView.swift @@ -43,10 +43,6 @@ struct AppIconView: View { containerWidth: proxy.size.width, style: appIcon, ) - let previewIconSize = AppIconLayout.previewIconSize( - containerSize: proxy.size, - style: appIcon, - ) ZStack(alignment: .bottom) { grids(metrics: grid) @@ -55,7 +51,7 @@ struct AppIconView: View { } if let option = preview { - previewPanel(for: option, iconSize: previewIconSize) + previewPanel(for: option, containerSize: proxy.size) .transition(.move(edge: .bottom)) } } @@ -147,88 +143,22 @@ struct AppIconView: View { .accessibilityAction { dismissPreview() } } - private func previewPanel(for option: AppIconOption, iconSize: CGFloat) -> some View { - VStack(spacing: appIcon.panel.spacing) { - Capsule() - .fill(.secondary.opacity(appIcon.panel.grabberOpacity)) - .frame( - width: appIcon.panel.grabberSize.width, - height: appIcon.panel.grabberSize.height, - ) - .padding(.top, appIcon.panel.grabberTopPadding) - - Button { - toggleAppearance() - } label: { - previewIcon(for: option, size: iconSize) - } - .buttonStyle(.plain) - .accessibilityLabel(option.displayName) - .accessibilityValue( - previewMode == .dark - ? String(localized: .appIconAppearanceDark) - : String(localized: .appIconAppearanceLight), - ) - .accessibilityHint(String(localized: .appIconAppearanceHint)) - - VStack(spacing: appIcon.panel.textSpacing) { - Text(option.displayName) - .font(.title3.weight(.semibold)) - Text(String(localized: .appIconAppearanceHint)) - .font(.footnote) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - } - - Button { - apply(option) - } label: { - Text(String(localized: .appIconSet)) - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .controlSize(.large) - .disabled(model.isSelected(option) || !model.supportsAlternateIcons) - } - .padding(.horizontal, appIcon.panel.horizontalPadding) - .padding(.bottom, appIcon.panel.bottomPadding) - .frame(maxWidth: .infinity) - .background { - UnevenRoundedRectangle( - topLeadingRadius: appIcon.panel.cornerRadius, - topTrailingRadius: appIcon.panel.cornerRadius, - style: .continuous, - ) - .fill(appIcon.panel.background) - .shadow( - color: appIcon.panel.shadowColor, - radius: appIcon.panel.shadowRadius, - y: appIcon.panel.shadowOffsetY, - ) - .ignoresSafeArea(edges: .bottom) - } + private func previewPanel(for option: AppIconOption, containerSize: CGSize) -> some View { + AppIconPreviewPanel( + option: option, + containerSize: containerSize, + previewMode: previewMode, + appearanceToggles: appearanceToggles, + canApply: !model.isSelected(option) && model.supportsAlternateIcons, + toggleAppearance: toggleAppearance, + apply: { apply(option) }, + ) .environment(\.colorScheme, previewMode) .bMode(.init(previewMode)) .offset(y: max(dragOffset, 0)) .gesture(dragToDismiss) } - /// The tappable preview icon. Tapping toggles light/dark, which crossfades - /// the art and background while this plays a quick scale-down-then-bounce. - /// Keyed off `appearanceToggles` so the bounce only fires on a real tap, not - /// when the panel first opens. - private func previewIcon(for option: AppIconOption, size: CGFloat) -> some View { - AppIconImage(name: option.previewImageName, size: size) - .keyframeAnimator(initialValue: 1.0, trigger: appearanceToggles) { content, scale in - content.scaleEffect(scale) - } keyframes: { _ in - KeyframeTrack { - CubicKeyframe(0.82, duration: 0.12) - SpringKeyframe(1.0, duration: 0.42, spring: .bouncy) - } - } - } - private var dragToDismiss: some Gesture { DragGesture() .onChanged { value in diff --git a/Where/WhereUI/Sources/Settings/AppearanceSettingsView.swift b/Where/WhereUI/Sources/Settings/AppearanceSettingsView.swift index 86f31b773..6189fa926 100644 --- a/Where/WhereUI/Sources/Settings/AppearanceSettingsView.swift +++ b/Where/WhereUI/Sources/Settings/AppearanceSettingsView.swift @@ -145,6 +145,7 @@ struct AppearanceSettingsView: View { #endif } } + .whereThemeActivity(!showAppIcon) .navigationTitle(String(localized: .settingsAppearanceGroup)) .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: $showAppIcon) { diff --git a/Where/WhereUI/Sources/Settings/DataSettingsView.swift b/Where/WhereUI/Sources/Settings/DataSettingsView.swift index 4ef728c9b..9b7439334 100644 --- a/Where/WhereUI/Sources/Settings/DataSettingsView.swift +++ b/Where/WhereUI/Sources/Settings/DataSettingsView.swift @@ -41,6 +41,7 @@ struct DataSettingsView: View { .navigationBarTitleDisplayMode(.inline) // Keep one presenter outside the Form's reusable section content. // The export row changes from progress to idle as the archive arrives. + .whereThemeActivity(presentedShareItem == nil) .sheet(item: $presentedShareItem, content: BackupShareSheet.init) .alert( String(localized: .settingsBackupErrorTitle), diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/EstimatedTime/EstimatedTimeFeaturesView.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/EstimatedTime/EstimatedTimeFeaturesView.swift index 7231f5bc5..009ac9a1c 100644 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/EstimatedTime/EstimatedTimeFeaturesView.swift +++ b/Where/WhereUI/Sources/Settings/FeaturePreviews/EstimatedTime/EstimatedTimeFeaturesView.swift @@ -124,6 +124,7 @@ private struct EstimatedTimeFeaturesContent: View { } .scrollContentBackground(.hidden) .background(FeatureDiscoveryBackground()) + .whereThemeActivity(!showingPlanner) .sheet(isPresented: $showingPlanner) { PlannedStaysView(report: report) } } @@ -138,6 +139,7 @@ private struct EstimatedTimeFeaturesContent: View { homeRegion: report.forecasts.planning.homeRegion, ) .saturation(report.showsEstimatedTimeAndPlanning ? 1 : 0) + .whereThemeArtworkSpace(.forecastPanel) } } diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/InsightsAccuracyFeaturesView.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/InsightsAccuracyFeaturesView.swift index 843ada658..e6a02a4ed 100644 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/InsightsAccuracyFeaturesView.swift +++ b/Where/WhereUI/Sources/Settings/FeaturePreviews/InsightsAccuracy/InsightsAccuracyFeaturesView.swift @@ -18,6 +18,7 @@ struct InsightsAccuracyFeaturesView: View { } .navigationTitle("") .navigationBarTitleDisplayMode(.inline) + .whereThemeActivity(!showingResolution) .sheet(isPresented: $showingResolution) { ResolutionView(report: report) } diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/Personalization/PersonalizationFeaturesView.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/Personalization/PersonalizationFeaturesView.swift index a2827341e..c1e06ea4a 100644 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/Personalization/PersonalizationFeaturesView.swift +++ b/Where/WhereUI/Sources/Settings/FeaturePreviews/Personalization/PersonalizationFeaturesView.swift @@ -39,6 +39,7 @@ struct PersonalizationFeaturesView: View { } .navigationTitle("") .navigationBarTitleDisplayMode(.inline) + .whereThemeActivity(presentedSheet == nil) .sheet(item: $presentedSheet) { sheet in switch sheet { case .regions: RegionsSettingsView(usedThisYear: regionsUsedThisYear) diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/PlacesYear/PlacesYearFeaturesContent.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/PlacesYear/PlacesYearFeaturesContent.swift index c75b86e32..a9b2a6f5b 100644 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/PlacesYear/PlacesYearFeaturesContent.swift +++ b/Where/WhereUI/Sources/Settings/FeaturePreviews/PlacesYear/PlacesYearFeaturesContent.swift @@ -20,6 +20,7 @@ struct PlacesYearFeaturesContent: View { yearLength: report.daysInSelectedYear, year: report.selectedYear, ) + .whereThemeArtworkSpace(.compactCard) } if let content = report.elsewhere.content(for: report.selectedYear), let region = content.regions.first @@ -32,6 +33,7 @@ struct PlacesYearFeaturesContent: View { showsRecordedPoints: report.showsRecordedLocationDots, recordedPointsID: content.cardLocations.id, ) + .whereThemeArtworkSpace(.elsewhereCard) } } .task(id: report.elsewhereInput) { diff --git a/Where/WhereUI/Sources/Settings/FeaturePreviews/Widgets/FeatureLockScreenExample.swift b/Where/WhereUI/Sources/Settings/FeaturePreviews/Widgets/FeatureLockScreenExample.swift index f679ead11..6f4d03fe3 100644 --- a/Where/WhereUI/Sources/Settings/FeaturePreviews/Widgets/FeatureLockScreenExample.swift +++ b/Where/WhereUI/Sources/Settings/FeaturePreviews/Widgets/FeatureLockScreenExample.swift @@ -7,6 +7,18 @@ struct FeatureLockScreenExample: View { let date: Date let snapshot: WidgetSnapshot + var body: some View { + FeatureLockScreenContent(date: date, snapshot: snapshot) + .environment(\.colorScheme, .dark) + .bMode(.dark) + } +} + +/// Reads the wallpaper and geometry after the miniature device's appearance is scoped. +private struct FeatureLockScreenContent: View { + let date: Date + let snapshot: WidgetSnapshot + @Environment(\.stylesheet) private var stylesheet var body: some View { @@ -58,8 +70,6 @@ struct FeatureLockScreenExample: View { ), in: .rect(cornerRadius: style.widgets.device.cornerRadius), ) - .environment(\.colorScheme, .dark) - .bMode(.dark) .accessibilityElement(children: .contain) .accessibilityLabel(String(localized: .settingsExploreWidgetsLockPreviewLabel)) } diff --git a/Where/WhereUI/Sources/Settings/PrivacyPassportCard.swift b/Where/WhereUI/Sources/Settings/PrivacyPassportCard.swift index c2fd6503e..d86ab5db9 100644 --- a/Where/WhereUI/Sources/Settings/PrivacyPassportCard.swift +++ b/Where/WhereUI/Sources/Settings/PrivacyPassportCard.swift @@ -1,22 +1,47 @@ +import BroadwayUI import SwiftUI /// Reassures the reader that Where's location history stays in their Apple storage. struct PrivacyPassportCard: View { let presentation: PrivacyPassportPresentation let disclosureInteraction: PrivacyPassportDisclosureInteraction + @Environment(\.stylesheet) private var stylesheet + + var body: some View { + PrivacyPassportDarkCard( + presentation: presentation, + disclosureInteraction: disclosureInteraction, + ambientShadow: ambientShadow, + ) + .environment(\.colorScheme, .dark) + .bMode(.dark) + } + + private var ambientShadow: WhereStylesheet.MatteSurface.Shadow? { + if case let .matte(paper) = stylesheet.privacyPassportCard.surface { paper.shadow } + else { nil } + } +} + +/// Resolves layout, typography, and reflection policy in the same mode as the paper. +private struct PrivacyPassportDarkCard: View { + let presentation: PrivacyPassportPresentation + let disclosureInteraction: PrivacyPassportDisclosureInteraction + let ambientShadow: WhereStylesheet.MatteSurface.Shadow? @State private var tilt = TiltProvider() @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components var body: some View { let style = stylesheet.privacyPassportCard - PrivacyPassportCardSurface(tilt: tilt) { + PrivacyPassportCardSurface(tilt: tilt, content: { VStack(alignment: .leading, spacing: style.sectionSpacing) { PrivacyPassportHeader() Text(presentation.locationDetail) - .font(style.detailFont) - .foregroundStyle(.secondary) + .font(components[.privacy].body ?? style.detailFont) + .foregroundStyle(.whereSecondary) .fixedSize(horizontal: false, vertical: true) VStack(spacing: style.disclosure.rowSpacing) { @@ -30,8 +55,9 @@ struct PrivacyPassportCard: View { } .padding(style.padding) .frame(maxWidth: .infinity, alignment: .leading) - } - .observesTilt(tilt, enabled: style.surface.usesReflection) + }, ambientShadowOverride: ambientShadow) + .observesTilt(tilt, enabled: style.surface.usesReflection) + .whereThemeArtworkSpace(.privacy) } } diff --git a/Where/WhereUI/Sources/Settings/PrivacyPassportCardSurface.swift b/Where/WhereUI/Sources/Settings/PrivacyPassportCardSurface.swift index 8a4e92fde..5f644d3b0 100644 --- a/Where/WhereUI/Sources/Settings/PrivacyPassportCardSurface.swift +++ b/Where/WhereUI/Sources/Settings/PrivacyPassportCardSurface.swift @@ -5,8 +5,36 @@ import SwiftUI struct PrivacyPassportCardSurface: View { let tilt: TiltProvider @ViewBuilder let content: Content + var ambientShadowOverride: WhereStylesheet.MatteSurface.Shadow? + @Environment(\.stylesheet) private var stylesheet + + var body: some View { + // Elevation responds to the surrounding page; the passport's paper, ink, and + // artwork share one dark appearance inside this boundary. + PrivacyPassportDarkSurface( + tilt: tilt, + ambientShadow: ambientShadowOverride ?? ambientShadow, + content: content, + ) + .foregroundStyle(Color(uiColor: .label)) + .environment(\.whereThemeSecondaryInk, nil) + .environment(\.colorScheme, .dark) + .bMode(.dark) + } + + private var ambientShadow: WhereStylesheet.MatteSurface.Shadow? { + if case let .matte(paper) = stylesheet.privacyPassportCard.surface { paper.shadow } + else { nil } + } +} + +private struct PrivacyPassportDarkSurface: View { + let tilt: TiltProvider + let ambientShadow: WhereStylesheet.MatteSurface.Shadow? + let content: Content @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components var body: some View { let style = stylesheet.privacyPassportCard @@ -33,19 +61,28 @@ struct PrivacyPassportCardSurface: View { ) } case .matte: - printedContent.matteSurface(style.surface, tint: reflection.accent, in: shape) + printedContent.matteSurface(surface, tint: reflection.accent, in: shape) } } + .overlay { WhereComponentArtwork(role: .privacy, plane: .exterior, shape: shape) } .tint(reflection.accent) - .environment(\.colorScheme, .dark) - .bMode(.dark) .contentShape(shape) } + private var surface: WhereStylesheet.SurfaceTreatment { + var surface = stylesheet.privacyPassportCard.surface + if case var .matte(paper) = surface, let ambientShadow { + paper.shadow = ambientShadow + surface = .matte(paper) + } + return surface + } + private var printedContent: some View { let style = stylesheet.privacyPassportCard let reflection = style.reflectiveSurface return content + .modifier(WhereComponentTextStyle(role: .privacy)) .background { ZStack { if style.surface.usesReflection { @@ -55,14 +92,22 @@ struct PrivacyPassportCardSurface: View { endPoint: .bottomTrailing, ) } - SecurityPrintRosette( - tint: reflection.accent, - wobble: style.rosette.wobble, - lineWidth: style.rosette.lineWidth, - primaryRingSpacing: style.rosette.primaryRingSpacing, - secondaryRingSpacing: style.rosette.secondaryRingSpacing, - primaryOpacity: style.rosette.primaryOpacity, - secondaryOpacity: style.rosette.secondaryOpacity, + if let background = components[.privacy].background { background } + if components[.privacy].nativeArtwork.contains(.rosette) { + SecurityPrintRosette( + tint: reflection.accent, + wobble: style.rosette.wobble, + lineWidth: style.rosette.lineWidth, + primaryRingSpacing: style.rosette.primaryRingSpacing, + secondaryRingSpacing: style.rosette.secondaryRingSpacing, + primaryOpacity: style.rosette.primaryOpacity, + secondaryOpacity: style.rosette.secondaryOpacity, + ) + } + WhereComponentArtwork( + role: .privacy, + plane: .interior, + shape: RoundedRectangle(cornerRadius: style.cornerRadius), ) } } diff --git a/Where/WhereUI/Sources/Settings/PrivacyPassportDisclosureHeader.swift b/Where/WhereUI/Sources/Settings/PrivacyPassportDisclosureHeader.swift index a602e3404..ccd002a04 100644 --- a/Where/WhereUI/Sources/Settings/PrivacyPassportDisclosureHeader.swift +++ b/Where/WhereUI/Sources/Settings/PrivacyPassportDisclosureHeader.swift @@ -29,7 +29,7 @@ struct PrivacyPassportDisclosureHeader: View { if showsSettingsIndicator { Image(systemSymbol: .chevronRight) .font(style.statusFont) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .accessibilityHidden(true) } } @@ -53,7 +53,7 @@ struct PrivacyPassportDisclosureHeader: View { Spacer(minLength: 0) Image(systemSymbol: .chevronRight) .font(style.statusFont) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .accessibilityHidden(true) } } diff --git a/Where/WhereUI/Sources/Settings/PrivacyPassportDisclosureText.swift b/Where/WhereUI/Sources/Settings/PrivacyPassportDisclosureText.swift index 01826a17c..f03751db9 100644 --- a/Where/WhereUI/Sources/Settings/PrivacyPassportDisclosureText.swift +++ b/Where/WhereUI/Sources/Settings/PrivacyPassportDisclosureText.swift @@ -17,7 +17,7 @@ struct PrivacyPassportDisclosureText: View { Text(disclosure.detail) .font(style.detailFont) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) .fixedSize(horizontal: false, vertical: true) } } diff --git a/Where/WhereUI/Sources/Settings/PrivacyPassportHeader.swift b/Where/WhereUI/Sources/Settings/PrivacyPassportHeader.swift index 013c0364f..0e7c8a1a8 100644 --- a/Where/WhereUI/Sources/Settings/PrivacyPassportHeader.swift +++ b/Where/WhereUI/Sources/Settings/PrivacyPassportHeader.swift @@ -1,35 +1,45 @@ +import SFSafeSymbols import SwiftUI /// Restacks the privacy seal above its title when accessibility text needs the width. struct PrivacyPassportHeader: View { @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereComponentStylesheet) private var components var body: some View { let style = stylesheet.privacyPassportCard if style.headerLayout == .stacked { VStack(alignment: .leading, spacing: style.headerSpacing) { - PassportSeal( - systemSymbol: .lockShieldFill, - tint: style.reflectiveSurface.accent, - ) + emblem Text(LocalizedStringResource.settingsPrivacyTitle) - .font(style.titleFont) + .font(components[.privacy].heading ?? style.titleFont) .bold() .foregroundStyle(.primary) } } else { HStack(spacing: style.headerSpacing) { - PassportSeal( - systemSymbol: .lockShieldFill, - tint: style.reflectiveSurface.accent, - ) + emblem Text(LocalizedStringResource.settingsPrivacyTitle) - .font(style.titleFont) + .font(components[.privacy].heading ?? style.titleFont) .bold() .foregroundStyle(.primary) } } } + + @ViewBuilder private var emblem: some View { + if components[.privacy].nativeArtwork.contains(.stamp) { + PassportSeal( + systemSymbol: .lockShieldFill, + tint: stylesheet.privacyPassportCard.reflectiveSurface.accent, + ) + } else { + Image(systemSymbol: .lockShieldFill) + .font(components[.privacy].heading ?? .headline) + .foregroundStyle(stylesheet.privacyPassportCard.reflectiveSurface.accent) + .accessibilityHidden(true) + } + } } #if DEBUG diff --git a/Where/WhereUI/Sources/Settings/SettingsRow.swift b/Where/WhereUI/Sources/Settings/SettingsRow.swift index bb9c367a8..40a923936 100644 --- a/Where/WhereUI/Sources/Settings/SettingsRow.swift +++ b/Where/WhereUI/Sources/Settings/SettingsRow.swift @@ -73,6 +73,7 @@ struct SettingsRowModifier: ViewModifier { func body(content: Content) -> some View { content + .modifier(WhereComponentTextStyle(role: .section)) .id(focus) .listRowBackground(background) .overlay { @@ -84,10 +85,13 @@ struct SettingsRowModifier: ViewModifier { } } - private var background: some View { + @ViewBuilder private var background: some View { // Ordinary settings rows keep the system grouped fill; marketing rows // opt into clear so their page background can show through. - isHighlighted ? Color.accentColor.opacity(0.25) : restingBackground.color + if isHighlighted { Color.accentColor.opacity(0.25) } + else if restingBackground == .grouped { + WhereThemeRowBackground() + } else { restingBackground.color } } } @@ -118,6 +122,7 @@ struct SettingsFocusScope: View { var body: some View { ScrollViewReader { proxy in content + .whereThemeScreen(.sheet) .environment(\.settingsHighlight, highlighted) .task(id: isReady) { guard isReady, !didReveal else { return } diff --git a/Where/WhereUI/Sources/Settings/SettingsView.swift b/Where/WhereUI/Sources/Settings/SettingsView.swift index 245127d24..4829c57c3 100644 --- a/Where/WhereUI/Sources/Settings/SettingsView.swift +++ b/Where/WhereUI/Sources/Settings/SettingsView.swift @@ -78,6 +78,7 @@ struct SettingsView: View { if isSearching { ForEach(searchResults) { result in searchNavigationRow(result) + .whereThemeListRow() } } else { if recordingWarning.isPresented { @@ -92,6 +93,7 @@ struct SettingsView: View { Section { ForEach(destinations, id: \.self) { destination in groupNavigationRow(destination) + .whereThemeListRow() } } header: { if let headerTitle = section.headerTitle { @@ -102,6 +104,8 @@ struct SettingsView: View { } } } + .whereThemeScreen(.settings) + .whereThemeActivity(!showRegions) .navigationTitle(String(localized: .settingsTitle)) .searchable(text: $searchText, prompt: String(localized: .settingsSearchPrompt)) .overlay { @@ -114,6 +118,7 @@ struct SettingsView: View { } .sheet(isPresented: $showRegions) { RegionsSettingsView(usedThisYear: regionsUsedThisYear) + .whereThemeScreen(.sheet) } } } @@ -127,7 +132,7 @@ struct SettingsView: View { .font(.headline) Text(.settingsRecordingWarningMessage) .font(.subheadline) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } } icon: { Image(systemSymbol: .exclamationmarkTriangleFill) @@ -215,7 +220,7 @@ struct SettingsView: View { LabeledContent { if let subtitle = subtitle(for: destination) { Text(subtitle) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } } label: { Label { @@ -255,7 +260,7 @@ struct SettingsView: View { Text(result.title) Text(result.destination.rowTitle) .font(.caption) - .foregroundStyle(.secondary) + .foregroundStyle(.whereSecondary) } } icon: { SettingsIcon( diff --git a/Where/WhereUI/Sources/Shared/WhereFlatTheme.swift b/Where/WhereUI/Sources/Shared/WhereFlatTheme.swift deleted file mode 100644 index cab283641..000000000 --- a/Where/WhereUI/Sources/Shared/WhereFlatTheme.swift +++ /dev/null @@ -1,89 +0,0 @@ -import BroadwayCore -import SwiftUI - -/// Flat's authored finish values, resolved over Standard's trait-adjusted base. -/// Region colors remain runtime inputs to the resolved surface descriptors. -enum WhereFlatTheme { - static func overrides(traits: BTraits, base: WhereStylesheet) -> WhereThemeOverrides { - let mode = traits.mode - var overrides = WhereThemeOverrides() - let increasedContrast = traits.accessibility.isDarkerSystemColorsEnabled - let outline = WhereStylesheet.MatteSurface.Outline( - opacity: increasedContrast ? 0.65 : BModeVarying(light: 0.32, dark: 0.4)[mode], - width: increasedContrast ? 1.5 : 1, - ) - let paper = WhereStylesheet.MatteSurface.Fill( - base: BModeVarying(light: Color.white, dark: Color(white: 0.12))[mode], - tintMix: 0, - ) - let shadow = WhereStylesheet.MatteSurface.Shadow( - color: .black.opacity(BModeVarying(light: 0.14, dark: 0.3)[mode]), - radius: 10, - offsetY: 4, - ) - let regionSurface = WhereStylesheet.MatteSurface( - fill: .init( - base: BModeVarying(light: Color.white, dark: .black)[mode], - tintMix: BModeVarying(light: 0.52, dark: 0.58)[mode], - ), - outline: outline, - shadow: shadow, - pressedTintOpacity: 0.1, - ) - overrides.card.regular.surface = .matte(regionSurface) - var compactSurface = regionSurface - compactSurface.shadow.radius = 6 - compactSurface.shadow.offsetY = 3 - overrides.card.compact.surface = .matte(compactSurface) - let contentTint = WhereStylesheet.TintAdjustment( - color: BModeVarying(light: Color.black, dark: .white)[mode], - fraction: increasedContrast ? 0.75 : 0.6, - ) - overrides.card.regular.contentTint = contentTint - overrides.card.compact.contentTint = contentTint - overrides.card.haloOpacity = 0 - overrides.card.backgroundBlendMode = .normal - - overrides.elsewhere.surface = WhereStylesheet.MatteSurface( - fill: .init(base: base.card.elsewherePaper.surface.fill.base, tintMix: 0), - outline: outline, - shadow: .init(color: shadow.color, radius: 8, offsetY: 3), - pressedTintOpacity: 0.08, - ) - overrides.welcome.surface = .matte(WhereStylesheet.MatteSurface( - fill: paper, - outline: outline, - shadow: shadow, - pressedTintOpacity: 0, - )) - overrides.welcome.close.surface = .matte(WhereStylesheet.MatteSurface( - fill: paper, - outline: outline, - shadow: .init(color: shadow.color, radius: 5, offsetY: 3), - pressedTintOpacity: 0.1, - )) - // WhereStylesheet.MatteSurface owns the complete elevation; omit the legacy outer shadows. - overrides.welcome.glowOpacity = 0 - overrides.welcome.liftOpacity = 0 - overrides.welcome.close.glowOpacity = 0 - overrides.welcome.close.liftOpacity = 0 - overrides.privacy.surface = .matte(WhereStylesheet.MatteSurface( - fill: .init(base: Color(red: 0.05, green: 0.125, blue: 0.25), tintMix: 0), - outline: .init( - opacity: increasedContrast ? 0.65 : base.privacyPassportCard.outlineOpacity, - width: increasedContrast ? 1.5 : base.privacyPassportCard.outlineWidth, - ), - shadow: .init(color: shadow.color, radius: 6, offsetY: 3), - pressedTintOpacity: 0, - )) - overrides.forecast.surfaceWashOpacity = 0 - overrides.forecast.summarySurface = .matte(WhereStylesheet.MatteSurface( - fill: .init(base: base.locationForecast.summary.paper, tintMix: 0), - outline: outline, - shadow: .init(color: shadow.color, radius: 8, offsetY: 3), - pressedTintOpacity: 0, - )) - overrides.forecast.summaryShadowOpacity = 0 - return overrides - } -} diff --git a/Where/WhereUI/Sources/Shared/WhereStylesheet.swift b/Where/WhereUI/Sources/Shared/WhereStylesheet.swift index 912eb4980..6e69e6c29 100644 --- a/Where/WhereUI/Sources/Shared/WhereStylesheet.swift +++ b/Where/WhereUI/Sources/Shared/WhereStylesheet.swift @@ -46,109 +46,215 @@ struct WhereStylesheet: BStylesheet { init() {} init(context: SlicingContext) throws { - // Start from the fixed scale (property defaults), then adjust the slice - // of tokens that should react to the current traits. Everything else - // stays constant, so a default/system context reproduces `default`. - let traits = context.traits - theme = context.themes[WhereTheme.self] + let resolved = try context.themes[WhereDocumentTheme.self].document?.resolved + ?? WhereBundledThemeDefinitions.resolved(context.themes[WhereTheme.self]) + try self.init(resolved: resolved, traits: context.traits) + } + init(resolved: ResolvedWhereTheme, traits: BTraits) throws { + theme = resolved.fallback.theme + try resolved.appearance.apply( + to: &self, + traits: traits, + appearance: BViewDocumentStylesheet(traits: traits).appearance, + path: "appearance", + ) developerOverlay.launcher.diameter = BScaledDimension.value( developerOverlay.launcher.diameter, relativeTo: .title2, category: traits.contentSizeCategory, ) + enforceThemeAccessibility(traits: traits, baseline: nil) + if traits.accessibility.isDarkerSystemColorsEnabled { + // Native accessibility floors apply to every authored paper surface. + var minimum = self + minimum.card.regular.surface = .reflective + minimum.card.compact.surface = .reflective + minimum.locationWelcome.surface = .reflective + minimum.locationWelcome.close.surface = .reflective + minimum.privacyPassportCard.surface = .reflective + minimum.locationForecast.summary.treatment = .reflective + minimum.card.elsewherePaper.surface.outline = .init(opacity: 0.65, width: 1.5) + preserveSurfaceContrast(from: minimum) + if case .matte = card.regular.surface { card.regular.contentTint.fraction = max( + 0.75, + card.regular.contentTint.fraction, + ) } + if case .matte = card.compact.surface { card.compact.contentTint.fraction = max( + 0.75, + card.compact.contentTint.fraction, + ) } + } + if traits.accessibility.isReduceTransparencyEnabled { + let appearance = BViewDocumentStylesheet(traits: traits).appearance + let canonical = try WhereBundledThemeDefinitions.resolved(theme).appearance + if let color = resolved.appearance.locationsBackground?.paper, + !WhereThemeValidation + .isOpaque(color.resolve(mode: traits.mode)) + { + if let paper = canonical.locationsBackground?.paper?.resolve(mode: traits.mode), + WhereThemeValidation.isOpaque(paper) + { + locationsBackground.paper = appearance.color(paper) + } else { + locationsBackground.paper = appearance.background + } + } + if let color = resolved.appearance.locationForecast?.summary?.paper, + !WhereThemeValidation + .isOpaque(color.resolve(mode: traits.mode)) + { + if let paper = canonical.locationForecast?.summary?.paper? + .resolve(mode: traits.mode), + WhereThemeValidation.isOpaque(paper) + { + locationForecast.summary.paper = appearance.color(paper) + } else { + locationForecast.summary.paper = appearance.background + } + } + } + } + + /// Custom finishes cannot weaken the fallback's high-contrast edge treatment. + private mutating func preserveSurfaceContrast(from baseline: Self) { + func protect(_ surface: SurfaceTreatment, minimum: SurfaceTreatment) -> SurfaceTreatment { + switch surface { + case .reflective: + return minimum + case var .matte(value): + let outline: MatteSurface.Outline = switch minimum { + case let .matte(inherited): inherited.outline + case .reflective: .init(opacity: 0.65, width: 1.5) + } + value.outline.opacity = max(value.outline.opacity, outline.opacity) + value.outline.width = max(value.outline.width, outline.width) + return .matte(value) + } + } + card.regular.surface = protect(card.regular.surface, minimum: baseline.card.regular.surface) + card.compact.surface = protect(card.compact.surface, minimum: baseline.card.compact.surface) + card.elsewherePaper.surface.outline.opacity = max( + card.elsewherePaper.surface.outline.opacity, + baseline.card.elsewherePaper.surface.outline.opacity, + ) + card.elsewherePaper.surface.outline.width = max( + card.elsewherePaper.surface.outline.width, + baseline.card.elsewherePaper.surface.outline.width, + ) + locationWelcome.surface = protect( + locationWelcome.surface, + minimum: baseline.locationWelcome.surface, + ) + locationWelcome.close.surface = protect( + locationWelcome.close.surface, + minimum: baseline.locationWelcome.close.surface, + ) + privacyPassportCard.surface = protect( + privacyPassportCard.surface, + minimum: baseline.privacyPassportCard.surface, + ) + locationForecast.summary.treatment = protect( + locationForecast.summary.treatment, + minimum: baseline.locationForecast.summary.treatment, + ) + } + private mutating func enforceThemeAccessibility(traits: BTraits, baseline: Self?) { // Grow day-grid tap targets at accessibility Dynamic Type sizes. if traits.contentSizeCategory.isAccessibilitySize { - locationWelcome.accessory.copy = .compact - locationForecast.header.layout = .stacked - locationForecast.row.layout = .stacked - locationForecast.controls.layout = .stacked - privacyPassportCard.headerLayout = .stacked - privacyPassportCard.disclosure.layout = .stacked - themePicker.layout = .stacked - yearModePicker.layout = .stacked - openSourceStamp.layout = .stacked - plannedStayWarningStamp.layout = .stacked - featureDiscovery.appIcon.layout = .stacked - featureDiscovery.regionStyle.layout = .stacked - featureDiscovery.shareSheet = .accessible - featureDiscovery.evidenceArchive = .accessible - calendar.overview.columns = 1 - calendar.overview.dayHeight = 28 - calendar.overview.dayFontSize = 16 - calendar.overview.titleFontSize = 22 - calendar.day.minHeight = 56 - calendar.day.numberFontSize = 24 - calendar.day.numberSize = 36 - calendar.month.weekdayFontSize = 14 - calendar.month.stacksFooter = true - timeline.overview.pinsToViewport = false - timeline.row.stacksDayCount = true - featureDiscovery.siri.bubble.indent = 0 + locationWelcome.accessory.copy = baseline?.locationWelcome.accessory.copy ?? .compact + locationForecast.header.layout = baseline?.locationForecast.header.layout ?? .stacked + locationForecast.row.layout = baseline?.locationForecast.row.layout ?? .stacked + locationForecast.controls.layout = baseline?.locationForecast.controls + .layout ?? .stacked + privacyPassportCard.headerLayout = baseline?.privacyPassportCard + .headerLayout ?? .stacked + privacyPassportCard.disclosure.layout = baseline?.privacyPassportCard.disclosure + .layout ?? .stacked + themePicker.layout = baseline?.themePicker.layout ?? .stacked + yearModePicker.layout = baseline?.yearModePicker.layout ?? .stacked + openSourceStamp.layout = baseline?.openSourceStamp.layout ?? .stacked + plannedStayWarningStamp.layout = baseline?.plannedStayWarningStamp.layout ?? .stacked + featureDiscovery.appIcon.layout = baseline?.featureDiscovery.appIcon.layout ?? .stacked + featureDiscovery.regionStyle.layout = baseline?.featureDiscovery.regionStyle + .layout ?? .stacked + featureDiscovery.shareSheet = baseline?.featureDiscovery.shareSheet ?? .accessible + featureDiscovery.evidenceArchive = baseline?.featureDiscovery + .evidenceArchive ?? .accessible + calendar.overview.columns = baseline?.calendar.overview.columns ?? 1 + calendar.overview.dayHeight = baseline?.calendar.overview.dayHeight ?? 28 + calendar.overview.dayFontSize = baseline?.calendar.overview.dayFontSize ?? 16 + calendar.overview.titleFontSize = baseline?.calendar.overview.titleFontSize ?? 22 + calendar.day.minHeight = baseline?.calendar.day.minHeight ?? 56 + calendar.day.numberFontSize = baseline?.calendar.day.numberFontSize ?? 24 + calendar.day.numberSize = baseline?.calendar.day.numberSize ?? 36 + calendar.month.weekdayFontSize = baseline?.calendar.month.weekdayFontSize ?? 14 + calendar.month.stacksFooter = baseline?.calendar.month.stacksFooter ?? true + timeline.overview.pinsToViewport = baseline?.timeline.overview.pinsToViewport ?? false + timeline.row.stacksDayCount = baseline?.timeline.row.stacksDayCount ?? true + featureDiscovery.siri.bubble.indent = baseline?.featureDiscovery.siri.bubble.indent ?? 0 } // Give every region a consistently labeled ribbon band when tint // alone is not an acceptable differentiator. if traits.accessibility.shouldDifferentiateWithoutColor { - timeline.overview.pinsToViewport = false - timeline.ribbon.separatesRegions = true + timeline.overview.pinsToViewport = baseline?.timeline.overview.pinsToViewport ?? false + timeline.ribbon.separatesRegions = baseline?.timeline.ribbon.separatesRegions ?? true } // Reduce Transparency flattens the cards: drop the decorative rim-glow // layer (the translucent halo) on both card variants. if traits.accessibility.isReduceTransparencyEnabled { - locationForecast.summary.usesOpaquePaper = true - locationForecast.summary.shadowOpacity = 0 - locationsBackground.showsInk = false - card.regular.glow.radius = 0 - card.compact.glow.radius = 0 - card.constellation.haloOpacity = 0 - privacyPassportCard.disclosure.fillOpacity = 0.16 + locationForecast.summary.usesOpaquePaper = baseline?.locationForecast.summary + .usesOpaquePaper ?? true + locationForecast.summary.shadowOpacity = baseline?.locationForecast.summary + .shadowOpacity ?? 0 + locationsBackground.showsInk = baseline?.locationsBackground.showsInk ?? false + card.regular.glow.radius = baseline?.card.regular.glow.radius ?? 0 + card.compact.glow.radius = baseline?.card.compact.glow.radius ?? 0 + card.constellation.haloOpacity = baseline?.card.constellation.haloOpacity ?? 0 + privacyPassportCard.disclosure.fillOpacity = baseline?.privacyPassportCard.disclosure + .fillOpacity ?? 0.16 } if traits.accessibility.isDarkerSystemColorsEnabled { - locationForecast.ink = .increasedContrast - openSourceStamp.ink = .increasedContrast - plannedStayWarningStamp.ink = .increasedContrast + let showsSurfaceWash = locationForecast.ink.surfaceWashOpacity > 0 + locationForecast.ink = baseline?.locationForecast.ink ?? .increasedContrast + // Contrast strengthens informational ink, but cannot restore removed ornament. + if !showsSurfaceWash { locationForecast.ink.surfaceWashOpacity = 0 } + openSourceStamp.ink = baseline?.openSourceStamp.ink ?? .increasedContrast + plannedStayWarningStamp.ink = baseline?.plannedStayWarningStamp + .ink ?? .increasedContrast } // Reduce Motion stops the cards' day count rolling its digits; it // crossfades to the new number instead. if traits.accessibility.isReduceMotionEnabled { - calendar.overview.usesSpatialMotion = false - launch.reveal = .crossfade - launch.revealAnimation = motion.reducedReveal - launch.captionAnimation = nil - settings.flashAnimation = nil - card.dayCount = .reducedMotion - locationCardStack.overtake = .reducedMotion - locationWelcome.motion = .reduced - developerOverlay.menu.motion = .reduced - } - - // Pale, luminosity-only ink lifts the background security print off - // dark glass without changing its hue or saturation on touch. - if traits.mode == .dark { - calendar.overview.regionBand.opacity = 0.55 - calendar.overview.regionBand.planned.fillOpacity = 0.24 - calendar.overview.regionBand.planned.hatchOpacity = 0.85 - locationForecast.summary.ink = Color(white: 0.78) - locationForecast.summary.paper = Color(white: 0.16) - settings.iconForeground = .black - card.securityPrint = .dark - card.elsewherePaper = .dark - featureDiscovery.siri.accent = Color(white: 0.42) - } - - // Resolve theme finishes after shared traits so they retain their depth cues. - let overrides = WhereThemeOverrides.resolve(theme: theme, traits: traits, base: self) - overrides.apply(to: &self) + calendar.overview.usesSpatialMotion = baseline?.calendar.overview + .usesSpatialMotion ?? false + launch.reveal = baseline?.launch.reveal ?? .crossfade + launch.revealAnimation = baseline?.launch.revealAnimation ?? motion.reducedReveal + launch.captionAnimation = baseline?.launch.captionAnimation ?? nil + settings.flashAnimation = baseline?.settings.flashAnimation ?? nil + card.dayCount = baseline?.card.dayCount ?? .reducedMotion + locationCardStack.overtake = baseline?.locationCardStack.overtake ?? .reducedMotion + locationWelcome.motion = baseline?.locationWelcome.motion ?? .reduced + developerOverlay.menu.motion = baseline?.developerOverlay.menu.motion ?? .reduced + } } /// The fixed token set: the fallback used off the `View` tree (layout /// helpers, tests) and when no Broadway root has seeded a context. - static let `default` = WhereStylesheet() + static let `default`: WhereStylesheet = { + do { return try WhereStylesheet( + resolved: WhereBundledThemeDefinitions.resolved(.standard), + traits: BTraits(), + ) } catch { + WhereBundledThemeDefinitions.reportPackagingFailure(error) + return WhereStylesheet() + } + }() } // MARK: - Flight status @@ -2755,11 +2861,26 @@ extension View { /// empty resolver yields fallback looks (previews, the region-map viewer). /// The root also owns and injects the region-outline `Path` cache so cards /// share render artifacts without a process-global UI singleton. + public func whereBroadwayRoot( + themeDocument: PreparedWhereThemeDocument, + regionStyles: RegionStyleResolver = .default, + ) -> some View { + modifier(WhereBroadwayRootModifier( + theme: themeDocument.resolved.fallback.theme, + regionStyles: regionStyles, + document: themeDocument, + )) + } + public func whereBroadwayRoot( theme: WhereTheme = .standard, regionStyles: RegionStyleResolver = .default, ) -> some View { - modifier(WhereBroadwayRootModifier(theme: theme, regionStyles: regionStyles)) + modifier(WhereBroadwayRootModifier( + theme: theme, + regionStyles: regionStyles, + document: WhereBundledThemes.preparedOrFallback(theme), + )) } } @@ -2769,11 +2890,19 @@ extension View { private struct WhereBroadwayRootModifier: ViewModifier { let theme: WhereTheme let regionStyles: RegionStyleResolver + let document: PreparedWhereThemeDocument? @State private var regionOutlinePathCache = RegionOutlinePathCache() + private var themes: BThemes { + var values = WhereThemes.current(theme: theme) + values[WhereDocumentTheme.self] = .init(document: document?.validated) + return values + } + func body(content: Content) -> some View { content - .broadwayRoot(themes: WhereThemes.current(theme: theme)) + .broadwayRoot(themes: themes) + .environment(\.whereThemeDocument, document) .environment(\.regionStyles, regionStyles) .environment(\.regionOutlinePathCache, regionOutlinePathCache) } @@ -2834,5 +2963,6 @@ extension WhereStylesheet { struct ThemePickerStyle: Equatable { var layout = ContentLayout.inline var spacing: CGFloat = 12 + var minimumOptionWidth: CGFloat = 180 } } diff --git a/Where/WhereUI/Sources/Shared/WhereTheme+Presentation.swift b/Where/WhereUI/Sources/Shared/WhereTheme+Presentation.swift index 44fbe5d57..4eda4d759 100644 --- a/Where/WhereUI/Sources/Shared/WhereTheme+Presentation.swift +++ b/Where/WhereUI/Sources/Shared/WhereTheme+Presentation.swift @@ -5,6 +5,7 @@ extension WhereTheme { switch self { case .standard: String(localized: .themeStandardTitle) case .alternate: String(localized: .themeAlternateTitle) + case .halloween: String(localized: .themeHalloweenTitle) } } @@ -12,6 +13,7 @@ extension WhereTheme { switch self { case .standard: String(localized: .themeStandardDetail) case .alternate: String(localized: .themeAlternateDetail) + case .halloween: String(localized: .themeHalloweenDetail) } } } diff --git a/Where/WhereUI/Sources/Shared/WhereThemeOverrides.swift b/Where/WhereUI/Sources/Shared/WhereThemeOverrides.swift deleted file mode 100644 index 240fd359c..000000000 --- a/Where/WhereUI/Sources/Shared/WhereThemeOverrides.swift +++ /dev/null @@ -1,118 +0,0 @@ -import BroadwayCore -import SwiftUI -import WhereCore - -/// Sparse, resolved component overrides. Nil inherits the trait-adjusted base; -/// concrete values, including zero, replace only the named field. -struct WhereThemeOverrides: Equatable { - var card = Card() - var elsewhere = Elsewhere() - var welcome = Welcome() - var privacy = Privacy() - var forecast = Forecast() - - struct Card: Equatable { - var regular = Variant() - var compact = Variant() - var haloOpacity: Double? - var backgroundBlendMode: BlendMode? - - struct Variant: Equatable { - var surface: WhereStylesheet.SurfaceTreatment? - var contentTint: WhereStylesheet.TintAdjustment? - } - } - - struct Elsewhere: Equatable { - var surface: WhereStylesheet.MatteSurface? - } - - struct Welcome: Equatable { - var surface: WhereStylesheet.SurfaceTreatment? - var glowOpacity: Double? - var liftOpacity: Double? - var close = Close() - - struct Close: Equatable { - var surface: WhereStylesheet.SurfaceTreatment? - var glowOpacity: Double? - var liftOpacity: Double? - } - } - - struct Privacy: Equatable { - var surface: WhereStylesheet.SurfaceTreatment? - } - - struct Forecast: Equatable { - var surfaceWashOpacity: Double? - var summarySurface: WhereStylesheet.SurfaceTreatment? - var summaryShadowOpacity: Double? - } - - static func resolve( - theme: WhereTheme, - traits: BTraits, - base: WhereStylesheet, - ) -> Self { - switch theme { - case .standard: Self() - case .alternate: WhereFlatTheme.overrides(traits: traits, base: base) - } - } - - /// Applies values only; theme selection and trait decisions belong to resolution. - func apply(to sheet: inout WhereStylesheet) { - if let value = card.regular.surface { - sheet.card.regular.surface = value - } - if let value = card.regular.contentTint { - sheet.card.regular.contentTint = value - } - if let value = card.compact.surface { - sheet.card.compact.surface = value - } - if let value = card.compact.contentTint { - sheet.card.compact.contentTint = value - } - if let value = card.haloOpacity { - sheet.card.constellation.haloOpacity = value - } - if let value = card.backgroundBlendMode { - sheet.card.securityPrint.backgroundBlendMode = value - } - if let value = elsewhere.surface { - sheet.card.elsewherePaper.surface = value - } - if let value = welcome.surface { - sheet.locationWelcome.surface = value - } - if let value = welcome.glowOpacity { - sheet.locationWelcome.glow.opacity = value - } - if let value = welcome.liftOpacity { - sheet.locationWelcome.lift.opacity = value - } - if let value = welcome.close.surface { - sheet.locationWelcome.close.surface = value - } - if let value = welcome.close.glowOpacity { - sheet.locationWelcome.close.glow.opacity = value - } - if let value = welcome.close.liftOpacity { - sheet.locationWelcome.close.lift.opacity = value - } - if let value = privacy.surface { - sheet.privacyPassportCard.surface = value - } - if let value = forecast.surfaceWashOpacity { - sheet.locationForecast.ink.surfaceWashOpacity = value - } - if let value = forecast.summarySurface { - sheet.locationForecast.summary.treatment = value - } - if let value = forecast.summaryShadowOpacity { - sheet.locationForecast.summary.shadowOpacity = value - } - } -} diff --git a/Where/WhereUI/Sources/Shared/WhereThemePicker.swift b/Where/WhereUI/Sources/Shared/WhereThemePicker.swift index d40bd0236..4ddfd41e9 100644 --- a/Where/WhereUI/Sources/Shared/WhereThemePicker.swift +++ b/Where/WhereUI/Sources/Shared/WhereThemePicker.swift @@ -12,30 +12,38 @@ struct WhereThemePicker: View { @Environment(\.stylesheet) private var stylesheet var body: some View { - let layout = stylesheet.themePicker.layout == .stacked - ? AnyLayout(VStackLayout(spacing: stylesheet.themePicker.spacing)) - : AnyLayout(HStackLayout(alignment: .top, spacing: stylesheet.themePicker.spacing)) - - layout { - ForEach(WhereTheme.allCases, id: \.self) { theme in - Button { - onSelect(theme) - } label: { - WhereThemeOption(theme: theme, isSelected: theme == selection) + ViewThatFits(in: .horizontal) { + if stylesheet.themePicker.layout == .inline { + HStack(alignment: .top, spacing: stylesheet.themePicker.spacing) { + options(minimumWidth: stylesheet.themePicker.minimumOptionWidth) } - .buttonStyle(.plain) - .accessibilityLabel(theme.title) - .accessibilityValue( - theme == selection - ? String(localized: .themeSelectedValue) - : String(localized: .themeNotSelectedValue), - ) - .accessibilityHint(theme.detail) - .accessibilityAddTraits(theme == selection ? .isSelected : []) + } + VStack(spacing: stylesheet.themePicker.spacing) { + options(minimumWidth: nil) } } .sensoryFeedback(.selection, trigger: selection) } + + private func options(minimumWidth: CGFloat?) -> some View { + ForEach(WhereTheme.allCases, id: \.self) { theme in + Button { + onSelect(theme) + } label: { + WhereThemeOption(theme: theme, isSelected: theme == selection) + } + .frame(minWidth: minimumWidth) + .buttonStyle(.plain) + .accessibilityLabel(theme.title) + .accessibilityValue( + theme == selection + ? String(localized: .themeSelectedValue) + : String(localized: .themeNotSelectedValue), + ) + .accessibilityHint(theme.detail) + .accessibilityAddTraits(theme == selection ? .isSelected : []) + } + } } private struct WhereThemeOption: View { @@ -47,16 +55,10 @@ private struct WhereThemeOption: View { var body: some View { VStack(alignment: .leading, spacing: stylesheet.spacing.medium) { - RegionSummaryCard( - regionDays: RegionDays(region: .california, days: 128), - variant: .compact, - renderPurpose: .themeSpecimen, - year: 2026, - showsRecordedPoints: false, - ) - .environment(\.regionOutlinePathCache, regionOutlinePathCache) - .whereBroadwayRoot(theme: theme) - .accessibilityHidden(true) + WhereThemeSpecimen() + .environment(\.regionOutlinePathCache, regionOutlinePathCache) + .whereBroadwayRoot(theme: theme) + .accessibilityHidden(true) VStack(alignment: .leading, spacing: stylesheet.spacing.xxSmall) { HStack(spacing: stylesheet.spacing.small) { @@ -70,7 +72,7 @@ private struct WhereThemeOption: View { .font(.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) - .lineLimit(3, reservesSpace: true) + .lineLimit(3...) } } .padding(stylesheet.spacing.medium) @@ -90,6 +92,23 @@ private struct WhereThemeOption: View { } } +/// Contains the card's full paint bounds inside the selection button, above its caption. +private struct WhereThemeSpecimen: View { + @Environment(\.whereComponentStylesheet) private var stylesheet + + var body: some View { + RegionSummaryCard( + regionDays: RegionDays(region: .california, days: 128), + variant: .compact, + renderPurpose: .themeSpecimen, + year: 2026, + showsRecordedPoints: false, + ) + .padding(stylesheet[.compactCard].outsets) + .allowsHitTesting(false) + } +} + #if DEBUG #Preview { @Previewable @State var selection = WhereTheme.standard diff --git a/Where/WhereUI/Sources/Themes/HalloweenThemeAssets.swift b/Where/WhereUI/Sources/Themes/HalloweenThemeAssets.swift new file mode 100644 index 000000000..9a9b2f8eb --- /dev/null +++ b/Where/WhereUI/Sources/Themes/HalloweenThemeAssets.swift @@ -0,0 +1,43 @@ +import SwiftUI +import UIKit +import ViewDocumentCore +import ViewDocumentUI + +/// Decode bundled illustrations once at the preparation boundary, never from a view body. +@MainActor +enum HalloweenThemeAssets { + static func load() throws -> ViewDocumentAssets { + var images: [ViewAssetID: Image] = [:] + for artwork in Artwork.allCases { + guard let url = Bundle.module.url(forResource: artwork.resource, withExtension: "png"), + let image = UIImage(contentsOfFile: url.path) + else { + throw WhereThemeValidationError( + path: artwork.resource, + reason: "Missing or unreadable theme illustration", + ) + } + images[artwork.id] = Image(uiImage: image) + } + return .init(images: images, symbols: [:]) + } + + private enum Artwork: CaseIterable { + case pumpkins, moon, vine + var id: ViewAssetID { + switch self { + case .pumpkins: .init(rawValue: "halloween.pumpkins") + case .moon: .init(rawValue: "halloween.moon") + case .vine: .init(rawValue: "halloween.vine") + } + } + + var resource: String { + switch self { + case .pumpkins: "HalloweenPumpkins" + case .moon: "HalloweenMoon" + case .vine: "HalloweenVine" + } + } + } +} diff --git a/Where/WhereUI/Sources/Themes/PreparedWhereThemeDocument.swift b/Where/WhereUI/Sources/Themes/PreparedWhereThemeDocument.swift new file mode 100644 index 000000000..31f33a531 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/PreparedWhereThemeDocument.swift @@ -0,0 +1,40 @@ +import BroadwayUI +import ViewDocumentUI + +/// Prepared resources are injected once at a Where root, independently of cached appearance values. +public struct PreparedWhereThemeDocument { + struct Artwork: Identifiable { + let id: WhereThemeArtwork.ID + let placement: BArtworkPlacement + let role: BSurfaceLayerRole + let document: PreparedViewDocument + } + + let componentArtwork: [WhereThemeComponents.Role: [Artwork]] + public let validated: ValidatedWhereThemeDocument + public var resolved: ResolvedWhereTheme { + validated.resolved + } + + public init(validated: ValidatedWhereThemeDocument, assets: ViewDocumentAssets) throws { + self.validated = validated + var artworkByRole: [WhereThemeComponents.Role: [Artwork]] = [:] + for role in WhereThemeComponents.Role.allCases { + let entries = validated.resolved.components[role]?.artwork ?? [] + let documents = validated.artwork[role] ?? [] + guard entries.count == documents.count else { throw WhereThemeValidationError( + path: "artwork", + reason: "Prepared artwork does not match its definition", + ) } + artworkByRole[role] = try zip(entries, documents).map { entry, document in + try Artwork( + id: entry.id, + placement: BArtworkPlacement(entry.placement), + role: entry.role, + document: PreparedViewDocument(document: document, assets: assets), + ) + } + } + componentArtwork = artworkByRole + } +} diff --git a/Where/WhereUI/Sources/Themes/ResolvedWhereTheme.swift b/Where/WhereUI/Sources/Themes/ResolvedWhereTheme.swift new file mode 100644 index 000000000..306ee8281 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/ResolvedWhereTheme.swift @@ -0,0 +1,170 @@ +import BroadwayCore +import ViewDocumentCore + +/// Complete authored values. Only a catalog can construct a resolved theme. +public struct ResolvedWhereTheme: Hashable, Sendable { + public let id: WhereThemeDocument.ID + public let values: WhereThemeValues + public let appearance: WhereThemeAppearance + public let components: WhereThemeComponents + public let fallback: WhereThemeDocument.Fallback + + init( + id: WhereThemeDocument.ID, + values: WhereThemeValues, + appearance: WhereThemeAppearance, + components: WhereThemeComponents, + fallback: WhereThemeDocument.Fallback, + ) { + self.id = id; self.values = values; self.appearance = appearance + self.components = components; self.fallback = fallback + } +} + +extension WhereThemeValues { + func inheriting(_ parent: Self?) throws -> Self { + func merged( + _ entries: [Value]?, + _ inherited: [Value]?, + path: String, + ) throws -> [Value] where Value.ID: Hashable { + let entries = entries ?? [] + guard entries.count <= 128, Set(entries.map(\.id)).count == entries.count else { + throw WhereThemeValidationError( + path: path, + reason: "Duplicate names or too many named values", + ) + } + var result = inherited ?? [] + for value in entries { + if let index = result + .firstIndex(where: { $0.id == value.id }) { result[index] = value } + else { result.append(value) } + } + guard result.count <= 128 else { throw WhereThemeValidationError( + path: path, + reason: "Too many inherited values", + ) } + return result + } + return try .init( + colors: merged(colors, parent?.colors, path: "values.colors"), + fonts: merged(fonts, parent?.fonts, path: "values.fonts"), + surfaces: merged(surfaces, parent?.surfaces, path: "values.surfaces"), + ) + } + + func color(_ reference: ColorValue.ID) throws -> BThemeValue { + guard let value = colors?.first(where: { $0.id == reference })?.value else { + throw WhereThemeValidationError( + path: "values.colors.\(reference.rawValue)", + reason: "Missing named color", + ) + } + return value + } + + func font(_ reference: FontValue.ID) throws -> BThemeValue { + guard let value = fonts?.first(where: { $0.id == reference })?.value else { + throw WhereThemeValidationError( + path: "values.fonts.\(reference.rawValue)", + reason: "Missing named font", + ) + } + return value + } + + func surface(_ reference: SurfaceValue.ID) throws -> BThemeValue { + guard let value = surfaces?.first(where: { $0.id == reference })?.value else { + throw WhereThemeValidationError( + path: "values.surfaces.\(reference.rawValue)", + reason: "Missing named surface", + ) + } + func resolve(_ mode: BMode) throws -> WhereThemeSurface { + switch value.resolve(mode: mode) { + case .reflective: .reflective + case let .paper(paper): + try .matte(.init( + fill: .init(base: color(paper.fill), tintMix: paper.tintMix), + outline: .init(opacity: paper.outline.opacity, width: paper.outline.width), + shadow: .init( + color: color(paper.shadow.color), + radius: paper.shadow.radius, + offsetY: paper.shadow.offsetY, + ), + pressedTintOpacity: paper.pressedTintOpacity, + )) + } + } + let light = try resolve(.light), dark = try resolve(.dark) + return light == dark ? .constant(light) : .modes(.init(light: light, dark: dark)) + } +} + +extension WhereThemeComponents { + /// Named surface roles own these fields. Other geometry stays in the typed appearance patch. + func applyingReferences( + to authored: WhereThemeAppearance, + values: WhereThemeValues, + ) throws -> WhereThemeAppearance { + var result = authored + for role in Role.allCases { + guard let component = self[role] else { continue } + if case let .value(reference) = component.background { + let color = try values.color(reference) + if role == .locations { + result.locationsBackground = result.locationsBackground ?? .init() + result.locationsBackground?.paper = color + } + } + if case let .value(reference) = component.foreground { _ = try values.color(reference) } + if case let .value(reference) = component.heading { _ = try values.font(reference) } + if case let .value(reference) = component.body { _ = try values.font(reference) } + if case let .value(reference) = component.surface { + let surface = try values.surface(reference) + switch role { + case .regularCard: + if result.card == nil { result.card = .init() } + if result.card?.regular == nil { result.card?.regular = .init() } + result.card?.regular?.surface = surface + case .compactCard: + if result.card == nil { result.card = .init() } + if result.card?.compact == nil { result.card?.compact = .init() } + result.card?.compact?.surface = surface + case .elsewhereCard: + func paper(_ mode: BMode) throws -> WhereThemeMatteSurface { + guard case let .matte(paper) = surface.resolve(mode: mode) + else { throw WhereThemeValidationError( + path: "components.elsewhereCard.surface", + reason: "Elsewhere cards require opaque paper", + ) } + return paper + } + if result.card == nil { result.card = .init() } + if result.card? + .elsewherePaper == nil { result.card?.elsewherePaper = .init() } + result.card?.elsewherePaper?.surface = try .modes(.init( + light: paper(.light), + dark: paper(.dark), + )) + case .welcome: + result.locationWelcome = result.locationWelcome ?? .init(); result + .locationWelcome?.surface = surface + case .privacy: + result.privacyPassportCard = result.privacyPassportCard ?? .init(); result + .privacyPassportCard?.surface = surface + case .summary: + if result.locationForecast == nil { result.locationForecast = .init() } + if result.locationForecast? + .summary == nil { result.locationForecast?.summary = .init() } + result.locationForecast?.summary?.treatment = surface + case .locations, .elsewhere, .year, .settings, .sheet, .forecastPanel, + .calendarMonth, + .timelineEntry, .section: break + } + } + } + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/ValidatedWhereThemeDocument.swift b/Where/WhereUI/Sources/Themes/ValidatedWhereThemeDocument.swift new file mode 100644 index 000000000..c7a4e9094 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/ValidatedWhereThemeDocument.swift @@ -0,0 +1,108 @@ +import BroadwayCore +import BroadwayUI +import Foundation +import ViewDocumentCore + +/// Immutable content accepted at the rendering boundary. Invalid input never enters the cache. +public struct ValidatedWhereThemeDocument: Hashable, Sendable { + public let document: WhereThemeDocument + + public let resolved: ResolvedWhereTheme + let artwork: [WhereThemeComponents.Role: [ValidatedViewDocument]] + + @MainActor + public init(document: WhereThemeDocument, resources: ViewDocumentResources) throws { + let bundled = try WhereBundledThemes.documents.get().filter { $0.id != document.id } + let catalog = try WhereThemeCatalog(documents: bundled + [document]) + try self.init( + document: document, + resolved: catalog.resolve(document.id), + resources: resources, + ) + } + + @MainActor + init( + document: WhereThemeDocument, + resolved: ResolvedWhereTheme, + resources: ViewDocumentResources, + ) throws { + var budget = ThemeLayerBudget() + var artwork: [WhereThemeComponents.Role: [ValidatedViewDocument]] = [:] + for role in WhereThemeComponents.Role.allCases { + let entries = resolved.components[role]?.artwork ?? [] + // Two version 1 slots can normalize into one component; retain their bounded capacity. + guard entries.count <= 16, Set(entries.map(\.id)).count == entries.count else { + throw WhereThemeValidationError( + path: "components.\(role).artwork", + reason: "Duplicate artwork identity or too many layers", + ) + } + artwork[role] = try entries.map { entry in + try entry.placement.validate() + for mode: BMode in [.light, .dark] { + try WhereThemeValidation.validate( + entry.opacity.resolve(mode: mode), + range: 0 ... 1, + path: "artwork.opacity", + ) + } + let layer = try ValidatedViewDocument( + document: entry.document, + resources: resources, + limits: .standard, + ) + budget.include(entry.document.root) + return layer + } + } + guard budget.nodes <= ViewDocumentLimits.standard.nodes, + budget.animations <= ViewDocumentLimits.standard.animatedNodes, + budget.modifiers <= ViewDocumentLimits.standard.modifiers + else { + throw WhereThemeValidationError( + path: "layers", + reason: "Combined theme layers exceed the rendering budget", + ) + } + self.document = try document.converted() + self.resolved = resolved + self.artwork = artwork + } + + @MainActor + public static func decode(_ data: Data, resources: ViewDocumentResources) throws -> Self { + guard data.count <= ViewDocumentLimits.standard.bytes + else { throw WhereThemeValidationError( + path: "$", + reason: "Theme exceeds byte limit", + ) } + return try Self( + document: JSONDecoder().decode(WhereThemeDocument.self, from: data), + resources: resources, + ) + } +} + +struct WhereDocumentTheme: BTheme { + var document: ValidatedWhereThemeDocument? + static let defaultValue = Self(document: nil) +} + +private struct ThemeLayerBudget { + var nodes = 0 + var animations = 0 + var modifiers = 0 + mutating func include(_ node: ViewDocument.Node) { + nodes += 1 + modifiers += node.modifiers.count + if node.animation != nil { animations += 1 } + switch node.content { + case let .group(_, _, _, children): + for child in children { + include(child) + } + case .text, .image, .raster, .symbol, .shape, .spacer: break + } + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereBundledThemes.swift b/Where/WhereUI/Sources/Themes/WhereBundledThemes.swift new file mode 100644 index 000000000..83a867f16 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereBundledThemes.swift @@ -0,0 +1,85 @@ +import Foundation +import PeriscopeCore +import ViewDocumentUI +import WhereCore + +/// Bundled definitions share the same catalog and preparation boundary as imported documents. +@MainActor +enum WhereBundledThemes { + static var documents: Result<[WhereThemeDocument], any Error> { + WhereBundledThemeDefinitions.documents + } + + private static let standard = Result { try catalog().prepare( + .init(rawValue: "standard"), + assets: .init(images: [:], symbols: [:]), + ) } + private static let flat = Result { try catalog().prepare( + .init(rawValue: "flat"), + assets: .init(images: [:], symbols: [:]), + ) } + private static let halloween = Result { try catalog().prepare( + .halloween, + assets: HalloweenThemeAssets.load(), + ) } + static func load(_ name: String) throws -> WhereThemeDocument { + try WhereBundledThemeDefinitions + .load(name) + } + + static func catalog() throws -> WhereThemeCatalog { + try .init(documents: documents.get()) + } + + static func preparedOrFallback(_ theme: WhereTheme) -> PreparedWhereThemeDocument? { + do { return try prepared(theme) } + catch { WhereBundledThemeDefinitions.reportPackagingFailure(error); return nil } + } + + static func prepared(_ theme: WhereTheme) throws -> PreparedWhereThemeDocument { + switch theme { + case .standard: try standard.get() + case .alternate: try flat.get() + case .halloween: try halloween.get() + } + } +} + +/// Thread-safe immutable definitions also supply off-tree stylesheet fallbacks. +enum WhereBundledThemeDefinitions { + static let documents: Result<[WhereThemeDocument], any Error> = Result { + try ["StandardTheme", "FlatTheme", "HalloweenTheme"].map { try load($0) } + } + + private static let standard = Result { + try WhereThemeCatalog(documents: documents.get()).resolve(.init(rawValue: "standard")) + } + + private static let flat = Result { + try WhereThemeCatalog(documents: documents.get()).resolve(.init(rawValue: "flat")) + } + + private static let halloween = Result { + try WhereThemeCatalog(documents: documents.get()).resolve(.halloween) + } + + static func resolved(_ theme: WhereTheme) throws -> ResolvedWhereTheme { + switch theme { + case .standard: try standard.get() + case .alternate: try flat.get() + case .halloween: try halloween.get() + } + } + + static func load(_ name: String) throws -> WhereThemeDocument { + guard let url = Bundle.module.url(forResource: name, withExtension: "json") else { + throw WhereThemeValidationError(path: name, reason: "Missing bundled theme") + } + return try JSONDecoder().decode(WhereThemeDocument.self, from: Data(contentsOf: url)) + } + + static func reportPackagingFailure(_ error: any Error) { + WhereLog.root(WhereThemeLog.self).bundleUnreadable(error: .restricted(.errorDetails, error)) + assertionFailure("Invalid bundled theme: \(error)") + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereComponentStylesheet.swift b/Where/WhereUI/Sources/Themes/WhereComponentStylesheet.swift new file mode 100644 index 000000000..d4ae7a624 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereComponentStylesheet.swift @@ -0,0 +1,153 @@ +import BroadwayCore +import BroadwayUI +import SwiftUI +import UIKit +import ViewDocumentCore +import ViewDocumentUI +import WhereCore + +/// Typed component roles resolve named references and accessibility policy before view +/// construction. +struct WhereComponentStylesheet: BStylesheet { + struct Style: Equatable { + var background: Color? + var foreground: Color? + var secondaryForeground: Color? + var heading: Font? + var body: Font? + var bodyDesign: Font.Design? + var surface: WhereStylesheet.SurfaceTreatment? + var nativeArtwork: Set = [] + var artworkOpacity: [WhereThemeArtwork.ID: Double] = [:] + var outsets = EdgeInsets() + } + + private var styles: [WhereThemeComponents.Role: Style] = [:] + subscript(role: WhereThemeComponents.Role) -> Style { + styles[role] ?? .init() + } + + init(context: SlicingContext) throws { + let resolved = try context.themes[WhereDocumentTheme.self].document?.resolved + ?? WhereBundledThemeDefinitions.resolved(context.themes[WhereTheme.self]) + try self.init(resolved: resolved, traits: context.traits) + } + + private init(resolved: ResolvedWhereTheme, traits: BTraits) throws { + let policy = BViewDocumentStylesheet(traits: traits) + for role in WhereThemeComponents.Role.allCases { + guard let component = resolved.components[role] else { continue } + var style = Style() + if case let .value(reference) = component + .background + { + let color = try resolved.values.color(reference).resolve(mode: traits.mode) + style.background = traits.accessibility + .isReduceTransparencyEnabled && !WhereThemeValidation.isOpaque(color) + ? policy.background : policy.appearance.color(color) + } + if case let .value(reference) = component + .foreground + { + style.foreground = try policy.appearance + .color(resolved.values.color(reference).resolve(mode: traits.mode)) + style.secondaryForeground = style.foreground?.opacity( + traits.accessibility.isDarkerSystemColorsEnabled ? 1 : 0.85, + ) + } + if case let .value(reference) = component + .heading + { + style.heading = try Self.nativeFont( + resolved.values.font(reference).resolve(mode: traits.mode), + relativeTo: .headline, + traits: traits, + ) + } + if case let .value(reference) = component + .body + { + let font = try resolved.values.font(reference).resolve(mode: traits.mode) + style.body = Self.nativeFont(font, relativeTo: .body, traits: traits) + style.bodyDesign = font.design?.fontDesign + } + if case let .value(reference) = component + .surface + { + style.surface = try resolved.values.surface(reference).resolve(mode: traits.mode) + .resolve( + mode: traits.mode, + appearance: policy.appearance, + base: .reflective, + ) + } + if var surface = style.surface { + if traits.accessibility.isReduceTransparencyEnabled, case .reflective = surface { + surface = .matte(.init( + fill: .init(base: policy.background, tintMix: 0), + outline: .init(opacity: 0, width: 0), + shadow: .init(color: .clear, radius: 0, offsetY: 0), + pressedTintOpacity: 0, + )) + } + if traits.accessibility.isDarkerSystemColorsEnabled, + case var .matte(paper) = surface + { + paper.outline.opacity = max(0.65, paper.outline.opacity) + paper.outline.width = max(1.5, paper.outline.width) + surface = .matte(paper) + } + style.surface = surface + } + style.nativeArtwork = Set(component.nativeArtwork ?? []) + for artwork in component.artwork ?? [] where policy.shows(artwork.role) { + style.artworkOpacity[artwork.id] = artwork.opacity.resolve(mode: traits.mode) + let outsets = artwork.placement.outsets + style.outsets.top = max(style.outsets.top, outsets.top) + style.outsets.leading = max(style.outsets.leading, outsets.leading) + style.outsets.bottom = max(style.outsets.bottom, outsets.bottom) + style.outsets.trailing = max(style.outsets.trailing, outsets.trailing) + } + // Limit gutters before reducing the space available to native labels and controls. + // Artwork uses the same allowance to scale its size, offset, and paint bounds together. + style.outsets.leading = min(24, style.outsets.leading) + style.outsets.trailing = min(24, style.outsets.trailing) + styles[role] = style + } + } + + private static func nativeFont( + _ descriptor: ViewFontDescriptor, + relativeTo textStyle: UIFont.TextStyle, + traits: BTraits, + ) -> Font { + var descriptor = descriptor + if case let .fixed(points) = descriptor.size { + // A point size defines the normal-size baseline for native text. Decorative + // ViewDocument text retains its independent fixed-size contract. + descriptor.size = .fixed(BScaledDimension.value( + points, + relativeTo: textStyle, + category: traits.contentSizeCategory, + )) + } + return descriptor.font + } + + private init() {} + static let `default`: Self = { + do { return try Self( + resolved: WhereBundledThemeDefinitions.resolved(.standard), + traits: BTraits(), + ) } catch { WhereBundledThemeDefinitions.reportPackagingFailure(error); return Self() } + }() +} + +extension EnvironmentValues { + var whereComponentStylesheet: WhereComponentStylesheet { + bContext.stylesheet( + WhereComponentStylesheet.self, + fallback: .default, + ) + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereComponentSurface.swift b/Where/WhereUI/Sources/Themes/WhereComponentSurface.swift new file mode 100644 index 000000000..db2cbf351 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereComponentSurface.swift @@ -0,0 +1,157 @@ +import BroadwayUI +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension EnvironmentValues { + @Entry var whereThemeDocument: PreparedWhereThemeDocument? + @Entry var whereThemeLayersAreActive = true + @Entry var whereThemeSecondaryInk: Color? +} + +/// One composition path for native content with interior and bounded exterior theme artwork. +struct WhereComponentSurface< + Shape: InsettableShape, + Finish: ViewModifier, + NativeBackground: View, +>: ViewModifier { + let role: WhereThemeComponents.Role + let shape: Shape + let finish: Finish + let nativeBackground: NativeBackground + @Environment(\.whereComponentStylesheet) private var stylesheet + + init( + role: WhereThemeComponents.Role, + shape: Shape, + finish: Finish, + @ViewBuilder background: () -> NativeBackground, + ) { + self.role = role + self.shape = shape + self.finish = finish + nativeBackground = background() + } + + init(role: WhereThemeComponents.Role, shape: Shape, finish: Finish) + where NativeBackground == EmptyView + { + self.init(role: role, shape: shape, finish: finish) { EmptyView() } + } + + func body(content: Content) -> some View { + BSurface(shape: shape, finish: finish) { + ZStack { + if let background = stylesheet[role].background { shape.fill(background) } + nativeBackground + } + } interior: { + WhereComponentArtwork(role: role, plane: .interior, shape: shape) + } content: { + content.modifier(WhereComponentTextStyle(role: role)) + } highlights: { + EmptyView() + } exterior: { + WhereComponentArtwork(role: role, plane: .exterior, shape: shape) + } + } +} + +/// Supplies document defaults while explicit data and control styles remain native. +struct WhereComponentTextStyle: ViewModifier { + let role: WhereThemeComponents.Role + @Environment(\.whereComponentStylesheet) private var stylesheet + + func body(content: Content) -> some View { + let style = stylesheet[role] + if let foreground = style.foreground { + typography(content) + .foregroundStyle(foreground) + .environment(\.whereThemeSecondaryInk, style.secondaryForeground) + } else { typography(content) } + } + + @ViewBuilder private func typography(_ content: Content) -> some View { + let style = stylesheet[role] + if let font = style.body { content.font(font).fontDesign(style.bodyDesign) } + else { content } + } +} + +struct WhereComponentArtwork: View { + let role: WhereThemeComponents.Role + let plane: ViewArtworkPlacement.Plane + let shape: Shape + @Environment(\.whereThemeDocument) private var theme + @Environment(\.whereComponentStylesheet) private var stylesheet + @Environment(\.whereThemeLayersAreActive) private var hostIsActive + @Environment(\.scenePhase) private var scenePhase + @MotionIsStatic private var motionIsStatic + @State private var isVisible = false + + var body: some View { + ZStack { + ForEach(theme?.componentArtwork[role] ?? []) { artwork in + if artwork.placement.value.plane == plane, + let opacity = stylesheet[role].artworkOpacity[artwork.id] + { + BArtworkLayer( + placement: artwork.placement, + shape: shape, + role: artwork.role, + maximumOutsets: .init( + top: stylesheet[role].outsets.top, + leading: stylesheet[role].outsets.leading, + bottom: stylesheet[role].outsets.bottom, + trailing: stylesheet[role].outsets.trailing, + ), + ) { + BViewDocumentLayer( + document: artwork.document, + isActive: isVisible && hostIsActive && scenePhase == .active, + motionIsStatic: motionIsStatic, + role: artwork.role, + ) + } + .opacity(opacity) + } + } + } + .allowsHitTesting(false) + .accessibilityHidden(true) + .onScrollVisibilityChange(threshold: 0.01) { isVisible = $0 } + .onAppear { isVisible = true } + .onDisappear { isVisible = false } + } +} + +/// A descendant cannot reactivate motion when an ancestor is covered or on another tab. +private struct WhereThemeActivity: ViewModifier { + let active: Bool + @Environment(\.whereThemeLayersAreActive) private var parentIsActive + func body(content: Content) -> some View { + content.environment(\.whereThemeLayersAreActive, parentIsActive && active) + } +} + +extension View { + func whereThemeActivity(_ active: Bool) + -> some View + { + modifier(WhereThemeActivity(active: active)) + } +} + +#if DEBUG + #Preview("Component surface") { + Text("Native content") + .padding(40) + .modifier(WhereComponentSurface( + role: .regularCard, + shape: RoundedRectangle(cornerRadius: 20), + finish: EmptyModifier(), + )) + .padding(40) + .whereBroadwayRoot() + } +#endif diff --git a/Where/WhereUI/Sources/Themes/WhereSecondaryContentStyle.swift b/Where/WhereUI/Sources/Themes/WhereSecondaryContentStyle.swift new file mode 100644 index 000000000..59e3e9485 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereSecondaryContentStyle.swift @@ -0,0 +1,18 @@ +import SwiftUI + +/// Keeps supporting native text readable when a document supplies the surrounding ink. +struct WhereSecondaryContentStyle: ShapeStyle { + func resolve(in environment: EnvironmentValues) -> AnyShapeStyle { + if let ink = environment.whereThemeSecondaryInk { + AnyShapeStyle(ink) + } else { + AnyShapeStyle(.secondary) + } + } +} + +extension ShapeStyle where Self == WhereSecondaryContentStyle { + static var whereSecondary: Self { + .init() + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAnimation.swift b/Where/WhereUI/Sources/Themes/WhereThemeAnimation.swift new file mode 100644 index 000000000..ad90380fa --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAnimation.swift @@ -0,0 +1,25 @@ +import SwiftUI + +/// Finite native transition timing. Document animation has its own bounded vocabulary. +public enum WhereThemeAnimation: Codable, Hashable, Sendable { + case linear(duration: Double) + case easeIn(duration: Double) + case easeOut(duration: Double) + case easeInOut(duration: Double) + case spring(duration: Double, bounce: Double) + case snappy(duration: Double, extraBounce: Double) + + var animation: Animation { + switch self { + case let .linear(duration): .linear(duration: duration) + case let .easeIn(duration): .easeIn(duration: duration) + case let .easeOut(duration): .easeOut(duration: duration) + case let .easeInOut(duration): .easeInOut(duration: duration) + case let .spring(duration, bounce): .spring(duration: duration, bounce: bounce) + case let .snappy(duration, extraBounce): .snappy( + duration: duration, + extraBounce: extraBounce, + ) + } + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.AppIconStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.AppIconStyle.swift new file mode 100644 index 000000000..37365b922 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.AppIconStyle.swift @@ -0,0 +1,210 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct AppIconStyle: Codable, Hashable, Sendable { + public var gridMax: BThemeValue? + public var previewMax: BThemeValue? + public var gridSpacing: BThemeValue? + public var columnSpacing: BThemeValue? + public var gridPadding: BThemeValue? + public var cellSpacing: BThemeValue? + public var cellLabelSpacing: BThemeValue? + public var backgroundedCellOpacity: BThemeValue? + public var scrim: BThemeValue? + @CopyOnWrite public var panel: WhereThemeAppearance.AppIconStyle.PanelStyle? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.AppIconStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = gridMax { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".gridMax") + value.gridMax = CGFloat(resolved) + } + if let authored = previewMax { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".previewMax") + value.previewMax = CGFloat(resolved) + } + if let authored = gridSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".gridSpacing") + value.gridSpacing = CGFloat(resolved) + } + if let authored = columnSpacing { + let resolved = authored.resolve(mode: traits.mode) + // Grid column counts divide by the icon width plus this spacing. + try WhereThemeValidation.validate( + resolved, + range: 0 ... 10000, + path: path + ".columnSpacing", + ) + value.columnSpacing = CGFloat(resolved) + } + if let authored = gridPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".gridPadding") + value.gridPadding = CGFloat(resolved) + } + if let authored = cellSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cellSpacing") + value.cellSpacing = CGFloat(resolved) + } + if let authored = cellLabelSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cellLabelSpacing") + value.cellLabelSpacing = CGFloat(resolved) + } + if let authored = backgroundedCellOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".backgroundedCellOpacity") + value.backgroundedCellOpacity = resolved + } + if let authored = scrim { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".scrim") + value.scrim = appearance.color(resolved) + } + try panel?.apply( + to: &value.panel, + traits: traits, + appearance: appearance, + path: path + ".panel", + ) + } + } +} + +extension WhereThemeAppearance.AppIconStyle { + public struct PanelStyle: Codable, Hashable, Sendable { + public var spacing: BThemeValue? + public var textSpacing: BThemeValue? + public var horizontalPadding: BThemeValue? + public var bottomPadding: BThemeValue? + public var cornerRadius: BThemeValue? + public var background: BThemeValue? + public var shadowColor: BThemeValue? + public var shadowRadius: BThemeValue? + public var shadowOffsetY: BThemeValue? + public var grabberSize: BThemeValue? + public var grabberOpacity: BThemeValue? + public var grabberTopPadding: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.AppIconStyle.PanelStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + if let authored = textSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".textSpacing") + value.textSpacing = CGFloat(resolved) + } + if let authored = horizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".horizontalPadding") + value.horizontalPadding = CGFloat(resolved) + } + if let authored = bottomPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".bottomPadding") + value.bottomPadding = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = background { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".background") + value.background = appearance.color(resolved) + } + if let authored = shadowColor { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowColor") + value.shadowColor = appearance.color(resolved) + } + if let authored = shadowRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowRadius") + value.shadowRadius = CGFloat(resolved) + } + if let authored = shadowOffsetY { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowOffsetY") + value.shadowOffsetY = CGFloat(resolved) + } + if let authored = grabberSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".grabberSize") + value.grabberSize = resolved.size + } + if let authored = grabberOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".grabberOpacity") + value.grabberOpacity = resolved + } + if let authored = grabberTopPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".grabberTopPadding") + value.grabberTopPadding = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.AppIconStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.gridMax = gridMax ?? parent.gridMax + result.previewMax = previewMax ?? parent.previewMax + result.gridSpacing = gridSpacing ?? parent.gridSpacing + result.columnSpacing = columnSpacing ?? parent.columnSpacing + result.gridPadding = gridPadding ?? parent.gridPadding + result.cellSpacing = cellSpacing ?? parent.cellSpacing + result.cellLabelSpacing = cellLabelSpacing ?? parent.cellLabelSpacing + result.backgroundedCellOpacity = backgroundedCellOpacity ?? parent.backgroundedCellOpacity + result.scrim = scrim ?? parent.scrim + result.panel = inheritPatch(panel, from: parent.panel) + return result + } +} + +extension WhereThemeAppearance.AppIconStyle.PanelStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.spacing = spacing ?? parent.spacing + result.textSpacing = textSpacing ?? parent.textSpacing + result.horizontalPadding = horizontalPadding ?? parent.horizontalPadding + result.bottomPadding = bottomPadding ?? parent.bottomPadding + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.background = background ?? parent.background + result.shadowColor = shadowColor ?? parent.shadowColor + result.shadowRadius = shadowRadius ?? parent.shadowRadius + result.shadowOffsetY = shadowOffsetY ?? parent.shadowOffsetY + result.grabberSize = grabberSize ?? parent.grabberSize + result.grabberOpacity = grabberOpacity ?? parent.grabberOpacity + result.grabberTopPadding = grabberTopPadding ?? parent.grabberTopPadding + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.CalendarStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.CalendarStyle.swift new file mode 100644 index 000000000..af1851a1f --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.CalendarStyle.swift @@ -0,0 +1,659 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct CalendarStyle: Codable, Hashable, Sendable { + public var overview: Overview? + public var monthSpacing: BThemeValue? + @CopyOnWrite public var month: WhereThemeAppearance.CalendarStyle.MonthStyle? = nil + public var dotSize: BThemeValue? + @CopyOnWrite public var regionBand: WhereThemeAppearance.CalendarStyle.RegionBand? = nil + @CopyOnWrite public var day: WhereThemeAppearance.CalendarStyle.DayStyle? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.CalendarStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try overview?.apply( + to: &value.overview, + traits: traits, + appearance: appearance, + path: path + ".overview", + ) + if let authored = monthSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".monthSpacing") + value.monthSpacing = CGFloat(resolved) + } + try month?.apply( + to: &value.month, + traits: traits, + appearance: appearance, + path: path + ".month", + ) + if let authored = dotSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".dotSize") + value.dotSize = CGFloat(resolved) + } + try regionBand?.apply( + to: &value.regionBand, + traits: traits, + appearance: appearance, + path: path + ".regionBand", + ) + try day?.apply( + to: &value.day, + traits: traits, + appearance: appearance, + path: path + ".day", + ) + } + } +} + +extension WhereThemeAppearance.CalendarStyle { + public struct DayBadge: Codable, Hashable, Sendable { + public var iconSize: BThemeValue? + public var iconSpacing: BThemeValue? + public var padding: BThemeValue? + public var offset: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CalendarStyle.DayBadge, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = iconSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconSize") + value.iconSize = CGFloat(resolved) + } + if let authored = iconSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconSpacing") + value.iconSpacing = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = offset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".offset") + value.offset = resolved.size + } + } + } +} + +extension WhereThemeAppearance.CalendarStyle { + public struct DayStyle: Codable, Hashable, Sendable { + public var minHeight: BThemeValue? + public var numberSize: BThemeValue? + public var numberFontSize: BThemeValue? + public var numberDotSpacing: BThemeValue? + public var dotSize: BThemeValue? + public var dotOverlap: BThemeValue? + public var dotStrokeWidth: BThemeValue? + public var contentSpacing: BThemeValue? + public var todayMarker: BThemeValue? + public var todayNumberColor: BThemeValue? + public var unresolvedMarker: BThemeValue? + public var unresolvedNumberColor: BThemeValue? + @CopyOnWrite public var badge: WhereThemeAppearance.CalendarStyle.DayBadge? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.CalendarStyle.DayStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = minHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".minHeight") + value.minHeight = CGFloat(resolved) + } + if let authored = numberSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".numberSize") + value.numberSize = CGFloat(resolved) + } + if let authored = numberFontSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".numberFontSize") + value.numberFontSize = CGFloat(resolved) + } + if let authored = numberDotSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".numberDotSpacing") + value.numberDotSpacing = CGFloat(resolved) + } + if let authored = dotSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".dotSize") + value.dotSize = CGFloat(resolved) + } + if let authored = dotOverlap { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".dotOverlap") + value.dotOverlap = CGFloat(resolved) + } + if let authored = dotStrokeWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".dotStrokeWidth") + value.dotStrokeWidth = CGFloat(resolved) + } + if let authored = contentSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentSpacing") + value.contentSpacing = CGFloat(resolved) + } + if let authored = todayMarker { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".todayMarker") + value.todayMarker = appearance.color(resolved) + } + if let authored = todayNumberColor { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".todayNumberColor") + value.todayNumberColor = appearance.color(resolved) + } + if let authored = unresolvedMarker { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".unresolvedMarker") + value.unresolvedMarker = appearance.color(resolved) + } + if let authored = unresolvedNumberColor { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".unresolvedNumberColor") + value.unresolvedNumberColor = appearance.color(resolved) + } + try badge?.apply( + to: &value.badge, + traits: traits, + appearance: appearance, + path: path + ".badge", + ) + } + } +} + +extension WhereThemeAppearance.CalendarStyle { + public struct MonthStyle: Codable, Hashable, Sendable { + public var sectionSpacing: BThemeValue? + public var gridSpacing: BThemeValue? + public var weekdayFontSize: BThemeValue? + public var padding: BThemeValue? + public var cornerRadius: BThemeValue? + @CopyOnWrite public var plain: WhereThemeAppearance.CalendarStyle.MonthStyle.Card? = nil + @CopyOnWrite public var current: WhereThemeAppearance.CalendarStyle.MonthStyle.Card? = nil + public var footerDividerSpacing: BThemeValue? + public var footerSpacing: BThemeValue? + public var footerRowSpacing: BThemeValue? + public var stacksFooter: BThemeValue? + public var unfocusedRowOpacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CalendarStyle.MonthStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = sectionSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".sectionSpacing") + value.sectionSpacing = CGFloat(resolved) + } + if let authored = gridSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".gridSpacing") + value.gridSpacing = CGFloat(resolved) + } + if let authored = weekdayFontSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".weekdayFontSize") + value.weekdayFontSize = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + try plain?.apply( + to: &value.plain, + traits: traits, + appearance: appearance, + path: path + ".plain", + ) + try current?.apply( + to: &value.current, + traits: traits, + appearance: appearance, + path: path + ".current", + ) + if let authored = footerDividerSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".footerDividerSpacing") + value.footerDividerSpacing = CGFloat(resolved) + } + if let authored = footerSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".footerSpacing") + value.footerSpacing = CGFloat(resolved) + } + if let authored = footerRowSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".footerRowSpacing") + value.footerRowSpacing = CGFloat(resolved) + } + if let authored = stacksFooter { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".stacksFooter") + value.stacksFooter = resolved + } + if let authored = unfocusedRowOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".unfocusedRowOpacity") + value.unfocusedRowOpacity = resolved + } + } + } +} + +extension WhereThemeAppearance.CalendarStyle { + public struct RegionBand: Codable, Hashable, Sendable { + public var opacity: BThemeValue? + public var cornerRadius: BThemeValue? + public var continuationRadius: BThemeValue? + public var verticalInset: BThemeValue? + @CopyOnWrite public var planned: WhereThemeAppearance.CalendarStyle.RegionBand + .Planned? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.CalendarStyle.RegionBand, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = continuationRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".continuationRadius") + value.continuationRadius = CGFloat(resolved) + } + if let authored = verticalInset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalInset") + value.verticalInset = CGFloat(resolved) + } + try planned?.apply( + to: &value.planned, + traits: traits, + appearance: appearance, + path: path + ".planned", + ) + } + } +} + +extension WhereThemeAppearance.CalendarStyle.MonthStyle { + public struct Card: Codable, Hashable, Sendable { + public var fill: BThemeValue? + public var border: BThemeValue? + public var borderWidth: BThemeValue? + public var foreground: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CalendarStyle.MonthStyle.Card, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = fill { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".fill") + value.fill = appearance.color(resolved) + } + if let authored = border { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".border") + value.border = appearance.color(resolved) + } + if let authored = borderWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".borderWidth") + value.borderWidth = CGFloat(resolved) + } + if let authored = foreground { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".foreground") + value.foreground = appearance.color(resolved) + } + } + } +} + +extension WhereThemeAppearance.CalendarStyle.RegionBand { + public struct Planned: Codable, Hashable, Sendable { + public var fillOpacity: BThemeValue? + public var hatchOpacity: BThemeValue? + public var hatchSpacing: BThemeValue? + public var hatchLineWidth: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CalendarStyle.RegionBand.Planned, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = fillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".fillOpacity") + value.fillOpacity = resolved + } + if let authored = hatchOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".hatchOpacity") + value.hatchOpacity = resolved + } + if let authored = hatchSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".hatchSpacing") + value.hatchSpacing = CGFloat(resolved) + } + if let authored = hatchLineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".hatchLineWidth") + value.hatchLineWidth = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.CalendarStyle { + public struct Overview: Codable, Hashable, Sendable { + public var columns: BThemeValue? + public var spacing: BThemeValue? + public var padding: BThemeValue? + public var cornerRadius: BThemeValue? + public var dayHeight: BThemeValue? + public var dayFontSize: BThemeValue? + public var titleFontSize: BThemeValue? + public var sectionSpacing: BThemeValue? + public var borderWidth: BThemeValue? + public var currentBorderWidth: BThemeValue? + public var unavailableForeground: BThemeValue? + public var usesSpatialMotion: BThemeValue? + public var duration: BThemeValue? + public var zoomDuration: BThemeValue? + public var zoomBackground: BThemeValue? + public var shakeDistance: BThemeValue? + public var regionBand: RegionBand? + public init() {} + func apply( + to value: inout WhereStylesheet.CalendarStyle.Overview, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = columns { + let resolved = authored.resolve(mode: traits.mode) + guard (1 ... 12).contains(resolved) else { throw WhereThemeValidationError( + path: path + ".columns", + reason: "Calendar columns must be between one and twelve", + ) } + value.columns = resolved + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = dayHeight { + let resolved = authored.resolve(mode: traits.mode) + guard resolved.isFinite, + (2 ... 10000).contains(resolved) + else { throw WhereThemeValidationError( + path: path + ".dayHeight", + reason: "Overview day height must be at least two points", + ) } + value.dayHeight = CGFloat(resolved) + } + if let authored = dayFontSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".dayFontSize") + value.dayFontSize = CGFloat(resolved) + } + if let authored = titleFontSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleFontSize") + value.titleFontSize = CGFloat(resolved) + } + if let authored = sectionSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".sectionSpacing") + value.sectionSpacing = CGFloat(resolved) + } + if let authored = borderWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".borderWidth") + value.borderWidth = CGFloat(resolved) + } + if let authored = currentBorderWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".currentBorderWidth") + value.currentBorderWidth = CGFloat(resolved) + } + if let authored = unavailableForeground { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".unavailableForeground") + value.unavailableForeground = appearance.color(resolved) + } + if let authored = usesSpatialMotion { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".usesSpatialMotion") + value.usesSpatialMotion = resolved + } + if let authored = duration { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".duration") + value.duration = resolved + } + if let authored = zoomDuration { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".zoomDuration") + value.zoomDuration = resolved + } + if let authored = zoomBackground { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".zoomBackground") + value.zoomBackground = appearance.color(resolved) + } + if let authored = shakeDistance { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shakeDistance") + value.shakeDistance = CGFloat(resolved) + } + try regionBand?.apply( + to: &value.regionBand, + traits: traits, + appearance: appearance, + path: path + ".regionBand", + ) + } + } +} + +extension WhereThemeAppearance.CalendarStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.overview = inheritPatch(overview, from: parent.overview) + result.monthSpacing = monthSpacing ?? parent.monthSpacing + result.month = inheritPatch(month, from: parent.month) + result.dotSize = dotSize ?? parent.dotSize + result.regionBand = inheritPatch(regionBand, from: parent.regionBand) + result.day = inheritPatch(day, from: parent.day) + return result + } +} + +extension WhereThemeAppearance.CalendarStyle.DayBadge: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.iconSize = iconSize ?? parent.iconSize + result.iconSpacing = iconSpacing ?? parent.iconSpacing + result.padding = padding ?? parent.padding + result.offset = offset ?? parent.offset + return result + } +} + +extension WhereThemeAppearance.CalendarStyle.DayStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.minHeight = minHeight ?? parent.minHeight + result.numberSize = numberSize ?? parent.numberSize + result.numberFontSize = numberFontSize ?? parent.numberFontSize + result.numberDotSpacing = numberDotSpacing ?? parent.numberDotSpacing + result.dotSize = dotSize ?? parent.dotSize + result.dotOverlap = dotOverlap ?? parent.dotOverlap + result.dotStrokeWidth = dotStrokeWidth ?? parent.dotStrokeWidth + result.contentSpacing = contentSpacing ?? parent.contentSpacing + result.todayMarker = todayMarker ?? parent.todayMarker + result.todayNumberColor = todayNumberColor ?? parent.todayNumberColor + result.unresolvedMarker = unresolvedMarker ?? parent.unresolvedMarker + result.unresolvedNumberColor = unresolvedNumberColor ?? parent.unresolvedNumberColor + result.badge = inheritPatch(badge, from: parent.badge) + return result + } +} + +extension WhereThemeAppearance.CalendarStyle.MonthStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.sectionSpacing = sectionSpacing ?? parent.sectionSpacing + result.gridSpacing = gridSpacing ?? parent.gridSpacing + result.weekdayFontSize = weekdayFontSize ?? parent.weekdayFontSize + result.padding = padding ?? parent.padding + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.plain = inheritPatch(plain, from: parent.plain) + result.current = inheritPatch(current, from: parent.current) + result.footerDividerSpacing = footerDividerSpacing ?? parent.footerDividerSpacing + result.footerSpacing = footerSpacing ?? parent.footerSpacing + result.footerRowSpacing = footerRowSpacing ?? parent.footerRowSpacing + result.stacksFooter = stacksFooter ?? parent.stacksFooter + result.unfocusedRowOpacity = unfocusedRowOpacity ?? parent.unfocusedRowOpacity + return result + } +} + +extension WhereThemeAppearance.CalendarStyle.RegionBand: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.opacity = opacity ?? parent.opacity + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.continuationRadius = continuationRadius ?? parent.continuationRadius + result.verticalInset = verticalInset ?? parent.verticalInset + result.planned = inheritPatch(planned, from: parent.planned) + return result + } +} + +extension WhereThemeAppearance.CalendarStyle.MonthStyle.Card: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.fill = fill ?? parent.fill + result.border = border ?? parent.border + result.borderWidth = borderWidth ?? parent.borderWidth + result.foreground = foreground ?? parent.foreground + return result + } +} + +extension WhereThemeAppearance.CalendarStyle.RegionBand.Planned: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.fillOpacity = fillOpacity ?? parent.fillOpacity + result.hatchOpacity = hatchOpacity ?? parent.hatchOpacity + result.hatchSpacing = hatchSpacing ?? parent.hatchSpacing + result.hatchLineWidth = hatchLineWidth ?? parent.hatchLineWidth + return result + } +} + +extension WhereThemeAppearance.CalendarStyle.Overview: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.columns = columns ?? parent.columns + result.spacing = spacing ?? parent.spacing + result.padding = padding ?? parent.padding + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.dayHeight = dayHeight ?? parent.dayHeight + result.dayFontSize = dayFontSize ?? parent.dayFontSize + result.titleFontSize = titleFontSize ?? parent.titleFontSize + result.sectionSpacing = sectionSpacing ?? parent.sectionSpacing + result.borderWidth = borderWidth ?? parent.borderWidth + result.currentBorderWidth = currentBorderWidth ?? parent.currentBorderWidth + result.unavailableForeground = unavailableForeground ?? parent.unavailableForeground + result.usesSpatialMotion = usesSpatialMotion ?? parent.usesSpatialMotion + result.duration = duration ?? parent.duration + result.zoomDuration = zoomDuration ?? parent.zoomDuration + result.zoomBackground = zoomBackground ?? parent.zoomBackground + result.shakeDistance = shakeDistance ?? parent.shakeDistance + result.regionBand = inheritPatch(regionBand, from: parent.regionBand) + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.CardStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.CardStyle.swift new file mode 100644 index 000000000..ee926a6fe --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.CardStyle.swift @@ -0,0 +1,1132 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct CardStyle: Codable, Hashable, Sendable { + public var surface: BThemeValue? + public var contentTint: BThemeValue? + public var cornerRadius: BThemeValue? + public var padding: BThemeValue? + public var contentSpacing: BThemeValue? + public var progressBarHeight: BThemeValue? + @CopyOnWrite public var entryStamp: WhereThemeAppearance.CardStyle.EntryStamp? = nil + @CopyOnWrite public var regionNameTypography: WhereThemeAppearance.CardStyle + .Typography? = nil + public var regionNameTracking: BThemeValue? + @CopyOnWrite public var heroNumberTypography: WhereThemeAppearance.CardStyle + .Typography? = nil + @CopyOnWrite public var dayUnitTypography: WhereThemeAppearance.CardStyle.Typography? = nil + public var watermarkFontSize: BThemeValue? + public var watermarkOffset: BThemeValue? + @CopyOnWrite public var regionShape: WhereThemeOptional? = nil + @CopyOnWrite public var sheen: WhereThemeAppearance.CardStyle.Sheen? = nil + @CopyOnWrite public var rosette: WhereThemeAppearance.CardStyle.Rosette? = nil + @CopyOnWrite public var glow: WhereThemeAppearance.CardStyle.Shadow? = nil + @CopyOnWrite public var lift: WhereThemeAppearance.CardStyle.Shadow? = nil + @CopyOnWrite public var paper: WhereThemeOptional? = + nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = surface { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".surface") + value.surface = try resolved.resolve( + mode: traits.mode, + appearance: appearance, + base: value.surface, + ) + } + if let authored = contentTint { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentTint") + value.contentTint = try resolved.resolve( + mode: traits.mode, + appearance: appearance, + base: value.contentTint, + ) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = contentSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentSpacing") + value.contentSpacing = CGFloat(resolved) + } + if let authored = progressBarHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".progressBarHeight") + value.progressBarHeight = CGFloat(resolved) + } + try entryStamp?.apply( + to: &value.entryStamp, + traits: traits, + appearance: appearance, + path: path + ".entryStamp", + ) + try regionNameTypography?.apply( + to: &value.regionNameTypography, + traits: traits, + appearance: appearance, + path: path + ".regionNameTypography", + ) + if let authored = regionNameTracking { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".regionNameTracking") + value.regionNameTracking = CGFloat(resolved) + } + try heroNumberTypography?.apply( + to: &value.heroNumberTypography, + traits: traits, + appearance: appearance, + path: path + ".heroNumberTypography", + ) + try dayUnitTypography?.apply( + to: &value.dayUnitTypography, + traits: traits, + appearance: appearance, + path: path + ".dayUnitTypography", + ) + if let authored = watermarkFontSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".watermarkFontSize") + value.watermarkFontSize = CGFloat(resolved) + } + if let authored = watermarkOffset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".watermarkOffset") + value.watermarkOffset = resolved.size + } + if let regionShape { + switch regionShape { + case .clear: value.regionShape = nil + case let .value(patch): + var component = value.regionShape ?? .standard + try patch.apply( + to: &component, + traits: traits, + appearance: appearance, + path: path + ".regionShape", + ) + value.regionShape = component + } + } + try sheen?.apply( + to: &value.sheen, + traits: traits, + appearance: appearance, + path: path + ".sheen", + ) + try rosette?.apply( + to: &value.rosette, + traits: traits, + appearance: appearance, + path: path + ".rosette", + ) + try glow?.apply( + to: &value.glow, + traits: traits, + appearance: appearance, + path: path + ".glow", + ) + try lift?.apply( + to: &value.lift, + traits: traits, + appearance: appearance, + path: path + ".lift", + ) + if let paper { + switch paper { + case .clear: value.paper = nil + case let .value(patch): + var component = value.paper ?? .light + try patch.apply( + to: &component, + traits: traits, + appearance: appearance, + path: path + ".paper", + ) + value.paper = component + } + } + } + } +} + +extension WhereThemeAppearance.CardStyle { + public struct EntryStamp: Codable, Hashable, Sendable { + public var size: BThemeValue? + @CopyOnWrite public var outerRing: WhereThemeAppearance.CardStyle.EntryStamp.Ring? = nil + @CopyOnWrite public var innerRing: WhereThemeAppearance.CardStyle.EntryStamp + .DashedRing? = nil + @CopyOnWrite public var content: WhereThemeAppearance.CardStyle.EntryStamp.Content? = nil + @CopyOnWrite public var arc: WhereThemeOptional? = nil + public var rotationDegrees: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.EntryStamp, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = size { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".size") + value.size = CGFloat(resolved) + } + try outerRing?.apply( + to: &value.outerRing, + traits: traits, + appearance: appearance, + path: path + ".outerRing", + ) + try innerRing?.apply( + to: &value.innerRing, + traits: traits, + appearance: appearance, + path: path + ".innerRing", + ) + try content?.apply( + to: &value.content, + traits: traits, + appearance: appearance, + path: path + ".content", + ) + if let arc { + switch arc { + case .clear: value.arc = nil + case let .value(patch): + var component = value.arc ?? .init( + radiusFraction: 0.37, + font: .init(sizeFraction: 0.1, weight: .semibold, design: .serif), + opacity: 0.7, + maximumSweepDegrees: 250, + sweepDegreesPerCharacter: 17, + ) + try patch.apply( + to: &component, + traits: traits, + appearance: appearance, + path: path + ".arc", + ) + value.arc = component + } + } + if let authored = rotationDegrees { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rotationDegrees") + value.rotationDegrees = resolved + } + } + } +} + +extension WhereThemeAppearance.CardStyle { + public struct Paper: Codable, Hashable, Sendable { + public var surface: BThemeValue? + public var contentTint: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.Paper, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = surface { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".surface") + value.surface = try resolved.resolve( + mode: traits.mode, + appearance: appearance, + base: value.surface, + ) + } + if let authored = contentTint { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentTint") + value.contentTint = try resolved.resolve( + mode: traits.mode, + appearance: appearance, + base: value.contentTint, + ) + } + } + } +} + +extension WhereThemeAppearance.CardStyle { + public struct RegionShape: Codable, Hashable, Sendable { + @CopyOnWrite public var watermark: WhereThemeAppearance.CardStyle.RegionShape.Artwork? = nil + @CopyOnWrite public var stamp: WhereThemeAppearance.CardStyle.RegionShape.Artwork? = nil + @CopyOnWrite public var securityBorder: WhereThemeAppearance.CardStyle.RegionShape + .SecurityBorder? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.RegionShape, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try watermark?.apply( + to: &value.watermark, + traits: traits, + appearance: appearance, + path: path + ".watermark", + ) + try stamp?.apply( + to: &value.stamp, + traits: traits, + appearance: appearance, + path: path + ".stamp", + ) + try securityBorder?.apply( + to: &value.securityBorder, + traits: traits, + appearance: appearance, + path: path + ".securityBorder", + ) + } + } +} + +extension WhereThemeAppearance.CardStyle { + public struct Rosette: Codable, Hashable, Sendable { + public var wobble: BThemeValue? + public var lineWidth: BThemeValue? + public var primaryRingSpacing: BThemeValue? + public var secondaryRingSpacing: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.Rosette, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = wobble { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".wobble") + value.wobble = CGFloat(resolved) + } + if let authored = lineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lineWidth") + value.lineWidth = CGFloat(resolved) + } + if let authored = primaryRingSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".primaryRingSpacing") + value.primaryRingSpacing = CGFloat(resolved) + } + if let authored = secondaryRingSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".secondaryRingSpacing") + value.secondaryRingSpacing = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.CardStyle { + public struct Shadow: Codable, Hashable, Sendable { + public var opacity: BThemeValue? + public var radius: BThemeValue? + public var offsetY: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.Shadow, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + if let authored = radius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".radius") + value.radius = CGFloat(resolved) + } + if let authored = offsetY { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".offsetY") + value.offsetY = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.CardStyle { + public struct Sheen: Codable, Hashable, Sendable { + public var intensity: BThemeValue? + public var staticGlintIntensity: BThemeValue? + @CopyOnWrite public var staticPose: WhereThemeAppearance.CardStyle.Sheen.Pose? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.Sheen, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = intensity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: 0 ... 1, + path: path + ".intensity", + ) + value.intensity = resolved + } + if let authored = staticGlintIntensity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: 0 ... 1, + path: path + ".staticGlintIntensity", + ) + value.staticGlintIntensity = resolved + } + try staticPose?.apply( + to: &value.staticPose, + traits: traits, + appearance: appearance, + path: path + ".staticPose", + ) + } + } +} + +extension WhereThemeAppearance.CardStyle { + public struct Typography: Codable, Hashable, Sendable { + public var size: BThemeValue? + public var weight: BThemeValue? + public var design: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.Typography, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = size { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".size") + value.size = resolved.native + } + if let authored = weight { + let resolved = authored.resolve(mode: traits.mode) + value.weight = resolved.native + } + if let authored = design { + let resolved = authored.resolve(mode: traits.mode) + value.design = resolved.native + } + } + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp { + public struct Arc: Codable, Hashable, Sendable { + public var radiusFraction: BThemeValue? + @CopyOnWrite public var font: WhereThemeAppearance.CardStyle.EntryStamp.Typography? = nil + public var opacity: BThemeValue? + public var maximumSweepDegrees: BThemeValue? + public var sweepDegreesPerCharacter: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.EntryStamp.Arc, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = radiusFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".radiusFraction") + value.radiusFraction = CGFloat(resolved) + } + try font?.apply( + to: &value.font, + traits: traits, + appearance: appearance, + path: path + ".font", + ) + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + if let authored = maximumSweepDegrees { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".maximumSweepDegrees") + value.maximumSweepDegrees = resolved + } + if let authored = sweepDegreesPerCharacter { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".sweepDegreesPerCharacter", + ) + value.sweepDegreesPerCharacter = resolved + } + } + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp { + public struct Content: Codable, Hashable, Sendable { + public var spacingFraction: BThemeValue? + public var artworkExtent: BThemeValue? + @CopyOnWrite public var symbolFont: WhereThemeAppearance.CardStyle.EntryStamp + .Typography? = nil + @CopyOnWrite public var yearFont: WhereThemeAppearance.CardStyle.EntryStamp + .Typography? = nil + public var opacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.EntryStamp.Content, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = spacingFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacingFraction") + value.spacingFraction = CGFloat(resolved) + } + if let authored = artworkExtent { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".artworkExtent") + value.artworkExtent = resolved.size + } + try symbolFont?.apply( + to: &value.symbolFont, + traits: traits, + appearance: appearance, + path: path + ".symbolFont", + ) + try yearFont?.apply( + to: &value.yearFont, + traits: traits, + appearance: appearance, + path: path + ".yearFont", + ) + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + } + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp { + public struct DashedRing: Codable, Hashable, Sendable { + public var opacity: BThemeValue? + public var lineWidthFraction: BThemeValue? + @CopyOnWrite public var dash: WhereThemeAppearance.CardStyle.EntryStamp.DashedRing + .Dash? = nil + public var insetFraction: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.EntryStamp.DashedRing, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + if let authored = lineWidthFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lineWidthFraction") + value.lineWidthFraction = CGFloat(resolved) + } + try dash?.apply( + to: &value.dash, + traits: traits, + appearance: appearance, + path: path + ".dash", + ) + if let authored = insetFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".insetFraction") + value.insetFraction = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp { + public struct Ring: Codable, Hashable, Sendable { + public var opacity: BThemeValue? + public var lineWidthFraction: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.EntryStamp.Ring, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + if let authored = lineWidthFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lineWidthFraction") + value.lineWidthFraction = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp { + public struct Typography: Codable, Hashable, Sendable { + public var sizeFraction: BThemeValue? + public var weight: BThemeValue? + public var design: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.EntryStamp.Typography, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = sizeFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".sizeFraction") + value.sizeFraction = CGFloat(resolved) + } + if let authored = weight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".weight") + value.weight = resolved.fontWeight + } + if let authored = design { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".design") + value.design = resolved.fontDesign + } + } + } +} + +extension WhereThemeAppearance.CardStyle.RegionShape { + public struct Artwork: Codable, Hashable, Sendable { + public var center: BThemeValue? + public var extent: BThemeValue? + public var scale: BThemeValue? + public var fillOpacity: BThemeValue? + @CopyOnWrite public var stroke: WhereThemeOptional? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.RegionShape.Artwork, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = center { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".center") + value.center = resolved.point + } + if let authored = extent { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".extent") + value.extent = resolved.size + } + if let authored = scale { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".scale") + value.scale = CGFloat(resolved) + } + if let authored = fillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".fillOpacity") + value.fillOpacity = resolved + } + if let stroke { + switch stroke { + case .clear: value.stroke = nil + case let .value(patch): + var component = value.stroke ?? .init(opacity: 0.28, width: 1.5) + try patch.apply( + to: &component, + traits: traits, + appearance: appearance, + path: path + ".stroke", + ) + value.stroke = component + } + } + } + } +} + +extension WhereThemeAppearance.CardStyle.RegionShape { + public struct SecurityBorder: Codable, Hashable, Sendable { + public var inset: BThemeValue? + public var glyphSize: BThemeValue? + public var spacing: BThemeValue? + public var opacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.RegionShape.SecurityBorder, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = inset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".inset") + value.inset = CGFloat(resolved) + } + if let authored = glyphSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".glyphSize") + value.glyphSize = CGFloat(resolved) + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + } + } +} + +extension WhereThemeAppearance.CardStyle.Sheen { + public struct Pose: Codable, Hashable, Sendable { + public var roll: BThemeValue? + public var pitch: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.Sheen.Pose, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = roll { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".roll") + value.roll = resolved + } + if let authored = pitch { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".pitch") + value.pitch = resolved + } + } + } +} + +extension WhereThemeAppearance.CardStyle.Typography { + public enum Design: Codable, Hashable, Sendable { + case `default` + case serif + case rounded + case monospaced + var native: WhereStylesheet.CardStyle.Typography.Design { + switch self { + case .default: .default + case .serif: .serif + case .rounded: .rounded + case .monospaced: .monospaced + } + } + } +} + +extension WhereThemeAppearance.CardStyle.Typography { + public enum Size: Codable, Hashable, Sendable { + case fixed(Double) + case semantic(ViewFontDescriptor.TextStyle) + var native: WhereStylesheet.CardStyle.Typography.Size { + switch self { + case let .fixed(points): .fixed(CGFloat(points)) + case let .semantic(style): + switch style { + case .largeTitle: .semantic(.largeTitle) + case .title: .semantic(.title) + case .title2: .semantic(.title2) + case .title3: .semantic(.title3) + case .headline: .semantic(.headline) + case .subheadline: .semantic(.subheadline) + case .body: .semantic(.body) + case .callout: .semantic(.callout) + case .footnote: .semantic(.footnote) + case .caption: .semantic(.caption) + case .caption2: .semantic(.caption2) + } + } + } + } +} + +extension WhereThemeAppearance.CardStyle.Typography { + public enum Weight: Codable, Hashable, Sendable { + case ultraLight + case thin + case light + case regular + case medium + case semibold + case bold + case heavy + case black + var native: WhereStylesheet.CardStyle.Typography.Weight { + switch self { + case .ultraLight: .ultraLight + case .thin: .thin + case .light: .light + case .regular: .regular + case .medium: .medium + case .semibold: .semibold + case .bold: .bold + case .heavy: .heavy + case .black: .black + } + } + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp.DashedRing { + public struct Dash: Codable, Hashable, Sendable { + public var lengthFraction: BThemeValue? + public var spacingFraction: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.EntryStamp.DashedRing.Dash, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = lengthFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lengthFraction") + value.lengthFraction = CGFloat(resolved) + } + if let authored = spacingFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacingFraction") + value.spacingFraction = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.CardStyle.RegionShape.Artwork { + public struct Stroke: Codable, Hashable, Sendable { + public var opacity: BThemeValue? + public var width: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyle.RegionShape.Artwork.Stroke, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + if let authored = width { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".width") + value.width = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.CardStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.surface = inheritModePatch(surface, from: parent.surface) + result.contentTint = inheritModePatch(contentTint, from: parent.contentTint) + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.padding = padding ?? parent.padding + result.contentSpacing = contentSpacing ?? parent.contentSpacing + result.progressBarHeight = progressBarHeight ?? parent.progressBarHeight + result.entryStamp = inheritPatch(entryStamp, from: parent.entryStamp) + result.regionNameTypography = inheritPatch( + regionNameTypography, + from: parent.regionNameTypography, + ) + result.regionNameTracking = regionNameTracking ?? parent.regionNameTracking + result.heroNumberTypography = inheritPatch( + heroNumberTypography, + from: parent.heroNumberTypography, + ) + result.dayUnitTypography = inheritPatch(dayUnitTypography, from: parent.dayUnitTypography) + result.watermarkFontSize = watermarkFontSize ?? parent.watermarkFontSize + result.watermarkOffset = watermarkOffset ?? parent.watermarkOffset + result.regionShape = inheritOptionalPatch(regionShape, from: parent.regionShape) + result.sheen = inheritPatch(sheen, from: parent.sheen) + result.rosette = inheritPatch(rosette, from: parent.rosette) + result.glow = inheritPatch(glow, from: parent.glow) + result.lift = inheritPatch(lift, from: parent.lift) + result.paper = inheritOptionalPatch(paper, from: parent.paper) + return result + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.size = size ?? parent.size + result.outerRing = inheritPatch(outerRing, from: parent.outerRing) + result.innerRing = inheritPatch(innerRing, from: parent.innerRing) + result.content = inheritPatch(content, from: parent.content) + result.arc = inheritOptionalPatch(arc, from: parent.arc) + result.rotationDegrees = rotationDegrees ?? parent.rotationDegrees + return result + } +} + +extension WhereThemeAppearance.CardStyle.Paper: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.surface = inheritModePatch(surface, from: parent.surface) + result.contentTint = inheritModePatch(contentTint, from: parent.contentTint) + return result + } +} + +extension WhereThemeAppearance.CardStyle.RegionShape: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.watermark = inheritPatch(watermark, from: parent.watermark) + result.stamp = inheritPatch(stamp, from: parent.stamp) + result.securityBorder = inheritPatch(securityBorder, from: parent.securityBorder) + return result + } +} + +extension WhereThemeAppearance.CardStyle.Rosette: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.wobble = wobble ?? parent.wobble + result.lineWidth = lineWidth ?? parent.lineWidth + result.primaryRingSpacing = primaryRingSpacing ?? parent.primaryRingSpacing + result.secondaryRingSpacing = secondaryRingSpacing ?? parent.secondaryRingSpacing + return result + } +} + +extension WhereThemeAppearance.CardStyle.Shadow: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.opacity = opacity ?? parent.opacity + result.radius = radius ?? parent.radius + result.offsetY = offsetY ?? parent.offsetY + return result + } +} + +extension WhereThemeAppearance.CardStyle.Sheen: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.intensity = intensity ?? parent.intensity + result.staticGlintIntensity = staticGlintIntensity ?? parent.staticGlintIntensity + result.staticPose = inheritPatch(staticPose, from: parent.staticPose) + return result + } +} + +extension WhereThemeAppearance.CardStyle.Typography: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.size = size ?? parent.size + result.weight = weight ?? parent.weight + result.design = design ?? parent.design + return result + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp.Arc: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.radiusFraction = radiusFraction ?? parent.radiusFraction + result.font = inheritPatch(font, from: parent.font) + result.opacity = opacity ?? parent.opacity + result.maximumSweepDegrees = maximumSweepDegrees ?? parent.maximumSweepDegrees + result.sweepDegreesPerCharacter = sweepDegreesPerCharacter ?? parent + .sweepDegreesPerCharacter + return result + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp.Content: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.spacingFraction = spacingFraction ?? parent.spacingFraction + result.artworkExtent = artworkExtent ?? parent.artworkExtent + result.symbolFont = inheritPatch(symbolFont, from: parent.symbolFont) + result.yearFont = inheritPatch(yearFont, from: parent.yearFont) + result.opacity = opacity ?? parent.opacity + return result + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp.DashedRing: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.opacity = opacity ?? parent.opacity + result.lineWidthFraction = lineWidthFraction ?? parent.lineWidthFraction + result.dash = inheritPatch(dash, from: parent.dash) + result.insetFraction = insetFraction ?? parent.insetFraction + return result + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp.Ring: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.opacity = opacity ?? parent.opacity + result.lineWidthFraction = lineWidthFraction ?? parent.lineWidthFraction + return result + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp.Typography: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.sizeFraction = sizeFraction ?? parent.sizeFraction + result.weight = weight ?? parent.weight + result.design = design ?? parent.design + return result + } +} + +extension WhereThemeAppearance.CardStyle.RegionShape.Artwork: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.center = center ?? parent.center + result.extent = extent ?? parent.extent + result.scale = scale ?? parent.scale + result.fillOpacity = fillOpacity ?? parent.fillOpacity + result.stroke = inheritOptionalPatch(stroke, from: parent.stroke) + return result + } +} + +extension WhereThemeAppearance.CardStyle.RegionShape.SecurityBorder: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.inset = inset ?? parent.inset + result.glyphSize = glyphSize ?? parent.glyphSize + result.spacing = spacing ?? parent.spacing + result.opacity = opacity ?? parent.opacity + return result + } +} + +extension WhereThemeAppearance.CardStyle.Sheen.Pose: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.roll = roll ?? parent.roll + result.pitch = pitch ?? parent.pitch + return result + } +} + +extension WhereThemeAppearance.CardStyle.EntryStamp.DashedRing.Dash: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.lengthFraction = lengthFraction ?? parent.lengthFraction + result.spacingFraction = spacingFraction ?? parent.spacingFraction + return result + } +} + +extension WhereThemeAppearance.CardStyle.RegionShape.Artwork.Stroke: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.opacity = opacity ?? parent.opacity + result.width = width ?? parent.width + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.CardStyles.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.CardStyles.swift new file mode 100644 index 000000000..469bacd8e --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.CardStyles.swift @@ -0,0 +1,440 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct CardStyles: Codable, Hashable, Sendable { + @CopyOnWrite public var regular: WhereThemeAppearance.CardStyle? = nil + @CopyOnWrite public var compact: WhereThemeAppearance.CardStyle? = nil + @CopyOnWrite public var elsewherePaper: WhereThemeAppearance.CardStyle.Paper? = nil + public var watermarkOpacity: BThemeValue? + public var glassTintOpacity: BThemeValue? + public var nameOpacity: BThemeValue? + public var estimatedProgressOpacity: BThemeValue? + @CopyOnWrite public var estimateSticker: WhereThemeAppearance.CardStyles + .EstimateSticker? = nil + @CopyOnWrite public var rosetteFill: WhereThemeAppearance.CardStyles.RosetteFill? = nil + @CopyOnWrite public var securityPrint: WhereThemeAppearance.CardStyles.SecurityPrint? = nil + @CopyOnWrite public var dayCount: WhereThemeAppearance.CardStyles.DayCountStyle? = nil + @CopyOnWrite public var constellation: WhereThemeAppearance.CardStyles.Constellation? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyles, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try regular?.apply( + to: &value.regular, + traits: traits, + appearance: appearance, + path: path + ".regular", + ) + try compact?.apply( + to: &value.compact, + traits: traits, + appearance: appearance, + path: path + ".compact", + ) + try elsewherePaper?.apply( + to: &value.elsewherePaper, + traits: traits, + appearance: appearance, + path: path + ".elsewherePaper", + ) + if let authored = watermarkOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".watermarkOpacity") + value.watermarkOpacity = resolved + } + if let authored = glassTintOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".glassTintOpacity") + value.glassTintOpacity = resolved + } + if let authored = nameOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".nameOpacity") + value.nameOpacity = resolved + } + if let authored = estimatedProgressOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".estimatedProgressOpacity", + ) + value.estimatedProgressOpacity = resolved + } + try estimateSticker?.apply( + to: &value.estimateSticker, + traits: traits, + appearance: appearance, + path: path + ".estimateSticker", + ) + try rosetteFill?.apply( + to: &value.rosetteFill, + traits: traits, + appearance: appearance, + path: path + ".rosetteFill", + ) + try securityPrint?.apply( + to: &value.securityPrint, + traits: traits, + appearance: appearance, + path: path + ".securityPrint", + ) + try dayCount?.apply( + to: &value.dayCount, + traits: traits, + appearance: appearance, + path: path + ".dayCount", + ) + try constellation?.apply( + to: &value.constellation, + traits: traits, + appearance: appearance, + path: path + ".constellation", + ) + } + } +} + +extension WhereThemeAppearance.CardStyles { + public struct Constellation: Codable, Hashable, Sendable { + public var coreDiameter: BThemeValue? + public var coreOpacity: BThemeValue? + public var coreWhiteMix: BThemeValue? + public var haloRadius: BThemeValue? + public var haloOpacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyles.Constellation, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = coreDiameter { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".coreDiameter") + value.coreDiameter = CGFloat(resolved) + } + if let authored = coreOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".coreOpacity") + value.coreOpacity = resolved + } + if let authored = coreWhiteMix { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: 0 ... 1, + path: path + ".coreWhiteMix", + ) + value.coreWhiteMix = resolved + } + if let authored = haloRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".haloRadius") + value.haloRadius = CGFloat(resolved) + } + if let authored = haloOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".haloOpacity") + value.haloOpacity = resolved + } + } + } +} + +extension WhereThemeAppearance.CardStyles { + public struct DayCountStyle: Codable, Hashable, Sendable { + public var morph: BThemeValue? + public var animation: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyles.DayCountStyle, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = morph { + let resolved = authored.resolve(mode: traits.mode) + value.morph = resolved.native + } + if let authored = animation { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".animation") + value.animation = resolved.animation + } + } + } +} + +extension WhereThemeAppearance.CardStyles { + public struct EstimateSticker: Codable, Hashable, Sendable { + @CopyOnWrite public var labelTypography: WhereThemeAppearance.CardStyle.Typography? = nil + @CopyOnWrite public var valueTypography: WhereThemeAppearance.CardStyle.Typography? = nil + public var contentOpacity: BThemeValue? + public var scale: BThemeValue? + public var cornerRadius: BThemeValue? + public var horizontalPadding: BThemeValue? + public var verticalPadding: BThemeValue? + public var rotationDegrees: BThemeValue? + public var fillOpacity: BThemeValue? + public var outlineOpacity: BThemeValue? + public var outlineWidth: BThemeValue? + public var innerInset: BThemeValue? + public var innerOutlineWidth: BThemeValue? + public var innerDash: BThemeValue<[Double]>? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyles.EstimateSticker, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try labelTypography?.apply( + to: &value.labelTypography, + traits: traits, + appearance: appearance, + path: path + ".labelTypography", + ) + try valueTypography?.apply( + to: &value.valueTypography, + traits: traits, + appearance: appearance, + path: path + ".valueTypography", + ) + if let authored = contentOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentOpacity") + value.contentOpacity = resolved + } + if let authored = scale { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".scale") + value.scale = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = horizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".horizontalPadding") + value.horizontalPadding = CGFloat(resolved) + } + if let authored = verticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalPadding") + value.verticalPadding = CGFloat(resolved) + } + if let authored = rotationDegrees { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rotationDegrees") + value.rotationDegrees = resolved + } + if let authored = fillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".fillOpacity") + value.fillOpacity = resolved + } + if let authored = outlineOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineOpacity") + value.outlineOpacity = resolved + } + if let authored = outlineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineWidth") + value.outlineWidth = CGFloat(resolved) + } + if let authored = innerInset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".innerInset") + value.innerInset = CGFloat(resolved) + } + if let authored = innerOutlineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".innerOutlineWidth") + value.innerOutlineWidth = CGFloat(resolved) + } + if let authored = innerDash { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".innerDash") + value.innerDash = resolved.map { CGFloat($0) } + } + } + } +} + +extension WhereThemeAppearance.CardStyles { + public struct RosetteFill: Codable, Hashable, Sendable { + public var primary: BThemeValue? + public var secondary: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyles.RosetteFill, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = primary { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, range: 0 ... 1, path: path + ".primary") + value.primary = resolved + } + if let authored = secondary { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: 0 ... 1, + path: path + ".secondary", + ) + value.secondary = resolved + } + } + } +} + +extension WhereThemeAppearance.CardStyles { + public struct SecurityPrint: Codable, Hashable, Sendable { + public var whiteMix: BThemeValue? + public var backgroundBlendMode: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.CardStyles.SecurityPrint, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = whiteMix { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: 0 ... 1, + path: path + ".whiteMix", + ) + value.whiteMix = resolved + } + if let authored = backgroundBlendMode { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".backgroundBlendMode") + value.backgroundBlendMode = resolved.blendMode + } + } + } +} + +extension WhereThemeAppearance.CardStyles.DayCountStyle { + public enum Morph: Codable, Hashable, Sendable { + case rollingDigits + case crossFade + var native: WhereStylesheet.CardStyles.DayCountStyle.Morph { + switch self { + case .rollingDigits: .rollingDigits + case .crossFade: .crossFade + } + } + } +} + +extension WhereThemeAppearance.CardStyles: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.regular = inheritPatch(regular, from: parent.regular) + result.compact = inheritPatch(compact, from: parent.compact) + result.elsewherePaper = inheritPatch(elsewherePaper, from: parent.elsewherePaper) + result.watermarkOpacity = watermarkOpacity ?? parent.watermarkOpacity + result.glassTintOpacity = glassTintOpacity ?? parent.glassTintOpacity + result.nameOpacity = nameOpacity ?? parent.nameOpacity + result.estimatedProgressOpacity = estimatedProgressOpacity ?? parent + .estimatedProgressOpacity + result.estimateSticker = inheritPatch(estimateSticker, from: parent.estimateSticker) + result.rosetteFill = inheritPatch(rosetteFill, from: parent.rosetteFill) + result.securityPrint = inheritPatch(securityPrint, from: parent.securityPrint) + result.dayCount = inheritPatch(dayCount, from: parent.dayCount) + result.constellation = inheritPatch(constellation, from: parent.constellation) + return result + } +} + +extension WhereThemeAppearance.CardStyles.Constellation: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.coreDiameter = coreDiameter ?? parent.coreDiameter + result.coreOpacity = coreOpacity ?? parent.coreOpacity + result.coreWhiteMix = coreWhiteMix ?? parent.coreWhiteMix + result.haloRadius = haloRadius ?? parent.haloRadius + result.haloOpacity = haloOpacity ?? parent.haloOpacity + return result + } +} + +extension WhereThemeAppearance.CardStyles.DayCountStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.morph = morph ?? parent.morph + result.animation = animation ?? parent.animation + return result + } +} + +extension WhereThemeAppearance.CardStyles.EstimateSticker: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.labelTypography = inheritPatch(labelTypography, from: parent.labelTypography) + result.valueTypography = inheritPatch(valueTypography, from: parent.valueTypography) + result.contentOpacity = contentOpacity ?? parent.contentOpacity + result.scale = scale ?? parent.scale + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.horizontalPadding = horizontalPadding ?? parent.horizontalPadding + result.verticalPadding = verticalPadding ?? parent.verticalPadding + result.rotationDegrees = rotationDegrees ?? parent.rotationDegrees + result.fillOpacity = fillOpacity ?? parent.fillOpacity + result.outlineOpacity = outlineOpacity ?? parent.outlineOpacity + result.outlineWidth = outlineWidth ?? parent.outlineWidth + result.innerInset = innerInset ?? parent.innerInset + result.innerOutlineWidth = innerOutlineWidth ?? parent.innerOutlineWidth + result.innerDash = innerDash ?? parent.innerDash + return result + } +} + +extension WhereThemeAppearance.CardStyles.RosetteFill: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.primary = primary ?? parent.primary + result.secondary = secondary ?? parent.secondary + return result + } +} + +extension WhereThemeAppearance.CardStyles.SecurityPrint: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.whiteMix = whiteMix ?? parent.whiteMix + result.backgroundBlendMode = backgroundBlendMode ?? parent.backgroundBlendMode + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.ContentLayout.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.ContentLayout.swift new file mode 100644 index 000000000..ba2a3c1a6 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.ContentLayout.swift @@ -0,0 +1,17 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public enum ContentLayout: Codable, Hashable, Sendable { + case inline + case stacked + var native: WhereStylesheet.ContentLayout { + switch self { + case .inline: .inline + case .stacked: .stacked + } + } + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.DeveloperOverlayStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.DeveloperOverlayStyle.swift new file mode 100644 index 000000000..75ac88905 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.DeveloperOverlayStyle.swift @@ -0,0 +1,450 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct DeveloperOverlayStyle: Codable, Hashable, Sendable { + @CopyOnWrite public var launcher: WhereThemeAppearance.DeveloperOverlayStyle.Launcher? = nil + public var edgeInset: BThemeValue? + public var presentationAnimation: BThemeValue? + @CopyOnWrite public var floatingWindow: WhereThemeAppearance.DeveloperOverlayStyle + .FloatingWindow? = nil + @CopyOnWrite public var panel: WhereThemeAppearance.DeveloperOverlayStyle.Panel? = nil + @CopyOnWrite public var menu: WhereThemeAppearance.DeveloperOverlayStyle.Menu? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.DeveloperOverlayStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try launcher?.apply( + to: &value.launcher, + traits: traits, + appearance: appearance, + path: path + ".launcher", + ) + if let authored = edgeInset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".edgeInset") + value.edgeInset = CGFloat(resolved) + } + if let authored = presentationAnimation { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".presentationAnimation") + value.presentationAnimation = resolved.animation + } + try floatingWindow?.apply( + to: &value.floatingWindow, + traits: traits, + appearance: appearance, + path: path + ".floatingWindow", + ) + try panel?.apply( + to: &value.panel, + traits: traits, + appearance: appearance, + path: path + ".panel", + ) + try menu?.apply( + to: &value.menu, + traits: traits, + appearance: appearance, + path: path + ".menu", + ) + } + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle { + public struct FloatingWindow: Codable, Hashable, Sendable { + public var maxWidth: BThemeValue? + public var maxHeight: BThemeValue? + public var heightFraction: BThemeValue? + public var minSize: BThemeValue? + public var maxContentInsetFraction: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.DeveloperOverlayStyle.FloatingWindow, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = maxWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".maxWidth") + value.maxWidth = CGFloat(resolved) + } + if let authored = maxHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".maxHeight") + value.maxHeight = CGFloat(resolved) + } + if let authored = heightFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".heightFraction") + value.heightFraction = CGFloat(resolved) + } + if let authored = minSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".minSize") + value.minSize = resolved.size + } + if let authored = maxContentInsetFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".maxContentInsetFraction") + value.maxContentInsetFraction = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle { + public struct Launcher: Codable, Hashable, Sendable { + public var diameter: BThemeValue? + public var glyphRatio: BThemeValue? + public var shadowColor: BThemeValue? + public var shadowRadius: BThemeValue? + public var shadowOffsetY: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.DeveloperOverlayStyle.Launcher, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = diameter { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".diameter") + value.diameter = CGFloat(resolved) + } + if let authored = glyphRatio { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".glyphRatio") + value.glyphRatio = CGFloat(resolved) + } + if let authored = shadowColor { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowColor") + value.shadowColor = appearance.color(resolved) + } + if let authored = shadowRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowRadius") + value.shadowRadius = CGFloat(resolved) + } + if let authored = shadowOffsetY { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowOffsetY") + value.shadowOffsetY = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle { + public struct Menu: Codable, Hashable, Sendable { + public var maxWidth: BThemeValue? + public var launcherSpacing: BThemeValue? + public var rowSpacing: BThemeValue? + public var horizontalPadding: BThemeValue? + public var verticalPadding: BThemeValue? + public var minRowHeight: BThemeValue? + public var cornerRadius: BThemeValue? + public var subtitleSpacing: BThemeValue? + public var iconWidth: BThemeValue? + @CopyOnWrite public var motion: WhereThemeAppearance.DeveloperOverlayStyle.MenuMotion? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.DeveloperOverlayStyle.Menu, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = maxWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".maxWidth") + value.maxWidth = CGFloat(resolved) + } + if let authored = launcherSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".launcherSpacing") + value.launcherSpacing = CGFloat(resolved) + } + if let authored = rowSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rowSpacing") + value.rowSpacing = CGFloat(resolved) + } + if let authored = horizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".horizontalPadding") + value.horizontalPadding = CGFloat(resolved) + } + if let authored = verticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalPadding") + value.verticalPadding = CGFloat(resolved) + } + if let authored = minRowHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".minRowHeight") + value.minRowHeight = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = subtitleSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".subtitleSpacing") + value.subtitleSpacing = CGFloat(resolved) + } + if let authored = iconWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconWidth") + value.iconWidth = CGFloat(resolved) + } + try motion?.apply( + to: &value.motion, + traits: traits, + appearance: appearance, + path: path + ".motion", + ) + } + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle { + public struct MenuMotion: Codable, Hashable, Sendable { + public var animation: BThemeValue? + public var stagger: BThemeValue? + public var scale: BThemeValue? + public var usesSpatialMotion: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.DeveloperOverlayStyle.MenuMotion, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = animation { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".animation") + value.animation = resolved.animation + } + if let authored = stagger { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: 0 ... 60, + path: path + ".stagger", + ) + value.stagger = resolved + } + if let authored = scale { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".scale") + value.scale = CGFloat(resolved) + } + if let authored = usesSpatialMotion { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".usesSpatialMotion") + value.usesSpatialMotion = resolved + } + } + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle { + public struct Panel: Codable, Hashable, Sendable { + public var cornerRadius: BThemeValue? + public var fullScreenInset: BThemeValue? + public var shadowOpacity: BThemeValue? + public var shadowRadius: BThemeValue? + public var shadowOffsetY: BThemeValue? + public var controlHorizontalPadding: BThemeValue? + public var controlVerticalPadding: BThemeValue? + public var dragHandleSize: BThemeValue? + public var dragHandleMinHeight: BThemeValue? + public var resizeGripSize: BThemeValue? + public var resizeIconSize: BThemeValue? + public var resizeGripClearance: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.DeveloperOverlayStyle.Panel, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = fullScreenInset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".fullScreenInset") + value.fullScreenInset = CGFloat(resolved) + } + if let authored = shadowOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowOpacity") + value.shadowOpacity = resolved + } + if let authored = shadowRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowRadius") + value.shadowRadius = CGFloat(resolved) + } + if let authored = shadowOffsetY { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowOffsetY") + value.shadowOffsetY = CGFloat(resolved) + } + if let authored = controlHorizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".controlHorizontalPadding", + ) + value.controlHorizontalPadding = CGFloat(resolved) + } + if let authored = controlVerticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".controlVerticalPadding") + value.controlVerticalPadding = CGFloat(resolved) + } + if let authored = dragHandleSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".dragHandleSize") + value.dragHandleSize = resolved.size + } + if let authored = dragHandleMinHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".dragHandleMinHeight") + value.dragHandleMinHeight = CGFloat(resolved) + } + if let authored = resizeGripSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".resizeGripSize") + value.resizeGripSize = CGFloat(resolved) + } + if let authored = resizeIconSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".resizeIconSize") + value.resizeIconSize = CGFloat(resolved) + } + if let authored = resizeGripClearance { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".resizeGripClearance") + value.resizeGripClearance = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.launcher = inheritPatch(launcher, from: parent.launcher) + result.edgeInset = edgeInset ?? parent.edgeInset + result.presentationAnimation = presentationAnimation ?? parent.presentationAnimation + result.floatingWindow = inheritPatch(floatingWindow, from: parent.floatingWindow) + result.panel = inheritPatch(panel, from: parent.panel) + result.menu = inheritPatch(menu, from: parent.menu) + return result + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle.FloatingWindow: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.maxWidth = maxWidth ?? parent.maxWidth + result.maxHeight = maxHeight ?? parent.maxHeight + result.heightFraction = heightFraction ?? parent.heightFraction + result.minSize = minSize ?? parent.minSize + result.maxContentInsetFraction = maxContentInsetFraction ?? parent.maxContentInsetFraction + return result + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle.Launcher: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.diameter = diameter ?? parent.diameter + result.glyphRatio = glyphRatio ?? parent.glyphRatio + result.shadowColor = shadowColor ?? parent.shadowColor + result.shadowRadius = shadowRadius ?? parent.shadowRadius + result.shadowOffsetY = shadowOffsetY ?? parent.shadowOffsetY + return result + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle.Menu: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.maxWidth = maxWidth ?? parent.maxWidth + result.launcherSpacing = launcherSpacing ?? parent.launcherSpacing + result.rowSpacing = rowSpacing ?? parent.rowSpacing + result.horizontalPadding = horizontalPadding ?? parent.horizontalPadding + result.verticalPadding = verticalPadding ?? parent.verticalPadding + result.minRowHeight = minRowHeight ?? parent.minRowHeight + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.subtitleSpacing = subtitleSpacing ?? parent.subtitleSpacing + result.iconWidth = iconWidth ?? parent.iconWidth + result.motion = inheritPatch(motion, from: parent.motion) + return result + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle.MenuMotion: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.animation = animation ?? parent.animation + result.stagger = stagger ?? parent.stagger + result.scale = scale ?? parent.scale + result.usesSpatialMotion = usesSpatialMotion ?? parent.usesSpatialMotion + return result + } +} + +extension WhereThemeAppearance.DeveloperOverlayStyle.Panel: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.fullScreenInset = fullScreenInset ?? parent.fullScreenInset + result.shadowOpacity = shadowOpacity ?? parent.shadowOpacity + result.shadowRadius = shadowRadius ?? parent.shadowRadius + result.shadowOffsetY = shadowOffsetY ?? parent.shadowOffsetY + result.controlHorizontalPadding = controlHorizontalPadding ?? parent + .controlHorizontalPadding + result.controlVerticalPadding = controlVerticalPadding ?? parent.controlVerticalPadding + result.dragHandleSize = dragHandleSize ?? parent.dragHandleSize + result.dragHandleMinHeight = dragHandleMinHeight ?? parent.dragHandleMinHeight + result.resizeGripSize = resizeGripSize ?? parent.resizeGripSize + result.resizeIconSize = resizeIconSize ?? parent.resizeIconSize + result.resizeGripClearance = resizeGripClearance ?? parent.resizeGripClearance + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.EvidenceStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.EvidenceStyle.swift new file mode 100644 index 000000000..e834e8a00 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.EvidenceStyle.swift @@ -0,0 +1,48 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct EvidenceStyle: Codable, Hashable, Sendable { + public var previewCornerRadius: BThemeValue? + public var pdfPreviewMinHeight: BThemeValue? + public var loadingMinHeight: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.EvidenceStyle, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = previewCornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".previewCornerRadius") + value.previewCornerRadius = CGFloat(resolved) + } + if let authored = pdfPreviewMinHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".pdfPreviewMinHeight") + value.pdfPreviewMinHeight = CGFloat(resolved) + } + if let authored = loadingMinHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".loadingMinHeight") + value.loadingMinHeight = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.EvidenceStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.previewCornerRadius = previewCornerRadius ?? parent.previewCornerRadius + result.pdfPreviewMinHeight = pdfPreviewMinHeight ?? parent.pdfPreviewMinHeight + result.loadingMinHeight = loadingMinHeight ?? parent.loadingMinHeight + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.FeatureDiscoveryStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.FeatureDiscoveryStyle.swift new file mode 100644 index 000000000..76ca654cb --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.FeatureDiscoveryStyle.swift @@ -0,0 +1,971 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct FeatureDiscoveryStyle: Codable, Hashable, Sendable { + @CopyOnWrite public var appIcon: WhereThemeAppearance.FeatureDiscoveryStyle + .AppIconPreview? = nil + @CopyOnWrite public var regionStyle: WhereThemeAppearance.FeatureDiscoveryStyle + .RegionStylePreview? = nil + @CopyOnWrite public var shareSheet: WhereThemeAppearance.FeatureDiscoveryStyle + .ShareSheetPreview? = nil + @CopyOnWrite public var evidenceArchive: WhereThemeAppearance.FeatureDiscoveryStyle + .EvidenceArchivePreview? = nil + @CopyOnWrite public var marketingHeader: WhereThemeAppearance.FeatureDiscoveryStyle + .MarketingHeader? = nil + @CopyOnWrite public var marketingPanel: WhereThemeAppearance.FeatureDiscoveryStyle + .MarketingPanel? = nil + @CopyOnWrite public var backgroundPattern: WhereThemeAppearance.FeatureDiscoveryStyle + .BackgroundPattern? = nil + @CopyOnWrite public var estimatedTime: WhereThemeAppearance.FeatureDiscoveryStyle + .EstimatedTime? = nil + @CopyOnWrite public var siri: WhereThemeAppearance.FeatureDiscoveryStyle.Siri? = nil + @CopyOnWrite public var widgets: WhereThemeAppearance.FeatureDiscoveryStyle.Widgets? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try appIcon?.apply( + to: &value.appIcon, + traits: traits, + appearance: appearance, + path: path + ".appIcon", + ) + try regionStyle?.apply( + to: &value.regionStyle, + traits: traits, + appearance: appearance, + path: path + ".regionStyle", + ) + try shareSheet?.apply( + to: &value.shareSheet, + traits: traits, + appearance: appearance, + path: path + ".shareSheet", + ) + try evidenceArchive?.apply( + to: &value.evidenceArchive, + traits: traits, + appearance: appearance, + path: path + ".evidenceArchive", + ) + try marketingHeader?.apply( + to: &value.marketingHeader, + traits: traits, + appearance: appearance, + path: path + ".marketingHeader", + ) + try marketingPanel?.apply( + to: &value.marketingPanel, + traits: traits, + appearance: appearance, + path: path + ".marketingPanel", + ) + try backgroundPattern?.apply( + to: &value.backgroundPattern, + traits: traits, + appearance: appearance, + path: path + ".backgroundPattern", + ) + try estimatedTime?.apply( + to: &value.estimatedTime, + traits: traits, + appearance: appearance, + path: path + ".estimatedTime", + ) + try siri?.apply( + to: &value.siri, + traits: traits, + appearance: appearance, + path: path + ".siri", + ) + try widgets?.apply( + to: &value.widgets, + traits: traits, + appearance: appearance, + path: path + ".widgets", + ) + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct AppIconPreview: Codable, Hashable, Sendable { + public var layout: BThemeValue? + public var spacing: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.AppIconPreview, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct BackgroundPattern: Codable, Hashable, Sendable { + public var contourSpacing: BThemeValue? + public var primaryDistortion: BThemeValue? + public var secondaryDistortion: BThemeValue? + public var horizontalScale: BThemeValue? + public var centerXRatio: BThemeValue? + public var centerYRatio: BThemeValue? + public var phaseStep: BThemeValue? + public var lineWidth: BThemeValue? + public var opacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.BackgroundPattern, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = contourSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contourSpacing") + value.contourSpacing = CGFloat(resolved) + } + if let authored = primaryDistortion { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".primaryDistortion") + value.primaryDistortion = CGFloat(resolved) + } + if let authored = secondaryDistortion { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".secondaryDistortion") + value.secondaryDistortion = CGFloat(resolved) + } + if let authored = horizontalScale { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".horizontalScale") + value.horizontalScale = CGFloat(resolved) + } + if let authored = centerXRatio { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".centerXRatio") + value.centerXRatio = CGFloat(resolved) + } + if let authored = centerYRatio { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".centerYRatio") + value.centerYRatio = CGFloat(resolved) + } + if let authored = phaseStep { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".phaseStep") + value.phaseStep = CGFloat(resolved) + } + if let authored = lineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lineWidth") + value.lineWidth = CGFloat(resolved) + } + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct EstimatedTime: Codable, Hashable, Sendable { + public var timelineHeight: BThemeValue? + public var timelineSpacing: BThemeValue? + public var calculationSpacing: BThemeValue? + public var segmentCornerRadius: BThemeValue? + public var legendDotSize: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.EstimatedTime, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = timelineHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".timelineHeight") + value.timelineHeight = CGFloat(resolved) + } + if let authored = timelineSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".timelineSpacing") + value.timelineSpacing = CGFloat(resolved) + } + if let authored = calculationSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".calculationSpacing") + value.calculationSpacing = CGFloat(resolved) + } + if let authored = segmentCornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".segmentCornerRadius") + value.segmentCornerRadius = CGFloat(resolved) + } + if let authored = legendDotSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".legendDotSize") + value.legendDotSize = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct EvidenceArchivePreview: Codable, Hashable, Sendable { + public var layout: BThemeValue? + public var spacing: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.EvidenceArchivePreview, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct MarketingHeader: Codable, Hashable, Sendable { + public var badgeSize: BThemeValue? + public var symbolPointSize: BThemeValue? + public var badgeTintOpacity: BThemeValue? + public var contentMaxWidth: BThemeValue? + public var spacing: BThemeValue? + public var verticalPadding: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.MarketingHeader, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = badgeSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".badgeSize") + value.badgeSize = CGFloat(resolved) + } + if let authored = symbolPointSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".symbolPointSize") + value.symbolPointSize = CGFloat(resolved) + } + if let authored = badgeTintOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".badgeTintOpacity") + value.badgeTintOpacity = resolved + } + if let authored = contentMaxWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentMaxWidth") + value.contentMaxWidth = CGFloat(resolved) + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + if let authored = verticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalPadding") + value.verticalPadding = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct MarketingPanel: Codable, Hashable, Sendable { + public var cornerRadius: BThemeValue? + public var maxWidth: BThemeValue? + public var padding: BThemeValue? + public var contentSpacing: BThemeValue? + public var rowVerticalInset: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.MarketingPanel, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = maxWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".maxWidth") + value.maxWidth = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = contentSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentSpacing") + value.contentSpacing = CGFloat(resolved) + } + if let authored = rowVerticalInset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rowVerticalInset") + value.rowVerticalInset = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct RegionStylePreview: Codable, Hashable, Sendable { + public var layout: BThemeValue? + public var spacing: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.RegionStylePreview, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct ShareSheetPreview: Codable, Hashable, Sendable { + public var sourcesLayout: BThemeValue? + public var sourceLayout: BThemeValue? + public var sourcesSpacing: BThemeValue? + public var titleAlignment: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.ShareSheetPreview, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = sourcesLayout { + let resolved = authored.resolve(mode: traits.mode) + value.sourcesLayout = resolved.native + } + if let authored = sourceLayout { + let resolved = authored.resolve(mode: traits.mode) + value.sourceLayout = resolved.native + } + if let authored = sourcesSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".sourcesSpacing") + value.sourcesSpacing = CGFloat(resolved) + } + if let authored = titleAlignment { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleAlignment") + value.titleAlignment = resolved.alignment + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct Siri: Codable, Hashable, Sendable { + @CopyOnWrite public var card: WhereThemeAppearance.FeatureDiscoveryStyle.Siri.Card? = nil + @CopyOnWrite public var bubble: WhereThemeAppearance.FeatureDiscoveryStyle.Siri + .Bubble? = nil + @CopyOnWrite public var speakerIcon: WhereThemeAppearance.FeatureDiscoveryStyle.Siri + .SpeakerIcon? = nil + public var accent: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.Siri, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try card?.apply( + to: &value.card, + traits: traits, + appearance: appearance, + path: path + ".card", + ) + try bubble?.apply( + to: &value.bubble, + traits: traits, + appearance: appearance, + path: path + ".bubble", + ) + try speakerIcon?.apply( + to: &value.speakerIcon, + traits: traits, + appearance: appearance, + path: path + ".speakerIcon", + ) + if let authored = accent { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".accent") + value.accent = appearance.color(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle { + public struct Widgets: Codable, Hashable, Sendable { + @CopyOnWrite public var device: WhereThemeAppearance.FeatureDiscoveryStyle.Widgets + .Device? = nil + @CopyOnWrite public var frame: WhereThemeAppearance.FeatureDiscoveryStyle.Widgets + .Frame? = nil + @CopyOnWrite public var wallpapers: WhereThemeAppearance.FeatureDiscoveryStyle.Widgets + .Wallpapers? = nil + public var lockWidgetHeight: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.Widgets, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try device?.apply( + to: &value.device, + traits: traits, + appearance: appearance, + path: path + ".device", + ) + try frame?.apply( + to: &value.frame, + traits: traits, + appearance: appearance, + path: path + ".frame", + ) + try wallpapers?.apply( + to: &value.wallpapers, + traits: traits, + appearance: appearance, + path: path + ".wallpapers", + ) + if let authored = lockWidgetHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lockWidgetHeight") + value.lockWidgetHeight = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Siri { + public struct Bubble: Codable, Hashable, Sendable { + public var cornerRadius: BThemeValue? + public var horizontalPadding: BThemeValue? + public var verticalPadding: BThemeValue? + public var indent: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.Siri.Bubble, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = horizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".horizontalPadding") + value.horizontalPadding = CGFloat(resolved) + } + if let authored = verticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalPadding") + value.verticalPadding = CGFloat(resolved) + } + if let authored = indent { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".indent") + value.indent = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Siri { + public struct Card: Codable, Hashable, Sendable { + public var cornerRadius: BThemeValue? + public var maxWidth: BThemeValue? + public var padding: BThemeValue? + public var spacing: BThemeValue? + public var rowVerticalInset: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.Siri.Card, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = maxWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".maxWidth") + value.maxWidth = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + if let authored = rowVerticalInset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rowVerticalInset") + value.rowVerticalInset = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Siri { + public struct SpeakerIcon: Codable, Hashable, Sendable { + public var containerSize: BThemeValue? + public var symbolPointSize: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.Siri.SpeakerIcon, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = containerSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".containerSize") + value.containerSize = CGFloat(resolved) + } + if let authored = symbolPointSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".symbolPointSize") + value.symbolPointSize = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Widgets { + public struct Device: Codable, Hashable, Sendable { + public var cornerRadius: BThemeValue? + public var contentMaxWidth: BThemeValue? + public var regularContentWidth: BThemeValue? + public var padding: BThemeValue? + public var spacing: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.Widgets.Device, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = contentMaxWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentMaxWidth") + value.contentMaxWidth = CGFloat(resolved) + } + if let authored = regularContentWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".regularContentWidth") + value.regularContentWidth = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Widgets { + public struct Frame: Codable, Hashable, Sendable { + public var cornerRadius: BThemeValue? + public var padding: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.Widgets.Frame, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Widgets { + public struct Wallpapers: Codable, Hashable, Sendable { + @CopyOnWrite public var home: WhereThemeAppearance.FeatureDiscoveryStyle.Widgets.Wallpapers + .Gradient? = nil + @CopyOnWrite public var lock: WhereThemeAppearance.FeatureDiscoveryStyle.Widgets.Wallpapers + .Gradient? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.Widgets.Wallpapers, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try home?.apply( + to: &value.home, + traits: traits, + appearance: appearance, + path: path + ".home", + ) + try lock?.apply( + to: &value.lock, + traits: traits, + appearance: appearance, + path: path + ".lock", + ) + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Widgets.Wallpapers { + public struct Gradient: Codable, Hashable, Sendable { + public var top: BThemeValue? + public var bottom: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FeatureDiscoveryStyle.Widgets.Wallpapers.Gradient, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = top { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".top") + value.top = appearance.color(resolved) + } + if let authored = bottom { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".bottom") + value.bottom = appearance.color(resolved) + } + } + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.appIcon = inheritPatch(appIcon, from: parent.appIcon) + result.regionStyle = inheritPatch(regionStyle, from: parent.regionStyle) + result.shareSheet = inheritPatch(shareSheet, from: parent.shareSheet) + result.evidenceArchive = inheritPatch(evidenceArchive, from: parent.evidenceArchive) + result.marketingHeader = inheritPatch(marketingHeader, from: parent.marketingHeader) + result.marketingPanel = inheritPatch(marketingPanel, from: parent.marketingPanel) + result.backgroundPattern = inheritPatch(backgroundPattern, from: parent.backgroundPattern) + result.estimatedTime = inheritPatch(estimatedTime, from: parent.estimatedTime) + result.siri = inheritPatch(siri, from: parent.siri) + result.widgets = inheritPatch(widgets, from: parent.widgets) + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.AppIconPreview: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + result.spacing = spacing ?? parent.spacing + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.BackgroundPattern: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.contourSpacing = contourSpacing ?? parent.contourSpacing + result.primaryDistortion = primaryDistortion ?? parent.primaryDistortion + result.secondaryDistortion = secondaryDistortion ?? parent.secondaryDistortion + result.horizontalScale = horizontalScale ?? parent.horizontalScale + result.centerXRatio = centerXRatio ?? parent.centerXRatio + result.centerYRatio = centerYRatio ?? parent.centerYRatio + result.phaseStep = phaseStep ?? parent.phaseStep + result.lineWidth = lineWidth ?? parent.lineWidth + result.opacity = opacity ?? parent.opacity + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.EstimatedTime: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.timelineHeight = timelineHeight ?? parent.timelineHeight + result.timelineSpacing = timelineSpacing ?? parent.timelineSpacing + result.calculationSpacing = calculationSpacing ?? parent.calculationSpacing + result.segmentCornerRadius = segmentCornerRadius ?? parent.segmentCornerRadius + result.legendDotSize = legendDotSize ?? parent.legendDotSize + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.EvidenceArchivePreview: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + result.spacing = spacing ?? parent.spacing + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.MarketingHeader: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.badgeSize = badgeSize ?? parent.badgeSize + result.symbolPointSize = symbolPointSize ?? parent.symbolPointSize + result.badgeTintOpacity = badgeTintOpacity ?? parent.badgeTintOpacity + result.contentMaxWidth = contentMaxWidth ?? parent.contentMaxWidth + result.spacing = spacing ?? parent.spacing + result.verticalPadding = verticalPadding ?? parent.verticalPadding + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.MarketingPanel: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.maxWidth = maxWidth ?? parent.maxWidth + result.padding = padding ?? parent.padding + result.contentSpacing = contentSpacing ?? parent.contentSpacing + result.rowVerticalInset = rowVerticalInset ?? parent.rowVerticalInset + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.RegionStylePreview: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + result.spacing = spacing ?? parent.spacing + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.ShareSheetPreview: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.sourcesLayout = sourcesLayout ?? parent.sourcesLayout + result.sourceLayout = sourceLayout ?? parent.sourceLayout + result.sourcesSpacing = sourcesSpacing ?? parent.sourcesSpacing + result.titleAlignment = titleAlignment ?? parent.titleAlignment + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Siri: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.card = inheritPatch(card, from: parent.card) + result.bubble = inheritPatch(bubble, from: parent.bubble) + result.speakerIcon = inheritPatch(speakerIcon, from: parent.speakerIcon) + result.accent = accent ?? parent.accent + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Widgets: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.device = inheritPatch(device, from: parent.device) + result.frame = inheritPatch(frame, from: parent.frame) + result.wallpapers = inheritPatch(wallpapers, from: parent.wallpapers) + result.lockWidgetHeight = lockWidgetHeight ?? parent.lockWidgetHeight + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Siri.Bubble: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.horizontalPadding = horizontalPadding ?? parent.horizontalPadding + result.verticalPadding = verticalPadding ?? parent.verticalPadding + result.indent = indent ?? parent.indent + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Siri.Card: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.maxWidth = maxWidth ?? parent.maxWidth + result.padding = padding ?? parent.padding + result.spacing = spacing ?? parent.spacing + result.rowVerticalInset = rowVerticalInset ?? parent.rowVerticalInset + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Siri.SpeakerIcon: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.containerSize = containerSize ?? parent.containerSize + result.symbolPointSize = symbolPointSize ?? parent.symbolPointSize + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Widgets.Device: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.contentMaxWidth = contentMaxWidth ?? parent.contentMaxWidth + result.regularContentWidth = regularContentWidth ?? parent.regularContentWidth + result.padding = padding ?? parent.padding + result.spacing = spacing ?? parent.spacing + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Widgets.Frame: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.padding = padding ?? parent.padding + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Widgets.Wallpapers: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.home = inheritPatch(home, from: parent.home) + result.lock = inheritPatch(lock, from: parent.lock) + return result + } +} + +extension WhereThemeAppearance.FeatureDiscoveryStyle.Widgets.Wallpapers.Gradient: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.top = top ?? parent.top + result.bottom = bottom ?? parent.bottom + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.FlightReviewPointStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.FlightReviewPointStyle.swift new file mode 100644 index 000000000..34a7bf391 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.FlightReviewPointStyle.swift @@ -0,0 +1,55 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct FlightReviewPointStyle: Codable, Hashable, Sendable { + public var titleFont: BThemeValue? + public var detailFont: BThemeValue? + public var minimumActionHeight: BThemeValue? + public var mapCornerRadius: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FlightReviewPointStyle, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = titleFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleFont") + value.titleFont = resolved.font + } + if let authored = detailFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".detailFont") + value.detailFont = resolved.font + } + if let authored = minimumActionHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".minimumActionHeight") + value.minimumActionHeight = CGFloat(resolved) + } + if let authored = mapCornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".mapCornerRadius") + value.mapCornerRadius = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.FlightReviewPointStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.titleFont = titleFont ?? parent.titleFont + result.detailFont = detailFont ?? parent.detailFont + result.minimumActionHeight = minimumActionHeight ?? parent.minimumActionHeight + result.mapCornerRadius = mapCornerRadius ?? parent.mapCornerRadius + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.FlightStatusStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.FlightStatusStyle.swift new file mode 100644 index 000000000..123308a61 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.FlightStatusStyle.swift @@ -0,0 +1,69 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct FlightStatusStyle: Codable, Hashable, Sendable { + public var padding: BThemeValue? + public var spacing: BThemeValue? + public var cornerRadius: BThemeValue? + public var titleFont: BThemeValue? + public var bodyFont: BThemeValue? + public var background: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.FlightStatusStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = titleFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleFont") + value.titleFont = resolved.font + } + if let authored = bodyFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".bodyFont") + value.bodyFont = resolved.font + } + if let authored = background { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".background") + value.background = appearance.color(resolved) + } + } + } +} + +extension WhereThemeAppearance.FlightStatusStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.padding = padding ?? parent.padding + result.spacing = spacing ?? parent.spacing + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.titleFont = titleFont ?? parent.titleFont + result.bodyFont = bodyFont ?? parent.bodyFont + result.background = background ?? parent.background + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LaunchStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LaunchStyle.swift new file mode 100644 index 000000000..908ed01e8 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LaunchStyle.swift @@ -0,0 +1,67 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct LaunchStyle: Codable, Hashable, Sendable { + public var reveal: BThemeValue? + public var revealAnimation: BThemeValue? + public var captionAnimation: WhereThemeOptional>? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LaunchStyle, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = reveal { + let resolved = authored.resolve(mode: traits.mode) + value.reveal = resolved.native + } + if let authored = revealAnimation { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".revealAnimation") + value.revealAnimation = resolved.animation + } + if let captionAnimation { + switch captionAnimation { + case .clear: value.captionAnimation = nil + case let .value(authored): + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".captionAnimation", + ) + value.captionAnimation = resolved.animation + } + } + } + } +} + +extension WhereThemeAppearance.LaunchStyle { + public enum Reveal: Codable, Hashable, Sendable { + case zoom + case crossfade + var native: WhereStylesheet.LaunchStyle.Reveal { + switch self { + case .zoom: .zoom + case .crossfade: .crossfade + } + } + } +} + +extension WhereThemeAppearance.LaunchStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.reveal = reveal ?? parent.reveal + result.revealAnimation = revealAnimation ?? parent.revealAnimation + result.captionAnimation = captionAnimation ?? parent.captionAnimation + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationCardStackStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationCardStackStyle.swift new file mode 100644 index 000000000..acebe1f5e --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationCardStackStyle.swift @@ -0,0 +1,115 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct LocationCardStackStyle: Codable, Hashable, Sendable { + @CopyOnWrite public var overtake: WhereThemeAppearance.LocationCardStackStyle + .OvertakeMotion? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationCardStackStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try overtake?.apply( + to: &value.overtake, + traits: traits, + appearance: appearance, + path: path + ".overtake", + ) + } + } +} + +extension WhereThemeAppearance.LocationCardStackStyle { + public struct OvertakeMotion: Codable, Hashable, Sendable { + public var duration: BThemeValue? + public var bounce: BThemeValue? + public var lateralArc: BThemeValue? + public var liftScale: BThemeValue? + public var rotationDegrees: BThemeValue? + public var settleScale: BThemeValue? + public var minimumOpacity: BThemeValue? + public var usesSpatialMotion: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationCardStackStyle.OvertakeMotion, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = duration { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".duration") + value.duration = resolved + } + if let authored = bounce { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".bounce") + value.bounce = resolved + } + if let authored = lateralArc { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lateralArc") + value.lateralArc = CGFloat(resolved) + } + if let authored = liftScale { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".liftScale") + value.liftScale = CGFloat(resolved) + } + if let authored = rotationDegrees { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rotationDegrees") + value.rotationDegrees = resolved + } + if let authored = settleScale { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".settleScale") + value.settleScale = CGFloat(resolved) + } + if let authored = minimumOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".minimumOpacity") + value.minimumOpacity = resolved + } + if let authored = usesSpatialMotion { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".usesSpatialMotion") + value.usesSpatialMotion = resolved + } + } + } +} + +extension WhereThemeAppearance.LocationCardStackStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.overtake = inheritPatch(overtake, from: parent.overtake) + return result + } +} + +extension WhereThemeAppearance.LocationCardStackStyle.OvertakeMotion: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.duration = duration ?? parent.duration + result.bounce = bounce ?? parent.bounce + result.lateralArc = lateralArc ?? parent.lateralArc + result.liftScale = liftScale ?? parent.liftScale + result.rotationDegrees = rotationDegrees ?? parent.rotationDegrees + result.settleScale = settleScale ?? parent.settleScale + result.minimumOpacity = minimumOpacity ?? parent.minimumOpacity + result.usesSpatialMotion = usesSpatialMotion ?? parent.usesSpatialMotion + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationForecastStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationForecastStyle.swift new file mode 100644 index 000000000..e3a58e6b8 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationForecastStyle.swift @@ -0,0 +1,804 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct LocationForecastStyle: Codable, Hashable, Sendable { + public var cornerRadius: BThemeValue? + public var padding: BThemeValue? + public var rowSpacing: BThemeValue? + public var expansionAnimation: BThemeValue? + @CopyOnWrite public var surface: WhereThemeAppearance.LocationForecastStyle.Surface? = nil + @CopyOnWrite public var header: WhereThemeAppearance.LocationForecastStyle.Header? = nil + @CopyOnWrite public var row: WhereThemeAppearance.LocationForecastStyle.Row? = nil + @CopyOnWrite public var progress: WhereThemeAppearance.LocationForecastStyle.Progress? = nil + @CopyOnWrite public var controls: WhereThemeAppearance.LocationForecastStyle.Controls? = nil + @CopyOnWrite public var ink: WhereThemeAppearance.LocationForecastStyle.Ink? = nil + @CopyOnWrite public var summary: WhereThemeAppearance.LocationForecastStyle.Summary? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationForecastStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = rowSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rowSpacing") + value.rowSpacing = CGFloat(resolved) + } + if let authored = expansionAnimation { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".expansionAnimation") + value.expansionAnimation = resolved.animation + } + try surface?.apply( + to: &value.surface, + traits: traits, + appearance: appearance, + path: path + ".surface", + ) + try header?.apply( + to: &value.header, + traits: traits, + appearance: appearance, + path: path + ".header", + ) + try row?.apply( + to: &value.row, + traits: traits, + appearance: appearance, + path: path + ".row", + ) + try progress?.apply( + to: &value.progress, + traits: traits, + appearance: appearance, + path: path + ".progress", + ) + try controls?.apply( + to: &value.controls, + traits: traits, + appearance: appearance, + path: path + ".controls", + ) + try ink?.apply( + to: &value.ink, + traits: traits, + appearance: appearance, + path: path + ".ink", + ) + try summary?.apply( + to: &value.summary, + traits: traits, + appearance: appearance, + path: path + ".summary", + ) + } + } +} + +extension WhereThemeAppearance.LocationForecastStyle { + public struct Controls: Codable, Hashable, Sendable { + public var layout: BThemeValue? + public var sectionSpacing: BThemeValue? + public var layoutSpacing: BThemeValue? + public var cornerRadius: BThemeValue? + public var horizontalPadding: BThemeValue? + public var minimumHeight: BThemeValue? + public var strokeWidth: BThemeValue? + public var font: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationForecastStyle.Controls, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + if let authored = sectionSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".sectionSpacing") + value.sectionSpacing = CGFloat(resolved) + } + if let authored = layoutSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".layoutSpacing") + value.layoutSpacing = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = horizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".horizontalPadding") + value.horizontalPadding = CGFloat(resolved) + } + if let authored = minimumHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".minimumHeight") + value.minimumHeight = CGFloat(resolved) + } + if let authored = strokeWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".strokeWidth") + value.strokeWidth = CGFloat(resolved) + } + if let authored = font { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".font") + value.font = resolved.font + } + } + } +} + +extension WhereThemeAppearance.LocationForecastStyle { + public struct Header: Codable, Hashable, Sendable { + public var layout: BThemeValue? + public var contentSpacing: BThemeValue? + public var textSpacing: BThemeValue? + public var titleFont: BThemeValue? + public var elapsedFont: BThemeValue? + public var minimumHeight: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationForecastStyle.Header, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + if let authored = contentSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentSpacing") + value.contentSpacing = CGFloat(resolved) + } + if let authored = textSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".textSpacing") + value.textSpacing = CGFloat(resolved) + } + if let authored = titleFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleFont") + value.titleFont = resolved.font + } + if let authored = elapsedFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".elapsedFont") + value.elapsedFont = resolved.font + } + if let authored = minimumHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".minimumHeight") + value.minimumHeight = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.LocationForecastStyle { + public struct Ink: Codable, Hashable, Sendable { + public var surfaceWashOpacity: BThemeValue? + public var surfaceOutlineOpacity: BThemeValue? + public var microprintOpacity: BThemeValue? + public var rosettePrimaryOpacity: BThemeValue? + public var rosetteSecondaryOpacity: BThemeValue? + public var sealOpacity: BThemeValue? + public var rowFillOpacity: BThemeValue? + public var rowOutlineOpacity: BThemeValue? + public var progressTrackOpacity: BThemeValue? + public var progressEstimateFillOpacity: BThemeValue? + public var progressHatchOpacity: BThemeValue? + public var controlFillOpacity: BThemeValue? + public var controlStrokeOpacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationForecastStyle.Ink, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = surfaceWashOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".surfaceWashOpacity") + value.surfaceWashOpacity = resolved + } + if let authored = surfaceOutlineOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".surfaceOutlineOpacity") + value.surfaceOutlineOpacity = resolved + } + if let authored = microprintOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".microprintOpacity") + value.microprintOpacity = resolved + } + if let authored = rosettePrimaryOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rosettePrimaryOpacity") + value.rosettePrimaryOpacity = resolved + } + if let authored = rosetteSecondaryOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rosetteSecondaryOpacity") + value.rosetteSecondaryOpacity = resolved + } + if let authored = sealOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".sealOpacity") + value.sealOpacity = resolved + } + if let authored = rowFillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rowFillOpacity") + value.rowFillOpacity = resolved + } + if let authored = rowOutlineOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rowOutlineOpacity") + value.rowOutlineOpacity = resolved + } + if let authored = progressTrackOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".progressTrackOpacity") + value.progressTrackOpacity = resolved + } + if let authored = progressEstimateFillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".progressEstimateFillOpacity", + ) + value.progressEstimateFillOpacity = resolved + } + if let authored = progressHatchOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".progressHatchOpacity") + value.progressHatchOpacity = resolved + } + if let authored = controlFillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".controlFillOpacity") + value.controlFillOpacity = resolved + } + if let authored = controlStrokeOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".controlStrokeOpacity") + value.controlStrokeOpacity = resolved + } + } + } +} + +extension WhereThemeAppearance.LocationForecastStyle { + public struct Progress: Codable, Hashable, Sendable { + public var height: BThemeValue? + public var hatchSpacing: BThemeValue? + public var hatchLineWidth: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationForecastStyle.Progress, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = height { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".height") + value.height = CGFloat(resolved) + } + if let authored = hatchSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".hatchSpacing") + value.hatchSpacing = CGFloat(resolved) + } + if let authored = hatchLineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".hatchLineWidth") + value.hatchLineWidth = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.LocationForecastStyle { + public struct Row: Codable, Hashable, Sendable { + public var layout: BThemeValue? + public var cornerRadius: BThemeValue? + public var padding: BThemeValue? + public var contentSpacing: BThemeValue? + public var estimateSpacing: BThemeValue? + public var regionFont: BThemeValue? + public var estimateFont: BThemeValue? + public var detailFont: BThemeValue? + public var outlineWidth: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationForecastStyle.Row, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = contentSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentSpacing") + value.contentSpacing = CGFloat(resolved) + } + if let authored = estimateSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".estimateSpacing") + value.estimateSpacing = CGFloat(resolved) + } + if let authored = regionFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".regionFont") + value.regionFont = resolved.font + } + if let authored = estimateFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".estimateFont") + value.estimateFont = resolved.font + } + if let authored = detailFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".detailFont") + value.detailFont = resolved.font + } + if let authored = outlineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineWidth") + value.outlineWidth = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.LocationForecastStyle { + public struct Summary: Codable, Hashable, Sendable { + @CopyOnWrite public var artwork: WhereThemeAppearance.LocationForecastStyle.Summary + .Artwork? = nil + public var cornerRadius: BThemeValue? + public var padding: BThemeValue? + public var ink: BThemeValue? + public var treatment: BThemeValue? + public var paper: BThemeValue? + public var usesOpaquePaper: BThemeValue? + public var glassTintOpacity: BThemeValue? + public var outlineColor: BThemeValue? + public var outlineWidth: BThemeValue? + public var shadowOpacity: BThemeValue? + public var shadowRadius: BThemeValue? + public var shadowOffsetY: BThemeValue? + public var rosetteOpacity: BThemeValue? + @CopyOnWrite public var rosette: WhereThemeAppearance.CardStyle.Rosette? = nil + @CopyOnWrite public var border: WhereThemeAppearance.CardStyle.RegionShape + .SecurityBorder? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationForecastStyle.Summary, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try artwork?.apply( + to: &value.artwork, + traits: traits, + appearance: appearance, + path: path + ".artwork", + ) + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = ink { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".ink") + value.ink = appearance.color(resolved) + } + if let authored = treatment { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".treatment") + value.treatment = try resolved.resolve( + mode: traits.mode, + appearance: appearance, + base: value.treatment, + ) + } + if let authored = paper { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".paper") + value.paper = appearance.color(resolved) + } + if let authored = usesOpaquePaper { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".usesOpaquePaper") + value.usesOpaquePaper = resolved + } + if let authored = glassTintOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".glassTintOpacity") + value.glassTintOpacity = resolved + } + if let authored = outlineColor { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineColor") + value.outlineColor = appearance.color(resolved) + } + if let authored = outlineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineWidth") + value.outlineWidth = CGFloat(resolved) + } + if let authored = shadowOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowOpacity") + value.shadowOpacity = resolved + } + if let authored = shadowRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowRadius") + value.shadowRadius = CGFloat(resolved) + } + if let authored = shadowOffsetY { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowOffsetY") + value.shadowOffsetY = CGFloat(resolved) + } + if let authored = rosetteOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rosetteOpacity") + value.rosetteOpacity = resolved + } + try rosette?.apply( + to: &value.rosette, + traits: traits, + appearance: appearance, + path: path + ".rosette", + ) + try border?.apply( + to: &value.border, + traits: traits, + appearance: appearance, + path: path + ".border", + ) + } + } +} + +extension WhereThemeAppearance.LocationForecastStyle { + public struct Surface: Codable, Hashable, Sendable { + public var outlineWidth: BThemeValue? + public var inset: BThemeValue? + public var microprintGlyphSize: BThemeValue? + public var microprintSpacing: BThemeValue? + public var rosetteWobble: BThemeValue? + public var rosetteLineWidth: BThemeValue? + public var primaryRingSpacing: BThemeValue? + public var secondaryRingSpacing: BThemeValue? + public var shadowColor: BThemeValue? + public var shadowRadius: BThemeValue? + public var shadowOffsetY: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationForecastStyle.Surface, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = outlineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineWidth") + value.outlineWidth = CGFloat(resolved) + } + if let authored = inset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".inset") + value.inset = CGFloat(resolved) + } + if let authored = microprintGlyphSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".microprintGlyphSize") + value.microprintGlyphSize = CGFloat(resolved) + } + if let authored = microprintSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".microprintSpacing") + value.microprintSpacing = CGFloat(resolved) + } + if let authored = rosetteWobble { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rosetteWobble") + value.rosetteWobble = CGFloat(resolved) + } + if let authored = rosetteLineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rosetteLineWidth") + value.rosetteLineWidth = CGFloat(resolved) + } + if let authored = primaryRingSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".primaryRingSpacing") + value.primaryRingSpacing = CGFloat(resolved) + } + if let authored = secondaryRingSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".secondaryRingSpacing") + value.secondaryRingSpacing = CGFloat(resolved) + } + if let authored = shadowColor { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowColor") + value.shadowColor = appearance.color(resolved) + } + if let authored = shadowRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowRadius") + value.shadowRadius = CGFloat(resolved) + } + if let authored = shadowOffsetY { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".shadowOffsetY") + value.shadowOffsetY = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.LocationForecastStyle.Summary { + public struct Artwork: Codable, Hashable, Sendable { + public var widthFraction: BThemeValue? + public var inset: BThemeValue? + public var gap: BThemeValue? + public var leadingOpacity: BThemeValue? + @CopyOnWrite public var silhouette: WhereThemeAppearance.CardStyle.RegionShape + .Artwork? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationForecastStyle.Summary.Artwork, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = widthFraction { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".widthFraction") + value.widthFraction = CGFloat(resolved) + } + if let authored = inset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".inset") + value.inset = CGFloat(resolved) + } + if let authored = gap { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".gap") + value.gap = CGFloat(resolved) + } + if let authored = leadingOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".leadingOpacity") + value.leadingOpacity = resolved + } + try silhouette?.apply( + to: &value.silhouette, + traits: traits, + appearance: appearance, + path: path + ".silhouette", + ) + } + } +} + +extension WhereThemeAppearance.LocationForecastStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.padding = padding ?? parent.padding + result.rowSpacing = rowSpacing ?? parent.rowSpacing + result.expansionAnimation = expansionAnimation ?? parent.expansionAnimation + result.surface = inheritPatch(surface, from: parent.surface) + result.header = inheritPatch(header, from: parent.header) + result.row = inheritPatch(row, from: parent.row) + result.progress = inheritPatch(progress, from: parent.progress) + result.controls = inheritPatch(controls, from: parent.controls) + result.ink = inheritPatch(ink, from: parent.ink) + result.summary = inheritPatch(summary, from: parent.summary) + return result + } +} + +extension WhereThemeAppearance.LocationForecastStyle.Controls: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + result.sectionSpacing = sectionSpacing ?? parent.sectionSpacing + result.layoutSpacing = layoutSpacing ?? parent.layoutSpacing + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.horizontalPadding = horizontalPadding ?? parent.horizontalPadding + result.minimumHeight = minimumHeight ?? parent.minimumHeight + result.strokeWidth = strokeWidth ?? parent.strokeWidth + result.font = font ?? parent.font + return result + } +} + +extension WhereThemeAppearance.LocationForecastStyle.Header: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + result.contentSpacing = contentSpacing ?? parent.contentSpacing + result.textSpacing = textSpacing ?? parent.textSpacing + result.titleFont = titleFont ?? parent.titleFont + result.elapsedFont = elapsedFont ?? parent.elapsedFont + result.minimumHeight = minimumHeight ?? parent.minimumHeight + return result + } +} + +extension WhereThemeAppearance.LocationForecastStyle.Ink: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.surfaceWashOpacity = surfaceWashOpacity ?? parent.surfaceWashOpacity + result.surfaceOutlineOpacity = surfaceOutlineOpacity ?? parent.surfaceOutlineOpacity + result.microprintOpacity = microprintOpacity ?? parent.microprintOpacity + result.rosettePrimaryOpacity = rosettePrimaryOpacity ?? parent.rosettePrimaryOpacity + result.rosetteSecondaryOpacity = rosetteSecondaryOpacity ?? parent.rosetteSecondaryOpacity + result.sealOpacity = sealOpacity ?? parent.sealOpacity + result.rowFillOpacity = rowFillOpacity ?? parent.rowFillOpacity + result.rowOutlineOpacity = rowOutlineOpacity ?? parent.rowOutlineOpacity + result.progressTrackOpacity = progressTrackOpacity ?? parent.progressTrackOpacity + result.progressEstimateFillOpacity = progressEstimateFillOpacity ?? parent + .progressEstimateFillOpacity + result.progressHatchOpacity = progressHatchOpacity ?? parent.progressHatchOpacity + result.controlFillOpacity = controlFillOpacity ?? parent.controlFillOpacity + result.controlStrokeOpacity = controlStrokeOpacity ?? parent.controlStrokeOpacity + return result + } +} + +extension WhereThemeAppearance.LocationForecastStyle.Progress: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.height = height ?? parent.height + result.hatchSpacing = hatchSpacing ?? parent.hatchSpacing + result.hatchLineWidth = hatchLineWidth ?? parent.hatchLineWidth + return result + } +} + +extension WhereThemeAppearance.LocationForecastStyle.Row: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.padding = padding ?? parent.padding + result.contentSpacing = contentSpacing ?? parent.contentSpacing + result.estimateSpacing = estimateSpacing ?? parent.estimateSpacing + result.regionFont = regionFont ?? parent.regionFont + result.estimateFont = estimateFont ?? parent.estimateFont + result.detailFont = detailFont ?? parent.detailFont + result.outlineWidth = outlineWidth ?? parent.outlineWidth + return result + } +} + +extension WhereThemeAppearance.LocationForecastStyle.Summary: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.artwork = inheritPatch(artwork, from: parent.artwork) + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.padding = padding ?? parent.padding + result.ink = ink ?? parent.ink + result.treatment = inheritModePatch(treatment, from: parent.treatment) + result.paper = paper ?? parent.paper + result.usesOpaquePaper = usesOpaquePaper ?? parent.usesOpaquePaper + result.glassTintOpacity = glassTintOpacity ?? parent.glassTintOpacity + result.outlineColor = outlineColor ?? parent.outlineColor + result.outlineWidth = outlineWidth ?? parent.outlineWidth + result.shadowOpacity = shadowOpacity ?? parent.shadowOpacity + result.shadowRadius = shadowRadius ?? parent.shadowRadius + result.shadowOffsetY = shadowOffsetY ?? parent.shadowOffsetY + result.rosetteOpacity = rosetteOpacity ?? parent.rosetteOpacity + result.rosette = inheritPatch(rosette, from: parent.rosette) + result.border = inheritPatch(border, from: parent.border) + return result + } +} + +extension WhereThemeAppearance.LocationForecastStyle.Surface: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.outlineWidth = outlineWidth ?? parent.outlineWidth + result.inset = inset ?? parent.inset + result.microprintGlyphSize = microprintGlyphSize ?? parent.microprintGlyphSize + result.microprintSpacing = microprintSpacing ?? parent.microprintSpacing + result.rosetteWobble = rosetteWobble ?? parent.rosetteWobble + result.rosetteLineWidth = rosetteLineWidth ?? parent.rosetteLineWidth + result.primaryRingSpacing = primaryRingSpacing ?? parent.primaryRingSpacing + result.secondaryRingSpacing = secondaryRingSpacing ?? parent.secondaryRingSpacing + result.shadowColor = shadowColor ?? parent.shadowColor + result.shadowRadius = shadowRadius ?? parent.shadowRadius + result.shadowOffsetY = shadowOffsetY ?? parent.shadowOffsetY + return result + } +} + +extension WhereThemeAppearance.LocationForecastStyle.Summary.Artwork: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.widthFraction = widthFraction ?? parent.widthFraction + result.inset = inset ?? parent.inset + result.gap = gap ?? parent.gap + result.leadingOpacity = leadingOpacity ?? parent.leadingOpacity + result.silhouette = inheritPatch(silhouette, from: parent.silhouette) + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationWelcomeStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationWelcomeStyle.swift new file mode 100644 index 000000000..fd46a51da --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationWelcomeStyle.swift @@ -0,0 +1,426 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct LocationWelcomeStyle: Codable, Hashable, Sendable { + public var surface: BThemeValue? + public var maxWidth: BThemeValue? + public var cornerRadius: BThemeValue? + public var padding: BThemeValue? + public var contentSpacing: BThemeValue? + public var paperOpacity: BThemeValue? + public var scrimOpacity: BThemeValue? + public var glassTintOpacity: BThemeValue? + @CopyOnWrite public var glow: WhereThemeAppearance.LocationWelcomeStyle.Shadow? = nil + @CopyOnWrite public var lift: WhereThemeAppearance.LocationWelcomeStyle.Shadow? = nil + @CopyOnWrite public var close: WhereThemeAppearance.LocationWelcomeStyle.Close? = nil + @CopyOnWrite public var accessory: WhereThemeAppearance.LocationWelcomeStyle + .Accessory? = nil + @CopyOnWrite public var motion: WhereThemeAppearance.LocationWelcomeStyle.Motion? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationWelcomeStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = surface { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".surface") + value.surface = try resolved.resolve( + mode: traits.mode, + appearance: appearance, + base: value.surface, + ) + } + if let authored = maxWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".maxWidth") + value.maxWidth = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = contentSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentSpacing") + value.contentSpacing = CGFloat(resolved) + } + if let authored = paperOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".paperOpacity") + value.paperOpacity = resolved + } + if let authored = scrimOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".scrimOpacity") + value.scrimOpacity = resolved + } + if let authored = glassTintOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".glassTintOpacity") + value.glassTintOpacity = resolved + } + try glow?.apply( + to: &value.glow, + traits: traits, + appearance: appearance, + path: path + ".glow", + ) + try lift?.apply( + to: &value.lift, + traits: traits, + appearance: appearance, + path: path + ".lift", + ) + try close?.apply( + to: &value.close, + traits: traits, + appearance: appearance, + path: path + ".close", + ) + try accessory?.apply( + to: &value.accessory, + traits: traits, + appearance: appearance, + path: path + ".accessory", + ) + try motion?.apply( + to: &value.motion, + traits: traits, + appearance: appearance, + path: path + ".motion", + ) + } + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle { + public struct Accessory: Codable, Hashable, Sendable { + public var copy: BThemeValue? + public var contentSpacing: BThemeValue? + public var horizontalPadding: BThemeValue? + public var verticalPadding: BThemeValue? + public var minimumActionHeight: BThemeValue? + public var symbolSize: BThemeValue? + public var titleFont: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationWelcomeStyle.Accessory, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = copy { + let resolved = authored.resolve(mode: traits.mode) + value.copy = resolved.native + } + if let authored = contentSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentSpacing") + value.contentSpacing = CGFloat(resolved) + } + if let authored = horizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".horizontalPadding") + value.horizontalPadding = CGFloat(resolved) + } + if let authored = verticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalPadding") + value.verticalPadding = CGFloat(resolved) + } + if let authored = minimumActionHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".minimumActionHeight") + value.minimumActionHeight = CGFloat(resolved) + } + if let authored = symbolSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".symbolSize") + value.symbolSize = CGFloat(resolved) + } + if let authored = titleFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleFont") + value.titleFont = resolved.font + } + } + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle { + public struct Close: Codable, Hashable, Sendable { + public var surface: BThemeValue? + public var offset: BThemeValue? + public var tintOpacity: BThemeValue? + @CopyOnWrite public var glow: WhereThemeAppearance.LocationWelcomeStyle.Shadow? = nil + @CopyOnWrite public var lift: WhereThemeAppearance.LocationWelcomeStyle.Shadow? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationWelcomeStyle.Close, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = surface { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".surface") + value.surface = try resolved.resolve( + mode: traits.mode, + appearance: appearance, + base: value.surface, + ) + } + if let authored = offset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".offset") + value.offset = resolved.size + } + if let authored = tintOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".tintOpacity") + value.tintOpacity = resolved + } + try glow?.apply( + to: &value.glow, + traits: traits, + appearance: appearance, + path: path + ".glow", + ) + try lift?.apply( + to: &value.lift, + traits: traits, + appearance: appearance, + path: path + ".lift", + ) + } + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle { + public struct Motion: Codable, Hashable, Sendable { + @CopyOnWrite public var arrival: WhereThemeAppearance.LocationWelcomeStyle.Motion + .Movement? = nil + @CopyOnWrite public var departure: WhereThemeAppearance.LocationWelcomeStyle.Motion + .Movement? = nil + public var scrimAnimation: BThemeValue? + public var usesSpatialMotion: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationWelcomeStyle.Motion, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try arrival?.apply( + to: &value.arrival, + traits: traits, + appearance: appearance, + path: path + ".arrival", + ) + try departure?.apply( + to: &value.departure, + traits: traits, + appearance: appearance, + path: path + ".departure", + ) + if let authored = scrimAnimation { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".scrimAnimation") + value.scrimAnimation = resolved.animation + } + if let authored = usesSpatialMotion { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".usesSpatialMotion") + value.usesSpatialMotion = resolved + } + } + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle { + public struct Shadow: Codable, Hashable, Sendable { + public var opacity: BThemeValue? + public var radius: BThemeValue? + public var offsetY: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationWelcomeStyle.Shadow, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = opacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".opacity") + value.opacity = resolved + } + if let authored = radius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".radius") + value.radius = CGFloat(resolved) + } + if let authored = offsetY { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".offsetY") + value.offsetY = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle.Accessory { + public enum Copy: Codable, Hashable, Sendable { + case full + case compact + var native: WhereStylesheet.LocationWelcomeStyle.Accessory.Copy { + switch self { + case .full: .full + case .compact: .compact + } + } + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle.Motion { + public struct Movement: Codable, Hashable, Sendable { + public var animation: BThemeValue? + public var scale: BThemeValue? + public var rotationDegrees: BThemeValue? + public var verticalOffset: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationWelcomeStyle.Motion.Movement, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = animation { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".animation") + value.animation = resolved.animation + } + if let authored = scale { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".scale") + value.scale = CGFloat(resolved) + } + if let authored = rotationDegrees { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rotationDegrees") + value.rotationDegrees = resolved + } + if let authored = verticalOffset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalOffset") + value.verticalOffset = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.surface = inheritModePatch(surface, from: parent.surface) + result.maxWidth = maxWidth ?? parent.maxWidth + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.padding = padding ?? parent.padding + result.contentSpacing = contentSpacing ?? parent.contentSpacing + result.paperOpacity = paperOpacity ?? parent.paperOpacity + result.scrimOpacity = scrimOpacity ?? parent.scrimOpacity + result.glassTintOpacity = glassTintOpacity ?? parent.glassTintOpacity + result.glow = inheritPatch(glow, from: parent.glow) + result.lift = inheritPatch(lift, from: parent.lift) + result.close = inheritPatch(close, from: parent.close) + result.accessory = inheritPatch(accessory, from: parent.accessory) + result.motion = inheritPatch(motion, from: parent.motion) + return result + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle.Accessory: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.copy = copy ?? parent.copy + result.contentSpacing = contentSpacing ?? parent.contentSpacing + result.horizontalPadding = horizontalPadding ?? parent.horizontalPadding + result.verticalPadding = verticalPadding ?? parent.verticalPadding + result.minimumActionHeight = minimumActionHeight ?? parent.minimumActionHeight + result.symbolSize = symbolSize ?? parent.symbolSize + result.titleFont = titleFont ?? parent.titleFont + return result + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle.Close: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.surface = inheritModePatch(surface, from: parent.surface) + result.offset = offset ?? parent.offset + result.tintOpacity = tintOpacity ?? parent.tintOpacity + result.glow = inheritPatch(glow, from: parent.glow) + result.lift = inheritPatch(lift, from: parent.lift) + return result + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle.Motion: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.arrival = inheritPatch(arrival, from: parent.arrival) + result.departure = inheritPatch(departure, from: parent.departure) + result.scrimAnimation = scrimAnimation ?? parent.scrimAnimation + result.usesSpatialMotion = usesSpatialMotion ?? parent.usesSpatialMotion + return result + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle.Shadow: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.opacity = opacity ?? parent.opacity + result.radius = radius ?? parent.radius + result.offsetY = offsetY ?? parent.offsetY + return result + } +} + +extension WhereThemeAppearance.LocationWelcomeStyle.Motion.Movement: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.animation = animation ?? parent.animation + result.scale = scale ?? parent.scale + result.rotationDegrees = rotationDegrees ?? parent.rotationDegrees + result.verticalOffset = verticalOffset ?? parent.verticalOffset + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationsBackgroundStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationsBackgroundStyle.swift new file mode 100644 index 000000000..74b150a3d --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.LocationsBackgroundStyle.swift @@ -0,0 +1,85 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct LocationsBackgroundStyle: Codable, Hashable, Sendable { + public var paper: BThemeValue? + public var ink: BThemeValue? + public var showsInk: BThemeValue? + public var preferredCellSize: BThemeValue? + @CopyOnWrite public var artwork: WhereThemeAppearance.CardStyle.RegionShape.Artwork? = nil + public var maximumAspectScale: BThemeValue? + @CopyOnWrite public var rosette: WhereThemeAppearance.CardStyle.Rosette? = nil + public var rosetteOpacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.LocationsBackgroundStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = paper { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".paper") + value.paper = appearance.color(resolved) + } + if let authored = ink { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".ink") + value.ink = appearance.color(resolved) + } + if let authored = showsInk { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".showsInk") + value.showsInk = resolved + } + if let authored = preferredCellSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".preferredCellSize") + value.preferredCellSize = CGFloat(resolved) + } + try artwork?.apply( + to: &value.artwork, + traits: traits, + appearance: appearance, + path: path + ".artwork", + ) + if let authored = maximumAspectScale { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".maximumAspectScale") + value.maximumAspectScale = CGFloat(resolved) + } + try rosette?.apply( + to: &value.rosette, + traits: traits, + appearance: appearance, + path: path + ".rosette", + ) + if let authored = rosetteOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rosetteOpacity") + value.rosetteOpacity = resolved + } + } + } +} + +extension WhereThemeAppearance.LocationsBackgroundStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.paper = paper ?? parent.paper + result.ink = ink ?? parent.ink + result.showsInk = showsInk ?? parent.showsInk + result.preferredCellSize = preferredCellSize ?? parent.preferredCellSize + result.artwork = inheritPatch(artwork, from: parent.artwork) + result.maximumAspectScale = maximumAspectScale ?? parent.maximumAspectScale + result.rosette = inheritPatch(rosette, from: parent.rosette) + result.rosetteOpacity = rosetteOpacity ?? parent.rosetteOpacity + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Motion.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Motion.swift new file mode 100644 index 000000000..2e2aaf971 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Motion.swift @@ -0,0 +1,100 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct Motion: Codable, Hashable, Sendable { + public var reveal: BThemeValue? + public var reducedReveal: BThemeValue? + public var captionFade: BThemeValue? + @CopyOnWrite public var staggeredReveal: WhereThemeAppearance.Motion.StaggeredReveal? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.Motion, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = reveal { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".reveal") + value.reveal = resolved.animation + } + if let authored = reducedReveal { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".reducedReveal") + value.reducedReveal = resolved.animation + } + if let authored = captionFade { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".captionFade") + value.captionFade = resolved.animation + } + try staggeredReveal?.apply( + to: &value.staggeredReveal, + traits: traits, + appearance: appearance, + path: path + ".staggeredReveal", + ) + } + } +} + +extension WhereThemeAppearance.Motion { + public struct StaggeredReveal: Codable, Hashable, Sendable { + public var animation: BThemeValue? + public var verticalOffset: BThemeValue? + public var delay: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.Motion.StaggeredReveal, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = animation { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".animation") + value.animation = resolved.animation + } + if let authored = verticalOffset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalOffset") + value.verticalOffset = CGFloat(resolved) + } + if let authored = delay { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".delay") + value.delay = resolved + } + } + } +} + +extension WhereThemeAppearance.Motion: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.reveal = reveal ?? parent.reveal + result.reducedReveal = reducedReveal ?? parent.reducedReveal + result.captionFade = captionFade ?? parent.captionFade + result.staggeredReveal = inheritPatch(staggeredReveal, from: parent.staggeredReveal) + return result + } +} + +extension WhereThemeAppearance.Motion.StaggeredReveal: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.animation = animation ?? parent.animation + result.verticalOffset = verticalOffset ?? parent.verticalOffset + result.delay = delay ?? parent.delay + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Palette.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Palette.swift new file mode 100644 index 000000000..390181cd5 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Palette.swift @@ -0,0 +1,176 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct Palette: Codable, Hashable, Sendable { + @CopyOnWrite public var primary: WhereThemeAppearance.Palette.Primary? = nil + @CopyOnWrite public var splash: WhereThemeAppearance.Palette.Splash? = nil + @CopyOnWrite public var onboarding: WhereThemeAppearance.Palette.Onboarding? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.Palette, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try primary?.apply( + to: &value.primary, + traits: traits, + appearance: appearance, + path: path + ".primary", + ) + try splash?.apply( + to: &value.splash, + traits: traits, + appearance: appearance, + path: path + ".splash", + ) + try onboarding?.apply( + to: &value.onboarding, + traits: traits, + appearance: appearance, + path: path + ".onboarding", + ) + } + } +} + +extension WhereThemeAppearance.Palette { + public struct Onboarding: Codable, Hashable, Sendable { + public var backgroundTop: BThemeValue? + public var backgroundBottom: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.Palette.Onboarding, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = backgroundTop { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".backgroundTop") + value.backgroundTop = appearance.color(resolved) + } + if let authored = backgroundBottom { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".backgroundBottom") + value.backgroundBottom = appearance.color(resolved) + } + } + } +} + +extension WhereThemeAppearance.Palette { + public struct Primary: Codable, Hashable, Sendable { + public var backgroundTop: BThemeValue? + public var backgroundBottom: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.Palette.Primary, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = backgroundTop { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".backgroundTop") + value.backgroundTop = appearance.color(resolved) + } + if let authored = backgroundBottom { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".backgroundBottom") + value.backgroundBottom = appearance.color(resolved) + } + } + } +} + +extension WhereThemeAppearance.Palette { + public struct Splash: Codable, Hashable, Sendable { + public var background: BThemeValue? + public var vignetteCenter: BThemeValue? + public var vignetteEdge: BThemeValue? + public var iconGlow: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.Palette.Splash, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = background { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".background") + value.background = appearance.color(resolved) + } + if let authored = vignetteCenter { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".vignetteCenter") + value.vignetteCenter = appearance.color(resolved) + } + if let authored = vignetteEdge { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".vignetteEdge") + value.vignetteEdge = appearance.color(resolved) + } + if let authored = iconGlow { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconGlow") + value.iconGlow = appearance.color(resolved) + } + } + } +} + +extension WhereThemeAppearance.Palette: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.primary = inheritPatch(primary, from: parent.primary) + result.splash = inheritPatch(splash, from: parent.splash) + result.onboarding = inheritPatch(onboarding, from: parent.onboarding) + return result + } +} + +extension WhereThemeAppearance.Palette.Onboarding: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.backgroundTop = backgroundTop ?? parent.backgroundTop + result.backgroundBottom = backgroundBottom ?? parent.backgroundBottom + return result + } +} + +extension WhereThemeAppearance.Palette.Primary: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.backgroundTop = backgroundTop ?? parent.backgroundTop + result.backgroundBottom = backgroundBottom ?? parent.backgroundBottom + return result + } +} + +extension WhereThemeAppearance.Palette.Splash: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.background = background ?? parent.background + result.vignetteCenter = vignetteCenter ?? parent.vignetteCenter + result.vignetteEdge = vignetteEdge ?? parent.vignetteEdge + result.iconGlow = iconGlow ?? parent.iconGlow + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.PassportSealStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.PassportSealStyle.swift new file mode 100644 index 000000000..4bb71d0a4 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.PassportSealStyle.swift @@ -0,0 +1,87 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct PassportSealStyle: Codable, Hashable, Sendable { + public var size: BThemeValue? + public var rotationDegrees: BThemeValue? + public var outerLineWidth: BThemeValue? + public var innerLineWidth: BThemeValue? + public var innerInset: BThemeValue? + public var dashLength: BThemeValue? + public var dashSpacing: BThemeValue? + public var symbolFont: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.PassportSealStyle, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = size { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".size") + value.size = CGFloat(resolved) + } + if let authored = rotationDegrees { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rotationDegrees") + value.rotationDegrees = resolved + } + if let authored = outerLineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outerLineWidth") + value.outerLineWidth = CGFloat(resolved) + } + if let authored = innerLineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".innerLineWidth") + value.innerLineWidth = CGFloat(resolved) + } + if let authored = innerInset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".innerInset") + value.innerInset = CGFloat(resolved) + } + if let authored = dashLength { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".dashLength") + value.dashLength = CGFloat(resolved) + } + if let authored = dashSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: 0 ... 10000, + path: path + ".dashSpacing", + ) + value.dashSpacing = CGFloat(resolved) + } + if let authored = symbolFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".symbolFont") + value.symbolFont = resolved.font + } + } + } +} + +extension WhereThemeAppearance.PassportSealStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.size = size ?? parent.size + result.rotationDegrees = rotationDegrees ?? parent.rotationDegrees + result.outerLineWidth = outerLineWidth ?? parent.outerLineWidth + result.innerLineWidth = innerLineWidth ?? parent.innerLineWidth + result.innerInset = innerInset ?? parent.innerInset + result.dashLength = dashLength ?? parent.dashLength + result.dashSpacing = dashSpacing ?? parent.dashSpacing + result.symbolFont = symbolFont ?? parent.symbolFont + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.PrivacyPassportCardStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.PrivacyPassportCardStyle.swift new file mode 100644 index 000000000..95b916850 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.PrivacyPassportCardStyle.swift @@ -0,0 +1,445 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct PrivacyPassportCardStyle: Codable, Hashable, Sendable { + public var surface: BThemeValue? + public var headerLayout: BThemeValue? + public var cornerRadius: BThemeValue? + public var padding: BThemeValue? + public var sectionSpacing: BThemeValue? + public var headerSpacing: BThemeValue? + public var titleFont: BThemeValue? + public var detailFont: BThemeValue? + @CopyOnWrite public var rosette: WhereThemeAppearance.PrivacyPassportCardStyle + .Rosette? = nil + @CopyOnWrite public var reflectiveSurface: WhereThemeAppearance.PrivacyPassportCardStyle + .ReflectiveSurface? = nil + @CopyOnWrite public var disclosure: WhereThemeAppearance.PrivacyPassportCardStyle + .Disclosure? = nil + public var outlineOpacity: BThemeValue? + public var outlineWidth: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.PrivacyPassportCardStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = surface { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".surface") + value.surface = try resolved.resolve( + mode: traits.mode, + appearance: appearance, + base: value.surface, + ) + } + if let authored = headerLayout { + let resolved = authored.resolve(mode: traits.mode) + value.headerLayout = resolved.native + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = sectionSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".sectionSpacing") + value.sectionSpacing = CGFloat(resolved) + } + if let authored = headerSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".headerSpacing") + value.headerSpacing = CGFloat(resolved) + } + if let authored = titleFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleFont") + value.titleFont = resolved.font + } + if let authored = detailFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".detailFont") + value.detailFont = resolved.font + } + try rosette?.apply( + to: &value.rosette, + traits: traits, + appearance: appearance, + path: path + ".rosette", + ) + try reflectiveSurface?.apply( + to: &value.reflectiveSurface, + traits: traits, + appearance: appearance, + path: path + ".reflectiveSurface", + ) + try disclosure?.apply( + to: &value.disclosure, + traits: traits, + appearance: appearance, + path: path + ".disclosure", + ) + if let authored = outlineOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineOpacity") + value.outlineOpacity = resolved + } + if let authored = outlineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineWidth") + value.outlineWidth = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.PrivacyPassportCardStyle { + public struct Disclosure: Codable, Hashable, Sendable { + public var layout: BThemeValue? + public var rowSpacing: BThemeValue? + public var cornerRadius: BThemeValue? + public var padding: BThemeValue? + public var contentSpacing: BThemeValue? + public var textSpacing: BThemeValue? + public var iconSize: BThemeValue? + public var symbolFont: BThemeValue? + public var titleFont: BThemeValue? + public var detailFont: BThemeValue? + public var statusFont: BThemeValue? + public var statusHorizontalPadding: BThemeValue? + public var statusVerticalPadding: BThemeValue? + public var fillOpacity: BThemeValue? + public var strokeOpacity: BThemeValue? + public var strokeWidth: BThemeValue? + public var statusFillOpacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.PrivacyPassportCardStyle.Disclosure, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + if let authored = rowSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".rowSpacing") + value.rowSpacing = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = contentSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentSpacing") + value.contentSpacing = CGFloat(resolved) + } + if let authored = textSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".textSpacing") + value.textSpacing = CGFloat(resolved) + } + if let authored = iconSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconSize") + value.iconSize = CGFloat(resolved) + } + if let authored = symbolFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".symbolFont") + value.symbolFont = resolved.font + } + if let authored = titleFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleFont") + value.titleFont = resolved.font + } + if let authored = detailFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".detailFont") + value.detailFont = resolved.font + } + if let authored = statusFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".statusFont") + value.statusFont = resolved.font + } + if let authored = statusHorizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".statusHorizontalPadding") + value.statusHorizontalPadding = CGFloat(resolved) + } + if let authored = statusVerticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".statusVerticalPadding") + value.statusVerticalPadding = CGFloat(resolved) + } + if let authored = fillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".fillOpacity") + value.fillOpacity = resolved + } + if let authored = strokeOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".strokeOpacity") + value.strokeOpacity = resolved + } + if let authored = strokeWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".strokeWidth") + value.strokeWidth = CGFloat(resolved) + } + if let authored = statusFillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".statusFillOpacity") + value.statusFillOpacity = resolved + } + } + } +} + +extension WhereThemeAppearance.PrivacyPassportCardStyle { + public struct ReflectiveSurface: Codable, Hashable, Sendable { + public var backgroundTop: BThemeValue? + public var backgroundBottom: BThemeValue? + public var accent: BThemeValue? + public var intensity: BThemeValue? + public var staticGlintIntensity: BThemeValue? + @CopyOnWrite public var staticPose: WhereThemeAppearance.PrivacyPassportCardStyle + .ReflectiveSurface.Pose? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.PrivacyPassportCardStyle.ReflectiveSurface, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = backgroundTop { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".backgroundTop") + value.backgroundTop = appearance.color(resolved) + } + if let authored = backgroundBottom { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".backgroundBottom") + value.backgroundBottom = appearance.color(resolved) + } + if let authored = accent { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".accent") + value.accent = appearance.color(resolved) + } + if let authored = intensity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: 0 ... 1, + path: path + ".intensity", + ) + value.intensity = resolved + } + if let authored = staticGlintIntensity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: 0 ... 1, + path: path + ".staticGlintIntensity", + ) + value.staticGlintIntensity = resolved + } + try staticPose?.apply( + to: &value.staticPose, + traits: traits, + appearance: appearance, + path: path + ".staticPose", + ) + } + } +} + +extension WhereThemeAppearance.PrivacyPassportCardStyle { + public struct Rosette: Codable, Hashable, Sendable { + public var wobble: BThemeValue? + public var lineWidth: BThemeValue? + public var primaryRingSpacing: BThemeValue? + public var secondaryRingSpacing: BThemeValue? + public var primaryOpacity: BThemeValue? + public var secondaryOpacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.PrivacyPassportCardStyle.Rosette, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = wobble { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".wobble") + value.wobble = CGFloat(resolved) + } + if let authored = lineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lineWidth") + value.lineWidth = CGFloat(resolved) + } + if let authored = primaryRingSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".primaryRingSpacing") + value.primaryRingSpacing = CGFloat(resolved) + } + if let authored = secondaryRingSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".secondaryRingSpacing") + value.secondaryRingSpacing = CGFloat(resolved) + } + if let authored = primaryOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".primaryOpacity") + value.primaryOpacity = resolved + } + if let authored = secondaryOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".secondaryOpacity") + value.secondaryOpacity = resolved + } + } + } +} + +extension WhereThemeAppearance.PrivacyPassportCardStyle.ReflectiveSurface { + public struct Pose: Codable, Hashable, Sendable { + public var roll: BThemeValue? + public var pitch: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.PrivacyPassportCardStyle.ReflectiveSurface.Pose, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = roll { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".roll") + value.roll = resolved + } + if let authored = pitch { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".pitch") + value.pitch = resolved + } + } + } +} + +extension WhereThemeAppearance.PrivacyPassportCardStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.surface = inheritModePatch(surface, from: parent.surface) + result.headerLayout = headerLayout ?? parent.headerLayout + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.padding = padding ?? parent.padding + result.sectionSpacing = sectionSpacing ?? parent.sectionSpacing + result.headerSpacing = headerSpacing ?? parent.headerSpacing + result.titleFont = titleFont ?? parent.titleFont + result.detailFont = detailFont ?? parent.detailFont + result.rosette = inheritPatch(rosette, from: parent.rosette) + result.reflectiveSurface = inheritPatch(reflectiveSurface, from: parent.reflectiveSurface) + result.disclosure = inheritPatch(disclosure, from: parent.disclosure) + result.outlineOpacity = outlineOpacity ?? parent.outlineOpacity + result.outlineWidth = outlineWidth ?? parent.outlineWidth + return result + } +} + +extension WhereThemeAppearance.PrivacyPassportCardStyle.Disclosure: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + result.rowSpacing = rowSpacing ?? parent.rowSpacing + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.padding = padding ?? parent.padding + result.contentSpacing = contentSpacing ?? parent.contentSpacing + result.textSpacing = textSpacing ?? parent.textSpacing + result.iconSize = iconSize ?? parent.iconSize + result.symbolFont = symbolFont ?? parent.symbolFont + result.titleFont = titleFont ?? parent.titleFont + result.detailFont = detailFont ?? parent.detailFont + result.statusFont = statusFont ?? parent.statusFont + result.statusHorizontalPadding = statusHorizontalPadding ?? parent.statusHorizontalPadding + result.statusVerticalPadding = statusVerticalPadding ?? parent.statusVerticalPadding + result.fillOpacity = fillOpacity ?? parent.fillOpacity + result.strokeOpacity = strokeOpacity ?? parent.strokeOpacity + result.strokeWidth = strokeWidth ?? parent.strokeWidth + result.statusFillOpacity = statusFillOpacity ?? parent.statusFillOpacity + return result + } +} + +extension WhereThemeAppearance.PrivacyPassportCardStyle.ReflectiveSurface: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.backgroundTop = backgroundTop ?? parent.backgroundTop + result.backgroundBottom = backgroundBottom ?? parent.backgroundBottom + result.accent = accent ?? parent.accent + result.intensity = intensity ?? parent.intensity + result.staticGlintIntensity = staticGlintIntensity ?? parent.staticGlintIntensity + result.staticPose = inheritPatch(staticPose, from: parent.staticPose) + return result + } +} + +extension WhereThemeAppearance.PrivacyPassportCardStyle.Rosette: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.wobble = wobble ?? parent.wobble + result.lineWidth = lineWidth ?? parent.lineWidth + result.primaryRingSpacing = primaryRingSpacing ?? parent.primaryRingSpacing + result.secondaryRingSpacing = secondaryRingSpacing ?? parent.secondaryRingSpacing + result.primaryOpacity = primaryOpacity ?? parent.primaryOpacity + result.secondaryOpacity = secondaryOpacity ?? parent.secondaryOpacity + return result + } +} + +extension WhereThemeAppearance.PrivacyPassportCardStyle.ReflectiveSurface.Pose: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.roll = roll ?? parent.roll + result.pitch = pitch ?? parent.pitch + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.RegionMapStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.RegionMapStyle.swift new file mode 100644 index 000000000..5d83d7014 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.RegionMapStyle.swift @@ -0,0 +1,131 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct RegionMapStyle: Codable, Hashable, Sendable { + @CopyOnWrite public var overview: WhereThemeAppearance.RegionMapStyle.Viewport? = nil + @CopyOnWrite public var pointPreview: WhereThemeAppearance.RegionMapStyle.Viewport? = nil + public var uncertaintyFillOpacity: BThemeValue? + public var uncertaintyStrokeOpacity: BThemeValue? + public var uncertaintyStrokeWidth: BThemeValue? + public var routeLineWidth: BThemeValue? + public var capturePointDiameter: BThemeValue? + public var captureInset: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.RegionMapStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try overview?.apply( + to: &value.overview, + traits: traits, + appearance: appearance, + path: path + ".overview", + ) + try pointPreview?.apply( + to: &value.pointPreview, + traits: traits, + appearance: appearance, + path: path + ".pointPreview", + ) + if let authored = uncertaintyFillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".uncertaintyFillOpacity") + value.uncertaintyFillOpacity = resolved + } + if let authored = uncertaintyStrokeOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".uncertaintyStrokeOpacity", + ) + value.uncertaintyStrokeOpacity = resolved + } + if let authored = uncertaintyStrokeWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".uncertaintyStrokeWidth") + value.uncertaintyStrokeWidth = CGFloat(resolved) + } + if let authored = routeLineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".routeLineWidth") + value.routeLineWidth = CGFloat(resolved) + } + if let authored = capturePointDiameter { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".capturePointDiameter") + value.capturePointDiameter = CGFloat(resolved) + } + if let authored = captureInset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".captureInset") + value.captureInset = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.RegionMapStyle { + public struct Viewport: Codable, Hashable, Sendable { + public var height: BThemeValue? + public var minimumSpanMeters: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.RegionMapStyle.Viewport, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = height { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".height") + value.height = CGFloat(resolved) + } + if let authored = minimumSpanMeters { + let resolved = authored.resolve(mode: traits.mode) + // A full-world span is about forty million meters; zero keeps the native bounds. + try WhereThemeValidation.validate( + resolved, + range: 0 ... 40_100_000, + path: path + ".minimumSpanMeters", + ) + value.minimumSpanMeters = resolved + } + } + } +} + +extension WhereThemeAppearance.RegionMapStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.overview = inheritPatch(overview, from: parent.overview) + result.pointPreview = inheritPatch(pointPreview, from: parent.pointPreview) + result.uncertaintyFillOpacity = uncertaintyFillOpacity ?? parent.uncertaintyFillOpacity + result.uncertaintyStrokeOpacity = uncertaintyStrokeOpacity ?? parent + .uncertaintyStrokeOpacity + result.uncertaintyStrokeWidth = uncertaintyStrokeWidth ?? parent.uncertaintyStrokeWidth + result.routeLineWidth = routeLineWidth ?? parent.routeLineWidth + result.capturePointDiameter = capturePointDiameter ?? parent.capturePointDiameter + result.captureInset = captureInset ?? parent.captureInset + return result + } +} + +extension WhereThemeAppearance.RegionMapStyle.Viewport: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.height = height ?? parent.height + result.minimumSpanMeters = minimumSpanMeters ?? parent.minimumSpanMeters + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.RegionPickerStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.RegionPickerStyle.swift new file mode 100644 index 000000000..da31434e5 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.RegionPickerStyle.swift @@ -0,0 +1,188 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct RegionPickerStyle: Codable, Hashable, Sendable { + public var mapCornerRadius: BThemeValue? + public var selectedFillOpacity: BThemeValue? + public var unselectedFillOpacity: BThemeValue? + public var selectedStrokeOpacity: BThemeValue? + public var unselectedStrokeOpacity: BThemeValue? + public var selectedStrokeWidth: BThemeValue? + public var unselectedStrokeWidth: BThemeValue? + public var colorSwatchSize: BThemeValue? + public var colorSwatchSelectionRing: BThemeValue? + public var colorSwatchMinWidth: BThemeValue? + public var glyphTileSize: BThemeValue? + public var glyphTileMinWidth: BThemeValue? + public var glyphCornerRadius: BThemeValue? + public var glyphSelectionStrokeWidth: BThemeValue? + public var glyphSelectedBackgroundOpacity: BThemeValue? + public var mapCenterLatitude: BThemeValue? + public var mapCenterLongitude: BThemeValue? + public var mapSpanLatitude: BThemeValue? + public var mapSpanLongitude: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.RegionPickerStyle, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = mapCornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".mapCornerRadius") + value.mapCornerRadius = CGFloat(resolved) + } + if let authored = selectedFillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".selectedFillOpacity") + value.selectedFillOpacity = resolved + } + if let authored = unselectedFillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".unselectedFillOpacity") + value.unselectedFillOpacity = resolved + } + if let authored = selectedStrokeOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".selectedStrokeOpacity") + value.selectedStrokeOpacity = resolved + } + if let authored = unselectedStrokeOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".unselectedStrokeOpacity") + value.unselectedStrokeOpacity = resolved + } + if let authored = selectedStrokeWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".selectedStrokeWidth") + value.selectedStrokeWidth = CGFloat(resolved) + } + if let authored = unselectedStrokeWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".unselectedStrokeWidth") + value.unselectedStrokeWidth = CGFloat(resolved) + } + if let authored = colorSwatchSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".colorSwatchSize") + value.colorSwatchSize = CGFloat(resolved) + } + if let authored = colorSwatchSelectionRing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".colorSwatchSelectionRing", + ) + value.colorSwatchSelectionRing = CGFloat(resolved) + } + if let authored = colorSwatchMinWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".colorSwatchMinWidth") + value.colorSwatchMinWidth = CGFloat(resolved) + } + if let authored = glyphTileSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".glyphTileSize") + value.glyphTileSize = CGFloat(resolved) + } + if let authored = glyphTileMinWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".glyphTileMinWidth") + value.glyphTileMinWidth = CGFloat(resolved) + } + if let authored = glyphCornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".glyphCornerRadius") + value.glyphCornerRadius = CGFloat(resolved) + } + if let authored = glyphSelectionStrokeWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".glyphSelectionStrokeWidth", + ) + value.glyphSelectionStrokeWidth = CGFloat(resolved) + } + if let authored = glyphSelectedBackgroundOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".glyphSelectedBackgroundOpacity", + ) + value.glyphSelectedBackgroundOpacity = resolved + } + if let authored = mapCenterLatitude { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: -90 ... 90, + path: path + ".mapCenterLatitude", + ) + value.mapCenterLatitude = resolved + } + if let authored = mapCenterLongitude { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: -180 ... 180, + path: path + ".mapCenterLongitude", + ) + value.mapCenterLongitude = resolved + } + if let authored = mapSpanLatitude { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: Double.leastNormalMagnitude ... 180, + path: path + ".mapSpanLatitude", + ) + value.mapSpanLatitude = resolved + } + if let authored = mapSpanLongitude { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + range: Double.leastNormalMagnitude ... 360, + path: path + ".mapSpanLongitude", + ) + value.mapSpanLongitude = resolved + } + } + } +} + +extension WhereThemeAppearance.RegionPickerStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.mapCornerRadius = mapCornerRadius ?? parent.mapCornerRadius + result.selectedFillOpacity = selectedFillOpacity ?? parent.selectedFillOpacity + result.unselectedFillOpacity = unselectedFillOpacity ?? parent.unselectedFillOpacity + result.selectedStrokeOpacity = selectedStrokeOpacity ?? parent.selectedStrokeOpacity + result.unselectedStrokeOpacity = unselectedStrokeOpacity ?? parent.unselectedStrokeOpacity + result.selectedStrokeWidth = selectedStrokeWidth ?? parent.selectedStrokeWidth + result.unselectedStrokeWidth = unselectedStrokeWidth ?? parent.unselectedStrokeWidth + result.colorSwatchSize = colorSwatchSize ?? parent.colorSwatchSize + result.colorSwatchSelectionRing = colorSwatchSelectionRing ?? parent + .colorSwatchSelectionRing + result.colorSwatchMinWidth = colorSwatchMinWidth ?? parent.colorSwatchMinWidth + result.glyphTileSize = glyphTileSize ?? parent.glyphTileSize + result.glyphTileMinWidth = glyphTileMinWidth ?? parent.glyphTileMinWidth + result.glyphCornerRadius = glyphCornerRadius ?? parent.glyphCornerRadius + result.glyphSelectionStrokeWidth = glyphSelectionStrokeWidth ?? parent + .glyphSelectionStrokeWidth + result.glyphSelectedBackgroundOpacity = glyphSelectedBackgroundOpacity ?? parent + .glyphSelectedBackgroundOpacity + result.mapCenterLatitude = mapCenterLatitude ?? parent.mapCenterLatitude + result.mapCenterLongitude = mapCenterLongitude ?? parent.mapCenterLongitude + result.mapSpanLatitude = mapSpanLatitude ?? parent.mapSpanLatitude + result.mapSpanLongitude = mapSpanLongitude ?? parent.mapSpanLongitude + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.SettingsStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.SettingsStyle.swift new file mode 100644 index 000000000..8d0f6e11a --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.SettingsStyle.swift @@ -0,0 +1,73 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct SettingsStyle: Codable, Hashable, Sendable { + public var iconForeground: BThemeValue? + public var iconSize: BThemeValue? + public var iconCornerRadius: BThemeValue? + public var iconSymbolSize: BThemeValue? + public var flashAnimation: WhereThemeOptional>? + public var flashDuration: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.SettingsStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = iconForeground { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconForeground") + value.iconForeground = appearance.color(resolved) + } + if let authored = iconSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconSize") + value.iconSize = CGFloat(resolved) + } + if let authored = iconCornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconCornerRadius") + value.iconCornerRadius = CGFloat(resolved) + } + if let authored = iconSymbolSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconSymbolSize") + value.iconSymbolSize = CGFloat(resolved) + } + if let flashAnimation { + switch flashAnimation { + case .clear: value.flashAnimation = nil + case let .value(authored): + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".flashAnimation") + value.flashAnimation = resolved.animation + } + } + if let authored = flashDuration { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".flashDuration") + value.flashDuration = Duration.seconds(resolved) + } + } + } +} + +extension WhereThemeAppearance.SettingsStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.iconForeground = iconForeground ?? parent.iconForeground + result.iconSize = iconSize ?? parent.iconSize + result.iconCornerRadius = iconCornerRadius ?? parent.iconCornerRadius + result.iconSymbolSize = iconSymbolSize ?? parent.iconSymbolSize + result.flashAnimation = flashAnimation ?? parent.flashAnimation + result.flashDuration = flashDuration ?? parent.flashDuration + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Size.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Size.swift new file mode 100644 index 000000000..bbb8cf8a7 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Size.swift @@ -0,0 +1,52 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct Size: Codable, Hashable, Sendable { + public var statusIconWidth: BThemeValue? + public var launchIcon: BThemeValue? + public var launchCaptionBottomInset: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.Size, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = statusIconWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".statusIconWidth") + value.statusIconWidth = CGFloat(resolved) + } + if let authored = launchIcon { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".launchIcon") + value.launchIcon = CGFloat(resolved) + } + if let authored = launchCaptionBottomInset { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".launchCaptionBottomInset", + ) + value.launchCaptionBottomInset = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.Size: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.statusIconWidth = statusIconWidth ?? parent.statusIconWidth + result.launchIcon = launchIcon ?? parent.launchIcon + result.launchCaptionBottomInset = launchCaptionBottomInset ?? parent + .launchCaptionBottomInset + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Spacing.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Spacing.swift new file mode 100644 index 000000000..fd9915578 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Spacing.swift @@ -0,0 +1,90 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct Spacing: Codable, Hashable, Sendable { + public var xxSmall: BThemeValue? + public var xSmall: BThemeValue? + public var small: BThemeValue? + public var medium: BThemeValue? + public var regular: BThemeValue? + public var large: BThemeValue? + public var xLarge: BThemeValue? + public var xxLarge: BThemeValue? + public var xxxLarge: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.Spacing, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = xxSmall { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".xxSmall") + value.xxSmall = CGFloat(resolved) + } + if let authored = xSmall { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".xSmall") + value.xSmall = CGFloat(resolved) + } + if let authored = small { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".small") + value.small = CGFloat(resolved) + } + if let authored = medium { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".medium") + value.medium = CGFloat(resolved) + } + if let authored = regular { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".regular") + value.regular = CGFloat(resolved) + } + if let authored = large { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".large") + value.large = CGFloat(resolved) + } + if let authored = xLarge { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".xLarge") + value.xLarge = CGFloat(resolved) + } + if let authored = xxLarge { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".xxLarge") + value.xxLarge = CGFloat(resolved) + } + if let authored = xxxLarge { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".xxxLarge") + value.xxxLarge = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.Spacing: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.xxSmall = xxSmall ?? parent.xxSmall + result.xSmall = xSmall ?? parent.xSmall + result.small = small ?? parent.small + result.medium = medium ?? parent.medium + result.regular = regular ?? parent.regular + result.large = large ?? parent.large + result.xLarge = xLarge ?? parent.xLarge + result.xxLarge = xxLarge ?? parent.xxLarge + result.xxxLarge = xxxLarge ?? parent.xxxLarge + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.StampBannerStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.StampBannerStyle.swift new file mode 100644 index 000000000..5900b3350 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.StampBannerStyle.swift @@ -0,0 +1,214 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct StampBannerStyle: Codable, Hashable, Sendable { + public var layout: BThemeValue? + public var tint: BThemeValue? + public var padding: BThemeValue? + public var contentSpacing: BThemeValue? + public var titleFont: BThemeValue? + public var detailFont: BThemeValue? + public var outlineWidth: BThemeValue? + @CopyOnWrite public var rosette: WhereThemeAppearance.StampBannerStyle.Rosette? = nil + @CopyOnWrite public var ink: WhereThemeAppearance.StampBannerStyle.Ink? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.StampBannerStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + if let authored = tint { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".tint") + value.tint = appearance.color(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = contentSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".contentSpacing") + value.contentSpacing = CGFloat(resolved) + } + if let authored = titleFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleFont") + value.titleFont = resolved.font + } + if let authored = detailFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".detailFont") + value.detailFont = resolved.font + } + if let authored = outlineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineWidth") + value.outlineWidth = CGFloat(resolved) + } + try rosette?.apply( + to: &value.rosette, + traits: traits, + appearance: appearance, + path: path + ".rosette", + ) + try ink?.apply( + to: &value.ink, + traits: traits, + appearance: appearance, + path: path + ".ink", + ) + } + } +} + +extension WhereThemeAppearance.StampBannerStyle { + public struct Ink: Codable, Hashable, Sendable { + public var outlineOpacity: BThemeValue? + public var primaryRosetteOpacity: BThemeValue? + public var secondaryRosetteOpacity: BThemeValue? + public var sealOpacity: BThemeValue? + public var titleOpacity: BThemeValue? + public var detailOpacity: BThemeValue? + public var accessoryOpacity: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.StampBannerStyle.Ink, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = outlineOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".outlineOpacity") + value.outlineOpacity = resolved + } + if let authored = primaryRosetteOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".primaryRosetteOpacity") + value.primaryRosetteOpacity = resolved + } + if let authored = secondaryRosetteOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".secondaryRosetteOpacity") + value.secondaryRosetteOpacity = resolved + } + if let authored = sealOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".sealOpacity") + value.sealOpacity = resolved + } + if let authored = titleOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".titleOpacity") + value.titleOpacity = resolved + } + if let authored = detailOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".detailOpacity") + value.detailOpacity = resolved + } + if let authored = accessoryOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".accessoryOpacity") + value.accessoryOpacity = resolved + } + } + } +} + +extension WhereThemeAppearance.StampBannerStyle { + public struct Rosette: Codable, Hashable, Sendable { + public var wobble: BThemeValue? + public var lineWidth: BThemeValue? + public var primaryRingSpacing: BThemeValue? + public var secondaryRingSpacing: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.StampBannerStyle.Rosette, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = wobble { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".wobble") + value.wobble = CGFloat(resolved) + } + if let authored = lineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lineWidth") + value.lineWidth = CGFloat(resolved) + } + if let authored = primaryRingSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".primaryRingSpacing") + value.primaryRingSpacing = CGFloat(resolved) + } + if let authored = secondaryRingSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".secondaryRingSpacing") + value.secondaryRingSpacing = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.StampBannerStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + result.tint = tint ?? parent.tint + result.padding = padding ?? parent.padding + result.contentSpacing = contentSpacing ?? parent.contentSpacing + result.titleFont = titleFont ?? parent.titleFont + result.detailFont = detailFont ?? parent.detailFont + result.outlineWidth = outlineWidth ?? parent.outlineWidth + result.rosette = inheritPatch(rosette, from: parent.rosette) + result.ink = inheritPatch(ink, from: parent.ink) + return result + } +} + +extension WhereThemeAppearance.StampBannerStyle.Ink: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.outlineOpacity = outlineOpacity ?? parent.outlineOpacity + result.primaryRosetteOpacity = primaryRosetteOpacity ?? parent.primaryRosetteOpacity + result.secondaryRosetteOpacity = secondaryRosetteOpacity ?? parent.secondaryRosetteOpacity + result.sealOpacity = sealOpacity ?? parent.sealOpacity + result.titleOpacity = titleOpacity ?? parent.titleOpacity + result.detailOpacity = detailOpacity ?? parent.detailOpacity + result.accessoryOpacity = accessoryOpacity ?? parent.accessoryOpacity + return result + } +} + +extension WhereThemeAppearance.StampBannerStyle.Rosette: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.wobble = wobble ?? parent.wobble + result.lineWidth = lineWidth ?? parent.lineWidth + result.primaryRingSpacing = primaryRingSpacing ?? parent.primaryRingSpacing + result.secondaryRingSpacing = secondaryRingSpacing ?? parent.secondaryRingSpacing + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.ThemePickerStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.ThemePickerStyle.swift new file mode 100644 index 000000000..875a6d94f --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.ThemePickerStyle.swift @@ -0,0 +1,47 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct ThemePickerStyle: Codable, Hashable, Sendable { + public var layout: BThemeValue? + public var spacing: BThemeValue? + public var minimumOptionWidth: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.ThemePickerStyle, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + if let authored = minimumOptionWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".minimumOptionWidth") + value.minimumOptionWidth = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.ThemePickerStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + result.spacing = spacing ?? parent.spacing + result.minimumOptionWidth = minimumOptionWidth ?? parent.minimumOptionWidth + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.TimelineStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.TimelineStyle.swift new file mode 100644 index 000000000..61611d824 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.TimelineStyle.swift @@ -0,0 +1,637 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct TimelineStyle: Codable, Hashable, Sendable { + @CopyOnWrite public var flight: WhereThemeAppearance.TimelineStyle.Flight? = nil + @CopyOnWrite public var overview: WhereThemeAppearance.TimelineStyle.Overview? = nil + @CopyOnWrite public var ribbon: WhereThemeAppearance.TimelineStyle.Ribbon? = nil + @CopyOnWrite public var rail: WhereThemeAppearance.TimelineStyle.Rail? = nil + @CopyOnWrite public var row: WhereThemeAppearance.TimelineStyle.Row? = nil + @CopyOnWrite public var planned: WhereThemeAppearance.TimelineStyle.Planned? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet.TimelineStyle, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try flight?.apply( + to: &value.flight, + traits: traits, + appearance: appearance, + path: path + ".flight", + ) + try overview?.apply( + to: &value.overview, + traits: traits, + appearance: appearance, + path: path + ".overview", + ) + try ribbon?.apply( + to: &value.ribbon, + traits: traits, + appearance: appearance, + path: path + ".ribbon", + ) + try rail?.apply( + to: &value.rail, + traits: traits, + appearance: appearance, + path: path + ".rail", + ) + try row?.apply( + to: &value.row, + traits: traits, + appearance: appearance, + path: path + ".row", + ) + try planned?.apply( + to: &value.planned, + traits: traits, + appearance: appearance, + path: path + ".planned", + ) + } + } +} + +extension WhereThemeAppearance.TimelineStyle { + public struct Flight: Codable, Hashable, Sendable { + public var iconFont: BThemeValue? + public var verticalPadding: BThemeValue? + public var labelSpacing: BThemeValue? + public var tint: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.TimelineStyle.Flight, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = iconFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".iconFont") + value.iconFont = resolved.font + } + if let authored = verticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalPadding") + value.verticalPadding = CGFloat(resolved) + } + if let authored = labelSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".labelSpacing") + value.labelSpacing = CGFloat(resolved) + } + if let authored = tint { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".tint") + value.tint = appearance.color(resolved) + } + } + } +} + +extension WhereThemeAppearance.TimelineStyle { + public struct Overview: Codable, Hashable, Sendable { + public var spacing: BThemeValue? + public var padding: BThemeValue? + public var cornerRadius: BThemeValue? + public var background: BThemeValue? + public var border: BThemeValue? + public var borderWidth: BThemeValue? + public var yearFont: BThemeValue? + public var pinsToViewport: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.TimelineStyle.Overview, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + if let authored = padding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".padding") + value.padding = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = background { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".background") + value.background = appearance.color(resolved) + } + if let authored = border { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".border") + value.border = appearance.color(resolved) + } + if let authored = borderWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".borderWidth") + value.borderWidth = CGFloat(resolved) + } + if let authored = yearFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".yearFont") + value.yearFont = resolved.font + } + if let authored = pinsToViewport { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".pinsToViewport") + value.pinsToViewport = resolved + } + } + } +} + +extension WhereThemeAppearance.TimelineStyle { + public struct Planned: Codable, Hashable, Sendable { + public var fillOpacity: BThemeValue? + public var borderOpacity: BThemeValue? + public var hatchOpacity: BThemeValue? + public var hatchSpacing: BThemeValue? + public var hatchLineWidth: BThemeValue? + public var labelOpacity: BThemeValue? + public var transitionHeight: BThemeValue? + public var joinedBaseHeight: BThemeValue? + public var joinedVerticalPadding: BThemeValue? + public var joinedLabelSpacing: BThemeValue? + public var joinedCountHorizontalPadding: BThemeValue? + public var joinedCountVerticalPadding: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.TimelineStyle.Planned, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = fillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".fillOpacity") + value.fillOpacity = resolved + } + if let authored = borderOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".borderOpacity") + value.borderOpacity = resolved + } + if let authored = hatchOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".hatchOpacity") + value.hatchOpacity = resolved + } + if let authored = hatchSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".hatchSpacing") + value.hatchSpacing = CGFloat(resolved) + } + if let authored = hatchLineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".hatchLineWidth") + value.hatchLineWidth = CGFloat(resolved) + } + if let authored = labelOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".labelOpacity") + value.labelOpacity = resolved + } + if let authored = transitionHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".transitionHeight") + value.transitionHeight = CGFloat(resolved) + } + if let authored = joinedBaseHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".joinedBaseHeight") + value.joinedBaseHeight = CGFloat(resolved) + } + if let authored = joinedVerticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".joinedVerticalPadding") + value.joinedVerticalPadding = CGFloat(resolved) + } + if let authored = joinedLabelSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".joinedLabelSpacing") + value.joinedLabelSpacing = CGFloat(resolved) + } + if let authored = joinedCountHorizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".joinedCountHorizontalPadding", + ) + value.joinedCountHorizontalPadding = CGFloat(resolved) + } + if let authored = joinedCountVerticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate( + resolved, + path: path + ".joinedCountVerticalPadding", + ) + value.joinedCountVerticalPadding = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.TimelineStyle { + public struct Rail: Codable, Hashable, Sendable { + public var lineWidth: BThemeValue? + public var toCardSpacing: BThemeValue? + public var nodeSize: BThemeValue? + public var nodeEmojiFont: BThemeValue? + public var nodeFillOpacity: BThemeValue? + public var nodeStrokeWidth: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.TimelineStyle.Rail, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = lineWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".lineWidth") + value.lineWidth = CGFloat(resolved) + } + if let authored = toCardSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".toCardSpacing") + value.toCardSpacing = CGFloat(resolved) + } + if let authored = nodeSize { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".nodeSize") + value.nodeSize = CGFloat(resolved) + } + if let authored = nodeEmojiFont { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".nodeEmojiFont") + value.nodeEmojiFont = resolved.font + } + if let authored = nodeFillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".nodeFillOpacity") + value.nodeFillOpacity = resolved + } + if let authored = nodeStrokeWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".nodeStrokeWidth") + value.nodeStrokeWidth = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.TimelineStyle { + public struct Ribbon: Codable, Hashable, Sendable { + public var monthLabelSpacing: BThemeValue? + public var height: BThemeValue? + public var track: BThemeValue? + public var border: BThemeValue? + public var borderWidth: BThemeValue? + public var regionSpacing: BThemeValue? + public var regionLabelSpacing: BThemeValue? + public var separatesRegions: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.TimelineStyle.Ribbon, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = monthLabelSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".monthLabelSpacing") + value.monthLabelSpacing = CGFloat(resolved) + } + if let authored = height { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".height") + value.height = CGFloat(resolved) + } + if let authored = track { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".track") + value.track = appearance.color(resolved) + } + if let authored = border { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".border") + value.border = appearance.color(resolved) + } + if let authored = borderWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".borderWidth") + value.borderWidth = CGFloat(resolved) + } + if let authored = regionSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".regionSpacing") + value.regionSpacing = CGFloat(resolved) + } + if let authored = regionLabelSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".regionLabelSpacing") + value.regionLabelSpacing = CGFloat(resolved) + } + if let authored = separatesRegions { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".separatesRegions") + value.separatesRegions = resolved + } + } + } +} + +extension WhereThemeAppearance.TimelineStyle { + public struct Row: Codable, Hashable, Sendable { + @CopyOnWrite public var singleDay: WhereThemeAppearance.TimelineStyle.Row.Sizing? = nil + public var spacing: BThemeValue? + public var labelSpacing: BThemeValue? + public var gap: BThemeValue? + public var baseHeight: BThemeValue? + public var yearScaleHeight: BThemeValue? + public var horizontalPadding: BThemeValue? + public var verticalPadding: BThemeValue? + public var cornerRadius: BThemeValue? + public var fillOpacity: BThemeValue? + public var borderOpacity: BThemeValue? + public var borderWidth: BThemeValue? + public var countHorizontalPadding: BThemeValue? + public var countVerticalPadding: BThemeValue? + public var countFillOpacity: BThemeValue? + public var stacksDayCount: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.TimelineStyle.Row, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try singleDay?.apply( + to: &value.singleDay, + traits: traits, + appearance: appearance, + path: path + ".singleDay", + ) + if let authored = spacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".spacing") + value.spacing = CGFloat(resolved) + } + if let authored = labelSpacing { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".labelSpacing") + value.labelSpacing = CGFloat(resolved) + } + if let authored = gap { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".gap") + value.gap = CGFloat(resolved) + } + if let authored = baseHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".baseHeight") + value.baseHeight = CGFloat(resolved) + } + if let authored = yearScaleHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".yearScaleHeight") + value.yearScaleHeight = CGFloat(resolved) + } + if let authored = horizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".horizontalPadding") + value.horizontalPadding = CGFloat(resolved) + } + if let authored = verticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalPadding") + value.verticalPadding = CGFloat(resolved) + } + if let authored = cornerRadius { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".cornerRadius") + value.cornerRadius = CGFloat(resolved) + } + if let authored = fillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".fillOpacity") + value.fillOpacity = resolved + } + if let authored = borderOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".borderOpacity") + value.borderOpacity = resolved + } + if let authored = borderWidth { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".borderWidth") + value.borderWidth = CGFloat(resolved) + } + if let authored = countHorizontalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".countHorizontalPadding") + value.countHorizontalPadding = CGFloat(resolved) + } + if let authored = countVerticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".countVerticalPadding") + value.countVerticalPadding = CGFloat(resolved) + } + if let authored = countFillOpacity { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".countFillOpacity") + value.countFillOpacity = resolved + } + if let authored = stacksDayCount { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".stacksDayCount") + value.stacksDayCount = resolved + } + } + } +} + +extension WhereThemeAppearance.TimelineStyle.Row { + public struct Sizing: Codable, Hashable, Sendable { + public var baseHeight: BThemeValue? + public var verticalPadding: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.TimelineStyle.Row.Sizing, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = baseHeight { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".baseHeight") + value.baseHeight = CGFloat(resolved) + } + if let authored = verticalPadding { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".verticalPadding") + value.verticalPadding = CGFloat(resolved) + } + } + } +} + +extension WhereThemeAppearance.TimelineStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.flight = inheritPatch(flight, from: parent.flight) + result.overview = inheritPatch(overview, from: parent.overview) + result.ribbon = inheritPatch(ribbon, from: parent.ribbon) + result.rail = inheritPatch(rail, from: parent.rail) + result.row = inheritPatch(row, from: parent.row) + result.planned = inheritPatch(planned, from: parent.planned) + return result + } +} + +extension WhereThemeAppearance.TimelineStyle.Flight: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.iconFont = iconFont ?? parent.iconFont + result.verticalPadding = verticalPadding ?? parent.verticalPadding + result.labelSpacing = labelSpacing ?? parent.labelSpacing + result.tint = tint ?? parent.tint + return result + } +} + +extension WhereThemeAppearance.TimelineStyle.Overview: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.spacing = spacing ?? parent.spacing + result.padding = padding ?? parent.padding + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.background = background ?? parent.background + result.border = border ?? parent.border + result.borderWidth = borderWidth ?? parent.borderWidth + result.yearFont = yearFont ?? parent.yearFont + result.pinsToViewport = pinsToViewport ?? parent.pinsToViewport + return result + } +} + +extension WhereThemeAppearance.TimelineStyle.Planned: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.fillOpacity = fillOpacity ?? parent.fillOpacity + result.borderOpacity = borderOpacity ?? parent.borderOpacity + result.hatchOpacity = hatchOpacity ?? parent.hatchOpacity + result.hatchSpacing = hatchSpacing ?? parent.hatchSpacing + result.hatchLineWidth = hatchLineWidth ?? parent.hatchLineWidth + result.labelOpacity = labelOpacity ?? parent.labelOpacity + result.transitionHeight = transitionHeight ?? parent.transitionHeight + result.joinedBaseHeight = joinedBaseHeight ?? parent.joinedBaseHeight + result.joinedVerticalPadding = joinedVerticalPadding ?? parent.joinedVerticalPadding + result.joinedLabelSpacing = joinedLabelSpacing ?? parent.joinedLabelSpacing + result.joinedCountHorizontalPadding = joinedCountHorizontalPadding ?? parent + .joinedCountHorizontalPadding + result.joinedCountVerticalPadding = joinedCountVerticalPadding ?? parent + .joinedCountVerticalPadding + return result + } +} + +extension WhereThemeAppearance.TimelineStyle.Rail: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.lineWidth = lineWidth ?? parent.lineWidth + result.toCardSpacing = toCardSpacing ?? parent.toCardSpacing + result.nodeSize = nodeSize ?? parent.nodeSize + result.nodeEmojiFont = nodeEmojiFont ?? parent.nodeEmojiFont + result.nodeFillOpacity = nodeFillOpacity ?? parent.nodeFillOpacity + result.nodeStrokeWidth = nodeStrokeWidth ?? parent.nodeStrokeWidth + return result + } +} + +extension WhereThemeAppearance.TimelineStyle.Ribbon: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.monthLabelSpacing = monthLabelSpacing ?? parent.monthLabelSpacing + result.height = height ?? parent.height + result.track = track ?? parent.track + result.border = border ?? parent.border + result.borderWidth = borderWidth ?? parent.borderWidth + result.regionSpacing = regionSpacing ?? parent.regionSpacing + result.regionLabelSpacing = regionLabelSpacing ?? parent.regionLabelSpacing + result.separatesRegions = separatesRegions ?? parent.separatesRegions + return result + } +} + +extension WhereThemeAppearance.TimelineStyle.Row: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.singleDay = inheritPatch(singleDay, from: parent.singleDay) + result.spacing = spacing ?? parent.spacing + result.labelSpacing = labelSpacing ?? parent.labelSpacing + result.gap = gap ?? parent.gap + result.baseHeight = baseHeight ?? parent.baseHeight + result.yearScaleHeight = yearScaleHeight ?? parent.yearScaleHeight + result.horizontalPadding = horizontalPadding ?? parent.horizontalPadding + result.verticalPadding = verticalPadding ?? parent.verticalPadding + result.cornerRadius = cornerRadius ?? parent.cornerRadius + result.fillOpacity = fillOpacity ?? parent.fillOpacity + result.borderOpacity = borderOpacity ?? parent.borderOpacity + result.borderWidth = borderWidth ?? parent.borderWidth + result.countHorizontalPadding = countHorizontalPadding ?? parent.countHorizontalPadding + result.countVerticalPadding = countVerticalPadding ?? parent.countVerticalPadding + result.countFillOpacity = countFillOpacity ?? parent.countFillOpacity + result.stacksDayCount = stacksDayCount ?? parent.stacksDayCount + return result + } +} + +extension WhereThemeAppearance.TimelineStyle.Row.Sizing: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.baseHeight = baseHeight ?? parent.baseHeight + result.verticalPadding = verticalPadding ?? parent.verticalPadding + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Typography.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Typography.swift new file mode 100644 index 000000000..35828586d --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.Typography.swift @@ -0,0 +1,48 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct Typography: Codable, Hashable, Sendable { + public var onboardingIcon: BThemeValue? + public var widgetHeroRegion: BThemeValue? + public var widgetTotalNumber: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.Typography, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path: String, + ) throws { + if let authored = onboardingIcon { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".onboardingIcon") + value.onboardingIcon = resolved.font + } + if let authored = widgetHeroRegion { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".widgetHeroRegion") + value.widgetHeroRegion = resolved.font + } + if let authored = widgetTotalNumber { + let resolved = authored.resolve(mode: traits.mode) + try WhereThemeValidation.validate(resolved, path: path + ".widgetTotalNumber") + value.widgetTotalNumber = resolved.font + } + } + } +} + +extension WhereThemeAppearance.Typography: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.onboardingIcon = onboardingIcon ?? parent.onboardingIcon + result.widgetHeroRegion = widgetHeroRegion ?? parent.widgetHeroRegion + result.widgetTotalNumber = widgetTotalNumber ?? parent.widgetTotalNumber + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.YearModePickerStyle.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.YearModePickerStyle.swift new file mode 100644 index 000000000..a9ef8131b --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.YearModePickerStyle.swift @@ -0,0 +1,33 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +extension WhereThemeAppearance { + public struct YearModePickerStyle: Codable, Hashable, Sendable { + public var layout: BThemeValue? + + public init() {} + + func apply( + to value: inout WhereStylesheet.YearModePickerStyle, + traits: BTraits, + appearance _: ViewDocumentAppearance, + path _: String, + ) throws { + if let authored = layout { + let resolved = authored.resolve(mode: traits.mode) + value.layout = resolved.native + } + } + } +} + +extension WhereThemeAppearance.YearModePickerStyle: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.layout = layout ?? parent.layout + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeAppearance.swift b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.swift new file mode 100644 index 000000000..71c45a22e --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeAppearance.swift @@ -0,0 +1,260 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +public struct WhereThemeAppearance: Codable, Hashable, Sendable { + @CopyOnWrite public var spacing: WhereThemeAppearance.Spacing? = nil + @CopyOnWrite public var size: WhereThemeAppearance.Size? = nil + @CopyOnWrite public var card: WhereThemeAppearance.CardStyles? = nil + @CopyOnWrite public var locationsBackground: WhereThemeAppearance + .LocationsBackgroundStyle? = nil + @CopyOnWrite public var locationCardStack: WhereThemeAppearance.LocationCardStackStyle? = nil + @CopyOnWrite public var locationWelcome: WhereThemeAppearance.LocationWelcomeStyle? = nil + @CopyOnWrite public var flightStatus: WhereThemeAppearance.FlightStatusStyle? = nil + @CopyOnWrite public var flightReviewPoint: WhereThemeAppearance.FlightReviewPointStyle? = nil + @CopyOnWrite public var calendar: WhereThemeAppearance.CalendarStyle? = nil + @CopyOnWrite public var appIcon: WhereThemeAppearance.AppIconStyle? = nil + @CopyOnWrite public var timeline: WhereThemeAppearance.TimelineStyle? = nil + @CopyOnWrite public var regionMap: WhereThemeAppearance.RegionMapStyle? = nil + @CopyOnWrite public var regionPicker: WhereThemeAppearance.RegionPickerStyle? = nil + @CopyOnWrite public var evidence: WhereThemeAppearance.EvidenceStyle? = nil + @CopyOnWrite public var locationForecast: WhereThemeAppearance.LocationForecastStyle? = nil + @CopyOnWrite public var palette: WhereThemeAppearance.Palette? = nil + @CopyOnWrite public var motion: WhereThemeAppearance.Motion? = nil + @CopyOnWrite public var launch: WhereThemeAppearance.LaunchStyle? = nil + @CopyOnWrite public var typography: WhereThemeAppearance.Typography? = nil + @CopyOnWrite public var themePicker: WhereThemeAppearance.ThemePickerStyle? = nil + @CopyOnWrite public var yearModePicker: WhereThemeAppearance.YearModePickerStyle? = nil + @CopyOnWrite public var settings: WhereThemeAppearance.SettingsStyle? = nil + @CopyOnWrite public var featureDiscovery: WhereThemeAppearance.FeatureDiscoveryStyle? = nil + @CopyOnWrite public var passportSeal: WhereThemeAppearance.PassportSealStyle? = nil + @CopyOnWrite public var privacyPassportCard: WhereThemeAppearance + .PrivacyPassportCardStyle? = nil + @CopyOnWrite public var openSourceStamp: WhereThemeAppearance.StampBannerStyle? = nil + @CopyOnWrite public var plannedStayWarningStamp: WhereThemeAppearance.StampBannerStyle? = nil + @CopyOnWrite public var developerOverlay: WhereThemeAppearance.DeveloperOverlayStyle? = nil + + public init() {} + + func apply( + to value: inout WhereStylesheet, + traits: BTraits, + appearance: ViewDocumentAppearance, + path: String, + ) throws { + try spacing?.apply( + to: &value.spacing, + traits: traits, + appearance: appearance, + path: path + ".spacing", + ) + try size?.apply( + to: &value.size, + traits: traits, + appearance: appearance, + path: path + ".size", + ) + try card?.apply( + to: &value.card, + traits: traits, + appearance: appearance, + path: path + ".card", + ) + try locationsBackground?.apply( + to: &value.locationsBackground, + traits: traits, + appearance: appearance, + path: path + ".locationsBackground", + ) + try locationCardStack?.apply( + to: &value.locationCardStack, + traits: traits, + appearance: appearance, + path: path + ".locationCardStack", + ) + try locationWelcome?.apply( + to: &value.locationWelcome, + traits: traits, + appearance: appearance, + path: path + ".locationWelcome", + ) + try flightStatus?.apply( + to: &value.flightStatus, + traits: traits, + appearance: appearance, + path: path + ".flightStatus", + ) + try flightReviewPoint?.apply( + to: &value.flightReviewPoint, + traits: traits, + appearance: appearance, + path: path + ".flightReviewPoint", + ) + try calendar?.apply( + to: &value.calendar, + traits: traits, + appearance: appearance, + path: path + ".calendar", + ) + try appIcon?.apply( + to: &value.appIcon, + traits: traits, + appearance: appearance, + path: path + ".appIcon", + ) + try timeline?.apply( + to: &value.timeline, + traits: traits, + appearance: appearance, + path: path + ".timeline", + ) + try regionMap?.apply( + to: &value.regionMap, + traits: traits, + appearance: appearance, + path: path + ".regionMap", + ) + try regionPicker?.apply( + to: &value.regionPicker, + traits: traits, + appearance: appearance, + path: path + ".regionPicker", + ) + try evidence?.apply( + to: &value.evidence, + traits: traits, + appearance: appearance, + path: path + ".evidence", + ) + try locationForecast?.apply( + to: &value.locationForecast, + traits: traits, + appearance: appearance, + path: path + ".locationForecast", + ) + try palette?.apply( + to: &value.palette, + traits: traits, + appearance: appearance, + path: path + ".palette", + ) + try motion?.apply( + to: &value.motion, + traits: traits, + appearance: appearance, + path: path + ".motion", + ) + try launch?.apply( + to: &value.launch, + traits: traits, + appearance: appearance, + path: path + ".launch", + ) + try typography?.apply( + to: &value.typography, + traits: traits, + appearance: appearance, + path: path + ".typography", + ) + try themePicker?.apply( + to: &value.themePicker, + traits: traits, + appearance: appearance, + path: path + ".themePicker", + ) + try yearModePicker?.apply( + to: &value.yearModePicker, + traits: traits, + appearance: appearance, + path: path + ".yearModePicker", + ) + try settings?.apply( + to: &value.settings, + traits: traits, + appearance: appearance, + path: path + ".settings", + ) + try featureDiscovery?.apply( + to: &value.featureDiscovery, + traits: traits, + appearance: appearance, + path: path + ".featureDiscovery", + ) + try passportSeal?.apply( + to: &value.passportSeal, + traits: traits, + appearance: appearance, + path: path + ".passportSeal", + ) + try privacyPassportCard?.apply( + to: &value.privacyPassportCard, + traits: traits, + appearance: appearance, + path: path + ".privacyPassportCard", + ) + try openSourceStamp?.apply( + to: &value.openSourceStamp, + traits: traits, + appearance: appearance, + path: path + ".openSourceStamp", + ) + try plannedStayWarningStamp?.apply( + to: &value.plannedStayWarningStamp, + traits: traits, + appearance: appearance, + path: path + ".plannedStayWarningStamp", + ) + try developerOverlay?.apply( + to: &value.developerOverlay, + traits: traits, + appearance: appearance, + path: path + ".developerOverlay", + ) + } +} + +extension WhereThemeAppearance: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + guard let parent else { return self } + var result = self + result.spacing = inheritPatch(spacing, from: parent.spacing) + result.size = inheritPatch(size, from: parent.size) + result.card = inheritPatch(card, from: parent.card) + result.locationsBackground = inheritPatch( + locationsBackground, + from: parent.locationsBackground, + ) + result.locationCardStack = inheritPatch(locationCardStack, from: parent.locationCardStack) + result.locationWelcome = inheritPatch(locationWelcome, from: parent.locationWelcome) + result.flightStatus = inheritPatch(flightStatus, from: parent.flightStatus) + result.flightReviewPoint = inheritPatch(flightReviewPoint, from: parent.flightReviewPoint) + result.calendar = inheritPatch(calendar, from: parent.calendar) + result.appIcon = inheritPatch(appIcon, from: parent.appIcon) + result.timeline = inheritPatch(timeline, from: parent.timeline) + result.regionMap = inheritPatch(regionMap, from: parent.regionMap) + result.regionPicker = inheritPatch(regionPicker, from: parent.regionPicker) + result.evidence = inheritPatch(evidence, from: parent.evidence) + result.locationForecast = inheritPatch(locationForecast, from: parent.locationForecast) + result.palette = inheritPatch(palette, from: parent.palette) + result.motion = inheritPatch(motion, from: parent.motion) + result.launch = inheritPatch(launch, from: parent.launch) + result.typography = inheritPatch(typography, from: parent.typography) + result.themePicker = inheritPatch(themePicker, from: parent.themePicker) + result.yearModePicker = inheritPatch(yearModePicker, from: parent.yearModePicker) + result.settings = inheritPatch(settings, from: parent.settings) + result.featureDiscovery = inheritPatch(featureDiscovery, from: parent.featureDiscovery) + result.passportSeal = inheritPatch(passportSeal, from: parent.passportSeal) + result.privacyPassportCard = inheritPatch( + privacyPassportCard, + from: parent.privacyPassportCard, + ) + result.openSourceStamp = inheritPatch(openSourceStamp, from: parent.openSourceStamp) + result.plannedStayWarningStamp = inheritPatch( + plannedStayWarningStamp, + from: parent.plannedStayWarningStamp, + ) + result.developerOverlay = inheritPatch(developerOverlay, from: parent.developerOverlay) + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeArtwork.swift b/Where/WhereUI/Sources/Themes/WhereThemeArtwork.swift new file mode 100644 index 000000000..97c691a3d --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeArtwork.swift @@ -0,0 +1,35 @@ +import BroadwayCore +import BroadwayUI +import ViewDocumentCore + +/// Read-only illustrated content. Native labels, controls, and geographic data remain in the host. +public struct WhereThemeArtwork: Codable, Hashable, Sendable, Identifiable { + public struct ID: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + public var id: ID + public var document: ViewDocument + public var placement: ViewArtworkPlacement + public var role: BSurfaceLayerRole + public var opacity: BThemeValue + + public init( + id: ID, + document: ViewDocument, + placement: ViewArtworkPlacement, + role: BSurfaceLayerRole, + opacity: BThemeValue, + ) { + self.id = id; self.document = document; self.placement = placement + self.role = role; self.opacity = opacity + } +} + +/// Native passport marks retain their component's document-owned geometry and geographic inputs. +public enum WhereThemeNativeArtwork: String, Codable, CaseIterable, Hashable, Sendable { + case rosette, microprint, watermark, stamp, regionPattern +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeArtworkSpace.swift b/Where/WhereUI/Sources/Themes/WhereThemeArtworkSpace.swift new file mode 100644 index 000000000..a8a6e7f57 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeArtworkSpace.swift @@ -0,0 +1,18 @@ +import SwiftUI + +/// Reserves paint outside the interactive or accessible child. Apply after button styling +/// and accessibility grouping, never inside a button label. +private struct WhereThemeArtworkSpace: ViewModifier { + let role: WhereThemeComponents.Role + @Environment(\.whereComponentStylesheet) private var stylesheet + + func body(content: Content) -> some View { + content.padding(stylesheet[role].outsets) + } +} + +extension View { + func whereThemeArtworkSpace(_ role: WhereThemeComponents.Role) -> some View { + modifier(WhereThemeArtworkSpace(role: role)) + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeBlend.swift b/Where/WhereUI/Sources/Themes/WhereThemeBlend.swift new file mode 100644 index 000000000..f22ea79a0 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeBlend.swift @@ -0,0 +1,32 @@ +import SwiftUI + +public enum WhereThemeBlend: String, Codable, Hashable, Sendable { + case normal, multiply, screen, overlay, darken, lighten, colorDodge, colorBurn + case softLight, hardLight, difference, exclusion, hue, saturation, color, luminosity + case sourceAtop, destinationOver, destinationOut, plusDarker, plusLighter + var blendMode: BlendMode { + switch self { + case .normal: .normal + case .multiply: .multiply + case .screen: .screen + case .overlay: .overlay + case .darken: .darken + case .lighten: .lighten + case .colorDodge: .colorDodge + case .colorBurn: .colorBurn + case .softLight: .softLight + case .hardLight: .hardLight + case .difference: .difference + case .exclusion: .exclusion + case .hue: .hue + case .saturation: .saturation + case .color: .color + case .luminosity: .luminosity + case .sourceAtop: .sourceAtop + case .destinationOver: .destinationOver + case .destinationOut: .destinationOut + case .plusDarker: .plusDarker + case .plusLighter: .plusLighter + } + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeCatalog.swift b/Where/WhereUI/Sources/Themes/WhereThemeCatalog.swift new file mode 100644 index 000000000..deec8f2d1 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeCatalog.swift @@ -0,0 +1,150 @@ +import BroadwayCore +import BroadwayUI +import Foundation +import ViewDocumentCore +import ViewDocumentUI + +/// Resolves a supplied, finite set of themes before any document reaches a view hierarchy. +public struct WhereThemeCatalog { + private let documents: [WhereThemeDocument.ID: WhereThemeDocument] + + public init(documents: [WhereThemeDocument]) throws { + guard documents.count <= 64, Set(documents.map(\.id)).count == documents.count else { + throw WhereThemeValidationError( + path: "catalog", + reason: "Duplicate theme identities or too many documents", + ) + } + var totalBytes = 0 + for document in documents { + guard document.schemaVersion == 1 || document.schemaVersion == 2 else { + throw WhereThemeValidationError( + path: "schemaVersion", + reason: "Unsupported theme schema version", + ) + } + guard !document.id.rawValue.isEmpty, document.id.rawValue.utf8.count <= 128 else { + throw WhereThemeValidationError(path: "id", reason: "Invalid theme identity") + } + let bytes = try JSONEncoder().encode(document).count + guard bytes <= ViewDocumentLimits.standard.bytes else { throw WhereThemeValidationError( + path: "$", + reason: "Theme exceeds byte limit", + ) } + totalBytes += bytes + } + guard totalBytes <= 4 * ViewDocumentLimits.standard.bytes + else { throw WhereThemeValidationError( + path: "catalog", + reason: "Catalog exceeds byte limit", + ) } + self.documents = try Dictionary(uniqueKeysWithValues: documents.map { try ( + $0.id, + $0.converted() + ) }) + } + + public func replacing(_ document: WhereThemeDocument) throws -> Self { + try .init(documents: documents.values.filter { $0.id != document.id } + [document]) + } + + public func resolve(_ id: WhereThemeDocument.ID) throws -> ResolvedWhereTheme { + var chain: [WhereThemeDocument] = [] + var visited: Set = [] + var next: WhereThemeDocument.ID? = id + while let current = next { + guard visited.insert(current).inserted, chain.count < 16 else { + throw WhereThemeValidationError( + path: "parent", + reason: "Theme inheritance cycle or excessive depth", + ) + } + guard let document = documents[current] else { + throw WhereThemeValidationError( + path: "parent", + reason: "Missing theme \(current.rawValue)", + ) + } + chain.append(document) + next = document.parent + } + var parent: ResolvedWhereTheme? + for document in chain.reversed() { + parent = try resolve(document, parent: parent) + } + guard let parent + else { throw WhereThemeValidationError(path: "id", reason: "Missing theme") } + return parent + } + + private func resolve( + _ document: WhereThemeDocument, + parent: ResolvedWhereTheme?, + ) throws -> ResolvedWhereTheme { + let id = document.id + let values = try document.values.inheriting(parent?.values) + let components = try document.components.assigningLiteralOwnership(document.appearance) + .inheriting(parent?.components) + var appearance = document.appearance.inheriting(parent?.appearance) + appearance = try components.applyingReferences(to: appearance, values: values) + try appearance.requireComplete(path: "appearance") + try components.requireComplete(path: "components") + try components.validateCapabilities() + // Validate every named value, including values not used by the selected components. + for mode: BMode in [.light, .dark] { + var traits = BTraits() + traits.mode = mode + let rendererAppearance = BViewDocumentStylesheet(traits: traits).appearance + for entry in values.colors ?? [] { + try WhereThemeValidation.validate( + entry.value.resolve(mode: mode), + path: "values.colors.\(entry.id.rawValue)", + ) + } + for entry in values.fonts ?? [] { + try WhereThemeValidation.validate( + entry.value.resolve(mode: mode), + path: "values.fonts.\(entry.id.rawValue)", + ) + } + for entry in values.surfaces ?? [] { + _ = try values.surface(entry.id).resolve(mode: mode).resolve( + mode: mode, + appearance: rendererAppearance, + base: .reflective, + ) + } + var sheet = WhereStylesheet() + try appearance.apply( + to: &sheet, + traits: traits, + appearance: rendererAppearance, + path: "appearance", + ) + } + return .init( + id: id, + values: values, + appearance: appearance, + components: components, + fallback: id == .flat ? .flat : parent?.fallback ?? .standard, + ) + } + + @MainActor + public func prepare( + _ id: WhereThemeDocument.ID, + assets: ViewDocumentAssets, + ) throws -> PreparedWhereThemeDocument { + guard let document = documents[id] else { throw WhereThemeValidationError( + path: "id", + reason: "Missing theme", + ) } + let validated = try ValidatedWhereThemeDocument( + document: document, + resolved: resolve(id), + resources: assets.resources, + ) + return try PreparedWhereThemeDocument(validated: validated, assets: assets) + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeComponents.Capabilities.swift b/Where/WhereUI/Sources/Themes/WhereThemeComponents.Capabilities.swift new file mode 100644 index 000000000..580617b3d --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeComponents.Capabilities.swift @@ -0,0 +1,43 @@ +extension WhereThemeComponents { + /// Reject fields a native host cannot honor instead of accepting invisible overrides. + func validateCapabilities() throws { + for role in Role.allCases { + guard let component = self[role] else { continue } + let nativeSources: Set = switch role { + case .locations, .elsewhere: + [.rosette, .regionPattern] + case .regularCard, .compactCard, .elsewhereCard, .summary, .welcome: + [.rosette, .microprint, .watermark, .stamp] + case .forecastPanel: + [.rosette, .microprint, .stamp] + case .privacy: + [.rosette, .stamp] + case .year, .settings, .sheet, .calendarMonth, .timelineEntry, .section: + [] + } + let authored = component.nativeArtwork ?? [] + guard Set(authored).count == authored.count, + Set(authored).isSubset(of: nativeSources) + else { throw unsupported(role, "nativeArtwork") } + switch role { + case .locations, .elsewhere, .year, .settings, .sheet, .section: + if case .value = component.surface { throw unsupported(role, "surface") } + if case .value = component.heading { throw unsupported(role, "heading") } + case .regularCard, .compactCard, .elsewhereCard, .summary, .forecastPanel, + .calendarMonth, .timelineEntry, .welcome, .privacy: break + } + for artwork in component.artwork ?? [] { + if role == .section || role == .timelineEntry, + artwork.placement.plane != .interior || artwork.placement.outsets != .zero + { throw unsupported(role, "artwork.placement") } + } + } + } + + private func unsupported(_ role: Role, _ field: String) -> WhereThemeValidationError { + .init( + path: "components.\(role.rawValue).\(field)", + reason: "Unsupported field for this native component", + ) + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeComponents.swift b/Where/WhereUI/Sources/Themes/WhereThemeComponents.swift new file mode 100644 index 000000000..2d430e15e --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeComponents.swift @@ -0,0 +1,139 @@ +/// Sparse component definitions. Omission inherits; clear removes an optional visual part. +public struct WhereThemeComponent: Codable, Hashable, Sendable, WhereThemePatch { + public var surface: WhereThemeOptional? + public var background: WhereThemeOptional? + public var foreground: WhereThemeOptional? + public var heading: WhereThemeOptional? + public var body: WhereThemeOptional? + public var nativeArtwork: [WhereThemeNativeArtwork]? + public var artwork: [WhereThemeArtwork]? + + public init() {} + + func inheriting(_ parent: Self?) -> Self { + var result = self + result.surface = surface ?? parent?.surface + result.background = background ?? parent?.background + result.foreground = foreground ?? parent?.foreground + result.heading = heading ?? parent?.heading + result.body = body ?? parent?.body + result.nativeArtwork = nativeArtwork ?? parent?.nativeArtwork + result.artwork = artwork ?? parent?.artwork + return result + } +} + +/// Component identity is a finite, typed contract between the document and native Where views. +public struct WhereThemeComponents: Codable, Hashable, Sendable, WhereThemePatch { + public enum Role: String, Codable, CaseIterable, Hashable, Sendable { + case locations, elsewhere, year, settings, sheet + case regularCard, compactCard, elsewhereCard, summary, forecastPanel + case calendarMonth, timelineEntry, section, welcome, privacy + } + + public var locations: WhereThemeComponent? + public var elsewhere: WhereThemeComponent? + public var year: WhereThemeComponent? + public var settings: WhereThemeComponent? + public var sheet: WhereThemeComponent? + public var regularCard: WhereThemeComponent? + public var compactCard: WhereThemeComponent? + public var elsewhereCard: WhereThemeComponent? + public var summary: WhereThemeComponent? + public var forecastPanel: WhereThemeComponent? + public var calendarMonth: WhereThemeComponent? + public var timelineEntry: WhereThemeComponent? + public var section: WhereThemeComponent? + public var welcome: WhereThemeComponent? + public var privacy: WhereThemeComponent? + + public init() {} + + public subscript(role: Role) -> WhereThemeComponent? { + get { + switch role { + case .locations: locations + case .elsewhere: elsewhere + case .year: year + case .settings: settings + case .sheet: sheet + case .regularCard: regularCard + case .compactCard: compactCard + case .elsewhereCard: elsewhereCard + case .summary: summary + case .forecastPanel: forecastPanel + case .calendarMonth: calendarMonth + case .timelineEntry: timelineEntry + case .section: section + case .welcome: welcome + case .privacy: privacy + } + } + set { + switch role { + case .locations: locations = newValue + case .elsewhere: elsewhere = newValue + case .year: year = newValue + case .settings: settings = newValue + case .sheet: sheet = newValue + case .regularCard: regularCard = newValue + case .compactCard: compactCard = newValue + case .elsewhereCard: elsewhereCard = newValue + case .summary: summary = newValue + case .forecastPanel: forecastPanel = newValue + case .calendarMonth: calendarMonth = newValue + case .timelineEntry: timelineEntry = newValue + case .section: section = newValue + case .welcome: welcome = newValue + case .privacy: privacy = newValue + } + } + } + + func inheriting(_ parent: Self?) -> Self { + var result = Self() + for role in Role + .allCases + { + result[role] = (self[role] ?? .init()).inheriting(parent?[role]) + } + return result + } +} + +extension WhereThemeComponents { + /// A direct value replaces an inherited reference. A document cannot author both for one field. + func assigningLiteralOwnership(_ appearance: WhereThemeAppearance) throws -> Self { + var result = self + func literalSurface(_ present: Bool, role: Role) throws { + guard present else { return } + var component = result[role] ?? .init() + if case .value = component.surface { + throw WhereThemeValidationError( + path: "components.\(role).surface", + reason: "Choose a named reference or a literal surface", + ) + } + component.surface = .clear + result[role] = component + } + try literalSurface(appearance.card?.regular?.surface != nil, role: .regularCard) + try literalSurface(appearance.card?.compact?.surface != nil, role: .compactCard) + try literalSurface(appearance.card?.elsewherePaper?.surface != nil, role: .elsewhereCard) + try literalSurface(appearance.locationWelcome?.surface != nil, role: .welcome) + try literalSurface(appearance.privacyPassportCard?.surface != nil, role: .privacy) + try literalSurface(appearance.locationForecast?.summary?.treatment != nil, role: .summary) + if appearance.locationsBackground?.paper != nil { + var component = result.locations ?? .init() + if case .value = component.background { + throw WhereThemeValidationError( + path: "components.locations.background", + reason: "Choose a named reference or a literal background", + ) + } + component.background = .clear + result.locations = component + } + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeDocument.swift b/Where/WhereUI/Sources/Themes/WhereThemeDocument.swift new file mode 100644 index 000000000..d313e0fdd --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeDocument.swift @@ -0,0 +1,218 @@ +import Foundation +import ViewDocumentCore +import WhereCore + +/// A complete root or a sparse child. Catalog resolution produces a complete, validated theme. +public struct WhereThemeDocument: Codable, Hashable, Sendable { + public struct ID: RawRepresentable, Codable, Hashable, Sendable { + public static let standard = Self(rawValue: "standard") + public static let flat = Self(rawValue: "flat") + public static let halloween = Self(rawValue: "halloween") + public var rawValue: String + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + public enum Fallback: String, Codable, Hashable, Sendable { + case standard, flat + var theme: WhereTheme { + switch self { case .standard: .standard; case .flat: .alternate } + } + } + + public var schemaVersion: Int + public var id: ID + public var parent: ID? + public var values: WhereThemeValues + public var components: WhereThemeComponents + + // Version 1 source compatibility. New documents use parent and component surfaces. + public var fallback: Fallback + public var appearance: WhereThemeAppearance + public var layers: Layers + + public init( + schemaVersion: Int, + id: ID, + fallback: Fallback, + appearance: WhereThemeAppearance, + layers: Layers, + ) { + parent = .init(rawValue: fallback == .standard ? "standard" : "flat") + values = .init(colors: nil, fonts: nil, surfaces: nil) + components = .init() + self.schemaVersion = schemaVersion + self.id = id + self.fallback = fallback + self.appearance = appearance + self.layers = layers + } + + public init( + id: ID, + parent: ID?, + values: WhereThemeValues, + appearance: WhereThemeAppearance, + components: WhereThemeComponents, + ) { + schemaVersion = 2 + self.id = id + self.parent = parent + self.values = values + self.appearance = appearance + self.components = components + fallback = parent == .flat || id == .flat ? .flat : .standard + layers = .init() + } + + private enum CodingKeys: String, CodingKey { + case schemaVersion, id, parent, values, appearance, components, fallback, layers + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + schemaVersion = try container.decode(Int.self, forKey: .schemaVersion) + guard schemaVersion == 1 || schemaVersion == 2 else { + throw WhereThemeValidationError( + path: "schemaVersion", + reason: "Unsupported theme schema version", + ) + } + id = try container.decode(ID.self, forKey: .id) + appearance = try container + .decodeIfPresent(WhereThemeAppearance.self, forKey: .appearance) ?? .init() + values = try container.decodeIfPresent(WhereThemeValues.self, forKey: .values) ?? .init( + colors: nil, + fonts: nil, + surfaces: nil, + ) + components = try container + .decodeIfPresent(WhereThemeComponents.self, forKey: .components) ?? .init() + layers = try container.decodeIfPresent(Layers.self, forKey: .layers) ?? .init() + fallback = try container.decodeIfPresent(Fallback.self, forKey: .fallback) ?? .standard + parent = schemaVersion == 1 ? .init(rawValue: fallback == .standard ? "standard" : "flat") : + try container.decodeIfPresent( + ID.self, + forKey: .parent, + ) + if schemaVersion == 1 { self = try converted() } + else { fallback = parent == .flat || id == .flat ? .flat : .standard } + } + + public func encode(to encoder: any Encoder) throws { + let document = try converted() + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(2, forKey: .schemaVersion) + try container.encode(document.id, forKey: .id) + try container.encodeIfPresent(document.parent, forKey: .parent) + try container.encode(document.values, forKey: .values) + try container.encode(document.appearance, forKey: .appearance) + try container.encode(document.components, forKey: .components) + } + + /// Imports retain native content and translate the old finite decoration slots once. + func converted() throws -> Self { + guard schemaVersion == 1 else { return self } + var result = self + result.schemaVersion = 2 + result.appearance = try WhereThemeLegacyConversion.appearance( + appearance, + fallback: fallback, + ) + for slot in Slot.allCases { + let documents = layers[slot] + guard documents.count <= 8, + Set(documents.map(\.root.id)).count == documents.count + else { + throw WhereThemeValidationError( + path: "layers.\(slot.rawValue)", + reason: "Too many layers or duplicate layer identity", + ) + } + } + for role in WhereThemeComponents.Role.allCases { + let slots: [Slot] = switch role { + case .locations: [.locationsBackground] + case .regularCard: [.regularCardBackground, .regularCardOverlay] + case .compactCard: [.compactCardBackground, .compactCardOverlay] + case .elsewhere, .year, .settings, .sheet, .elsewhereCard, .summary, .forecastPanel, + .calendarMonth, .timelineEntry, .section, .welcome, .privacy: [] + } + let art = slots.flatMap { slot in + layers[slot].enumerated().map { index, document in + WhereThemeArtwork( + id: .init(rawValue: "\(slot.rawValue)-\(index)"), + document: document, + placement: .init( + anchor: .center, + size: .relative( + width: 1, + height: 1, + maximumWidth: 2048, + maximumHeight: 2048, + ), + offset: .init(x: 0, y: 0), + plane: .interior, + outsets: .init(top: 0, leading: 0, bottom: 0, trailing: 0), + ), + role: .ornament, + opacity: .constant(1), + ) + } + } + if !art.isEmpty { + var component = result.components[role] ?? .init() + component.artwork = art + result.components[role] = component + } + } + // A legacy literal remains the owner of its value after conversion. + if appearance.card?.regular? + .surface != + nil + { + result.components.regularCard = result.components.regularCard ?? .init(); result + .components.regularCard?.surface = .clear + } + if appearance.card?.compact? + .surface != + nil + { + result.components.compactCard = result.components.compactCard ?? .init(); result + .components.compactCard?.surface = .clear + } + if appearance.locationsBackground? + .paper != + nil + { + result.components.locations = result.components.locations ?? .init(); result.components + .locations?.background = .clear + } + result.layers = .init() + return result + } + + public enum Slot: String, CaseIterable, Codable, Hashable, Sendable { + case locationsBackground, regularCardBackground, regularCardOverlay, compactCardBackground, + compactCardOverlay + } + + public struct Layers: Codable, Hashable, Sendable { + public var locationsBackground: [ViewDocument]? + public var regularCardBackground: [ViewDocument]? + public var regularCardOverlay: [ViewDocument]? + public var compactCardBackground: [ViewDocument]? + public var compactCardOverlay: [ViewDocument]? + public init() {} + public subscript(slot: Slot) -> [ViewDocument] { + switch slot { + case .locationsBackground: locationsBackground ?? [] + case .regularCardBackground: regularCardBackground ?? [] + case .regularCardOverlay: regularCardOverlay ?? [] + case .compactCardBackground: compactCardBackground ?? [] + case .compactCardOverlay: compactCardOverlay ?? [] + } + } + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeExamples.swift b/Where/WhereUI/Sources/Themes/WhereThemeExamples.swift new file mode 100644 index 000000000..ba55f2ec4 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeExamples.swift @@ -0,0 +1,13 @@ +#if DEBUG + import Foundation + + /// Snapshot specimens use the app's bundled theme preparation path. + @MainActor + enum WhereThemeExamples: String, CaseIterable { + case halloween = "HalloweenTheme" + + func prepare() throws -> PreparedWhereThemeDocument { + try WhereBundledThemes.prepared(.halloween) + } + } +#endif diff --git a/Where/WhereUI/Sources/Themes/WhereThemeGeometry.swift b/Where/WhereUI/Sources/Themes/WhereThemeGeometry.swift new file mode 100644 index 000000000..c8f7746a9 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeGeometry.swift @@ -0,0 +1,27 @@ +import CoreGraphics + +public struct WhereThemeSize: Codable, Hashable, Sendable { + public var width: Double + public var height: Double + public init(width: Double, height: Double) { + self.width = width + self.height = height + } + + var size: CGSize { + .init(width: width, height: height) + } +} + +public struct WhereThemePoint: Codable, Hashable, Sendable { + public var x: Double + public var y: Double + public init(x: Double, y: Double) { + self.x = x + self.y = y + } + + var point: CGPoint { + .init(x: x, y: y) + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeLegacyConversion.swift b/Where/WhereUI/Sources/Themes/WhereThemeLegacyConversion.swift new file mode 100644 index 000000000..fe669e90b --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeLegacyConversion.swift @@ -0,0 +1,135 @@ +import BroadwayCore +import ViewDocumentCore + +/// Freezes version 1's implicit defaults when an import enables a previously absent visual part. +/// Version 2 definitions must supply those values themselves or inherit them from a parent. +enum WhereThemeLegacyConversion { + static func appearance( + _ patch: WhereThemeAppearance, + fallback: WhereThemeDocument.Fallback, + ) throws -> WhereThemeAppearance { + let base = try WhereBundledThemeDefinitions.resolved(fallback.theme).appearance + let standard = try WhereBundledThemeDefinitions.resolved(.standard).appearance + var result = patch + result.card?.regular = card( + patch.card?.regular, + base: base.card?.regular, + standard: standard, + ) + result.card?.compact = card( + patch.card?.compact, + base: base.card?.compact, + standard: standard, + ) + result.locationWelcome?.surface = surface( + patch.locationWelcome?.surface, + base: base.locationWelcome?.surface, + ) + result.locationWelcome?.close?.surface = surface( + patch.locationWelcome?.close?.surface, + base: base.locationWelcome?.close?.surface, + ) + result.privacyPassportCard?.surface = surface( + patch.privacyPassportCard?.surface, + base: base.privacyPassportCard?.surface, + ) + result.locationForecast?.summary?.treatment = surface( + patch.locationForecast?.summary?.treatment, + base: base.locationForecast?.summary?.treatment, + ) + return result + } + + private static func surface( + _ patch: BThemeValue?, + base: BThemeValue?, + ) -> BThemeValue? { + guard let patch else { return nil } + func resolve(_ mode: BMode) -> WhereThemeSurface { + switch patch.resolve(mode: mode) { + case .reflective: return .reflective + case let .matte(value): + let inherited: WhereThemeMatteSurface = if case let .matte(existing) = base? + .resolve(mode: mode) { existing } + else { neutralPaper } + return .matte(value.inheriting(inherited)) + } + } + return .modes(.init(light: resolve(.light), dark: resolve(.dark))) + } + + private static var neutralPaper: WhereThemeMatteSurface { + .init( + fill: .init(base: .constant(.semantic(.background)), tintMix: 0), + outline: .init(opacity: 0, width: 0), + shadow: .init( + color: .constant(.rgba(red: 0, green: 0, blue: 0, alpha: 0)), + radius: 0, + offsetY: 0, + ), + pressedTintOpacity: 0, + ) + } + + private static func card( + _ patch: WhereThemeAppearance.CardStyle?, + base: WhereThemeAppearance.CardStyle?, + standard: WhereThemeAppearance, + ) -> WhereThemeAppearance.CardStyle? { + guard var result = patch else { return nil } + result.surface = surface(result.surface, base: base?.surface) + if case let .value(paper) = result.paper { + var inherited: WhereThemeAppearance.CardStyle.Paper? + if case let .value(value) = base?.paper { inherited = value } + else if var value = standard.card?.elsewherePaper { + value.surface = value.surface.map { + var paper = $0.resolve(mode: .light) + let fill = paper.fill?.base + .map { BThemeValue.constant($0.resolve(mode: .light)) } + let shadow = paper.shadow?.color + .map { BThemeValue.constant($0.resolve(mode: .light)) } + paper.fill?.base = fill + paper.shadow?.color = shadow + return .constant(paper) + } + value.contentTint = value.contentTint.map { + var tint = $0.resolve(mode: .light) + tint.color = tint.color.map { .constant($0.resolve(mode: .light)) } + return .constant(tint) + } + inherited = value + } + result.paper = .value(paper.inheriting(inherited)) + } + if case let .value(arc) = result.entryStamp?.arc { + let inherited: WhereThemeAppearance.CardStyle.EntryStamp + .Arc? = if case let .value(value) = base?.entryStamp?.arc { value } + else if case let .value(value) = standard.card?.regular?.entryStamp?.arc { value } + else { nil } + result.entryStamp?.arc = .value(arc.inheriting(inherited)) + } + if case let .value(region) = result.regionShape { + let inherited: WhereThemeAppearance.CardStyle + .RegionShape? = if case let .value(value) = base?.regionShape { value } + else if case let .value(value) = standard.card?.regular?.regionShape { value } + else { nil } + var value = region.inheriting(inherited) + value.watermark = artwork(value.watermark) + value.stamp = artwork(value.stamp) + result.regionShape = .value(value) + } + return result + } + + private static func artwork(_ patch: WhereThemeAppearance.CardStyle.RegionShape + .Artwork?) -> WhereThemeAppearance.CardStyle.RegionShape.Artwork? + { + guard var result = patch else { return nil } + if case var .value(stroke) = result.stroke { + stroke.opacity = stroke.opacity ?? .constant(0.28) + stroke.width = stroke.width ?? .constant(1.5) + result.stroke = .value(stroke) + } + return result + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeOptional.swift b/Where/WhereUI/Sources/Themes/WhereThemeOptional.swift new file mode 100644 index 000000000..4fd053254 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeOptional.swift @@ -0,0 +1,5 @@ +/// Omission inherits; an explicit clear removes an optional value. +public enum WhereThemeOptional: Codable, Hashable, Sendable { + case clear + case value(Value) +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemePanel.swift b/Where/WhereUI/Sources/Themes/WhereThemePanel.swift new file mode 100644 index 000000000..27f62f422 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemePanel.swift @@ -0,0 +1,105 @@ +import SwiftUI + +/// Shared paper treatment for native calendar, timeline, and section content. +private struct WhereThemePanel: ViewModifier { + let role: WhereThemeComponents.Role + let shape: Shape + let tint: Color + let nativeBackground: NativeBackground + @Environment(\.whereComponentStylesheet) private var stylesheet + + func body(content: Content) -> some View { + let style = stylesheet[role] + if style.surface != nil || style.background != nil || !style.artworkOpacity.isEmpty { + content.modifier(WhereComponentSurface( + role: role, + shape: shape, + finish: WherePaperFinish(surface: style.surface, tint: tint, shape: shape), + ) { nativeBackground }) + } else { + content.modifier(WhereComponentTextStyle(role: role)).background { nativeBackground } + } + } +} + +private struct WherePaperFinish: ViewModifier { + let surface: WhereStylesheet.SurfaceTreatment? + let tint: Color + let shape: Shape + func body(content: Content) -> some View { + switch surface { + case .none: content + case .some(.reflective): + content + .background { shape.fill(.clear).glassEffect( + .regular, + in: shape, + ) } + .clipShape(shape) + .contentShape(shape) + case let .some(.matte(paper)): + content.matteSurface(.matte(paper), tint: tint, in: shape) + } + } +} + +private struct WhereThemeListRow: ViewModifier { + @Environment(\.whereComponentStylesheet) private var stylesheet + func body(content: Content) -> some View { + if stylesheet[.section].background != nil || !stylesheet[.section].artworkOpacity.isEmpty { + content + .modifier(WhereComponentTextStyle(role: .section)) + .listRowBackground(WhereThemeRowBackground()) + } else { content.modifier(WhereComponentTextStyle(role: .section)) } + } +} + +/// List owns row clipping and interaction. Documents can paint only the row interior. +struct WhereThemeRowBackground: View { + @Environment(\.whereComponentStylesheet) private var stylesheet + var body: some View { + (stylesheet[.section].background ?? Color(.secondarySystemGroupedBackground)) + .overlay { WhereComponentArtwork(role: .section, plane: .interior, shape: Rectangle()) } + .allowsHitTesting(false) + .accessibilityHidden(true) + } +} + +extension View { + func whereThemeListRow() -> some View { + modifier(WhereThemeListRow()) + } + + func whereThemePanel( + _ role: WhereThemeComponents.Role, + in shape: some InsettableShape, + tint: Color = .primary, + ) -> some View { + modifier(WhereThemePanel( + role: role, + shape: shape, + tint: tint, + nativeBackground: EmptyView(), + )) + } + + func whereThemePanel( + _ role: WhereThemeComponents.Role, + in shape: some InsettableShape, + tint: Color, + @ViewBuilder background: () -> some View, + ) -> some View { + modifier(WhereThemePanel( + role: role, + shape: shape, + tint: tint, + nativeBackground: background(), + )) + } +} + +#if DEBUG + #Preview("Theme row") { + WhereThemeRowBackground().frame(height: 52).whereBroadwayRoot() + } +#endif diff --git a/Where/WhereUI/Sources/Themes/WhereThemePatch.swift b/Where/WhereUI/Sources/Themes/WhereThemePatch.swift new file mode 100644 index 000000000..661bb6a7a --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemePatch.swift @@ -0,0 +1,79 @@ +import BroadwayCore + +/// Typed inheritance runs before trait slicing. Reflection checks completeness only, never styling. +protocol WhereThemePatch { + func inheriting(_ parent: Self?) -> Self +} + +extension WhereThemePatch { + func requireComplete(path: String) throws { + for child in Mirror(reflecting: self).children { + let wrapped = child.value as? any WhereThemeBox + let label = child.label ?? "value" + let fieldPath = path + "." + (wrapped == nil ? label : String(label.dropFirst())) + let field = wrapped?.themeValue ?? child.value + let mirror = Mirror(reflecting: field) + if mirror.displayStyle == .optional { + guard let value = mirror.children.first?.value else { + throw WhereThemeValidationError( + path: fieldPath, + reason: "A root theme must define this value", + ) + } + try WhereThemeCompleteness.check(value, path: fieldPath) + } else { + try WhereThemeCompleteness.check(field, path: fieldPath) + } + } + } +} + +private protocol WhereThemeBox { + var themeValue: Any { get } +} + +extension CopyOnWrite: WhereThemeBox { + fileprivate var themeValue: Any { + wrappedValue + } +} + +private enum WhereThemeCompleteness { + static func check(_ value: Any, path: String) throws { + if let patch = value as? any WhereThemePatch { + try patch.requireComplete(path: path) + return + } + // Wrapper enums carry patches; ordinary portable values may have legitimate nil fields. + for child in Mirror(reflecting: value).children { + try check(child.value, path: path + "." + (child.label ?? "value")) + } + } +} + +func inheritPatch(_ value: Value?, from parent: Value?) -> Value? { + value?.inheriting(parent) ?? parent +} + +func inheritOptionalPatch( + _ value: WhereThemeOptional?, + from parent: WhereThemeOptional?, +) -> WhereThemeOptional? { + guard let value else { return parent } + switch value { + case .clear: return .clear + case let .value(patch): + if case let .value(inherited) = parent { return .value(patch.inheriting(inherited)) } + return .value(patch) + } +} + +func inheritModePatch( + _ value: BThemeValue?, + from parent: BThemeValue?, +) -> BThemeValue? { + guard let value else { return parent } + let light = value.resolve(mode: .light).inheriting(parent?.resolve(mode: .light)) + let dark = value.resolve(mode: .dark).inheriting(parent?.resolve(mode: .dark)) + return light == dark ? .constant(light) : .modes(.init(light: light, dark: dark)) +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemePreview.swift b/Where/WhereUI/Sources/Themes/WhereThemePreview.swift new file mode 100644 index 000000000..4d7500e81 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemePreview.swift @@ -0,0 +1,141 @@ +#if DEBUG + import BroadwayUI + import SnapshotKit + import SwiftUI + import WhereCore + + /// Uses production cards, background, root injection, and surface composition. + struct WhereThemePreview: View { + let theme: PreparedWhereThemeDocument + var body: some View { + content.whereBroadwayRoot(themeDocument: theme) + } + + private var content: some View { + VStack(spacing: 94) { + RegionSummaryCard(regionDays: .init(region: .california, days: 148), year: 2026) + RegionSummaryCard( + regionDays: .init(region: .europeanUnion, days: 22), + variant: .compact, + year: 2026, + ) + } + .padding(24) + .padding(.bottom, 38) + .background { WhereThemePreviewBackground() } + } + + @MainActor + static func example(_ example: WhereThemeExamples) -> Self { + do { return try Self(theme: example.prepare()) } + catch { preconditionFailure("Invalid bundled theme: \(error)") } + } + } + + extension WhereThemePreview { + enum AccessibilityPolicy: String, CaseIterable { + case reduceMotion = "ReduceMotion" + case reduceTransparency = "ReduceTransparency" + } + + static func snapshot(_ policy: AccessibilityPolicy) throws -> SnapshotCase { + let theme = try WhereThemeExamples.halloween.prepare() + return SnapshotCase( + name: policy.rawValue, + configurations: SnapshotConfiguration.combinations( + devices: [.init(name: "iPhone", size: .intrinsic(maxWidth: 402))], + colorSchemes: [.light, .dark], + ), + referenceGroup: .init(rawValue: "halloween"), + ) { + WhereThemePreview(theme: theme).content + .bTraitOverrides { traits, overrides in + var accessibility = traits.accessibility + switch policy { + case .reduceMotion: accessibility.isReduceMotionEnabled = true + case .reduceTransparency: accessibility + .isReduceTransparencyEnabled = true + } + overrides.accessibility = accessibility + } + .whereBroadwayRoot(themeDocument: theme) + } + } + + enum Subject: String, CaseIterable { + case forecast = "Forecast" + case welcome = "Welcome" + case privacy = "Privacy" + case timeline = "Timeline" + case appearance = "Appearance" + case planning = "Planning" + } + + static func snapshot(_ subject: Subject) throws -> SnapshotCase { + let theme = try WhereThemeExamples.halloween.prepare() + let cache = RegionOutlinePathCache() + let report = PreviewSupport.itineraryYearReportModel() + let model = PreviewSupport.loadedModel() + let session = PreviewSupport.loadedSession() + model.previewTheme(.halloween) + let configurations: [SnapshotConfiguration] = switch subject { + case .forecast, .welcome, .privacy: .componentLightDark + case .timeline, .appearance, .planning: .fullContentPhoneLightDark + } + return SnapshotCase( + name: subject.rawValue, + configurations: configurations, + referenceGroup: .init(rawValue: "halloween"), + onReadyToSnapshot: { + await PreviewSupport.loadCardArtwork( + for: [.california, .newYork], + cache: cache, + ) + }, + ) { + Group { + switch subject { + case .forecast: + LocationForecastPanel( + forecasts: report.forecasts.leadingForecasts(report: report.report), + microprintRegions: report.ranking.primary.map(\.region), + homeRegion: report.forecasts.planning.homeRegion, + planningAction: {}, + ).padding(24).background { WhereThemePreviewBackground() } + case .welcome: + RegionWelcomeCard( + presentation: .init(region: .newYork, greeting: .returnVisit), + dismissAction: {}, + planStayAction: { _ in }, + ) + .padding(40).background { WhereThemePreviewBackground() } + case .privacy: + PrivacyPassportCard( + presentation: .init(configuration: .defaults(isDebugBuild: false)), + disclosureInteraction: .linkToSettings, + ) + .padding(24).background { WhereThemePreviewBackground() } + case .timeline: + YearView(report: report, initialMode: .timeline) + case .appearance: + NavigationStack { AppearanceSettingsView(report: report) } + case .planning: + PlannedStaysView(report: report) + } + } + .environment(model).environment(session) + .environment(\.regionOutlinePathCache, cache) + .whereBroadwayRoot(themeDocument: theme) + } + } + } + + private struct WhereThemePreviewBackground: View { + @Environment(\.whereComponentStylesheet) private var components + var body: some View { + (components[.locations].background ?? Color(.systemBackground)).ignoresSafeArea() + } + } + + #Preview("Halloween") { WhereThemePreview.example(.halloween) } +#endif diff --git a/Where/WhereUI/Sources/Themes/WhereThemeScreen.swift b/Where/WhereUI/Sources/Themes/WhereThemeScreen.swift new file mode 100644 index 000000000..68b1a2c8c --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeScreen.swift @@ -0,0 +1,54 @@ +import SwiftUI + +/// Document-owned screen paint and typography leave navigation and controls native. +private struct WhereThemeScreen: ViewModifier { + let role: WhereThemeComponents.Role + @Environment(\.whereComponentStylesheet) private var stylesheet + + func body(content: Content) -> some View { + let style = stylesheet[role] + if style.background != nil || !style.artworkOpacity.isEmpty || style.body != nil || style + .foreground != nil + { + content + .scrollContentBackground(.hidden) + .font(style.body) + .fontDesign(style.bodyDesign) + .foregroundStyle(style.foreground ?? .primary) + .environment(\.whereThemeSecondaryInk, style.secondaryForeground) + .tint(style.foreground ?? .accentColor) + .background { + (style.background ?? nativeBackground).ignoresSafeArea() + .overlay { WhereComponentArtwork( + role: role, + plane: .interior, + shape: Rectangle(), + ) } + .overlay { WhereComponentArtwork( + role: role, + plane: .exterior, + shape: Rectangle(), + ) } + .clipped() + } + } else { + content + } + } + + private var nativeBackground: Color { + switch role { + case .settings, .sheet: Color(.systemGroupedBackground) + case .locations, .elsewhere, .year, .regularCard, .compactCard, .elsewhereCard, + .summary, + .forecastPanel, .calendarMonth, .timelineEntry, .section, .welcome, + .privacy: Color(.systemBackground) + } + } +} + +extension View { + func whereThemeScreen(_ role: WhereThemeComponents.Role) -> some View { + modifier(WhereThemeScreen(role: role)) + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeSurface.swift b/Where/WhereUI/Sources/Themes/WhereThemeSurface.swift new file mode 100644 index 000000000..3dcbd9cb1 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeSurface.swift @@ -0,0 +1,187 @@ +import BroadwayCore +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI + +/// Selecting matte preserves an inherited matte finish; reflective fallbacks start with neutral +/// paper. +public enum WhereThemeSurface: Codable, Hashable, Sendable { + case reflective + case matte(WhereThemeMatteSurface) + + func resolve( + mode: BMode, + appearance: ViewDocumentAppearance, + base: WhereStylesheet.SurfaceTreatment, + ) throws -> WhereStylesheet.SurfaceTreatment { + switch self { + case .reflective: .reflective + case let .matte(patch): + switch base { + case let .matte(inherited): try .matte(patch.resolve( + mode: mode, + appearance: appearance, + base: inherited, + )) + case .reflective: try .matte(patch.resolve( + mode: mode, + appearance: appearance, + base: Self.neutralPaper(background: appearance.background), + )) + } + } + } + + private static func neutralPaper(background: Color) -> WhereStylesheet.MatteSurface { + .init( + fill: .init(base: background, tintMix: 0), + outline: .init(opacity: 0, width: 0), + shadow: .init(color: .clear, radius: 0, offsetY: 0), + pressedTintOpacity: 0, + ) + } +} + +extension WhereThemeSurface: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + switch self { + case .reflective: .reflective + case let .matte(patch): + if case let .matte(inherited) = parent { .matte(patch.inheriting(inherited)) } + else { .matte(patch) } + } + } +} + +public struct WhereThemeMatteSurface: Codable, Hashable, Sendable { + public var fill: Fill? + public var outline: Outline? + public var shadow: Shadow? + public var pressedTintOpacity: Double? + public init() {} + public init(fill: Fill?, outline: Outline?, shadow: Shadow?, pressedTintOpacity: Double?) { + self.fill = fill + self.outline = outline + self.shadow = shadow + self + .pressedTintOpacity = pressedTintOpacity + } + + public struct Fill: Codable, Hashable, Sendable { + public var base: BThemeValue? + public var tintMix: Double? + public init() {} + public init(base: BThemeValue?, tintMix: Double?) { + self.base = base + self.tintMix = tintMix + } + } + + public struct Outline: Codable, Hashable, Sendable { + public var opacity: Double? + public var width: Double? + public init() {} + public init(opacity: Double?, width: Double?) { + self.opacity = opacity + self.width = width + } + } + + public struct Shadow: Codable, Hashable, Sendable { + public var color: BThemeValue? + public var radius: Double? + public var offsetY: Double? + public init() {} + public init(color: BThemeValue?, radius: Double?, offsetY: Double?) { + self.color = color + self.radius = radius + self.offsetY = offsetY + } + } + + func resolve( + mode: BMode, + appearance: ViewDocumentAppearance, + base: WhereStylesheet.MatteSurface, + ) throws -> WhereStylesheet.MatteSurface { + var result = base + if let value = fill?.base?.resolve(mode: mode) { + try WhereThemeValidation.validate(value, path: "surface.fill.base") + guard WhereThemeValidation.isOpaque(value) else { + throw WhereThemeValidationError( + path: "surface.fill.base", + reason: "Matte paper requires opaque RGBA, primary, or background color", + ) + } + result.fill.base = appearance.color(value) + } + if let value = fill?.tintMix { + try WhereThemeValidation.validate(value, path: "surface.fill.tintMix") + result.fill.tintMix = value + } + if let value = outline?.opacity { + try WhereThemeValidation.validate(value, path: "surface.outline.opacity") + result.outline.opacity = value + } + if let value = outline?.width { + try WhereThemeValidation.validate(value, path: "surface.outline.width") + result.outline.width = value + } + if let value = shadow?.color?.resolve(mode: mode) { + try WhereThemeValidation.validate(value, path: "surface.shadow.color") + result.shadow.color = appearance.color(value) + } + if let value = shadow?.radius { + try WhereThemeValidation.validate(value, path: "surface.shadow.radius") + result.shadow.radius = value + } + if let value = shadow?.offsetY { + try WhereThemeValidation.validate(value, path: "surface.shadow.offsetY") + result.shadow.offsetY = value + } + if let value = pressedTintOpacity { + try WhereThemeValidation.validate(value, path: "surface.pressedTintOpacity") + result.pressedTintOpacity = value + } + return result + } +} + +extension WhereThemeMatteSurface: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + var result = self + result.fill = inheritPatch(fill, from: parent?.fill) + result.outline = inheritPatch(outline, from: parent?.outline) + result.shadow = inheritPatch(shadow, from: parent?.shadow) + result.pressedTintOpacity = pressedTintOpacity ?? parent?.pressedTintOpacity + return result + } +} + +extension WhereThemeMatteSurface.Fill: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + .init( + base: base ?? parent?.base, + tintMix: tintMix ?? parent?.tintMix, + ) + } +} + +extension WhereThemeMatteSurface.Outline: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + .init( + opacity: opacity ?? parent?.opacity, + width: width ?? parent?.width, + ) + } +} + +extension WhereThemeMatteSurface.Shadow: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + .init( + color: color ?? parent?.color, + radius: radius ?? parent?.radius, + offsetY: offsetY ?? parent?.offsetY, + ) + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeTextAlignment.swift b/Where/WhereUI/Sources/Themes/WhereThemeTextAlignment.swift new file mode 100644 index 000000000..3832c10dc --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeTextAlignment.swift @@ -0,0 +1,12 @@ +import SwiftUI + +public enum WhereThemeTextAlignment: String, Codable, Hashable, Sendable { + case leading, center, trailing + var alignment: TextAlignment { + switch self { + case .leading: .leading + case .center: .center + case .trailing: .trailing + } + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeTintAdjustment.swift b/Where/WhereUI/Sources/Themes/WhereThemeTintAdjustment.swift new file mode 100644 index 000000000..d9007efe2 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeTintAdjustment.swift @@ -0,0 +1,36 @@ +import BroadwayCore +import ViewDocumentCore +import ViewDocumentUI + +public struct WhereThemeTintAdjustment: Codable, Hashable, Sendable { + public var color: BThemeValue? + public var fraction: Double? + public init() {} + public init(color: BThemeValue?, fraction: Double?) { + self.color = color + self.fraction = fraction + } + + func resolve( + mode: BMode, + appearance: ViewDocumentAppearance, + base: WhereStylesheet.TintAdjustment, + ) throws -> WhereStylesheet.TintAdjustment { + var result = base + if let color = color?.resolve(mode: mode) { + try WhereThemeValidation.validate(color, path: "tint.color") + result.color = appearance.color(color) + } + if let fraction { + try WhereThemeValidation.validate(fraction, path: "tint.fraction") + result.fraction = fraction + } + return result + } +} + +extension WhereThemeTintAdjustment: WhereThemePatch { + func inheriting(_ parent: Self?) -> Self { + .init(color: color ?? parent?.color, fraction: fraction ?? parent?.fraction) + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeValidation.swift b/Where/WhereUI/Sources/Themes/WhereThemeValidation.swift new file mode 100644 index 000000000..60485e709 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeValidation.swift @@ -0,0 +1,193 @@ +import Foundation +import ViewDocumentCore + +/// Validation runs before a document can enter Broadway's stylesheet cache. +enum WhereThemeValidation { + /// Secondary has alpha; an inherited accent can also be translucent. + static func isOpaque(_ value: ViewColorDescriptor) -> Bool { + switch value { + case let .rgba(_, _, _, alpha): alpha == 1 + case let .opacity(value, alpha): alpha == 1 && isOpaque(value) + case let .mix(first, second, _): isOpaque(first) && isOpaque(second) + case .semantic(.primary), .semantic(.background), .semantic(.secondaryBackground), + .semantic(.groupedBackground), .semantic(.secondaryGroupedBackground), + .semantic(.red), .semantic(.orange), + .semantic(.yellow), .semantic(.green), .semantic(.mint), .semantic(.teal), + .semantic(.cyan), .semantic(.blue), .semantic(.indigo), .semantic(.purple), + .semantic(.pink), .semantic(.brown), .semantic(.gray), .semantic(.black), + .semantic(.white): true + case .semantic(.secondary), .semantic(.accent), .semantic(.clear): false + } + } + + /// Use explicit units for domain values that are not ordinary layout dimensions. + static func validate(_ value: Double, range: ClosedRange, path: String) throws { + guard value.isFinite, range.contains(value) else { + throw WhereThemeValidationError( + path: path, + reason: "Value is outside the supported range \(range)", + ) + } + } + + static func validate(_ value: Double, path: String) throws { + let field = path.split(separator: ".").last.map(String.init)?.lowercased() ?? path + // These values feed divisors and repeated artwork, so zero and tiny positive values + // cannot use the general dimension range. + if path.contains(".backgroundPattern.") && field == "horizontalscale" { + guard value.isFinite, (0.25 ... 4).contains(value) else { + throw WhereThemeValidationError( + path: path, + reason: "Contour horizontal scale must be between 0.25 and 4", + ) + } + return + } + if path.contains(".backgroundPattern.") && ["centerxratio", "centeryratio"] + .contains(field) + { + guard value.isFinite, (0 ... 1).contains(value) else { + throw WhereThemeValidationError( + path: path, + reason: "Contour center must stay inside the canvas", + ) + } + return + } + if field == "preferredcellsize" { + guard value.isFinite, (32 ... 10000).contains(value) else { + throw WhereThemeValidationError( + path: path, + reason: "Background artwork cells must be at least 32 points", + ) + } + return + } + let signed = ["offset", "rotation", "roll", "pitch", "spacing", "indent", "tracking"] + .contains { field.contains($0) } + let fraction = ["opacity", "fraction", "tintmix"].contains { field.contains($0) } + let density = field + .contains("ringspacing") || field == "hatchspacing" || field == "contourspacing" || + field == "microprintspacing" + || field == "preferredcellsize" || + (field == "spacing" && (path.contains("securityBorder") || path.contains(".border."))) + if density && (!value.isFinite || value < 2) { + throw WhereThemeValidationError( + path: path, + reason: "Pattern spacing must be at least two points", + ) + } + if field == "bounce" { + guard value.isFinite, (-1 ... 1).contains(value) else { + throw WhereThemeValidationError(path: path, reason: "Invalid spring bounce") + } + return + } + if field.contains("duration") || field == "delay" { + guard value.isFinite, (0 ... 60).contains(value) else { + throw WhereThemeValidationError( + path: path, + reason: "Presentation timing exceeds sixty seconds", + ) + } + return + } + let range: ClosedRange = fraction ? 0 ... 1 : + (signed ? -10000 ... 10000 : 0 ... 10000) + guard value.isFinite, range.contains(value) else { + throw WhereThemeValidationError( + path: path, + reason: "Value is outside the supported range \(range)", + ) + } + } + + static func validate( + _ value: WhereThemeAppearance.CardStyle.Typography.Size, + path: String, + ) throws { + switch value { + case let .fixed(points): + guard points.isFinite, (1 ... 1000).contains(points) else { + throw WhereThemeValidationError(path: path, reason: "Invalid fixed font size") + } + case .semantic: break + } + } + + static func validate(_: Bool, path _: String) throws {} + static func validate(_ values: [Double], path: String) throws { + guard values.count <= 64 else { throw WhereThemeValidationError( + path: path, + reason: "Too many values", + ) } + for value in values { + try validate(value, path: path) + } + } + + static func validate(_ value: WhereThemeSize, path: String) throws { + let isOffset = path.lowercased().contains("offset") + try validate(value.width, path: path + (isOffset ? ".offsetX" : ".width")) + try validate(value.height, path: path + (isOffset ? ".offsetY" : ".height")) + } + + static func validate(_ value: WhereThemePoint, path: String) throws { + try validate(value.x, path: path + ".offsetX") + try validate(value.y, path: path + ".offsetY") + } + + static func validate(_ value: ViewColorDescriptor, path: String) throws { + try validateModifier(.foreground(.color(value)), path: path) + } + + static func validate(_ value: ViewFontDescriptor, path: String) throws { + try validateModifier(.font(value), path: path) + } + + private static func validateModifier(_ value: ViewModifierDescriptor, path: String) throws { + do { + _ = try ValidatedViewDocument(document: .init(schemaVersion: 1, root: .init( + id: .init(rawValue: path), + content: .text(""), + modifiers: [value], + animation: nil, + accessibility: .hidden, + )), resources: .init(images: [], symbols: []), limits: .standard) + } catch let error as ViewDocumentValidationError { + throw WhereThemeValidationError(path: path, underlying: error) + } + } + + static func validate(_ value: WhereThemeAnimation, path: String) throws { + let duration: Double + switch value { + case let .linear(seconds), let .easeIn(seconds), let .easeOut(seconds), + let .easeInOut(seconds): duration = + seconds + case let .spring(seconds, bounce), let .snappy(seconds, bounce): + duration = seconds + guard bounce.isFinite, + (-1 ... 1).contains(bounce) + else { throw WhereThemeValidationError( + path: path, + reason: "Invalid spring bounce", + ) } + } + guard duration.isFinite, + (0 ... 60).contains(duration) + else { throw WhereThemeValidationError( + path: path, + reason: "Invalid animation duration", + ) } + } + + static func validate(_: ViewFontDescriptor.Weight, path _: String) throws {} + static func validate(_: ViewFontDescriptor.Design, path _: String) throws {} + static func validate(_: WhereThemeBlend, path _: String) throws {} + static func validate(_: WhereThemeTextAlignment, path _: String) throws {} + // Surface resolvers validate their constituent fields before creating native values. + static func validate(_: WhereThemeSurface, path _: String) throws {} + static func validate(_: WhereThemeMatteSurface, path _: String) throws {} + static func validate(_: WhereThemeTintAdjustment, path _: String) throws {} +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeValidationError.swift b/Where/WhereUI/Sources/Themes/WhereThemeValidationError.swift new file mode 100644 index 000000000..db70da705 --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeValidationError.swift @@ -0,0 +1,24 @@ +import Foundation +import ViewDocumentCore + +/// A theme schema or appearance failure with an author-facing field path. +public struct WhereThemeValidationError: Error, LocalizedError, Equatable, Sendable { + public let path: String + public let reason: String + public let underlying: ViewDocumentValidationError? + public var errorDescription: String? { + "\(path): \(reason)" + } + + init(path: String, reason: String) { + self.path = path + self.reason = reason + underlying = nil + } + + init(path: String, underlying: ViewDocumentValidationError) { + self.path = path + reason = underlying.reason + self.underlying = underlying + } +} diff --git a/Where/WhereUI/Sources/Themes/WhereThemeValues.swift b/Where/WhereUI/Sources/Themes/WhereThemeValues.swift new file mode 100644 index 000000000..62a676f0b --- /dev/null +++ b/Where/WhereUI/Sources/Themes/WhereThemeValues.swift @@ -0,0 +1,101 @@ +import BroadwayCore +import ViewDocumentCore + +/// Named authored values. Components hold typed references rather than styling paths. +public struct WhereThemeValues: Codable, Hashable, Sendable { + public var colors: [ColorValue]? + public var fonts: [FontValue]? + public var surfaces: [SurfaceValue]? + + public init(colors: [ColorValue]?, fonts: [FontValue]?, surfaces: [SurfaceValue]?) { + self.colors = colors + self.fonts = fonts + self.surfaces = surfaces + } + + public struct ColorValue: Codable, Hashable, Sendable, Identifiable { + public struct ID: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + public var id: ID + public var value: BThemeValue + public init(id: ID, value: BThemeValue) { + self.id = id; self.value = value + } + } + + public struct FontValue: Codable, Hashable, Sendable, Identifiable { + public struct ID: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + public var id: ID + public var value: BThemeValue + public init(id: ID, value: BThemeValue) { + self.id = id; self.value = value + } + } + + public struct SurfaceValue: Codable, Hashable, Sendable, Identifiable { + public struct ID: RawRepresentable, Codable, Hashable, Sendable { + public let rawValue: String + public init(rawValue: String) { + self.rawValue = rawValue + } + } + + public var id: ID + public var value: BThemeValue + public init(id: ID, value: BThemeValue) { + self.id = id; self.value = value + } + } + + public enum Surface: Codable, Hashable, Sendable { + case reflective + case paper(Paper) + } + + public struct Paper: Codable, Hashable, Sendable { + public var fill: ColorValue.ID + public var tintMix: Double + public var outline: Outline + public var shadow: Shadow + public var pressedTintOpacity: Double + + public init( + fill: ColorValue.ID, + tintMix: Double, + outline: Outline, + shadow: Shadow, + pressedTintOpacity: Double, + ) { + self.fill = fill; self.tintMix = tintMix; self.outline = outline + self.shadow = shadow; self.pressedTintOpacity = pressedTintOpacity + } + + public struct Outline: Codable, Hashable, Sendable { + public var opacity: Double + public var width: Double + public init(opacity: Double, width: Double) { + self.opacity = opacity; self.width = width + } + } + + public struct Shadow: Codable, Hashable, Sendable { + public var color: ColorValue.ID + public var radius: Double + public var offsetY: Double + public init(color: ColorValue.ID, radius: Double, offsetY: Double) { + self.color = color; self.radius = radius; self.offsetY = offsetY + } + } + } +} diff --git a/Where/WhereUI/Tests/AppIconPreviewPanelTests.swift b/Where/WhereUI/Tests/AppIconPreviewPanelTests.swift new file mode 100644 index 000000000..0e0af0266 --- /dev/null +++ b/Where/WhereUI/Tests/AppIconPreviewPanelTests.swift @@ -0,0 +1,65 @@ +import BroadwayUI +import SwiftUI +import TestHostSupport +import Testing +import UIKit +@testable import WhereUI + +@MainActor +struct AppIconPreviewPanelTests { + @Test func invertedPreviewDoesNotInheritConcretePageInk() throws { + let option = try #require(AppIconCatalog.load().first) + let panel = AppIconPreviewPanel( + option: option, + containerSize: CGSize(width: 402, height: 874), + previewMode: .dark, + appearanceToggles: 0, + canApply: true, + toggleAppearance: {}, + apply: {}, + ) + .frame(width: 402) + .environment(\.colorScheme, .dark) + .bMode(.dark) + .whereBroadwayRoot() + let expected = ImageRenderer(content: panel) + let actual = ImageRenderer(content: panel.foregroundStyle(Color.black)) + #expect(try #require(actual.uiImage?.pngData()) == #require(expected.uiImage?.pngData())) + } + + @Test func previewPaperResolvesInsideItsSelectedMode() throws { + var document = WhereThemeDocument( + id: .init(rawValue: "icon-preview-modes"), + parent: .standard, + values: .init(colors: nil, fonts: nil, surfaces: nil), + appearance: .init(), + components: .init(), + ) + document.appearance.appIcon = .init() + document.appearance.appIcon?.panel = .init() + document.appearance.appIcon?.panel?.background = .modes(.init( + light: .rgba(red: 1, green: 1, blue: 1, alpha: 1), + dark: .rgba(red: 1, green: 0, blue: 0, alpha: 1), + )) + let theme = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [document]) + .prepare(document.id, assets: .init(images: [:], symbols: [:])) + let option = try #require(AppIconCatalog.load().first) + let host = UIHostingController(rootView: AppIconPreviewPanel( + option: option, + containerSize: CGSize(width: 402, height: 874), + previewMode: .dark, + appearanceToggles: 0, + canApply: true, + toggleAppearance: {}, + apply: {}, + ) + .environment(\.colorScheme, .dark) + .bMode(.dark) + .whereBroadwayRoot(themeDocument: theme) + .environment(\.colorScheme, .light)) + try show(host) { host in + try waitFor { themeBackgroundIsRed(in: host.view) } + } + } +} diff --git a/Where/WhereUI/Tests/FeatureLockScreenExampleTests.swift b/Where/WhereUI/Tests/FeatureLockScreenExampleTests.swift new file mode 100644 index 000000000..c619df33c --- /dev/null +++ b/Where/WhereUI/Tests/FeatureLockScreenExampleTests.swift @@ -0,0 +1,40 @@ +import SwiftUI +import TestHostSupport +import Testing +import UIKit +@testable import WhereUI + +@MainActor +struct FeatureLockScreenExampleTests { + @Test func wallpaperUsesTheSameDarkModeAsItsContent() throws { + var document = WhereThemeDocument( + id: .init(rawValue: "lock-preview-modes"), + parent: .standard, + values: .init(colors: nil, fonts: nil, surfaces: nil), + appearance: .init(), + components: .init(), + ) + document.appearance.featureDiscovery = .init() + document.appearance.featureDiscovery?.widgets = .init() + document.appearance.featureDiscovery?.widgets?.wallpapers = .init() + document.appearance.featureDiscovery?.widgets?.wallpapers?.lock = .init() + document.appearance.featureDiscovery?.widgets?.wallpapers?.lock?.top = .modes(.init( + light: .rgba(red: 1, green: 1, blue: 1, alpha: 1), + dark: .rgba(red: 1, green: 0, blue: 0, alpha: 1), + )) + let top = document.appearance.featureDiscovery?.widgets?.wallpapers?.lock?.top + document.appearance.featureDiscovery?.widgets?.wallpapers?.lock?.bottom = top + let theme = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [document]) + .prepare(document.id, assets: .init(images: [:], symbols: [:])) + let host = UIHostingController(rootView: FeatureLockScreenExample( + date: PreviewSupport.referenceNow, + snapshot: PreviewSupport.sampleWidgetSnapshot(), + ) + .whereBroadwayRoot(themeDocument: theme) + .environment(\.colorScheme, .light)) + try show(host) { host in + try waitFor { themeBackgroundIsRed(in: host.view) } + } + } +} diff --git a/Where/WhereUI/Tests/Fixtures/Themes/ChristmasTheme-v1.json b/Where/WhereUI/Tests/Fixtures/Themes/ChristmasTheme-v1.json new file mode 100644 index 000000000..2b4fd9ab8 --- /dev/null +++ b/Where/WhereUI/Tests/Fixtures/Themes/ChristmasTheme-v1.json @@ -0,0 +1,298 @@ +{ + "schemaVersion": 1, + "id": "christmas", + "fallback": "flat", + "appearance": {}, + "layers": { + "regularCardOverlay": [ + { + "schemaVersion": 1, + "root": { + "id": "christmas-card", + "content": { + "group": { + "axis": "vertical", + "alignment": "center", + "spacing": 0, + "children": [ + { + "id": "top-row", + "content": { + "group": { + "axis": "horizontal", + "alignment": "center", + "spacing": 0, + "children": [ + { + "id": "leading", + "content": { + "spacer": { + "minimum": 0 + } + }, + "modifiers": [], + "accessibility": { + "hidden": {} + } + }, + { + "id": "bells", + "content": { + "symbol": { + "_0": "seasonal.bell" + } + }, + "modifiers": [ + { + "font": { + "_0": { + "size": { + "semantic": { + "_0": "title" + } + } + } + } + }, + { + "foreground": { + "_0": { + "color": { + "_0": { + "rgba": { + "red": 0.85, + "green": 0.55, + "blue": 0.05, + "alpha": 1 + } + } + } + } + } + } + ], + "accessibility": { + "hidden": {} + }, + "animation": { + "from": { + "opacity": 1, + "offsetX": 0, + "offsetY": 0, + "scaleX": 1, + "scaleY": 1, + "rotation": -10 + }, + "to": { + "opacity": 1, + "offsetX": 0, + "offsetY": 0, + "scaleX": 1, + "scaleY": 1, + "rotation": 10 + }, + "staticPose": { + "opacity": 1, + "offsetX": 0, + "offsetY": 0, + "scaleX": 1, + "scaleY": 1, + "rotation": 0 + }, + "duration": 2, + "delay": 0, + "curve": "easeInOut", + "repetition": { + "forever": { + "autoreverses": true + } + } + } + } + ] + } + }, + "modifiers": [], + "accessibility": { + "hidden": {} + } + }, + { + "id": "below", + "content": { + "spacer": { + "minimum": 0 + } + }, + "modifiers": [], + "accessibility": { + "hidden": {} + } + } + ] + } + }, + "modifiers": [ + { + "padding": { + "_0": { + "points": { + "_0": 14 + } + } + } + } + ], + "accessibility": { + "hidden": {} + } + } + } + ], + "compactCardOverlay": [ + { + "schemaVersion": 1, + "root": { + "id": "christmas-card", + "content": { + "group": { + "axis": "vertical", + "alignment": "center", + "spacing": 0, + "children": [ + { + "id": "top-row", + "content": { + "group": { + "axis": "horizontal", + "alignment": "center", + "spacing": 0, + "children": [ + { + "id": "leading", + "content": { + "spacer": { + "minimum": 0 + } + }, + "modifiers": [], + "accessibility": { + "hidden": {} + } + }, + { + "id": "bells", + "content": { + "symbol": { + "_0": "seasonal.bell" + } + }, + "modifiers": [ + { + "font": { + "_0": { + "size": { + "semantic": { + "_0": "title" + } + } + } + } + }, + { + "foreground": { + "_0": { + "color": { + "_0": { + "rgba": { + "red": 0.85, + "green": 0.55, + "blue": 0.05, + "alpha": 1 + } + } + } + } + } + } + ], + "accessibility": { + "hidden": {} + }, + "animation": { + "from": { + "opacity": 1, + "offsetX": 0, + "offsetY": 0, + "scaleX": 1, + "scaleY": 1, + "rotation": -10 + }, + "to": { + "opacity": 1, + "offsetX": 0, + "offsetY": 0, + "scaleX": 1, + "scaleY": 1, + "rotation": 10 + }, + "staticPose": { + "opacity": 1, + "offsetX": 0, + "offsetY": 0, + "scaleX": 1, + "scaleY": 1, + "rotation": 0 + }, + "duration": 2, + "delay": 0, + "curve": "easeInOut", + "repetition": { + "forever": { + "autoreverses": true + } + } + } + } + ] + } + }, + "modifiers": [], + "accessibility": { + "hidden": {} + } + }, + { + "id": "below", + "content": { + "spacer": { + "minimum": 0 + } + }, + "modifiers": [], + "accessibility": { + "hidden": {} + } + } + ] + } + }, + "modifiers": [ + { + "padding": { + "_0": { + "points": { + "_0": 14 + } + } + } + } + ], + "accessibility": { + "hidden": {} + } + } + } + ] + } +} diff --git a/Where/WhereUI/Tests/Fixtures/Themes/HalloweenTheme-v1.json b/Where/WhereUI/Tests/Fixtures/Themes/HalloweenTheme-v1.json new file mode 100644 index 000000000..55087f229 --- /dev/null +++ b/Where/WhereUI/Tests/Fixtures/Themes/HalloweenTheme-v1.json @@ -0,0 +1,116 @@ +{ + "schemaVersion": 1, + "id": "halloween", + "fallback": "standard", + "appearance": { + "locationsBackground": { + "rosetteOpacity": { + "constant": { + "_0": 0 + } + } + } + }, + "layers": { + "locationsBackground": [ + { + "schemaVersion": 1, + "root": { + "id": "halloween-background", + "content": { + "group": { + "axis": "vertical", + "alignment": "center", + "spacing": 0, + "children": [ + { + "id": "above", + "content": { + "spacer": { + "minimum": 0 + } + }, + "modifiers": [], + "accessibility": { + "hidden": {} + } + }, + { + "id": "bottom-row", + "content": { + "group": { + "axis": "horizontal", + "alignment": "center", + "spacing": 0, + "children": [ + { + "id": "leading", + "content": { + "spacer": { + "minimum": 0 + } + }, + "modifiers": [], + "accessibility": { + "hidden": {} + } + }, + { + "id": "pumpkin", + "content": { + "text": { + "_0": "🎃" + } + }, + "modifiers": [ + { + "font": { + "_0": { + "size": { + "fixed": { + "_0": 88 + } + } + } + } + }, + { + "opacity": { + "_0": 0.7 + } + } + ], + "accessibility": { + "hidden": {} + } + } + ] + } + }, + "modifiers": [], + "accessibility": { + "hidden": {} + } + } + ] + } + }, + "modifiers": [ + { + "padding": { + "_0": { + "points": { + "_0": 24 + } + } + } + } + ], + "accessibility": { + "hidden": {} + } + } + } + ] + } +} diff --git a/Where/WhereUI/Tests/LegacyThemeFixtures.swift b/Where/WhereUI/Tests/LegacyThemeFixtures.swift new file mode 100644 index 000000000..e2bc1d3dc --- /dev/null +++ b/Where/WhereUI/Tests/LegacyThemeFixtures.swift @@ -0,0 +1,37 @@ +import Foundation +import SFSafeSymbols +import SwiftUI +import ViewDocumentCore +import ViewDocumentUI +@testable import WhereUI + +/// Original version 1 payloads remain test resources after their app demonstrations retire. +@MainActor +enum LegacyThemeFixtures { + enum Theme: String, + CaseIterable { case halloween = "HalloweenTheme-v1", christmas = "ChristmasTheme-v1" } + private final class BundleToken {} + static func document(_ theme: Theme) throws -> WhereThemeDocument { + guard let url = Bundle(for: BundleToken.self).url( + forResource: theme.rawValue, + withExtension: "json", + ) else { + throw WhereThemeValidationError( + path: theme.rawValue, + reason: "Missing version 1 fixture", + ) + } + return try JSONDecoder().decode(WhereThemeDocument.self, from: Data(contentsOf: url)) + } + + static func prepare(_ theme: Theme) throws -> PreparedWhereThemeDocument { + let assets = ViewDocumentAssets( + images: [:], + symbols: [.init(rawValue: "seasonal.bell"): Image(systemSymbol: .bellFill)], + ) + return try PreparedWhereThemeDocument( + validated: .init(document: document(theme), resources: assets.resources), + assets: assets, + ) + } +} diff --git a/Where/WhereUI/Tests/LocationsViewTests.swift b/Where/WhereUI/Tests/LocationsViewTests.swift index 496d856a7..3d67108fe 100644 --- a/Where/WhereUI/Tests/LocationsViewTests.swift +++ b/Where/WhereUI/Tests/LocationsViewTests.swift @@ -1,8 +1,131 @@ import SwiftUI +import TestHostSupport import Testing +import UIKit @testable import WhereUI struct LocationsViewTests { + @MainActor + @Test func halloweenCardsScrollWithinTheClippedViewport() async throws { + let theme = try WhereThemeExamples.halloween.prepare() + let report = PreviewSupport.loadedYearReportModel() + let cache = RegionOutlinePathCache() + await PreviewSupport.loadCardArtwork(for: [.california, .newYork], cache: cache) + let root = LocationsView(report: report) + .environment(\.regionOutlinePathCache, cache) + .whereBroadwayRoot(themeDocument: theme) + .environment(\.scenePhase, .active) + .frame(height: 480) + try await show(UIHostingController(rootView: root)) { host in + try await ThemeInteractionTestSupport.wait("the scrollable illustrated cards") { + ThemeInteractionTestSupport.button("California", in: host.view) != nil + } + let scrollView = try #require(ThemeInteractionTestSupport.objects(in: host.view) + .compactMap { $0 as? UIScrollView } + .first { $0.contentSize.height > $0.bounds.height + 100 }) + #expect(scrollView.clipsToBounds) + let initialOffset = scrollView.contentOffset + let initialFrame = try #require(ThemeInteractionTestSupport + .button("California", in: host.view)).accessibilityFrame + scrollView.setContentOffset( + CGPoint(x: initialOffset.x, y: initialOffset.y + 80), + animated: false, + ) + try await ThemeInteractionTestSupport.wait("the card to scroll with its content") { + guard let card = ThemeInteractionTestSupport.button("California", in: host.view) + else { return false } + return card.accessibilityFrame.minY < initialFrame.minY - 60 + } + #expect(scrollView.clipsToBounds) + scrollView.setContentOffset(initialOffset, animated: false) + try await ThemeInteractionTestSupport + .wait("the card to return to its original position") { + guard let card = ThemeInteractionTestSupport.button("California", in: host.view) + else { return false } + return abs(card.accessibilityFrame.minY - initialFrame.minY) < 1 + } + let returnedFrame = try #require(ThemeInteractionTestSupport + .button("California", in: host.view)).accessibilityFrame + #expect(returnedFrame.width == initialFrame.width) + } + } + + @MainActor + @Test func halloweenCardsKeepNativeFocusBoundsThroughRepeatedNavigationAndSheets() async throws { + let theme = try WhereThemeExamples.halloween.prepare() + let report = PreviewSupport.loadedYearReportModel() + let cache = RegionOutlinePathCache() + await PreviewSupport.loadCardArtwork(for: [.california, .newYork], cache: cache) + let root = LocationsView(report: report) + .environment(\.regionOutlinePathCache, cache) + .whereBroadwayRoot(themeDocument: theme) + .environment(\.scenePhase, .active) + try await show(UIHostingController(rootView: root)) { host in + host.view.window?.layer.speed = 1 + try await ThemeInteractionTestSupport.wait("the native region links") { + ThemeInteractionTestSupport.button("California", in: host.view) != nil + && ThemeInteractionTestSupport.button("New York", in: host.view) != nil + } + try assertNativeCardBounds(in: host.view) + let navigation = try #require(ThemeInteractionTestSupport.navigation(in: host)) + for _ in 0 ..< 2 { + try ThemeInteractionTestSupport.activate( + #require(ThemeInteractionTestSupport.button("California", in: host.view)), + ) + try await ThemeInteractionTestSupport.wait("the card zoom to finish") { + navigation.viewControllers.count == 2 && navigation.transitionCoordinator == nil + } + navigation.popViewController(animated: true) + try await ThemeInteractionTestSupport.wait("the card zoom to return") { + navigation.viewControllers.count == 1 && navigation.transitionCoordinator == nil + && ThemeInteractionTestSupport.button("California", in: host.view) != nil + } + try assertNativeCardBounds(in: host.view) + } + try ThemeInteractionTestSupport.activate( + #require(ThemeInteractionTestSupport.button("Planned stays", in: host.view)), + ) + try await ThemeInteractionTestSupport.wait("the planning sheet") { + ThemeInteractionTestSupport.presented(in: host) != nil + } + let sheet = try #require(ThemeInteractionTestSupport.presented(in: host)) + try await ThemeInteractionTestSupport.wait("the sheet controls") { + ThemeInteractionTestSupport.button("Done", in: sheet.view) != nil + } + try ThemeInteractionTestSupport.activate( + #require(ThemeInteractionTestSupport.button("Done", in: sheet.view)), + ) + try await ThemeInteractionTestSupport.wait("the sheet to dismiss") { + ThemeInteractionTestSupport.presented(in: host) == nil + } + #expect(ThemeInteractionTestSupport.button("California", in: host.view) != nil) + try assertNativeCardBounds(in: host.view) + } + } + + @MainActor + private func assertNativeCardBounds(in view: UIView) throws { + let california = try #require(ThemeInteractionTestSupport.button("California", in: view)) + let newYork = try #require(ThemeInteractionTestSupport.button("New York", in: view)) + // The pumpkin occupies this gap without joining either native focus target. + #expect(newYork.accessibilityFrame.minY - california.accessibilityFrame.maxY >= 70) + #expect(california.accessibilityFrame.width > 0) + #expect(california.accessibilityLabel?.contains("148") == true) + } + + @MainActor + @Test func emptyLocationsCanAnimateTheirThemeBackground() throws { + let theme = try activeThemeBackground() + let host = + UIHostingController(rootView: LocationsView(report: PreviewSupport + .emptyYearReportModel()) + .whereBroadwayRoot(themeDocument: theme) + .environment(\.scenePhase, .active)) + try show(host) { host in + try waitFor { themeBackgroundIsRed(in: host.view) } + } + } + @Test func screenCompositionKeepsViewValuesWithinStackBudget() { #expect(MemoryLayout.size < 16384) } diff --git a/Where/WhereUI/Tests/MainTabsTestSupport.swift b/Where/WhereUI/Tests/MainTabsTestSupport.swift index 0adcabb0b..32806fe2c 100644 --- a/Where/WhereUI/Tests/MainTabsTestSupport.swift +++ b/Where/WhereUI/Tests/MainTabsTestSupport.swift @@ -35,11 +35,9 @@ struct UpdateSceneHost: View { .environment(session) .environment(model) .environment(updates) + .background { ThemeScenePhaseProbe { fixture.renderedPhase = $0 } } .environment(\.scenePhase, fixture.phase) .whereBroadwayRoot() - .onChange(of: fixture.phase, initial: true) { _, phase in - fixture.renderedPhase = phase - } } } diff --git a/Where/WhereUI/Tests/MainTabsTests.swift b/Where/WhereUI/Tests/MainTabsTests.swift index eb839098e..e071ed81e 100644 --- a/Where/WhereUI/Tests/MainTabsTests.swift +++ b/Where/WhereUI/Tests/MainTabsTests.swift @@ -1,4 +1,5 @@ import InstallerKit +@_spi(Capture) import SnapshotKit @_spi(Testing) import InstallerKitUI import SwiftUI import TestHostSupport @@ -8,6 +9,43 @@ import WhereCore @MainActor struct MainTabsTests { + @Test func halloweenSwitchesTabsAndReturnsFromBackground() async throws { + let snapshot = try await MainTabs.halloweenSnapshot(selection: .locations) + guard case let .single(content) = snapshot.capture else { + Issue.record("Expected the production tab fixture") + return + } + var observedPhase: ScenePhase? + func root(_ phase: ScenePhase) -> some View { + content() + .background { ThemeScenePhaseProbe { observedPhase = $0 } } + .environment(\.scenePhase, phase) + } + let host = UIHostingController(rootView: root(.active)) + try await show(host) { host in + try await waitForUpdateScene { findTabController(in: host)?.tabs.count == 4 } + let tabs = try #require(findTabController(in: host)) + for index in [1, 2, 3, 0] { + tabs.selectedIndex = index + if let selected = tabs.selectedViewController { + tabs.delegate?.tabBarController?(tabs, didSelect: selected) + } + try await waitForUpdateScene { + tabs.selectedIndex == index && tabs.selectedViewController?.viewIfLoaded? + .window != nil + } + } + host.rootView = root(.background) + try await waitForUpdateScene { observedPhase == .background } + host.rootView = root(.active) + try await waitForUpdateScene { + observedPhase == .active && findTabController(in: host)?.selectedIndex == 0 + && ThemeInteractionTestSupport.button("California", in: host.view) != nil + } + #expect(tabs.tabs.map(\.title) == ["Locations", "Elsewhere", "Your Year", "Settings"]) + } + } + @Test func elsewhereTabAppearsForAVisitAndFallsBackAfterItsRemoval() async throws { let session = PreviewSupport.loadedSession() session.preferences.showsLocationWelcome = false @@ -61,7 +99,9 @@ struct MainTabsTests { try await waitForUpdateScene { fixture.renderedPhase == .background } #expect(client.fetchCount == 0) fixture.phase = .active - try await waitForUpdateScene { updates.needsAttention } + try await waitForUpdateScene { + fixture.renderedPhase == .active && updates.needsAttention && !updates.isChecking + } #expect(client.fetchCount == 1) fixture.phase = .background try await waitForUpdateScene { fixture.renderedPhase == .background } diff --git a/Where/WhereUI/Tests/PreparedWhereThemeDocumentTests.swift b/Where/WhereUI/Tests/PreparedWhereThemeDocumentTests.swift new file mode 100644 index 000000000..410d5adaa --- /dev/null +++ b/Where/WhereUI/Tests/PreparedWhereThemeDocumentTests.swift @@ -0,0 +1,20 @@ +import Testing +import ViewDocumentCore +import ViewDocumentUI +@testable import WhereUI + +@MainActor +struct PreparedWhereThemeDocumentTests { + @Test func missingPreparedResourcesRejectsWholeTheme() throws { + let theme = try LegacyThemeFixtures.prepare(.christmas) + #expect(throws: ViewDocumentView.ResourceError.self) { + try PreparedWhereThemeDocument( + validated: theme.validated, + assets: .init(images: [:], symbols: [:]), + ) + } + #expect(theme.componentArtwork[.locations]?.isEmpty == true) + #expect(theme.componentArtwork[.regularCard]?.count == 1) + #expect(theme.componentArtwork[.compactCard]?.count == 1) + } +} diff --git a/Where/WhereUI/Tests/PrivacyPassportCardSurfaceTests.swift b/Where/WhereUI/Tests/PrivacyPassportCardSurfaceTests.swift new file mode 100644 index 000000000..777e95dba --- /dev/null +++ b/Where/WhereUI/Tests/PrivacyPassportCardSurfaceTests.swift @@ -0,0 +1,62 @@ +import SwiftUI +import TestHostSupport +import Testing +import UIKit +@testable import WhereUI + +@MainActor +struct PrivacyPassportCardSurfaceTests { + @Test func fixedDarkPaperDoesNotInheritLightPageInk() throws { + var document = WhereThemeDocument( + id: .init(rawValue: "page-ink"), + parent: .flat, + values: .init(colors: [.init(id: .init(rawValue: "page.ink"), value: .modes(.init( + light: .rgba(red: 0, green: 0, blue: 0, alpha: 1), + dark: .rgba(red: 1, green: 1, blue: 1, alpha: 1), + )))], fonts: nil, surfaces: nil), + appearance: .init(), + components: .init(), + ) + document.components.sheet = .init() + document.components.sheet?.foreground = .value(.init(rawValue: "page.ink")) + let theme = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [document]) + .prepare(document.id, assets: .init(images: [:], symbols: [:])) + let surface = PrivacyPassportCardSurface(tilt: .preview) { + VStack { + Text("Primary") + Text("Supporting text").foregroundStyle(.whereSecondary) + }.padding(24) + } + let expected = ImageRenderer(content: surface + .whereBroadwayRoot(themeDocument: theme).environment(\.colorScheme, .light)) + let actual = ImageRenderer(content: surface + .modifier(WhereComponentTextStyle(role: .sheet)) + .whereBroadwayRoot(themeDocument: theme).environment(\.colorScheme, .light)) + #expect(try #require(actual.uiImage?.pngData()) == #require(expected.uiImage?.pngData())) + } + + @Test func passportPaperUsesTheSameDarkModeAsItsContent() throws { + var document = WhereThemeDocument( + id: .init(rawValue: "privacy-modes"), + parent: .flat, + values: .init(colors: [.init(id: .init(rawValue: "privacy.fill"), value: .modes(.init( + light: .rgba(red: 1, green: 1, blue: 1, alpha: 1), + dark: .rgba(red: 1, green: 0, blue: 0, alpha: 1), + )))], fonts: nil, surfaces: nil), + appearance: .init(), + components: .init(), + ) + document.components.privacy = .init() + document.components.privacy?.nativeArtwork = [] + let theme = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [document]) + .prepare(document.id, assets: .init(images: [:], symbols: [:])) + let host = + UIHostingController(rootView: PrivacyPassportCardSurface(tilt: .preview) { Color.clear } + .whereBroadwayRoot(themeDocument: theme).environment(\.colorScheme, .light)) + try show(host) { host in + try waitFor { themeBackgroundIsRed(in: host.view) } + } + } +} diff --git a/Where/WhereUI/Tests/PrivacyPassportCardTests.swift b/Where/WhereUI/Tests/PrivacyPassportCardTests.swift index 0e9b3425c..c4a905f1e 100644 --- a/Where/WhereUI/Tests/PrivacyPassportCardTests.swift +++ b/Where/WhereUI/Tests/PrivacyPassportCardTests.swift @@ -5,6 +5,41 @@ import Testing @MainActor struct PrivacyPassportCardTests { + @Test func typographyAndLayoutResolveInsideTheDarkPassportBoundary() throws { + var document = WhereThemeDocument( + id: .init(rawValue: "privacy-layout"), + parent: .flat, + values: .init( + colors: nil, + fonts: [.init(id: .init(rawValue: "privacy-body"), value: .modes(.init( + light: .init(size: .semantic(.caption2), weight: nil, design: nil), + dark: .init(size: .semantic(.title), weight: nil, design: nil), + )))], + surfaces: nil, + ), + appearance: .init(), + components: .init(), + ) + document.components.privacy = .init() + document.components.privacy?.body = .value(.init(rawValue: "privacy-body")) + document.appearance.privacyPassportCard = .init() + document.appearance.privacyPassportCard?.padding = .modes(.init(light: 4, dark: 30)) + let theme = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [document]) + .prepare(document.id, assets: .init(images: [:], symbols: [:])) + func size(_ mode: ColorScheme) throws -> CGSize { + let card = PrivacyPassportCard( + presentation: .init(configuration: .defaults(isDebugBuild: false)), + disclosureInteraction: .staticContent, + ) + .frame(width: 360) + .whereBroadwayRoot(themeDocument: theme) + .environment(\.colorScheme, mode) + return try #require(ImageRenderer(content: card).uiImage).size + } + #expect(try size(.light) == size(.dark)) + } + @Test func hosts() throws { let rootView = PrivacyPassportCard(presentation: PrivacyPassportPresentation( configuration: .defaults(isDebugBuild: false), diff --git a/Where/WhereUI/Tests/RankingAnimationPreviewTests.swift b/Where/WhereUI/Tests/RankingAnimationPreviewTests.swift new file mode 100644 index 000000000..63c46dbb7 --- /dev/null +++ b/Where/WhereUI/Tests/RankingAnimationPreviewTests.swift @@ -0,0 +1,56 @@ +import SwiftUI +import TestHostSupport +import Testing +import UIKit +@testable import WhereUI + +@MainActor +struct RankingAnimationPreviewTests { + @Test(arguments: [false, true]) + func halloweenCardsCompleteTwoOvertakesWithNativeFocusOrder(reduceMotion: Bool) async throws { + let model = RankingAnimationLabModel() + let theme = try WhereThemeExamples.halloween.prepare() + let cache = RegionOutlinePathCache() + await PreviewSupport.loadCardArtwork(for: [.california, .newYork], cache: cache) + let root = RankingAnimationPreview( + model: model, + motion: reduceMotion ? .reducedMotion : .standard, + isVisible: true, + ) + .padding(24) + .environment(\.regionOutlinePathCache, cache) + .whereBroadwayRoot(themeDocument: theme) + .environment(\.scenePhase, .active) + try await show(UIHostingController(rootView: root)) { host in + host.view.window?.layer.speed = 1 + try await ThemeInteractionTestSupport.wait("the ranked card content") { + card("California", in: host.view) != nil && card("New York", in: host.view) != nil + } + for sequence in 1 ... 2 { + model.playNextOvertake() + try await ThemeInteractionTestSupport.wait("the overtake to settle") { + model.presentation.latestOvertake?.sequence == sequence + && model.presentation.overtakeMovement == nil + && model.presentation.presented(model.current) == model.current + } + let winner = sequence == 1 ? "New York" : "California" + let loser = sequence == 1 ? "California" : "New York" + try await ThemeInteractionTestSupport.wait("the settled native focus frames") { + guard let first = card(winner, in: host.view), + let second = card(loser, in: host.view) + else { return false } + return first.accessibilityFrame.maxY < second.accessibilityFrame.minY + } + let labels = ThemeInteractionTestSupport.objects(in: host.view) + .filter(\.isAccessibilityElement).compactMap(\.accessibilityLabel) + #expect(!labels.contains { $0.localizedCaseInsensitiveContains("pumpkin") }) + } + } + } + + private func card(_ name: String, in view: UIView) -> NSObject? { + ThemeInteractionTestSupport.objects(in: view).first { + $0.isAccessibilityElement && $0.accessibilityLabel?.hasPrefix(name) == true + } + } +} diff --git a/Where/WhereUI/Tests/RegionArtworkTransformTests.swift b/Where/WhereUI/Tests/RegionArtworkTransformTests.swift index 27247bf21..456ee77bd 100644 --- a/Where/WhereUI/Tests/RegionArtworkTransformTests.swift +++ b/Where/WhereUI/Tests/RegionArtworkTransformTests.swift @@ -3,6 +3,30 @@ import Testing @testable import WhereUI struct RegionArtworkTransformTests { + @Test(arguments: [CGFloat.zero, -.infinity, .infinity, .nan]) + func rejectsDegenerateArtworkScale(_ scale: CGFloat) throws { + let path = Path(CGRect(x: 0, y: 0, width: 100, height: 50)) + var style = try #require(WhereStylesheet.default.card.regular.regionShape?.watermark) + style.scale = scale + #expect(RegionArtworkTransform( + path: path, + size: .init(width: 320, height: 180), + style: style, + ) == nil) + } + + @Test func zeroExtentAndCollapsedLayoutHaveNoTransform() throws { + let path = Path(CGRect(x: 0, y: 0, width: 100, height: 50)) + var style = try #require(WhereStylesheet.default.card.regular.regionShape?.watermark) + #expect(RegionArtworkTransform(path: path, size: .zero, style: style) == nil) + style.extent.width = 0 + #expect(RegionArtworkTransform( + path: path, + size: .init(width: 320, height: 180), + style: style, + ) == nil) + } + @Test func fitsAndCentersProjectedGeometryUsingTheArtworkSpec() throws { let path = Path { $0.addRect(CGRect(x: 0, y: 0, width: 100, height: 50)) } let style = try #require(WhereStylesheet.default.card.regular.regionShape?.watermark) diff --git a/Where/WhereUI/Tests/ThemeInteractionTestSupport.swift b/Where/WhereUI/Tests/ThemeInteractionTestSupport.swift new file mode 100644 index 000000000..920fe721b --- /dev/null +++ b/Where/WhereUI/Tests/ThemeInteractionTestSupport.swift @@ -0,0 +1,73 @@ +import AccessibilitySnapshotParser +import SwiftUI +import TestHostSupport +import Testing +import UIKit + +/// Native accessibility actions and controller transitions for themed production views. +@MainActor +enum ThemeInteractionTestSupport { + static func objects(in root: NSObject) -> [NSObject] { + var visited: Set = [] + func visit(_ object: NSObject) -> [NSObject] { + guard visited.insert(ObjectIdentifier(object)).inserted else { return [] } + var result = [object] + if let view = object as? UIView { result += view.subviews.flatMap(visit) } + let count = object.accessibilityElementCount() + if count > 0, count < 10000 { + for index in 0 ..< count { + if let child = object.accessibilityElement(at: index) as? NSObject { + result += visit(child) + } + } + } + return result + } + return visit(root) + } + + static func button(_ prefix: String, in root: UIView) -> NSObject? { + objects(in: root).first { + $0.accessibilityLabel?.hasPrefix(prefix) == true + && $0.accessibilityTraits.contains(.button) + } + } + + static func activate(_ object: NSObject) throws { + if let control = object as? UIControl { control.sendActions(for: .touchUpInside) } + else { try #require(object.accessibilityActivate()) } + } + + static func navigation(in root: UIViewController) -> UINavigationController? { + if let navigation = root as? UINavigationController { return navigation } + return root.children.lazy.compactMap { navigation(in: $0) }.first + } + + static func presented(in root: UIViewController) -> UIViewController? { + if let presented = root.presentedViewController { return presented } + return root.children.lazy.compactMap { presented(in: $0) }.first + } + + static func wait(_ condition: String, until predicate: () -> Bool) async throws { + let clock = ContinuousClock() + let deadline = clock.now.advanced(by: .seconds(10)) + while !predicate() { + try Task.checkCancellation() + guard clock.now < deadline else { + throw TestHostError("Timed out waiting for \(condition)") + } + try await Task.sleep(for: .milliseconds(10)) + } + } +} + +struct ThemeScenePhaseProbe: View { + let observed: (ScenePhase) -> Void + @Environment(\.scenePhase) private var phase + + var body: some View { + // Observe the environment after SwiftUI schedules this phase's tasks. An + // onChange callback can precede that boundary and coalesce a rapid return. + Color.clear.task(id: phase) { observed(phase) } + } +} diff --git a/Where/WhereUI/Tests/ValidatedWhereThemeDocumentTests.swift b/Where/WhereUI/Tests/ValidatedWhereThemeDocumentTests.swift new file mode 100644 index 000000000..0131cbf3e --- /dev/null +++ b/Where/WhereUI/Tests/ValidatedWhereThemeDocumentTests.swift @@ -0,0 +1,186 @@ +import BroadwayCore +import Foundation +import Testing +import ViewDocumentCore +@testable import WhereUI + +@MainActor +struct ValidatedWhereThemeDocumentTests { + @Test func rasterLayersRequirePreparedAssets() throws { + let asset = ViewAssetID(rawValue: "illustration") + var document = themeDocument(appearance: .init(), fallback: .flat) + document.layers.regularCardBackground = [.init(schemaVersion: 1, root: .init( + id: .init(rawValue: "art"), + content: .raster(.init(asset: asset, sizing: .fill)), + modifiers: [], + animation: nil, + accessibility: .hidden, + ))] + #expect(throws: ViewDocumentValidationError.self) { + try ValidatedWhereThemeDocument( + document: document, + resources: .init(images: [], symbols: []), + ) + } + let accepted = try ValidatedWhereThemeDocument( + document: document, + resources: .init(images: [asset], symbols: []), + ) + #expect(accepted.artwork[.regularCard]?.first?.resources.images == [asset]) + } + + @Test func modifierBudgetIsSharedAcrossSlots() throws { + let modifiers: [ViewModifierDescriptor] = Array(repeating: .opacity(1), count: 32) + let layer = ViewDocument(schemaVersion: 1, root: .init( + id: .init(rawValue: "root"), + content: .group( + axis: .overlay, + alignment: .center, + spacing: 0, + children: (0 ..< 31).map { + .init( + id: .init(rawValue: "child-\($0)"), + content: .text(""), + modifiers: modifiers, + animation: nil, + accessibility: .hidden, + ) + }, + ), + modifiers: modifiers, + animation: nil, + accessibility: .hidden, + )) + let resources = ViewDocumentResources(images: [], symbols: []) + var document = themeDocument(appearance: .init(), fallback: .standard) + document.layers.locationsBackground = [layer] + _ = try ValidatedWhereThemeDocument(document: document, resources: resources) + document.layers.regularCardOverlay = [layer] + #expect(throws: WhereThemeValidationError.self) { + try ValidatedWhereThemeDocument(document: document, resources: resources) + } + } + + @Test func rejectsUnsafePatternsThroughJSONImport() throws { + for patch in [ + #"{"featureDiscovery":{"backgroundPattern":{"horizontalScale":{"constant":{"_0":0}}}}}"#, + #"{"locationForecast":{"surface":{"microprintSpacing":{"constant":{"_0":1e-300}}}}}"#, + #"{"locationsBackground":{"preferredCellSize":{"constant":{"_0":2}}}}"#, + #"{"card":{"regular":{"sheen":{"intensity":{"constant":{"_0":42}}}}}}"#, + #"{"card":{"regular":{"sheen":{"staticGlintIntensity":{"constant":{"_0":42}}}}}}"#, + #"{"privacyPassportCard":{"reflectiveSurface":{"intensity":{"constant":{"_0":42}}}}}"#, + #"{"privacyPassportCard":{"reflectiveSurface":{"staticGlintIntensity":{"constant":{"_0":42}}}}}"#, + #"{"card":{"rosetteFill":{"primary":{"constant":{"_0":42}}}}}"#, + #"{"card":{"rosetteFill":{"secondary":{"constant":{"_0":42}}}}}"#, + #"{"card":{"constellation":{"coreWhiteMix":{"constant":{"_0":42}}}}}"#, + #"{"card":{"securityPrint":{"whiteMix":{"constant":{"_0":42}}}}}"#, + #"{"developerOverlay":{"menu":{"motion":{"stagger":{"constant":{"_0":10000}}}}}}"#, + #"{"passportSeal":{"dashSpacing":{"constant":{"_0":-1}}}}"#, + ] { + let json = + "{\"schemaVersion\":1,\"id\":\"unsafe\",\"fallback\":\"standard\",\"appearance\":" + + patch + ",\"layers\":{}}" + #expect(throws: WhereThemeValidationError.self) { + try ValidatedWhereThemeDocument.decode( + Data(json.utf8), + resources: .init(images: [], symbols: []), + ) + } + } + } + + @Test func layerBudgetIsSharedAcrossSlots() throws { + let layer = ViewDocument(schemaVersion: 1, root: .init( + id: .init(rawValue: "root"), + content: .group( + axis: .overlay, + alignment: .center, + spacing: 0, + children: (0 ..< 128).map { + .init( + id: .init(rawValue: "child-\($0)"), + content: .text(""), + modifiers: [], + animation: nil, + accessibility: .hidden, + ) + }, + ), + modifiers: [], + animation: nil, + accessibility: .hidden, + )) + let resources = ViewDocumentResources(images: [], symbols: []) + _ = try ValidatedViewDocument(document: layer, resources: resources, limits: .standard) + var document = themeDocument(appearance: .init(), fallback: .standard) + document.layers.locationsBackground = [layer] + _ = try ValidatedWhereThemeDocument(document: document, resources: resources) + document.layers.regularCardOverlay = [layer] + #expect(throws: WhereThemeValidationError.self) { + try ValidatedWhereThemeDocument(document: document, resources: resources) + } + } + + @Test func rejectsInvalidInactiveModeBeforeSelection() throws { + var appearance = WhereThemeAppearance() + appearance.flightStatus = .init() + appearance.flightStatus?.padding = .modes(.init(light: 8, dark: -20)) + #expect(throws: WhereThemeValidationError.self) { + try ValidatedWhereThemeDocument( + document: themeDocument(appearance: appearance, fallback: .standard), + resources: .init(images: [], symbols: []), + ) + } + } + + @Test func rejectsVersionAndPayloadBeforeRendering() throws { + var document = themeDocument(appearance: .init(), fallback: .standard) + document.schemaVersion = 99 + #expect(throws: WhereThemeValidationError.self) { + try ValidatedWhereThemeDocument( + document: document, + resources: .init(images: [], symbols: []), + ) + } + #expect(throws: WhereThemeValidationError.self) { + try ValidatedWhereThemeDocument.decode( + Data(repeating: 0, count: ViewDocumentLimits.standard.bytes + 1), + resources: .init(images: [], symbols: []), + ) + } + } + + @Test func accessibilityWinsOverCustomMotionAndGlow() throws { + var appearance = WhereThemeAppearance() + appearance.card = .init() + appearance.card?.regular = .init() + appearance.card?.regular?.glow = .init() + appearance.card?.regular?.glow?.radius = .constant(20) + appearance.launch = .init() + appearance.launch?.reveal = .constant(.zoom) + var traits = BTraits.system + traits.accessibility = .init(isReduceMotionEnabled: true, isReduceTransparencyEnabled: true) + let resolved = try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: traits, + ) + #expect(resolved.card.regular.glow.radius == 0) + #expect(resolved.launch.reveal == .crossfade) + } + + @Test func clearDiffersFromInheritanceAndArraysReplace() throws { + var appearance = WhereThemeAppearance() + appearance.card = .init() + appearance.card?.regular = .init() + appearance.card?.regular?.regionShape = .clear + appearance.card?.estimateSticker = .init() + appearance.card?.estimateSticker?.innerDash = .constant([]) + let resolved = try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: .system, + ) + #expect(resolved.card.regular.regionShape == nil) + #expect(resolved.card.compact.regionShape != nil) + #expect(resolved.card.estimateSticker.innerDash.isEmpty) + } +} diff --git a/Where/WhereUI/Tests/WhereFlatThemeTests.swift b/Where/WhereUI/Tests/WhereBundledThemesTests.swift similarity index 69% rename from Where/WhereUI/Tests/WhereFlatThemeTests.swift rename to Where/WhereUI/Tests/WhereBundledThemesTests.swift index 7d192b6c5..78488bb4b 100644 --- a/Where/WhereUI/Tests/WhereFlatThemeTests.swift +++ b/Where/WhereUI/Tests/WhereBundledThemesTests.swift @@ -5,14 +5,29 @@ import WhereCore @testable import WhereUI @MainActor -struct WhereFlatThemeTests { +struct WhereBundledThemesTests { + @Test(arguments: WhereTheme.allCases) + func everySavedThemePreparesThroughTheBundledCatalog(theme: WhereTheme) throws { + let prepared = try WhereBundledThemes.prepared(theme) + let resolved = try WhereBundledThemeDefinitions.resolved(theme) + #expect(prepared.resolved == resolved) + try resolved.appearance.requireComplete(path: "appearance") + if theme == .halloween { + #expect(resolved.id == .halloween) + #expect(prepared.componentArtwork[.regularCard]?.isEmpty == false) + } + } + @Test(arguments: [BMode.light, .dark], [false, true]) func resolvesModeAndContrast(mode: BMode, increasedContrast: Bool) throws { var traits = BTraits.system traits.mode = mode traits.accessibility = BAccessibility(isDarkerSystemColorsEnabled: increasedContrast) - let overrides = WhereFlatTheme.overrides(traits: traits, base: .default) - let surface = try #require(overrides.card.regular.surface) + let sheet = try WhereStylesheet( + resolved: WhereBundledThemeDefinitions.resolved(.alternate), + traits: traits, + ) + let surface = sheet.card.regular.surface guard case let .matte(matte) = surface else { Issue.record("Flat requires an opaque matte surface") return @@ -21,26 +36,7 @@ struct WhereFlatThemeTests { #expect(matte.fill.tintMix == (mode == .light ? 0.52 : 0.58)) #expect(matte.outline.opacity == (increasedContrast ? 0.65 : (mode == .light ? 0.32 : 0.4))) #expect(matte.outline.width == (increasedContrast ? 1.5 : 1)) - #expect(overrides.card.regular.contentTint?.fraction == (increasedContrast ? 0.75 : 0.6)) - } - - @Test func inheritsPaperAndPrivacyOutlineFromSuppliedBase() throws { - var base = WhereStylesheet.default - base.card.elsewherePaper.surface.fill.base = .purple - base.privacyPassportCard.outlineOpacity = 0.83 - base.privacyPassportCard.outlineWidth = 2.25 - var traits = BTraits.system - traits.accessibility = BAccessibility() - let overrides = WhereFlatTheme.overrides(traits: traits, base: base) - let elsewhere = try #require(overrides.elsewhere.surface) - let privacy = try #require(overrides.privacy.surface) - guard case let .matte(passport) = privacy else { - Issue.record("Flat requires matte paper and passport surfaces") - return - } - #expect(elsewhere.fill.base == .purple) - #expect(passport.outline.opacity == 0.83) - #expect(passport.outline.width == 2.25) + #expect(sheet.card.regular.contentTint.fraction == (increasedContrast ? 0.75 : 0.6)) } @Test(arguments: [BMode.light, .dark], [false, true]) diff --git a/Where/WhereUI/Tests/WhereComponentStylesheetTests.swift b/Where/WhereUI/Tests/WhereComponentStylesheetTests.swift new file mode 100644 index 000000000..6b4824230 --- /dev/null +++ b/Where/WhereUI/Tests/WhereComponentStylesheetTests.swift @@ -0,0 +1,72 @@ +import BroadwayCore +import SwiftUI +import Testing +import ViewDocumentCore +@testable import WhereUI + +@MainActor +struct WhereComponentStylesheetTests { + @Test func authoredPointSizesStillScaleForNativeDynamicType() throws { + var document = WhereThemeDocument( + id: .init(rawValue: "scaled-font"), + parent: .standard, + values: .init( + colors: nil, + fonts: [.init( + id: .init(rawValue: "point-size"), + value: .constant(.init(size: .fixed(18), weight: .regular, design: .rounded)), + )], + surfaces: nil, + ), + appearance: .init(), + components: .init(), + ) + document.components.welcome = .init() + document.components.welcome?.heading = .value(.init(rawValue: "point-size")) + document.components.welcome?.body = .value(.init(rawValue: "point-size")) + let theme = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [document]) + .prepare(document.id, assets: .init(images: [:], symbols: [:])) + var themes = BThemes() + themes[WhereDocumentTheme.self] = .init(document: theme.validated) + var traits = BTraits() + let normal = try BContext(traits: traits, themes: themes).stylesheets + .get(WhereComponentStylesheet.self) + traits.contentSizeCategory = .accessibilityExtraExtraExtraLarge + let large = try BContext(traits: traits, themes: themes).stylesheets + .get(WhereComponentStylesheet.self) + #expect(normal[.welcome].heading != large[.welcome].heading) + #expect(normal[.welcome].body != large[.welcome].body) + } + + @Test func accessibilityKeepsEssentialSurfacesAndBoundsOptionalArtwork() throws { + let prepared = try WhereThemeExamples.halloween.prepare() + var themes = BThemes() + themes[WhereDocumentTheme.self] = .init(document: prepared.validated) + var traits = BTraits() + let regular = try BContext(traits: traits, themes: themes).stylesheets + .get(WhereComponentStylesheet.self) + #expect(regular[.locations].background != nil) + #expect(regular[.welcome].secondaryForeground == regular[.welcome].foreground? + .opacity(0.85)) + #expect(!regular[.regularCard].artworkOpacity.isEmpty) + #expect(regular[.regularCard].outsets.bottom == 70) + traits.accessibility = .init( + isDarkerSystemColorsEnabled: true, + isReduceTransparencyEnabled: true, + ) + let accessible = try BContext(traits: traits, themes: themes).stylesheets + .get(WhereComponentStylesheet.self) + #expect(accessible[.locations].background == regular[.locations].background) + #expect(accessible[.welcome].secondaryForeground == accessible[.welcome].foreground? + .opacity(1)) + #expect(Set(accessible[.regularCard].artworkOpacity.keys) == [.init(rawValue: "edge")]) + #expect(accessible[.regularCard].outsets == EdgeInsets()) + guard case let .matte(paper) = accessible[.calendarMonth].surface else { + Issue.record("Essential paper surface disappeared") + return + } + #expect(paper.outline.width >= 1.5) + #expect(paper.outline.opacity >= 0.65) + } +} diff --git a/Where/WhereUI/Tests/WhereComponentSurfaceTests.swift b/Where/WhereUI/Tests/WhereComponentSurfaceTests.swift new file mode 100644 index 000000000..970c9c164 --- /dev/null +++ b/Where/WhereUI/Tests/WhereComponentSurfaceTests.swift @@ -0,0 +1,46 @@ +import SwiftUI +import TestHostSupport +import Testing +import UIKit +@testable import WhereUI + +@MainActor +struct WhereComponentSurfaceTests { + @Test func hostActivityAndScenePhaseStopAndRestartLayerPlayback() throws { + let theme = try activeThemeBackground() + func content(isActive: Bool, scenePhase: ScenePhase) -> some View { + Color.clear + .modifier(WhereComponentSurface( + role: .locations, + shape: Rectangle(), + finish: EmptyModifier(), + )) + .whereBroadwayRoot(themeDocument: theme) + .environment(\.whereThemeLayersAreActive, isActive) + .environment(\.scenePhase, scenePhase) + } + let host = UIHostingController(rootView: content(isActive: true, scenePhase: .active)) + try show(host) { host in + try waitFor { themeBackgroundIsRed(in: host.view) } + host.rootView = content(isActive: false, scenePhase: .active) + try waitFor { !themeBackgroundIsRed(in: host.view) } + host.rootView = content(isActive: true, scenePhase: .active) + try waitFor { themeBackgroundIsRed(in: host.view) } + host.rootView = content(isActive: true, scenePhase: .inactive) + try waitFor { !themeBackgroundIsRed(in: host.view) } + } + } + + @Test func artworkPreservesHostBounds() throws { + let theme = try LegacyThemeFixtures.prepare(.christmas) + let host = Color.white.frame(width: 90, height: 60) + .modifier(WhereComponentSurface( + role: .regularCard, + shape: RoundedRectangle(cornerRadius: 12), + finish: EmptyModifier(), + )) + .whereBroadwayRoot(themeDocument: theme) + let image = try #require(ImageRenderer(content: host).uiImage) + #expect(image.size == CGSize(width: 90, height: 60)) + } +} diff --git a/Where/WhereUI/Tests/WhereModelTests.swift b/Where/WhereUI/Tests/WhereModelTests.swift index 14cfa80d0..77283da68 100644 --- a/Where/WhereUI/Tests/WhereModelTests.swift +++ b/Where/WhereUI/Tests/WhereModelTests.swift @@ -64,26 +64,27 @@ struct WhereModelTests { ) } - @Test func loadsPreviewsCommitsAndResetsTheme() throws { + @Test(arguments: [WhereTheme.alternate, .halloween]) + func loadsPreviewsCommitsAndResetsTheme(theme: WhereTheme) throws { let preferences = makePreferences() - preferences.theme = .alternate + preferences.theme = theme let model = try WhereModel( services: makeServices(), preferences: preferences, logSystem: .isolated(), ) - #expect(model.theme == .alternate) + #expect(model.theme == theme) model.previewTheme(.standard) #expect(model.theme == .standard) - #expect(preferences.theme == .alternate) + #expect(preferences.theme == theme) model.completeOnboarding() #expect(preferences.theme == .standard) - model.selectTheme(.alternate) - #expect(model.theme == .alternate) - #expect(preferences.theme == .alternate) + model.selectTheme(theme) + #expect(model.theme == theme) + #expect(preferences.theme == theme) try model.resetPreferences() #expect(model.theme == .standard) @@ -113,7 +114,8 @@ struct WhereModelTests { #expect(await gate.delivered == [.standard]) } - @Test func onboardingCommitLaunchSynchronizationAndResetPublishTheme() async throws { + @Test(arguments: [WhereTheme.alternate, .halloween]) + func onboardingCommitLaunchSynchronizationAndResetPublishTheme(theme: WhereTheme) async throws { let preferences = makePreferences() let model = try WhereModel( services: makeServices(), @@ -123,24 +125,24 @@ struct WhereModelTests { let recorder = ThemeRecorder() model.onThemeChanged = { await recorder.receive($0) } - model.previewTheme(.alternate) + model.previewTheme(theme) model.completeOnboarding() while await recorder.delivered.count < 1 { await Task.yield() } - #expect(await recorder.delivered == [.alternate]) + #expect(await recorder.delivered == [theme]) model.synchronizeTheme() while await recorder.delivered.count < 2 { await Task.yield() } - #expect(await recorder.delivered == [.alternate, .alternate]) + #expect(await recorder.delivered == [theme, theme]) try model.resetPreferences() while await recorder.delivered.count < 3 { await Task.yield() } - #expect(await recorder.delivered == [.alternate, .alternate, .standard]) + #expect(await recorder.delivered == [theme, theme, .standard]) } @Test func lateLogStoreArrivalPublishesReadyState() async throws { diff --git a/Where/WhereUI/Tests/WhereSecondaryContentStyleTests.swift b/Where/WhereUI/Tests/WhereSecondaryContentStyleTests.swift new file mode 100644 index 000000000..32bef3218 --- /dev/null +++ b/Where/WhereUI/Tests/WhereSecondaryContentStyleTests.swift @@ -0,0 +1,30 @@ +import SwiftUI +import Testing +@testable import WhereUI + +@MainActor +struct WhereSecondaryContentStyleTests { + @Test(arguments: [ColorScheme.light, .dark]) + func builtInSecondaryStyleIsUnchanged(scheme: ColorScheme) throws { + let actual = Rectangle().fill(.whereSecondary) + .frame(width: 20, height: 20).environment(\.colorScheme, scheme) + let expected = Rectangle().fill(.secondary) + .frame(width: 20, height: 20).environment(\.colorScheme, scheme) + #expect(try pixels(actual) == pixels(expected)) + } + + @Test func authoredInkDoesNotUseTheFaintHierarchicalSecondary() throws { + let ink = Color(red: 0.3, green: 0.2, blue: 0.35) + let actual = Rectangle().fill(.whereSecondary) + .frame(width: 20, height: 20) + .foregroundStyle(ink) + .environment(\.whereThemeSecondaryInk, ink.opacity(0.85)) + let expected = Rectangle().fill(ink.opacity(0.85)) + .frame(width: 20, height: 20) + #expect(try pixels(actual) == pixels(expected)) + } + + private func pixels(_ view: some View) throws -> Data { + try #require(ImageRenderer(content: view).uiImage?.pngData()) + } +} diff --git a/Where/WhereUI/Tests/WhereStylesheetTests.swift b/Where/WhereUI/Tests/WhereStylesheetTests.swift index 355fe73f6..b61f60ad2 100644 --- a/Where/WhereUI/Tests/WhereStylesheetTests.swift +++ b/Where/WhereUI/Tests/WhereStylesheetTests.swift @@ -12,6 +12,44 @@ import WhereCore /// `UIConstants`. These assertions pin those values so the migration — and any /// later trait-aware derivation — can't silently drift the defaults. struct WhereStylesheetTests { + @MainActor + @Test(arguments: [0.0, 1.0], [BMode.light, .dark]) + func reduceTransparencyKeepsPaperOpaque(alpha: Double, mode: BMode) throws { + var appearance = WhereThemeAppearance() + appearance.locationForecast = .init() + appearance.locationForecast?.summary = .init() + appearance.locationForecast?.summary?.paper = .constant(.rgba( + red: 1, + green: 0, + blue: 0, + alpha: alpha, + )) + appearance.locationsBackground = .init() + appearance.locationsBackground?.paper = .constant(.rgba( + red: 1, + green: 0, + blue: 0, + alpha: alpha, + )) + var traits = BTraits.system + traits.accessibility = .init(isReduceTransparencyEnabled: true) + traits.mode = mode + let baseline = try resolveTheme( + themeDocument(appearance: .init(), fallback: .standard), + traits: traits, + ) + let result = try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: traits, + ) + let red = Color(.sRGB, red: 1, green: 0, blue: 0, opacity: 1) + #expect(result.locationForecast.summary + .paper == (alpha == 1 ? red : baseline.locationForecast.summary.paper)) + #expect(result.locationsBackground + .paper == (alpha == 1 ? red : baseline.locationsBackground.paper)) + #expect(result.locationForecast.summary.usesOpaquePaper) + } + @MainActor @Test(arguments: DynamicTypeSize.allCases) func launcherMatchesSwiftUIScaledMetric(size: DynamicTypeSize) throws { @@ -31,6 +69,24 @@ struct WhereStylesheetTests { private let style = WhereStylesheet.default + private func expectStandardBackground( + _ actual: Color, + matches expected: UIColor, + sourceLocation: SourceLocation = #_sourceLocation, + ) { + for level in [UIUserInterfaceLevel.base, .elevated] { + let traits = UITraitCollection(traitsFrom: [ + .init(userInterfaceStyle: .light), + .init(accessibilityContrast: .normal), + .init(userInterfaceLevel: level), + ]) + #expect( + UIColor(actual).resolvedColor(with: traits) == expected.resolvedColor(with: traits), + sourceLocation: sourceLocation, + ) + } + } + @Test func yearModePickerDefaultsToInline() { #expect(style.yearModePicker.layout == .inline) } @@ -156,7 +212,10 @@ struct WhereStylesheetTests { #expect(style.flightStatus.cornerRadius == 14) #expect(style.flightStatus.titleFont == .headline) #expect(style.flightStatus.bodyFont == .subheadline) - #expect(style.flightStatus.background == Color(uiColor: .secondarySystemGroupedBackground)) + expectStandardBackground( + style.flightStatus.background, + matches: .secondarySystemGroupedBackground, + ) } @Test func flightReviewPointStyle() { @@ -499,7 +558,7 @@ struct WhereStylesheetTests { )) let day = calendar.day - #expect(style.timeline.flight.iconFont == .system(size: 16, weight: .semibold)) + #expect(style.timeline.flight.iconFont == .system(size: 16).weight(.semibold)) #expect(style.timeline.flight.verticalPadding == 12) #expect(style.timeline.flight.labelSpacing == 2) #expect(style.timeline.flight.tint == .accentColor) @@ -655,7 +714,7 @@ struct WhereStylesheetTests { #expect(panel.horizontalPadding == 20) #expect(panel.bottomPadding == 16) #expect(panel.cornerRadius == 28) - #expect(panel.background == Color(.systemBackground)) + expectStandardBackground(panel.background, matches: .systemBackground) #expect(panel.shadowColor == Color.black.opacity(0.18)) #expect(panel.shadowRadius == 18) #expect(panel.shadowOffsetY == -4) @@ -673,7 +732,7 @@ struct WhereStylesheetTests { #expect(overview.background == Color.primary.opacity(0.035)) #expect(overview.border == Color.primary.opacity(0.1)) #expect(overview.borderWidth == 1) - #expect(overview.yearFont == .system(.title2, design: .serif).bold()) + #expect(overview.yearFont == .system(.title2, design: .serif).weight(.bold)) #expect(overview.pinsToViewport) let ribbon = timeline.ribbon @@ -777,7 +836,7 @@ struct WhereStylesheetTests { @Test func locationsBackgroundStyle() { let background = style.locationsBackground - #expect(background.paper == Color(uiColor: .systemBackground)) + expectStandardBackground(background.paper, matches: .systemBackground) #expect(background.ink == Color.primary) #expect(background.showsInk) #expect(background.preferredCellSize == 54) @@ -802,7 +861,7 @@ struct WhereStylesheetTests { let typography = style.typography #expect(typography.onboardingIcon == .system(size: 72)) #expect(typography.widgetHeroRegion == .system(.headline, design: .serif).weight(.semibold)) - #expect(typography.widgetTotalNumber == .system(.body, design: .rounded, weight: .bold)) + #expect(typography.widgetTotalNumber == .system(.body, design: .rounded).weight(.bold)) } @Test func motionAnimations() { @@ -1047,11 +1106,11 @@ struct WhereStylesheetTests { let palette = style.palette #expect(palette.primary.backgroundTop == Color(red: 0.07, green: 0.08, blue: 0.13)) #expect(palette.primary.backgroundBottom == Color(red: 0.02, green: 0.02, blue: 0.05)) - #expect(palette.splash.background == Color(.systemBackground)) - #expect(palette.splash.vignetteCenter == Color(.secondarySystemBackground)) - #expect(palette.splash.vignetteEdge == Color(.systemBackground)) + expectStandardBackground(palette.splash.background, matches: .systemBackground) + expectStandardBackground(palette.splash.vignetteCenter, matches: .secondarySystemBackground) + expectStandardBackground(palette.splash.vignetteEdge, matches: .systemBackground) #expect(palette.splash.iconGlow == .accentColor) - #expect(palette.onboarding.backgroundTop == Color(.systemBackground)) + expectStandardBackground(palette.onboarding.backgroundTop, matches: .systemBackground) #expect(palette.onboarding.backgroundBottom == Color.accentColor.opacity(0.12)) } @@ -1151,7 +1210,7 @@ struct WhereStylesheetTests { } @MainActor - @Test(arguments: [BMode.light, .dark], WhereTheme.allCases) + @Test(arguments: [BMode.light, .dark], [WhereTheme.standard, .alternate]) func calendarOverviewInkRemainsDistinctFromDetail(mode: BMode, theme: WhereTheme) throws { var context = BContext(traits: .system) context.traitOverrides.mode = mode @@ -1201,6 +1260,53 @@ struct WhereStylesheetTests { /// BroadwayUI's `BContextEnvironmentTests`. @MainActor struct WhereStylesheetEnvironmentTests { + @Test(arguments: WhereTheme.allCases, [BMode.light, .dark]) + func savedAndPreparedThemesUseTheSameRoot(theme: WhereTheme, mode: BMode) throws { + let saved = StylesheetProbeBox() + let supplied = StylesheetProbeBox() + let prepared = try WhereBundledThemes.prepared(theme) + let host = UIHostingController(rootView: VStack { + StylesheetProbe(box: saved).bMode(mode).whereBroadwayRoot(theme: theme) + StylesheetProbe(box: supplied).bMode(mode).whereBroadwayRoot(themeDocument: prepared) + }) + try show(host) { _ in + try waitFor { saved.sheet != nil && supplied.sheet != nil } + #expect(saved.sheet == supplied.sheet) + #expect(saved.document == prepared.validated) + #expect(supplied.document == prepared.validated) + } + } + + @Test func customDocumentUpdatesMountedRootWithoutChangingIdentity() throws { + func prepare(radius: Double) throws -> PreparedWhereThemeDocument { + var appearance = WhereThemeAppearance() + appearance.card = .init() + appearance.card?.regular = .init() + appearance.card?.regular?.cornerRadius = .constant(radius) + let validated = try ValidatedWhereThemeDocument( + document: themeDocument(appearance: appearance, fallback: .flat), + resources: .init(images: [], symbols: []), + ) + return try PreparedWhereThemeDocument( + validated: validated, + assets: .init(images: [:], symbols: [:]), + ) + } + let box = StylesheetProbeBox() + func content(_ theme: PreparedWhereThemeDocument) -> some View { + StylesheetProbe(box: box).whereBroadwayRoot(themeDocument: theme) + } + let first = try prepare(radius: 12) + let second = try prepare(radius: 24) + let host = UIHostingController(rootView: content(first)) + try show(host) { _ in + try waitFor { box.sheet?.card.regular.cornerRadius == 12 } + #expect(box.sheet?.theme == .alternate) + host.rootView = content(second) + try waitFor { box.sheet?.card.regular.cornerRadius == 24 } + } + } + @Test func fallsBackToDefaultWithoutAContext() { #expect(EnvironmentValues().stylesheet == .default) } @@ -1303,9 +1409,13 @@ struct WhereStylesheetEnvironmentTests { host.rootView = content(theme: .alternate) try waitFor { box.sheet?.card.regular.surface.usesReflection == false } #expect(box.sheet?.privacyPassportCard.surface.usesReflection == false) + host.rootView = content(theme: .halloween) + try waitFor { box.document?.resolved.id == .halloween } + #expect(box.sheet?.card.regular.cornerRadius == 26) host.rootView = content(theme: .standard) try waitFor { box.sheet?.card.regular.surface.usesReflection == true } #expect(box.sheet?.privacyPassportCard.surface.usesReflection == true) + #expect(box.document?.resolved.id == .standard) } } } @@ -1314,15 +1424,18 @@ private final class StylesheetProbeBox { var calendarDayMinHeight: CGFloat? var theme: WhereTheme? var sheet: WhereStylesheet? + var document: ValidatedWhereThemeDocument? } private struct StylesheetProbe: View { let box: StylesheetProbeBox @Environment(\.stylesheet) private var stylesheet + @Environment(\.whereThemeDocument) private var document var body: some View { Color.clear + .onChange(of: document?.validated, initial: true) { _, value in box.document = value } .onChange(of: stylesheet, initial: true) { _, value in box.sheet = value } .onChange(of: stylesheet.calendar.day.minHeight, initial: true) { _, newValue in box.calendarDayMinHeight = newValue diff --git a/Where/WhereUI/Tests/WhereThemeAnimationTests.swift b/Where/WhereUI/Tests/WhereThemeAnimationTests.swift new file mode 100644 index 000000000..5cde42de6 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAnimationTests.swift @@ -0,0 +1,19 @@ +import Foundation +import SwiftUI +import Testing +@testable import WhereUI + +struct WhereThemeAnimationTests { + @Test func finiteTimingRoundTripsAndRejectsInvalidDuration() throws { + let value = WhereThemeAnimation.spring(duration: 0.4, bounce: 0.2) + #expect(try JSONDecoder().decode( + WhereThemeAnimation.self, + from: JSONEncoder().encode(value), + ) == value) + #expect(value.animation == Animation.spring(duration: 0.4, bounce: 0.2)) + #expect(throws: WhereThemeValidationError.self) { try WhereThemeValidation.validate( + .easeOut(duration: 100), + path: "animation", + ) } + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.AppIconStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.AppIconStyleTests.swift new file mode 100644 index 000000000..04f04fc96 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.AppIconStyleTests.swift @@ -0,0 +1,120 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeAppIconStylePatchTests { + @Test(arguments: [-150.0, -149.999999999, -1]) + func rejectsNegativeGridColumnSpacing(_ spacing: Double) throws { + var appearance = WhereThemeAppearance() + appearance.appIcon = .init() + appearance.appIcon?.columnSpacing = .constant(spacing) + #expect(throws: WhereThemeValidationError.self) { + try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: .system, + ) + } + } + + @Test func zeroGridColumnSpacingRemainsUsable() throws { + var appearance = WhereThemeAppearance() + appearance.appIcon = .init() + appearance.appIcon?.columnSpacing = .constant(0) + let sheet = try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: .system, + ) + let metrics = AppIconLayout.gridMetrics(containerWidth: 390, style: sheet.appIcon) + #expect(metrics.columnCount == 2) + #expect(metrics.iconSize.isFinite && metrics.iconSize > 0) + } + + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"appIcon":{"gridMax":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.appIcon.gridMax, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"previewMax":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.appIcon.previewMax, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"gridSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.appIcon.gridSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"columnSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.appIcon.columnSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"gridPadding":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.appIcon.gridPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"cellSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.appIcon.cellSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"cellLabelSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.appIcon.cellLabelSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"backgroundedCellOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.appIcon.backgroundedCellOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"appIcon":{"panel":{"spacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.appIcon.panel.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"panel":{"textSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.appIcon.panel.textSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"panel":{"horizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.appIcon.panel.horizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"panel":{"bottomPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.appIcon.panel.bottomPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"panel":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.appIcon.panel.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"panel":{"shadowRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.appIcon.panel.shadowRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"panel":{"shadowOffsetY":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.appIcon.panel.shadowOffsetY, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"appIcon":{"panel":{"grabberOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.appIcon.panel.grabberOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"appIcon":{"panel":{"grabberTopPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.appIcon.panel.grabberTopPadding, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.CalendarStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.CalendarStyleTests.swift new file mode 100644 index 000000000..b51e8c66d --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.CalendarStyleTests.swift @@ -0,0 +1,197 @@ +import BroadwayCore +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeCalendarStylePatchTests { + @Test func overviewPatchesPreserveAccessibilityConstraints() throws { + var appearance = WhereThemeAppearance() + appearance.calendar = .init() + appearance.calendar?.overview = .init() + appearance.calendar?.overview?.columns = .constant(4) + appearance.calendar?.overview?.dayHeight = .constant(18) + appearance.calendar?.overview?.usesSpatialMotion = .constant(true) + let document = themeDocument(appearance: appearance, fallback: .flat) + let ordinary = try resolveTheme(document, traits: .system) + #expect(ordinary.calendar.overview.columns == 4) + #expect(ordinary.calendar.overview.dayHeight == 18) + var accessible = BTraits.system + accessible.contentSizeCategory = .accessibilityExtraExtraExtraLarge + accessible.accessibility = .init(isReduceMotionEnabled: true) + let resolved = try resolveTheme(document, traits: accessible) + #expect(resolved.calendar.overview.columns == 1) + #expect(resolved.calendar.overview.dayHeight == 28) + #expect(!resolved.calendar.overview.usesSpatialMotion) + appearance.calendar?.overview?.columns = .constant(0) + #expect(throws: WhereThemeValidationError.self) { + try resolveTheme( + themeDocument(appearance: appearance, fallback: .flat), + traits: .system, + ) + } + } + + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"calendar":{"monthSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.calendar.monthSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"dotSize":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.calendar.dotSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"sectionSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.sectionSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"gridSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.gridSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"weekdayFontSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.weekdayFontSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"padding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"footerDividerSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.footerDividerSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"footerSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.footerSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"footerRowSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.footerRowSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"stacksFooter":{"constant":{"_0":false}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.stacksFooter, + replacement: false, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"unfocusedRowOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.unfocusedRowOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"calendar":{"regionBand":{"opacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.calendar.regionBand.opacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"calendar":{"regionBand":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.regionBand.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"regionBand":{"continuationRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.regionBand.continuationRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"regionBand":{"verticalInset":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.regionBand.verticalInset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"minHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.minHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"numberSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.numberSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"numberFontSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.numberFontSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"numberDotSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.numberDotSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"dotSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.dotSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"dotOverlap":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.dotOverlap, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"dotStrokeWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.dotStrokeWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"contentSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.contentSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"month":{"plain":{"borderWidth":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.calendar.month.plain.borderWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"regionBand":{"planned":{"fillOpacity":{"constant":{"_0":0.25}}}}}}"#, + keyPath: \WhereStylesheet.calendar.regionBand.planned.fillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"calendar":{"regionBand":{"planned":{"hatchOpacity":{"constant":{"_0":0.25}}}}}}"#, + keyPath: \WhereStylesheet.calendar.regionBand.planned.hatchOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"calendar":{"regionBand":{"planned":{"hatchSpacing":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.calendar.regionBand.planned.hatchSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"regionBand":{"planned":{"hatchLineWidth":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.calendar.regionBand.planned.hatchLineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"badge":{"iconSize":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.badge.iconSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"badge":{"iconSpacing":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.badge.iconSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"calendar":{"day":{"badge":{"padding":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.calendar.day.badge.padding, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.CardStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.CardStyleTests.swift new file mode 100644 index 000000000..2cd2f58a4 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.CardStyleTests.swift @@ -0,0 +1,184 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeCardStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"card":{"regular":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.regular.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"padding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.regular.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"contentSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.regular.contentSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"progressBarHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.regular.progressBarHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"regionNameTracking":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.regular.regionNameTracking, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"watermarkFontSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.regular.watermarkFontSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationsBackground":{"artwork":{"scale":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.artwork.scale, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationsBackground":{"artwork":{"fillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.artwork.fillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationsBackground":{"rosette":{"wobble":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.rosette.wobble, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationsBackground":{"rosette":{"lineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.rosette.lineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationsBackground":{"rosette":{"primaryRingSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.rosette.primaryRingSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationsBackground":{"rosette":{"secondaryRingSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.rosette.secondaryRingSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"size":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.size, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"rotationDegrees":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.rotationDegrees, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"sheen":{"intensity":{"constant":{"_0":0.25}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.sheen.intensity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"sheen":{"staticGlintIntensity":{"constant":{"_0":0.25}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.sheen.staticGlintIntensity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"glow":{"opacity":{"constant":{"_0":0.25}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.glow.opacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"glow":{"radius":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.glow.radius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"glow":{"offsetY":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.glow.offsetY, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"border":{"inset":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.border.inset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"border":{"glyphSize":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.border.glyphSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"border":{"spacing":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.border.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"border":{"opacity":{"constant":{"_0":0.25}}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.border.opacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"outerRing":{"opacity":{"constant":{"_0":0.25}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.outerRing.opacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"outerRing":{"lineWidthFraction":{"constant":{"_0":0.25}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.outerRing.lineWidthFraction, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"innerRing":{"opacity":{"constant":{"_0":0.25}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.innerRing.opacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"innerRing":{"lineWidthFraction":{"constant":{"_0":0.25}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.innerRing.lineWidthFraction, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"innerRing":{"insetFraction":{"constant":{"_0":0.25}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.innerRing.insetFraction, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"content":{"spacingFraction":{"constant":{"_0":0.25}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.content.spacingFraction, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"content":{"opacity":{"constant":{"_0":0.25}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.content.opacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"sheen":{"staticPose":{"roll":{"constant":{"_0":42}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.sheen.staticPose.roll, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"sheen":{"staticPose":{"pitch":{"constant":{"_0":42}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.sheen.staticPose.pitch, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"innerRing":{"dash":{"lengthFraction":{"constant":{"_0":0.25}}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.innerRing.dash.lengthFraction, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"innerRing":{"dash":{"spacingFraction":{"constant":{"_0":0.25}}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.innerRing.dash.spacingFraction, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"regular":{"entryStamp":{"content":{"symbolFont":{"sizeFraction":{"constant":{"_0":0.25}}}}}}}}"#, + keyPath: \WhereStylesheet.card.regular.entryStamp.content.symbolFont.sizeFraction, + replacement: 0.25, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.CardStylesTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.CardStylesTests.swift new file mode 100644 index 000000000..34629023b --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.CardStylesTests.swift @@ -0,0 +1,124 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeCardStylesPatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"card":{"watermarkOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.card.watermarkOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"glassTintOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.card.glassTintOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"nameOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.card.nameOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"estimatedProgressOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.card.estimatedProgressOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"contentOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.contentOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"scale":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.scale, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"horizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.horizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"verticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.verticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"rotationDegrees":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.rotationDegrees, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"fillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.fillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"outlineOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.outlineOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"outlineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.outlineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"innerInset":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.innerInset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"estimateSticker":{"innerOutlineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.estimateSticker.innerOutlineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"rosetteFill":{"primary":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.card.rosetteFill.primary, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"rosetteFill":{"secondary":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.card.rosetteFill.secondary, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"securityPrint":{"whiteMix":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.card.securityPrint.whiteMix, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"constellation":{"coreDiameter":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.constellation.coreDiameter, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"constellation":{"coreOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.card.constellation.coreOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"constellation":{"coreWhiteMix":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.card.constellation.coreWhiteMix, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"card":{"constellation":{"haloRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.card.constellation.haloRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"card":{"constellation":{"haloOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.card.constellation.haloOpacity, + replacement: 0.25, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.ContentLayoutTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.ContentLayoutTests.swift new file mode 100644 index 000000000..3e25d54a8 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.ContentLayoutTests.swift @@ -0,0 +1,13 @@ +import Foundation +import Testing +@testable import WhereUI + +struct WhereThemeContentLayoutTests { + @Test func wireLayoutResolvesNativePolicy() throws { + let value = try JSONDecoder().decode( + WhereThemeAppearance.ContentLayout.self, + from: Data(#"{"stacked":{}}"#.utf8), + ) + #expect(value.native == .stacked) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.DeveloperOverlayStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.DeveloperOverlayStyleTests.swift new file mode 100644 index 000000000..57dcd8f05 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.DeveloperOverlayStyleTests.swift @@ -0,0 +1,169 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeDeveloperOverlayStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"developerOverlay":{"edgeInset":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.edgeInset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"launcher":{"diameter":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.launcher.diameter, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"launcher":{"glyphRatio":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.launcher.glyphRatio, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"launcher":{"shadowRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.launcher.shadowRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"launcher":{"shadowOffsetY":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.launcher.shadowOffsetY, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"floatingWindow":{"maxWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.floatingWindow.maxWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"floatingWindow":{"maxHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.floatingWindow.maxHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"floatingWindow":{"heightFraction":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.floatingWindow.heightFraction, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"floatingWindow":{"maxContentInsetFraction":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.floatingWindow.maxContentInsetFraction, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"fullScreenInset":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.fullScreenInset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"shadowOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.shadowOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"shadowRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.shadowRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"shadowOffsetY":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.shadowOffsetY, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"controlHorizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.controlHorizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"controlVerticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.controlVerticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"dragHandleMinHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.dragHandleMinHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"resizeGripSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.resizeGripSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"resizeIconSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.resizeIconSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"panel":{"resizeGripClearance":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.panel.resizeGripClearance, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"maxWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.maxWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"launcherSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.launcherSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"rowSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.rowSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"horizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.horizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"verticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.verticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"minRowHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.minRowHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"subtitleSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.subtitleSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"iconWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.iconWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"motion":{"stagger":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.motion.stagger, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"motion":{"scale":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.motion.scale, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"developerOverlay":{"menu":{"motion":{"usesSpatialMotion":{"constant":{"_0":false}}}}}}"#, + keyPath: \WhereStylesheet.developerOverlay.menu.motion.usesSpatialMotion, + replacement: false, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.EvidenceStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.EvidenceStyleTests.swift new file mode 100644 index 000000000..08436d0c8 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.EvidenceStyleTests.swift @@ -0,0 +1,24 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeEvidenceStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"evidence":{"previewCornerRadius":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.evidence.previewCornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"evidence":{"pdfPreviewMinHeight":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.evidence.pdfPreviewMinHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"evidence":{"loadingMinHeight":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.evidence.loadingMinHeight, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.FeatureDiscoveryStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.FeatureDiscoveryStyleTests.swift new file mode 100644 index 000000000..b66904f22 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.FeatureDiscoveryStyleTests.swift @@ -0,0 +1,249 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeFeatureDiscoveryStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"featureDiscovery":{"appIcon":{"spacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.appIcon.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"regionStyle":{"spacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.regionStyle.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"shareSheet":{"sourcesSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.shareSheet.sourcesSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"evidenceArchive":{"spacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.evidenceArchive.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingHeader":{"badgeSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingHeader.badgeSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingHeader":{"symbolPointSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingHeader.symbolPointSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingHeader":{"badgeTintOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingHeader.badgeTintOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingHeader":{"contentMaxWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingHeader.contentMaxWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingHeader":{"spacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingHeader.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingHeader":{"verticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingHeader.verticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingPanel":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingPanel.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingPanel":{"maxWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingPanel.maxWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingPanel":{"padding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingPanel.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingPanel":{"contentSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingPanel.contentSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"marketingPanel":{"rowVerticalInset":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.marketingPanel.rowVerticalInset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"backgroundPattern":{"contourSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.backgroundPattern.contourSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"backgroundPattern":{"primaryDistortion":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.backgroundPattern.primaryDistortion, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"backgroundPattern":{"secondaryDistortion":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.backgroundPattern.secondaryDistortion, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"backgroundPattern":{"horizontalScale":{"constant":{"_0":1.4}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.backgroundPattern.horizontalScale, + replacement: 1.4, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"backgroundPattern":{"centerXRatio":{"constant":{"_0":0.3}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.backgroundPattern.centerXRatio, + replacement: 0.3, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"backgroundPattern":{"centerYRatio":{"constant":{"_0":0.7}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.backgroundPattern.centerYRatio, + replacement: 0.7, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"backgroundPattern":{"phaseStep":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.backgroundPattern.phaseStep, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"backgroundPattern":{"lineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.backgroundPattern.lineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"backgroundPattern":{"opacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.backgroundPattern.opacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"estimatedTime":{"timelineHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.estimatedTime.timelineHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"estimatedTime":{"timelineSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.estimatedTime.timelineSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"estimatedTime":{"calculationSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.estimatedTime.calculationSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"estimatedTime":{"segmentCornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.estimatedTime.segmentCornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"estimatedTime":{"legendDotSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.estimatedTime.legendDotSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"widgets":{"lockWidgetHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.widgets.lockWidgetHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"card":{"cornerRadius":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.card.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"card":{"maxWidth":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.card.maxWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"card":{"padding":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.card.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"card":{"spacing":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.card.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"card":{"rowVerticalInset":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.card.rowVerticalInset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"bubble":{"cornerRadius":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.bubble.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"bubble":{"horizontalPadding":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.bubble.horizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"bubble":{"verticalPadding":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.bubble.verticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"bubble":{"indent":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.bubble.indent, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"speakerIcon":{"containerSize":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.speakerIcon.containerSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"siri":{"speakerIcon":{"symbolPointSize":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.siri.speakerIcon.symbolPointSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"widgets":{"device":{"cornerRadius":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.widgets.device.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"widgets":{"device":{"contentMaxWidth":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.widgets.device.contentMaxWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"widgets":{"device":{"regularContentWidth":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.widgets.device.regularContentWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"widgets":{"device":{"padding":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.widgets.device.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"widgets":{"device":{"spacing":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.widgets.device.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"widgets":{"frame":{"cornerRadius":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.widgets.frame.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"featureDiscovery":{"widgets":{"frame":{"padding":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.featureDiscovery.widgets.frame.padding, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.FlightReviewPointStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.FlightReviewPointStyleTests.swift new file mode 100644 index 000000000..79b6ce5d3 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.FlightReviewPointStyleTests.swift @@ -0,0 +1,19 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeFlightReviewPointStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"flightReviewPoint":{"minimumActionHeight":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.flightReviewPoint.minimumActionHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"flightReviewPoint":{"mapCornerRadius":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.flightReviewPoint.mapCornerRadius, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.FlightStatusStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.FlightStatusStyleTests.swift new file mode 100644 index 000000000..1c6fb611f --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.FlightStatusStyleTests.swift @@ -0,0 +1,24 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeFlightStatusStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"flightStatus":{"padding":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.flightStatus.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"flightStatus":{"spacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.flightStatus.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"flightStatus":{"cornerRadius":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.flightStatus.cornerRadius, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.LaunchStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.LaunchStyleTests.swift new file mode 100644 index 000000000..2c272a9c6 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.LaunchStyleTests.swift @@ -0,0 +1,13 @@ +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeLaunchPatchTests { + @Test func revealOverrideLeavesLifecycleTimingsNative() throws { + try verifyThemePatch( + json: #"{"launch":{"reveal":{"constant":{"_0":{"crossfade":{}}}}}}"#, + keyPath: \WhereStylesheet.launch.reveal, + replacement: .crossfade, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.LocationCardStackStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.LocationCardStackStyleTests.swift new file mode 100644 index 000000000..fb3805b13 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.LocationCardStackStyleTests.swift @@ -0,0 +1,49 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeLocationCardStackStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"locationCardStack":{"overtake":{"duration":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationCardStack.overtake.duration, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationCardStack":{"overtake":{"bounce":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationCardStack.overtake.bounce, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationCardStack":{"overtake":{"lateralArc":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationCardStack.overtake.lateralArc, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationCardStack":{"overtake":{"liftScale":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationCardStack.overtake.liftScale, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationCardStack":{"overtake":{"rotationDegrees":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationCardStack.overtake.rotationDegrees, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationCardStack":{"overtake":{"settleScale":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationCardStack.overtake.settleScale, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationCardStack":{"overtake":{"minimumOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationCardStack.overtake.minimumOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationCardStack":{"overtake":{"usesSpatialMotion":{"constant":{"_0":false}}}}}"#, + keyPath: \WhereStylesheet.locationCardStack.overtake.usesSpatialMotion, + replacement: false, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.LocationForecastStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.LocationForecastStyleTests.swift new file mode 100644 index 000000000..2946ab15a --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.LocationForecastStyleTests.swift @@ -0,0 +1,289 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeLocationForecastStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"locationForecast":{"cornerRadius":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.locationForecast.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"padding":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.locationForecast.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"rowSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.locationForecast.rowSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"outlineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.outlineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"inset":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.inset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"microprintGlyphSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.microprintGlyphSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"microprintSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.microprintSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"rosetteWobble":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.rosetteWobble, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"rosetteLineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.rosetteLineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"primaryRingSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.primaryRingSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"secondaryRingSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.secondaryRingSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"shadowRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.shadowRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"surface":{"shadowOffsetY":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.surface.shadowOffsetY, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"header":{"contentSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.header.contentSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"header":{"textSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.header.textSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"header":{"minimumHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.header.minimumHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"row":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.row.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"row":{"padding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.row.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"row":{"contentSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.row.contentSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"row":{"estimateSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.row.estimateSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"row":{"outlineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.row.outlineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"progress":{"height":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.progress.height, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"progress":{"hatchSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.progress.hatchSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"progress":{"hatchLineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.progress.hatchLineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"controls":{"sectionSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.controls.sectionSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"controls":{"layoutSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.controls.layoutSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"controls":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.controls.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"controls":{"horizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.controls.horizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"controls":{"minimumHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.controls.minimumHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"controls":{"strokeWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.controls.strokeWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"surfaceWashOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.surfaceWashOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"surfaceOutlineOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.surfaceOutlineOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"microprintOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.microprintOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"rosettePrimaryOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.rosettePrimaryOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"rosetteSecondaryOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.rosetteSecondaryOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"sealOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.sealOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"rowFillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.rowFillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"rowOutlineOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.rowOutlineOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"progressTrackOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.progressTrackOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"progressEstimateFillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.progressEstimateFillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"progressHatchOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.progressHatchOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"controlFillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.controlFillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"ink":{"controlStrokeOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.ink.controlStrokeOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"padding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"usesOpaquePaper":{"constant":{"_0":false}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.usesOpaquePaper, + replacement: false, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"glassTintOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.glassTintOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"outlineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.outlineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"shadowOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.shadowOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"shadowRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.shadowRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"shadowOffsetY":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.shadowOffsetY, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"rosetteOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.rosetteOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"artwork":{"widthFraction":{"constant":{"_0":0.25}}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.artwork.widthFraction, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"artwork":{"inset":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.artwork.inset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"artwork":{"gap":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.artwork.gap, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationForecast":{"summary":{"artwork":{"leadingOpacity":{"constant":{"_0":0.25}}}}}}"#, + keyPath: \WhereStylesheet.locationForecast.summary.artwork.leadingOpacity, + replacement: 0.25, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.LocationWelcomeStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.LocationWelcomeStyleTests.swift new file mode 100644 index 000000000..e8b646787 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.LocationWelcomeStyleTests.swift @@ -0,0 +1,109 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeLocationWelcomeStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"locationWelcome":{"maxWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.maxWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"cornerRadius":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"padding":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"contentSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.contentSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"paperOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.paperOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"scrimOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.scrimOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"glassTintOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.glassTintOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"glow":{"opacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.glow.opacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"glow":{"radius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.glow.radius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"glow":{"offsetY":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.glow.offsetY, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"close":{"tintOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.close.tintOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"accessory":{"contentSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.accessory.contentSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"accessory":{"horizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.accessory.horizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"accessory":{"verticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.accessory.verticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"accessory":{"minimumActionHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.accessory.minimumActionHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"accessory":{"symbolSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.accessory.symbolSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"motion":{"usesSpatialMotion":{"constant":{"_0":false}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.motion.usesSpatialMotion, + replacement: false, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"motion":{"arrival":{"scale":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.motion.arrival.scale, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"motion":{"arrival":{"rotationDegrees":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.motion.arrival.rotationDegrees, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationWelcome":{"motion":{"arrival":{"verticalOffset":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.locationWelcome.motion.arrival.verticalOffset, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.LocationsBackgroundStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.LocationsBackgroundStyleTests.swift new file mode 100644 index 000000000..d11967ff2 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.LocationsBackgroundStyleTests.swift @@ -0,0 +1,29 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeLocationsBackgroundStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"locationsBackground":{"showsInk":{"constant":{"_0":false}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.showsInk, + replacement: false, + ) + try verifyThemePatch( + json: #"{"locationsBackground":{"preferredCellSize":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.preferredCellSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationsBackground":{"maximumAspectScale":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.maximumAspectScale, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"locationsBackground":{"rosetteOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.locationsBackground.rosetteOpacity, + replacement: 0.25, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.MotionTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.MotionTests.swift new file mode 100644 index 000000000..878ff6e26 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.MotionTests.swift @@ -0,0 +1,19 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeMotionPatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"motion":{"staggeredReveal":{"verticalOffset":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.motion.staggeredReveal.verticalOffset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"motion":{"staggeredReveal":{"delay":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.motion.staggeredReveal.delay, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.PaletteTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.PaletteTests.swift new file mode 100644 index 000000000..9537e8a16 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.PaletteTests.swift @@ -0,0 +1,15 @@ +import Foundation +import SwiftUI +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemePalettePatchTests { + @Test func overridesColorWithoutReplacingSiblingPalette() throws { + try verifyThemePatch( + json: #"{"palette":{"primary":{"backgroundTop":{"constant":{"_0":{"rgba":{"red":1,"green":0,"blue":0,"alpha":1}}}}}}}"#, + keyPath: \WhereStylesheet.palette.primary.backgroundTop, + replacement: Color(.sRGB, red: 1, green: 0, blue: 0, opacity: 1), + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.PassportSealStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.PassportSealStyleTests.swift new file mode 100644 index 000000000..2379c4699 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.PassportSealStyleTests.swift @@ -0,0 +1,44 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemePassportSealStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"passportSeal":{"size":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.passportSeal.size, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"passportSeal":{"rotationDegrees":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.passportSeal.rotationDegrees, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"passportSeal":{"outerLineWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.passportSeal.outerLineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"passportSeal":{"innerLineWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.passportSeal.innerLineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"passportSeal":{"innerInset":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.passportSeal.innerInset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"passportSeal":{"dashLength":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.passportSeal.dashLength, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"passportSeal":{"dashSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.passportSeal.dashSpacing, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.PrivacyPassportCardStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.PrivacyPassportCardStyleTests.swift new file mode 100644 index 000000000..5a585b4c2 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.PrivacyPassportCardStyleTests.swift @@ -0,0 +1,149 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemePrivacyPassportCardStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"privacyPassportCard":{"cornerRadius":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"padding":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"sectionSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.sectionSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"headerSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.headerSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"outlineOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.outlineOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"outlineWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.outlineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"rosette":{"wobble":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.rosette.wobble, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"rosette":{"lineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.rosette.lineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"rosette":{"primaryRingSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.rosette.primaryRingSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"rosette":{"secondaryRingSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.rosette.secondaryRingSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"rosette":{"primaryOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.rosette.primaryOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"rosette":{"secondaryOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.rosette.secondaryOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"reflectiveSurface":{"intensity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.reflectiveSurface.intensity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"reflectiveSurface":{"staticGlintIntensity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.reflectiveSurface.staticGlintIntensity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"rowSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.rowSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"padding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"contentSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.contentSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"textSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.textSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"iconSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.iconSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"statusHorizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.statusHorizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"statusVerticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.statusVerticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"fillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.fillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"strokeOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.strokeOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"strokeWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.strokeWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"disclosure":{"statusFillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.disclosure.statusFillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"reflectiveSurface":{"staticPose":{"roll":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.reflectiveSurface.staticPose.roll, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"privacyPassportCard":{"reflectiveSurface":{"staticPose":{"pitch":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.privacyPassportCard.reflectiveSurface.staticPose.pitch, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.RegionMapStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.RegionMapStyleTests.swift new file mode 100644 index 000000000..8bcce40c5 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.RegionMapStyleTests.swift @@ -0,0 +1,62 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeRegionMapStylePatchTests { + @Test(arguments: [0.0, 50000.0]) + func meterSpansAcceptTheNativeDefaults(_ span: Double) throws { + var appearance = WhereThemeAppearance() + appearance.regionMap = .init() + appearance.regionMap?.pointPreview = .init() + appearance.regionMap?.pointPreview?.minimumSpanMeters = .constant(span) + let sheet = try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: .system, + ) + #expect(sheet.regionMap.pointPreview.minimumSpanMeters == span) + } + + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"regionMap":{"uncertaintyFillOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.regionMap.uncertaintyFillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"regionMap":{"uncertaintyStrokeOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.regionMap.uncertaintyStrokeOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"regionMap":{"uncertaintyStrokeWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionMap.uncertaintyStrokeWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionMap":{"routeLineWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionMap.routeLineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionMap":{"capturePointDiameter":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionMap.capturePointDiameter, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionMap":{"captureInset":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionMap.captureInset, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionMap":{"overview":{"height":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.regionMap.overview.height, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionMap":{"overview":{"minimumSpanMeters":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.regionMap.overview.minimumSpanMeters, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.RegionPickerStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.RegionPickerStyleTests.swift new file mode 100644 index 000000000..324d54246 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.RegionPickerStyleTests.swift @@ -0,0 +1,164 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeRegionPickerStylePatchTests { + @Test func geographicCentersAcceptWesternAndSouthernCoordinates() throws { + var appearance = WhereThemeAppearance() + appearance.regionPicker = .init() + appearance.regionPicker?.mapCenterLatitude = .constant(-33.86) + appearance.regionPicker?.mapCenterLongitude = .constant(-98.35) + let sheet = try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: .system, + ) + #expect(sheet.regionPicker.mapCenterLatitude == -33.86) + #expect(sheet.regionPicker.mapCenterLongitude == -98.35) + } + + @Test func rejectsInvalidGeographicCentersAndSpans() throws { + for latitude in [-90.01, 90.01] { + var appearance = WhereThemeAppearance() + appearance.regionPicker = .init() + appearance.regionPicker?.mapCenterLatitude = .constant(latitude) + #expect(throws: WhereThemeValidationError.self) { + try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: .system, + ) + } + } + for longitude in [-180.01, 180.01] { + var appearance = WhereThemeAppearance() + appearance.regionPicker = .init() + appearance.regionPicker?.mapCenterLongitude = .constant(longitude) + #expect(throws: WhereThemeValidationError.self) { + try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: .system, + ) + } + } + for span in [0.0, -1, 180.01] { + var appearance = WhereThemeAppearance() + appearance.regionPicker = .init() + appearance.regionPicker?.mapSpanLatitude = .constant(span) + #expect(throws: WhereThemeValidationError.self) { + _ = try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: .system, + ) + } + } + for span in [0.0, -1, 360.01] { + var appearance = WhereThemeAppearance() + appearance.regionPicker = .init() + appearance.regionPicker?.mapSpanLongitude = .constant(span) + #expect(throws: WhereThemeValidationError.self) { + try resolveTheme( + themeDocument(appearance: appearance, fallback: .standard), + traits: .system, + ) + } + } + } + + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"regionPicker":{"mapCornerRadius":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.mapCornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"selectedFillOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.regionPicker.selectedFillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"unselectedFillOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.regionPicker.unselectedFillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"selectedStrokeOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.regionPicker.selectedStrokeOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"unselectedStrokeOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.regionPicker.unselectedStrokeOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"selectedStrokeWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.selectedStrokeWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"unselectedStrokeWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.unselectedStrokeWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"colorSwatchSize":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.colorSwatchSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"colorSwatchSelectionRing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.colorSwatchSelectionRing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"colorSwatchMinWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.colorSwatchMinWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"glyphTileSize":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.glyphTileSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"glyphTileMinWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.glyphTileMinWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"glyphCornerRadius":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.glyphCornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"glyphSelectionStrokeWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.glyphSelectionStrokeWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"glyphSelectedBackgroundOpacity":{"constant":{"_0":0.25}}}}"#, + keyPath: \WhereStylesheet.regionPicker.glyphSelectedBackgroundOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"mapCenterLatitude":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.mapCenterLatitude, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"mapCenterLongitude":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.mapCenterLongitude, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"mapSpanLatitude":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.mapSpanLatitude, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"regionPicker":{"mapSpanLongitude":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.regionPicker.mapSpanLongitude, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.SettingsStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.SettingsStyleTests.swift new file mode 100644 index 000000000..2e85b15f4 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.SettingsStyleTests.swift @@ -0,0 +1,24 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeSettingsStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"settings":{"iconSize":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.settings.iconSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"settings":{"iconCornerRadius":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.settings.iconCornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"settings":{"iconSymbolSize":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.settings.iconSymbolSize, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.SizeTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.SizeTests.swift new file mode 100644 index 000000000..0db263f3a --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.SizeTests.swift @@ -0,0 +1,24 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeSizePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"size":{"statusIconWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.size.statusIconWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"size":{"launchIcon":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.size.launchIcon, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"size":{"launchCaptionBottomInset":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.size.launchCaptionBottomInset, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.SpacingTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.SpacingTests.swift new file mode 100644 index 000000000..f5a112bc9 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.SpacingTests.swift @@ -0,0 +1,54 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeSpacingPatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"spacing":{"xxSmall":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.spacing.xxSmall, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"spacing":{"xSmall":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.spacing.xSmall, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"spacing":{"small":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.spacing.small, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"spacing":{"medium":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.spacing.medium, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"spacing":{"regular":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.spacing.regular, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"spacing":{"large":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.spacing.large, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"spacing":{"xLarge":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.spacing.xLarge, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"spacing":{"xxLarge":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.spacing.xxLarge, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"spacing":{"xxxLarge":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.spacing.xxxLarge, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.StampBannerStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.StampBannerStyleTests.swift new file mode 100644 index 000000000..fc2d8c2e5 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.StampBannerStyleTests.swift @@ -0,0 +1,79 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeStampBannerStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"openSourceStamp":{"padding":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"contentSpacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.contentSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"outlineWidth":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.outlineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"rosette":{"wobble":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.rosette.wobble, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"rosette":{"lineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.rosette.lineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"rosette":{"primaryRingSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.rosette.primaryRingSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"rosette":{"secondaryRingSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.rosette.secondaryRingSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"ink":{"outlineOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.ink.outlineOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"ink":{"primaryRosetteOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.ink.primaryRosetteOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"ink":{"secondaryRosetteOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.ink.secondaryRosetteOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"ink":{"sealOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.ink.sealOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"ink":{"titleOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.ink.titleOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"ink":{"detailOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.ink.detailOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"openSourceStamp":{"ink":{"accessoryOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.openSourceStamp.ink.accessoryOpacity, + replacement: 0.25, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.ThemePickerStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.ThemePickerStyleTests.swift new file mode 100644 index 000000000..d9e12cc53 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.ThemePickerStyleTests.swift @@ -0,0 +1,22 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeThemePickerStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"themePicker":{"spacing":{"constant":{"_0":42}}}}"#, + keyPath: \WhereStylesheet.themePicker.spacing, + replacement: 42, + ) + } + + @Test func minimumOptionWidthRoundTripsAndApplies() throws { + try verifyThemePatch( + json: #"{"themePicker":{"minimumOptionWidth":{"constant":{"_0":210}}}}"#, + keyPath: \WhereStylesheet.themePicker.minimumOptionWidth, + replacement: 210, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.TimelineStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.TimelineStyleTests.swift new file mode 100644 index 000000000..b0f55097f --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.TimelineStyleTests.swift @@ -0,0 +1,244 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeTimelineStylePatchTests { + @Test func changesOnlyAuthoredFieldsAfterRoundTrip() throws { + try verifyThemePatch( + json: #"{"timeline":{"flight":{"verticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.flight.verticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"flight":{"labelSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.flight.labelSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"overview":{"spacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.overview.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"overview":{"padding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.overview.padding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"overview":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.overview.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"overview":{"borderWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.overview.borderWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"overview":{"pinsToViewport":{"constant":{"_0":false}}}}}"#, + keyPath: \WhereStylesheet.timeline.overview.pinsToViewport, + replacement: false, + ) + try verifyThemePatch( + json: #"{"timeline":{"ribbon":{"monthLabelSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.ribbon.monthLabelSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"ribbon":{"height":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.ribbon.height, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"ribbon":{"borderWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.ribbon.borderWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"ribbon":{"regionSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.ribbon.regionSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"ribbon":{"regionLabelSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.ribbon.regionLabelSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"ribbon":{"separatesRegions":{"constant":{"_0":false}}}}}"#, + keyPath: \WhereStylesheet.timeline.ribbon.separatesRegions, + replacement: false, + ) + try verifyThemePatch( + json: #"{"timeline":{"rail":{"lineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.rail.lineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"rail":{"toCardSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.rail.toCardSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"rail":{"nodeSize":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.rail.nodeSize, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"rail":{"nodeFillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.timeline.rail.nodeFillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"timeline":{"rail":{"nodeStrokeWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.rail.nodeStrokeWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"spacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.spacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"labelSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.labelSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"gap":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.gap, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"baseHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.baseHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"yearScaleHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.yearScaleHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"horizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.horizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"verticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.verticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"cornerRadius":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.cornerRadius, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"fillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.fillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"borderOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.borderOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"borderWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.borderWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"countHorizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.countHorizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"countVerticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.countVerticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"countFillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.countFillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"stacksDayCount":{"constant":{"_0":false}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.stacksDayCount, + replacement: false, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"fillOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.fillOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"borderOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.borderOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"hatchOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.hatchOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"hatchSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.hatchSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"hatchLineWidth":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.hatchLineWidth, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"labelOpacity":{"constant":{"_0":0.25}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.labelOpacity, + replacement: 0.25, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"transitionHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.transitionHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"joinedBaseHeight":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.joinedBaseHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"joinedVerticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.joinedVerticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"joinedLabelSpacing":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.joinedLabelSpacing, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"joinedCountHorizontalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.joinedCountHorizontalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"planned":{"joinedCountVerticalPadding":{"constant":{"_0":42}}}}}"#, + keyPath: \WhereStylesheet.timeline.planned.joinedCountVerticalPadding, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"singleDay":{"baseHeight":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.singleDay.baseHeight, + replacement: 42, + ) + try verifyThemePatch( + json: #"{"timeline":{"row":{"singleDay":{"verticalPadding":{"constant":{"_0":42}}}}}}"#, + keyPath: \WhereStylesheet.timeline.row.singleDay.verticalPadding, + replacement: 42, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.TypographyTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.TypographyTests.swift new file mode 100644 index 000000000..a47cfad46 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.TypographyTests.swift @@ -0,0 +1,18 @@ +import Foundation +import SwiftUI +import Testing +import ViewDocumentCore +import ViewDocumentUI +@testable import WhereUI + +@MainActor +struct WhereThemeTypographyPatchTests { + @Test func semanticFontOverridePreservesUnrelatedFaces() throws { + let font = ViewFontDescriptor(size: .semantic(.body), weight: .bold, design: .serif) + try verifyThemePatch( + json: #"{"typography":{"widgetHeroRegion":{"constant":{"_0":{"size":{"semantic":{"_0":"body"}},"weight":"bold","design":"serif"}}}}}"#, + keyPath: \WhereStylesheet.typography.widgetHeroRegion, + replacement: font.font, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearance.YearModePickerStyleTests.swift b/Where/WhereUI/Tests/WhereThemeAppearance.YearModePickerStyleTests.swift new file mode 100644 index 000000000..2260f1fe6 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearance.YearModePickerStyleTests.swift @@ -0,0 +1,13 @@ +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeYearModePickerPatchTests { + @Test func layoutOverridesRemainTyped() throws { + try verifyThemePatch( + json: #"{"yearModePicker":{"layout":{"constant":{"_0":{"stacked":{}}}}}}"#, + keyPath: \WhereStylesheet.yearModePicker.layout, + replacement: .stacked, + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeAppearanceTests.swift b/Where/WhereUI/Tests/WhereThemeAppearanceTests.swift new file mode 100644 index 000000000..5da79f490 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeAppearanceTests.swift @@ -0,0 +1,52 @@ +import BroadwayCore +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeAppearanceTests { + @Test nonisolated func boxedGroupsKeepBackgroundCodingBoundedAndCopiesIndependent() throws { + #expect(MemoryLayout.size <= 512) + #expect(MemoryLayout.size <= 1024) + #expect(MemoryLayout.size <= 2048) + let original = try WhereBundledThemeDefinitions.resolved(.standard).appearance + let encoded = try JSONEncoder().encode(original) + var decoded = try JSONDecoder().decode(WhereThemeAppearance.self, from: encoded) + #expect(decoded == original) + decoded.card?.regular?.cornerRadius = .constant(41) + #expect(original.card?.regular?.cornerRadius != .constant(41)) + #expect(decoded.card?.regular?.cornerRadius == .constant(41)) + decoded.card = nil + #expect(throws: WhereThemeValidationError.self) { + try decoded.requireComplete(path: "appearance") + } + } + + @Test func explicitZeroPreservesUnrelatedComponentState() throws { + var parent = try WhereBundledThemeDefinitions.resolved(.standard).appearance + parent.card?.regular?.padding = .constant(39) + var patch = WhereThemeAppearance() + patch.locationWelcome = .init() + patch.locationWelcome?.glow = .init() + patch.locationWelcome?.glow?.opacity = .constant(0) + let result = patch.inheriting(parent) + #expect(result.locationWelcome?.glow?.opacity == .constant(0)) + #expect(result.card == parent.card) + #expect(result.locationWelcome?.cornerRadius == parent.locationWelcome?.cornerRadius) + #expect(result.locationForecast == parent.locationForecast) + } + + @Test func emptyAppearanceEncodesAsEmptyObjectAndPreservesNativeState() throws { + let patch = WhereThemeAppearance() + #expect(try String(decoding: JSONEncoder().encode(patch), as: UTF8.self) == "{}") + var actual = WhereStylesheet.default + let expected = actual + try patch.apply( + to: &actual, + traits: .system, + appearance: .init(background: .white, dimensionScales: [:]), + path: "appearance", + ) + #expect(actual == expected) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeBlendTests.swift b/Where/WhereUI/Tests/WhereThemeBlendTests.swift new file mode 100644 index 000000000..6e2174540 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeBlendTests.swift @@ -0,0 +1,15 @@ +import Foundation +import SwiftUI +import Testing +@testable import WhereUI + +struct WhereThemeBlendTests { + @Test func preservesLuminosityWireRole() throws { + let value = try JSONDecoder().decode( + WhereThemeBlend.self, + from: Data(#""luminosity""#.utf8), + ) + #expect(value.blendMode == .luminosity) + #expect(try JSONEncoder().encode(value) == Data(#""luminosity""#.utf8)) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeCatalogTests.swift b/Where/WhereUI/Tests/WhereThemeCatalogTests.swift new file mode 100644 index 000000000..bf515fd7a --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeCatalogTests.swift @@ -0,0 +1,190 @@ +import BroadwayCore +import Foundation +import Testing +import ViewDocumentCore +import ViewDocumentUI +@testable import WhereUI + +@MainActor +struct WhereThemeCatalogTests { + @Test func rejectedReplacementsPreserveTheAcceptedCatalogAndArtwork() throws { + let accepted = try WhereBundledThemes.catalog() + let assets = try HalloweenThemeAssets.load() + let original = try accepted.prepare(.halloween, assets: assets) + var invalid = original.validated.document + invalid.parent = invalid.id + #expect(throws: WhereThemeValidationError.self) { + try accepted.replacing(invalid).prepare(.halloween, assets: assets) + } + var missingAsset = original.validated.document + missingAsset.components.regularCard?.artwork?[0].document.root.content = .raster(.init( + asset: .init(rawValue: "not-bundled"), + sizing: .fit, + )) + #expect(throws: ViewDocumentValidationError.self) { + try accepted.replacing(missingAsset).prepare(.halloween, assets: assets) + } + #expect(try accepted.prepare(.halloween, assets: assets).validated == original.validated) + + var updated = original.validated.document + updated.appearance.card?.regular?.cornerRadius = .constant(35) + let replacement = try accepted.replacing(updated).prepare(.halloween, assets: assets) + #expect(replacement.resolved.id == .halloween) + #expect(replacement.resolved.appearance.card?.regular?.cornerRadius == .constant(35)) + #expect(try accepted.prepare(.halloween, assets: assets).validated == original.validated) + } + + @Test nonisolated func maximumParentDepthResolvesOnAWorkerThread() async throws { + try await Task.detached { + let root = try WhereBundledThemeDefinitions.load("StandardTheme") + var documents = [root] + for index in 1 ... 15 { + documents.append(.init( + id: .init(rawValue: "child-\(index)"), + parent: documents.last!.id, + values: .init(colors: nil, fonts: nil, surfaces: nil), + appearance: .init(), + components: .init(), + )) + } + let catalog = try WhereThemeCatalog(documents: documents) + let resolved = try catalog.resolve(documents.last!.id) + #expect(resolved.appearance.card?.regular?.padding == root.appearance.card?.regular? + .padding) + documents.append(.init( + id: .init(rawValue: "too-deep"), + parent: documents.last!.id, + values: .init(colors: nil, fonts: nil, surfaces: nil), + appearance: .init(), + components: .init(), + )) + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeCatalog(documents: documents).resolve(.init(rawValue: "too-deep")) + } + }.value + } + + @Test func bundledRootsAreCompleteAndFlatInheritsGeometry() throws { + let catalog = try WhereBundledThemes.catalog() + let standard = try catalog.resolve(.init(rawValue: "standard")) + let flat = try catalog.resolve(.init(rawValue: "flat")) + try standard.appearance.requireComplete(path: "appearance") + try flat.appearance.requireComplete(path: "appearance") + #expect(standard.appearance.card?.regular?.padding == flat.appearance.card?.regular? + .padding) + #expect(standard.appearance.card?.regular?.surface != flat.appearance.card?.regular? + .surface) + #expect(flat.fallback == .flat) + } + + @Test func rejectsIncompleteRootsMissingParentsAndCycles() throws { + let root = child("root", parent: nil) + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeCatalog(documents: [root]).resolve(root.id) + } + let missing = child("missing", parent: "absent") + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeCatalog(documents: [missing]).resolve(missing.id) + } + let first = child("first", parent: "second"), second = child("second", parent: "first") + #expect(throws: WhereThemeValidationError.self) { try WhereThemeCatalog(documents: [ + first, + second, + ]).resolve(first.id) } + } + + @Test func artworkReplacesAsAUnitAndEmptyRemovesIt() throws { + var parent = child("parent", parent: "flat") + parent.components.regularCard = .init() + parent.components.regularCard?.artwork = [artwork()] + var child = child("child", parent: "parent") + child.components.regularCard = .init() + child.components.regularCard?.artwork = [] + child.components.regularCard?.nativeArtwork = [] + let catalog = try WhereThemeCatalog(documents: WhereBundledThemes.documents.get() + [ + parent, + child, + ]) + #expect(try catalog.resolve(parent.id).components.regularCard?.artwork?.count == 1) + #expect(try catalog.resolve(child.id).components.regularCard?.artwork == []) + #expect(try catalog.resolve(child.id).components.regularCard?.nativeArtwork == []) + } + + @Test func missingReferencesAndAssetsFailBeforeInjection() throws { + var document = child("invalid", parent: "flat") + document.components.regularCard = .init() + document.components.regularCard?.surface = .value(.init(rawValue: "missing")) + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeCatalog(documents: WhereBundledThemes.documents.get() + [document]) + .resolve(document.id) + } + document.components.regularCard?.surface = nil + document.components.regularCard?.artwork = [artwork()] + let catalog = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [document]) + #expect(throws: ViewDocumentValidationError.self) { + try catalog.prepare(document.id, assets: .init(images: [:], symbols: [:])) + } + } + + @Test func sameIdentityWithChangedContentProducesANewSlice() throws { + var first = child("same", parent: "standard") + first.appearance.card = .init() + first.appearance.card?.regular = .init() + first.appearance.card?.regular?.cornerRadius = .constant(18) + var second = first + second.appearance.card?.regular?.cornerRadius = .constant(40) + let a = try resolveTheme(first, traits: .system) + let b = try resolveTheme(second, traits: .system) + #expect(a.card.regular.cornerRadius == 18) + #expect(b.card.regular.cornerRadius == 40) + } + + @Test func explicitRemovalSurvivesEncodingAndInheritance() throws { + var document = child("removed", parent: "flat") + document.appearance.card = .init() + document.appearance.card?.regular = .init() + document.appearance.card?.regular?.regionShape = .clear + let encoded = try JSONEncoder().encode(document) + let decoded = try JSONDecoder().decode(WhereThemeDocument.self, from: encoded) + let result = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [decoded]).resolve(decoded.id) + #expect(result.appearance.card?.regular?.regionShape == .clear) + #expect(result.appearance.card?.compact?.regionShape != .clear) + } + + private func child(_ name: String, parent: String?) -> WhereThemeDocument { + .init( + id: .init(rawValue: name), + parent: parent.map { .init(rawValue: $0) }, + values: .init(colors: nil, fonts: nil, surfaces: nil), + appearance: .init(), + components: .init(), + ) + } + + private func artwork() -> WhereThemeArtwork { + .init( + id: .init(rawValue: "art"), + document: .init( + schemaVersion: 1, + root: .init( + id: .init(rawValue: "image"), + content: .raster(.init(asset: .init(rawValue: "missing-image"), sizing: .fit)), + modifiers: [], + animation: nil, + accessibility: .hidden, + ), + ), + placement: .init( + anchor: .bottomTrailing, + size: .fixed(width: 80, height: 60), + offset: .zero, + plane: .exterior, + outsets: .init(top: 0, leading: 0, bottom: 20, trailing: 20), + ), + role: .ornament, + opacity: .constant(1), + ) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeComponents.CapabilitiesTests.swift b/Where/WhereUI/Tests/WhereThemeComponents.CapabilitiesTests.swift new file mode 100644 index 000000000..f2692104c --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeComponents.CapabilitiesTests.swift @@ -0,0 +1,47 @@ +import Testing +@testable import WhereUI + +struct WhereThemeComponentsCapabilitiesTests { + @Test(arguments: [ + WhereThemeComponents.Role.locations, + .elsewhere, + .year, + .settings, + .sheet, + .section, + ]) + func nativeHostsRejectUnsupportedSurfaceAndHeading(role: WhereThemeComponents.Role) throws { + var components = WhereThemeComponents() + components[role] = .init() + components[role]?.surface = .value(.init(rawValue: "paper")) + #expect(throws: WhereThemeValidationError.self) { try components.validateCapabilities() } + components[role]?.surface = .clear + components[role]?.heading = .value(.init(rawValue: "heading")) + #expect(throws: WhereThemeValidationError.self) { try components.validateCapabilities() } + components[role]?.heading = .clear + try components.validateCapabilities() + } + + @Test func nativeArtworkCannotSilentlyDisappearOnTheWrongHost() throws { + var components = WhereThemeComponents() + components.privacy = .init() + components.privacy?.nativeArtwork = [.regionPattern] + #expect(throws: WhereThemeValidationError.self) { try components.validateCapabilities() } + components.privacy?.nativeArtwork = [.stamp] + try components.validateCapabilities() + components.privacy?.nativeArtwork = [] + try components.validateCapabilities() + } + + @Test(arguments: [WhereThemeComponents.Role.section, .timelineEntry]) + func nativeJoinedRowsRejectOverflow(role: WhereThemeComponents.Role) throws { + var components = WhereThemeComponents() + components[role] = .init() + let theme = try WhereBundledThemeDefinitions.load("HalloweenTheme") + components[role]?.artwork = theme.components.regularCard?.artwork + #expect(throws: WhereThemeValidationError.self) { try components.validateCapabilities() } + let interior = components[role]?.artwork?.filter { $0.placement.plane == .interior } + components[role]?.artwork = interior + try components.validateCapabilities() + } +} diff --git a/Where/WhereUI/Tests/WhereThemeDocumentTestSupport.swift b/Where/WhereUI/Tests/WhereThemeDocumentTestSupport.swift new file mode 100644 index 000000000..686e211e0 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeDocumentTestSupport.swift @@ -0,0 +1,105 @@ +import BroadwayCore +import Foundation +import SwiftUI +import Testing +import UIKit +import ViewDocumentCore +import ViewDocumentUI +import WhereCore +@testable import WhereUI + +/// A red background while active, with an invisible deterministic inactive pose. +@MainActor +func activeThemeBackground() throws -> PreparedWhereThemeDocument { + var document = themeDocument(appearance: .init(), fallback: .standard) + var hidden = ViewAnimationDescriptor.Pose.identity + hidden.opacity = 0 + document.layers.locationsBackground = [.init(schemaVersion: 1, root: .init( + id: .init(rawValue: "activity-probe"), + content: .shape(.rectangle), + modifiers: [.foreground(.color(.rgba(red: 1, green: 0, blue: 0, alpha: 1)))], + animation: .init( + from: .identity, + to: .identity, + staticPose: hidden, + duration: 1, + delay: 0, + curve: .linear, + repetition: .forever(autoreverses: false), + ), + accessibility: .hidden, + ))] + let assets = ViewDocumentAssets(images: [:], symbols: [:]) + return try PreparedWhereThemeDocument( + validated: ValidatedWhereThemeDocument(document: document, resources: assets.resources), + assets: assets, + ) +} + +@MainActor +func themeBackgroundIsRed(in view: UIView) -> Bool { + var pixel = [UInt8](repeating: 0, count: 4) + pixel.withUnsafeMutableBytes { bytes in + guard let context = CGContext( + data: bytes.baseAddress, + width: 1, + height: 1, + bitsPerComponent: 8, + bytesPerRow: 4, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue, + ) else { return } + context.translateBy(x: -8, y: -view.bounds.midY) + view.layer.render(in: context) + } + return pixel[0] > 240 && pixel[1] < 15 && pixel[2] < 15 && pixel[3] > 240 +} + +@MainActor +func themeDocument( + appearance: WhereThemeAppearance, + fallback: WhereThemeDocument.Fallback, +) -> WhereThemeDocument { + .init( + schemaVersion: 1, + id: .init(rawValue: "test"), + fallback: fallback, + appearance: appearance, + layers: .init(), + ) +} + +@MainActor +func resolveTheme(_ document: WhereThemeDocument, traits: BTraits) throws -> WhereStylesheet { + let validated = try ValidatedWhereThemeDocument( + document: document, + resources: .init(images: [], symbols: []), + ) + var themes = BThemes() + themes[WhereDocumentTheme.self] = .init(document: validated) + return BContext(traits: traits, themes: themes).stylesheet( + WhereStylesheet.self, + fallback: .default, + ) +} + +@MainActor +func verifyThemePatch( + json: String, + keyPath: WritableKeyPath, + replacement: Value, +) throws { + let patch = try JSONDecoder().decode(WhereThemeAppearance.self, from: Data(json.utf8)) + #expect(try JSONDecoder() + .decode(WhereThemeAppearance.self, from: JSONEncoder().encode(patch)) == patch) + var actual = WhereStylesheet.default + var expected = actual + expected[keyPath: keyPath] = replacement + try patch.apply( + to: &actual, + traits: .system, + appearance: .init(background: .white, dimensionScales: [:]), + path: "appearance", + ) + #expect(actual == expected) +} diff --git a/Where/WhereUI/Tests/WhereThemeDocumentTests.swift b/Where/WhereUI/Tests/WhereThemeDocumentTests.swift new file mode 100644 index 000000000..adbb2c529 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeDocumentTests.swift @@ -0,0 +1,53 @@ +import BroadwayCore +import Foundation +import Testing +import ViewDocumentCore +import WhereCore +@testable import WhereUI + +@MainActor +struct WhereThemeDocumentTests { + @Test func sparseJSONRoundTripsAndPreservesExplicitZero() throws { + let json = Data( + #"{"schemaVersion":1,"id":"example","fallback":"flat","appearance":{"card":{"regular":{"cornerRadius":{"constant":{"_0":0}}}}},"layers":{}}"# + .utf8, + ) + let decoded = try JSONDecoder().decode(WhereThemeDocument.self, from: json) + #expect(decoded.id.rawValue == "example") + #expect(decoded.fallback == .flat) + #expect(decoded.appearance.card?.regular?.cornerRadius == .constant(0)) + #expect(decoded.appearance.card?.compact == nil) + #expect(try JSONDecoder().decode( + WhereThemeDocument.self, + from: JSONEncoder().encode(decoded), + ) == decoded) + } + + @Test func emptyDocumentMatchesFallbackAcrossModesAndAccessibility() throws { + for fallback: WhereThemeDocument.Fallback in [.standard, .flat] { + for mode: BMode in [.light, .dark] { + for accessibility in [ + BAccessibility(), + .init(isReduceMotionEnabled: true), + .init(isReduceTransparencyEnabled: true), + .init(isDarkerSystemColorsEnabled: true), + .init(shouldDifferentiateWithoutColor: true), + ] { + var traits = BTraits.system + traits.mode = mode + traits.contentSizeCategory = .accessibilityExtraExtraExtraLarge + traits.accessibility = accessibility + let original = BContext( + traits: traits, + themes: WhereThemes.current(theme: fallback.theme), + ).stylesheet(WhereStylesheet.self, fallback: .default) + let resolved = try resolveTheme( + themeDocument(appearance: .init(), fallback: fallback), + traits: traits, + ) + #expect(resolved == original) + } + } + } + } +} diff --git a/Where/WhereUI/Tests/WhereThemeExamplesTests.swift b/Where/WhereUI/Tests/WhereThemeExamplesTests.swift new file mode 100644 index 000000000..12fc03f75 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeExamplesTests.swift @@ -0,0 +1,18 @@ +import Foundation +import Testing +@testable import WhereUI + +@MainActor +struct WhereThemeExamplesTests { + @Test func bundledExamplesDecodePrepareAndRoundTrip() throws { + for example in WhereThemeExamples.allCases { + let prepared = try example.prepare() + let document = prepared.validated.document + #expect(try JSONDecoder().decode( + WhereThemeDocument.self, + from: JSONEncoder().encode(document), + ) == document) + #expect(prepared.componentArtwork.values.contains { !$0.isEmpty }) + } + } +} diff --git a/Where/WhereUI/Tests/WhereThemeGeometryTests.swift b/Where/WhereUI/Tests/WhereThemeGeometryTests.swift new file mode 100644 index 000000000..79f3b0948 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeGeometryTests.swift @@ -0,0 +1,20 @@ +import Foundation +import Testing +@testable import WhereUI + +struct WhereThemeGeometryTests { + @Test func pointRoundTripsSignedCoordinates() throws { + let value = WhereThemePoint(x: -12, y: 8) + #expect(try JSONDecoder() + .decode(WhereThemePoint.self, from: JSONEncoder().encode(value)) == value) + #expect(value.point.x == -12) + try WhereThemeValidation.validate(value, path: "center") + } + + @Test func sizeRejectsNegativeExtents() { + #expect(throws: WhereThemeValidationError.self) { try WhereThemeValidation.validate( + WhereThemeSize(width: -1, height: 10), + path: "extent", + ) } + } +} diff --git a/Where/WhereUI/Tests/WhereThemeLegacyConversionTests.swift b/Where/WhereUI/Tests/WhereThemeLegacyConversionTests.swift new file mode 100644 index 000000000..71b2b1319 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeLegacyConversionTests.swift @@ -0,0 +1,113 @@ +import BroadwayCore +import Foundation +import Testing +import ViewDocumentCore +@testable import WhereUI + +struct WhereThemeLegacyConversionTests { + @Test @MainActor func legacyFixturesConvertPrepareAndEncodeOnlyVersionTwo() throws { + for fixture in LegacyThemeFixtures.Theme.allCases { + let prepared = try LegacyThemeFixtures.prepare(fixture) + #expect(prepared.validated.document.schemaVersion == 2) + #expect(prepared.componentArtwork.values.contains { !$0.isEmpty }) + let data = try JSONEncoder().encode(prepared.validated.document) + let roundTrip = try JSONDecoder().decode(WhereThemeDocument.self, from: data) + #expect(roundTrip == prepared.validated.document) + #expect(roundTrip.layers == .init()) + } + } + + @Test func legacyMatteEnablesNeutralPaperButVersionTwoRequiresCompletePaper() throws { + let data = Data( + #"{"schemaVersion":1,"id":"legacy","fallback":"standard","appearance":{"card":{"regular":{"surface":{"constant":{"_0":{"matte":{"_0":{"outline":{"width":2}}}}}}}}},"layers":{}}"# + .utf8, + ) + let imported = try JSONDecoder().decode(WhereThemeDocument.self, from: data) + #expect(imported.schemaVersion == 2) + let resolved = try WhereThemeCatalog(documents: WhereBundledThemeDefinitions.documents + .get() + [imported]).resolve(imported.id) + guard case let .matte(paper) = resolved.appearance.card?.regular?.surface? + .resolve(mode: .light) + else { + Issue.record("Expected imported matte surface") + return + } + #expect(paper.outline?.width == 2) + #expect(paper.fill?.tintMix == 0) + #expect(paper.shadow?.radius == 0) + var strict = imported + strict.id = .init(rawValue: "strict") + strict.appearance.card?.regular?.surface = .constant(.matte(.init( + fill: nil, + outline: .init(opacity: nil, width: 2), + shadow: nil, + pressedTintOpacity: nil, + ))) + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeCatalog(documents: WhereBundledThemeDefinitions.documents + .get() + [strict]).resolve(strict.id) + } + } + + @Test func legacyOptionalPartsReceiveTheirOldDefaults() throws { + let data = Data( + #"{"schemaVersion":1,"id":"optional","fallback":"standard","appearance":{"card":{"compact":{"paper":{"value":{"_0":{}}},"entryStamp":{"arc":{"value":{"_0":{"opacity":{"constant":{"_0":0.4}}}}}},"regionShape":{"value":{"_0":{"watermark":{"stroke":{"value":{"_0":{"opacity":{"constant":{"_0":0.8}}}}}}}}}}}},"layers":{}}"# + .utf8, + ) + let imported = try JSONDecoder().decode(WhereThemeDocument.self, from: data) + let resolved = try WhereThemeCatalog(documents: WhereBundledThemeDefinitions.documents + .get() + [imported]).resolve(imported.id) + try resolved.appearance.requireComplete(path: "appearance") + guard case let .value(paper) = resolved.appearance.card?.compact?.paper else { + Issue.record("Expected enabled paper") + return + } + #expect(paper.surface?.resolve(mode: .dark).fill?.base?.resolve(mode: .dark) == .rgba( + red: 1, + green: 1, + blue: 1, + alpha: 1, + )) + #expect(paper.contentTint?.resolve(mode: .dark).color?.resolve(mode: .dark) == .rgba( + red: 0, + green: 0, + blue: 0, + alpha: 1, + )) + + #expect(try JSONDecoder().decode( + WhereThemeDocument.self, + from: JSONEncoder().encode(imported), + ) == imported) + } + + @Test @MainActor func legacySlotCapacitySurvivesConversion() throws { + func layer(_ index: Int) -> ViewDocument { + .init( + schemaVersion: 1, + root: .init( + id: .init(rawValue: "layer-\(index)"), + content: .shape(.rectangle), + modifiers: [], + animation: nil, + accessibility: .hidden, + ), + ) + } + var document = themeDocument(appearance: .init(), fallback: .flat) + document.layers.regularCardBackground = (0 ..< 8).map(layer) + document.layers.regularCardOverlay = (8 ..< 16).map(layer) + let validated = try ValidatedWhereThemeDocument( + document: document, + resources: .init(images: [], symbols: []), + ) + #expect(validated.artwork[.regularCard]?.count == 16) + document.layers.regularCardBackground?.append(layer(16)) + #expect(throws: WhereThemeValidationError.self) { + try ValidatedWhereThemeDocument( + document: document, + resources: .init(images: [], symbols: []), + ) + } + } +} diff --git a/Where/WhereUI/Tests/WhereThemeOptionalTests.swift b/Where/WhereUI/Tests/WhereThemeOptionalTests.swift new file mode 100644 index 000000000..32140de69 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeOptionalTests.swift @@ -0,0 +1,15 @@ +import Foundation +import Testing +@testable import WhereUI + +struct WhereThemeOptionalTests { + @Test func wireFormatDistinguishesClearFromAnExplicitZero() throws { + let decoder = JSONDecoder() + #expect(try decoder + .decode(WhereThemeOptional.self, from: Data(#"{"clear":{}}"#.utf8)) == .clear) + #expect(try decoder.decode( + WhereThemeOptional.self, + from: Data(#"{"value":{"_0":0}}"#.utf8), + ) == .value(0)) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeOverridesTests.swift b/Where/WhereUI/Tests/WhereThemeOverridesTests.swift deleted file mode 100644 index 8015ab210..000000000 --- a/Where/WhereUI/Tests/WhereThemeOverridesTests.swift +++ /dev/null @@ -1,68 +0,0 @@ -import BroadwayCore -import SwiftUI -import Testing -import WhereCore -@testable import WhereUI - -@MainActor -struct WhereThemeOverridesTests { - @Test func emptyOverridesPreserveAnAdjustedBase() { - var sheet = WhereStylesheet.default - sheet.card.regular.cornerRadius = 37 - sheet.locationWelcome.glow.opacity = 0.43 - sheet.locationForecast.ink.surfaceWashOpacity = 0.71 - let original = sheet - WhereThemeOverrides().apply(to: &sheet) - #expect(sheet == original) - #expect(WhereThemeOverrides.resolve(theme: .standard, traits: .system, base: sheet) - == WhereThemeOverrides()) - } - - @Test func explicitZeroChangesOnlyTheSpecifiedField() { - var sheet = WhereStylesheet.default - sheet.card.regular.cornerRadius = 37 - sheet.locationWelcome.glow.opacity = 0.43 - var expected = sheet - expected.locationWelcome.glow.opacity = 0 - expected.locationForecast.summary.shadowOpacity = 0 - - var overrides = WhereThemeOverrides() - overrides.welcome.glowOpacity = 0 - overrides.forecast.summaryShadowOpacity = 0 - overrides.apply(to: &sheet) - #expect(sheet == expected) - } - - @Test func componentOverridesDoNotReplaceSiblingState() { - var sheet = WhereStylesheet.default - sheet.card.regular.padding = 39 - sheet.card.compact.padding = 17 - sheet.card.dayCount = .reducedMotion - let compact = sheet.card.compact - var overrides = WhereThemeOverrides() - overrides.card.regular.contentTint = .init(color: .purple, fraction: 0.5) - overrides.apply(to: &sheet) - #expect(sheet.card.regular.contentTint == .init(color: .purple, fraction: 0.5)) - #expect(sheet.card.regular.padding == 39) - #expect(sheet.card.compact == compact) - #expect(sheet.card.dayCount == .reducedMotion) - } - - @Test func elsewhereSurfaceKeepsCompactGeometryAndOriginalTextTreatment() { - var sheet = WhereStylesheet.default - sheet.card.compact.padding = 17 - let original = sheet.card[.elsewhere] - var surface = sheet.card.elsewherePaper.surface - surface.outline.width = 2 - surface.shadow.radius = 8 - var overrides = WhereThemeOverrides() - overrides.elsewhere.surface = surface - overrides.apply(to: &sheet) - let elsewhere = sheet.card[.elsewhere] - #expect(elsewhere.surface == .matte(surface)) - #expect(elsewhere.padding == 17) - #expect(elsewhere.contentTint == original.contentTint) - #expect(elsewhere.showsProgress == false) - #expect(sheet.card.compact.paper == nil) - } -} diff --git a/Where/WhereUI/Tests/WhereThemePanelTests.swift b/Where/WhereUI/Tests/WhereThemePanelTests.swift new file mode 100644 index 000000000..0f65b799b --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemePanelTests.swift @@ -0,0 +1,70 @@ +import SwiftUI +import TestHostSupport +import Testing +import UIKit +import ViewDocumentCore +import ViewDocumentUI +@testable import WhereUI + +@MainActor +struct WhereThemePanelTests { + @Test func backgroundAndTypographyApplyWithoutSurfaceOrArtwork() throws { + var document = WhereThemeDocument( + id: .init(rawValue: "paint-and-type"), + parent: .standard, + values: .init( + colors: [.init( + id: .init(rawValue: "red"), + value: .constant(.rgba(red: 1, green: 0, blue: 0, alpha: 1)), + )], + fonts: [.init( + id: .init(rawValue: "title"), + value: .constant(.init(size: .semantic(.title), weight: nil, design: nil)), + )], + surfaces: nil, + ), + appearance: .init(), + components: .init(), + ) + document.components.calendarMonth = .init() + document.components.calendarMonth?.background = .value(.init(rawValue: "red")) + document.components.calendarMonth?.body = .value(.init(rawValue: "title")) + let theme = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [document]) + .prepare(document.id, assets: .init(images: [:], symbols: [:])) + var observedFont: Font? + let host = UIHostingController(rootView: FontProbe { observedFont = $0 } + .whereThemePanel(.calendarMonth, in: Rectangle()) + .whereBroadwayRoot(themeDocument: theme)) + try show(host) { host in + try waitFor { themeBackgroundIsRed(in: host.view) && observedFont == .title } + } + } + + @Test func artworkCanInheritTheNativeSurfaceWithoutReplacingIt() throws { + let existing = try activeThemeBackground() + var document = existing.validated.document + document.id = .init(rawValue: "art-only") + document.components.calendarMonth = .init() + document.components.calendarMonth?.artwork = existing.resolved.components.locations?.artwork + document.components.calendarMonth?.artwork?[0].document.root.animation = nil + let assets = ViewDocumentAssets(images: [:], symbols: [:]) + let prepared = try WhereThemeCatalog(documents: WhereBundledThemes.documents + .get() + [document]) + .prepare(document.id, assets: assets) + let host = UIHostingController(rootView: Color.clear + .whereThemePanel(.calendarMonth, in: Rectangle(), tint: .primary) { Color.white } + .whereBroadwayRoot(themeDocument: prepared)) + try show(host) { host in + try waitFor { themeBackgroundIsRed(in: host.view) } + } + } +} + +private struct FontProbe: View { + let observed: (Font?) -> Void + @Environment(\.font) private var font + var body: some View { + Color.clear.onAppear { observed(font) } + } +} diff --git a/Where/WhereUI/Tests/WhereThemeSurfaceTests.swift b/Where/WhereUI/Tests/WhereThemeSurfaceTests.swift new file mode 100644 index 000000000..0ec3d14fb --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeSurfaceTests.swift @@ -0,0 +1,121 @@ +import BroadwayCore +import Foundation +import SwiftUI +import Testing +import ViewDocumentCore +import ViewDocumentUI +@testable import WhereUI + +struct WhereThemeSurfaceTests { + @Test(arguments: [ + ViewColorDescriptor.semantic(.secondary), + .semantic(.accent), + .rgba(red: 1, green: 0, blue: 0, alpha: 0.5), + ]) + func matteFillRejectsColorsWithoutOpaqueAlpha(_ color: ViewColorDescriptor) throws { + let surface = WhereThemeMatteSurface( + fill: .init(base: .constant(color), tintMix: 0), + outline: nil, + shadow: nil, + pressedTintOpacity: nil, + ) + #expect(throws: WhereThemeValidationError.self) { + try surface.resolve( + mode: .light, + appearance: .init(background: .white, dimensionScales: [:]), + base: WhereStylesheet.default.card.elsewherePaper.surface, + ) + } + } + + @Test(arguments: [ + ViewColorDescriptor.semantic(.primary), + .semantic(.background), + .rgba(red: 1, green: 0, blue: 0, alpha: 1), + ]) + func matteFillAcceptsOpaqueColors(_ color: ViewColorDescriptor) throws { + let surface = WhereThemeMatteSurface( + fill: .init(base: .constant(color), tintMix: 0), + outline: nil, + shadow: nil, + pressedTintOpacity: nil, + ) + let appearance = ViewDocumentAppearance(background: .white, dimensionScales: [:]) + let result = try surface.resolve( + mode: .light, + appearance: appearance, + base: WhereStylesheet.default.card.elsewherePaper.surface, + ) + #expect(result.fill.base == appearance.color(color)) + } + + @MainActor + @Test func sparseMattePatchInheritsFlatFillAndProtectsContrast() throws { + var patch = WhereThemeMatteSurface() + patch.shadow = .init() + patch.shadow?.radius = 0 + patch.outline = .init() + patch.outline?.opacity = 0 + var appearance = WhereThemeAppearance() + appearance.card = .init() + appearance.card?.regular = .init() + appearance.card?.regular?.surface = .constant(.matte(patch)) + var traits = BTraits.system + traits.accessibility = .init(isDarkerSystemColorsEnabled: true) + let base = try resolveTheme( + themeDocument(appearance: .init(), fallback: .flat), + traits: traits, + ) + let result = try resolveTheme( + themeDocument(appearance: appearance, fallback: .flat), + traits: traits, + ) + guard case let .matte(original) = base.card.regular.surface, + case let .matte(changed) = result.card.regular.surface + else { + Issue.record("Expected matte surfaces") + return + } + #expect(changed.fill == original.fill) + #expect(changed.outline == original.outline) + #expect(changed.shadow.radius == 0) + } + + @Test func matteSurfaceRoundTripsAndResolvesExplicitMode() throws { + let value = WhereThemeMatteSurface( + fill: .init( + base: .modes(.init( + light: .rgba(red: 1, green: 1, blue: 1, alpha: 1), + dark: .rgba(red: 0, green: 0, blue: 0, alpha: 1), + )), + tintMix: 0.5, + ), + outline: .init(opacity: 0.4, width: 1), + shadow: .init( + color: .constant(.rgba(red: 0, green: 0, blue: 0, alpha: 0.2)), + radius: 6, + offsetY: 3, + ), + pressedTintOpacity: 0.1, + ) + let surface = WhereThemeSurface.matte(value) + #expect(try JSONDecoder().decode( + WhereThemeSurface.self, + from: JSONEncoder().encode(surface), + ) == surface) + let base = WhereStylesheet.default.card.elsewherePaper.surface + let appearance = ViewDocumentAppearance(background: .white, dimensionScales: [:]) + let light = try value.resolve(mode: .light, appearance: appearance, base: base) + let dark = try value.resolve(mode: .dark, appearance: appearance, base: base) + #expect(light.fill.base != dark.fill.base) + #expect(light.fill.tintMix == 0.5) + #expect(light.outline == dark.outline) + var invalid = value + invalid.outline?.opacity = 2 + #expect(throws: WhereThemeValidationError.self) { try invalid.resolve( + mode: .light, + appearance: appearance, + base: base, + ) } + } +} diff --git a/Where/WhereUI/Tests/WhereThemeTextAlignmentTests.swift b/Where/WhereUI/Tests/WhereThemeTextAlignmentTests.swift new file mode 100644 index 000000000..e1bff79bc --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeTextAlignmentTests.swift @@ -0,0 +1,14 @@ +import Foundation +import SwiftUI +import Testing +@testable import WhereUI + +struct WhereThemeTextAlignmentTests { + @Test func wireAlignmentResolvesNativeRole() throws { + let value = try JSONDecoder().decode( + WhereThemeTextAlignment.self, + from: Data(#""trailing""#.utf8), + ) + #expect(value.alignment == .trailing) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeTintAdjustmentTests.swift b/Where/WhereUI/Tests/WhereThemeTintAdjustmentTests.swift new file mode 100644 index 000000000..43d38e087 --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeTintAdjustmentTests.swift @@ -0,0 +1,24 @@ +import Foundation +import SwiftUI +import Testing +import ViewDocumentUI +@testable import WhereUI + +struct WhereThemeTintAdjustmentTests { + @Test func explicitZeroLeavesRuntimeRegionTintUnchanged() throws { + let value = WhereThemeTintAdjustment( + color: .constant(.rgba(red: 0, green: 0, blue: 0, alpha: 1)), + fraction: 0, + ) + #expect(try JSONDecoder().decode( + WhereThemeTintAdjustment.self, + from: JSONEncoder().encode(value), + ) == value) + let resolved = try value.resolve( + mode: .light, + appearance: .init(background: .white, dimensionScales: [:]), + base: .unchanged, + ) + #expect(resolved.apply(to: .orange) == .orange) + } +} diff --git a/Where/WhereUI/Tests/WhereThemeValidationErrorTests.swift b/Where/WhereUI/Tests/WhereThemeValidationErrorTests.swift new file mode 100644 index 000000000..7bfe6d83d --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeValidationErrorTests.swift @@ -0,0 +1,9 @@ +import Testing +@testable import WhereUI + +struct WhereThemeValidationErrorTests { + @Test func diagnosticKeepsFieldPath() { + let error = WhereThemeValidationError(path: "appearance.card", reason: "Invalid value") + #expect(error.errorDescription == "appearance.card: Invalid value") + } +} diff --git a/Where/WhereUI/Tests/WhereThemeValidationTests.swift b/Where/WhereUI/Tests/WhereThemeValidationTests.swift new file mode 100644 index 000000000..03b50e89e --- /dev/null +++ b/Where/WhereUI/Tests/WhereThemeValidationTests.swift @@ -0,0 +1,80 @@ +import Testing +import ViewDocumentCore +@testable import WhereUI + +struct WhereThemeValidationTests { + @Test func rejectsPatternDivisorsAndExcessiveArtworkDensity() throws { + for scale in [0.0, 1e-300, 0.24, 4.01] { + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeValidation.validate( + scale, + path: "appearance.featureDiscovery.backgroundPattern.horizontalScale", + ) + } + } + for center in [-0.1, 1.1, 10000] { + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeValidation.validate( + center, + path: "appearance.featureDiscovery.backgroundPattern.centerXRatio", + ) + } + } + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeValidation.validate( + 1e-300, + path: "appearance.locationForecast.surface.microprintSpacing", + ) + } + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeValidation.validate( + 2.0, + path: "appearance.locationsBackground.preferredCellSize", + ) + } + try WhereThemeValidation.validate( + 0.25, + path: "appearance.featureDiscovery.backgroundPattern.horizontalScale", + ) + try WhereThemeValidation.validate( + 2.0, + path: "appearance.locationForecast.surface.microprintSpacing", + ) + try WhereThemeValidation.validate( + 32.0, + path: "appearance.locationsBackground.preferredCellSize", + ) + } + + @Test func descriptorFailureRetainsTheAppearancePathAndUnderlyingError() throws { + do { + try WhereThemeValidation.validate( + ViewFontDescriptor(size: .fixed(-1), weight: nil, design: nil), + path: "appearance.typography.widgetHeroRegion", + ) + Issue.record("Expected invalid font rejection") + } catch let error as WhereThemeValidationError { + #expect(error.path == "appearance.typography.widgetHeroRegion") + #expect(error.underlying != nil) + } + } + + @Test func rejectsUnboundedPatternDensityAndInvalidTypography() throws { + for spacing in [0.0, 0.00001, -1, .infinity, .nan] { + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeValidation.validate( + spacing, + path: "appearance.card.regular.rosette.primaryRingSpacing", + ) + } + } + #expect(throws: WhereThemeValidationError.self) { + try WhereThemeValidation.validate( + WhereThemeAppearance.CardStyle.Typography.Size.fixed(-1), + path: "font", + ) + } + try WhereThemeValidation.validate(0.0, path: "glow.opacity") + try WhereThemeValidation.validate(-5.0, path: "artwork.offsetY") + } +}