Skip to content

Add Where multi-theme foundation - #244

Merged
kyleve merged 10 commits into
mainfrom
codex/where-theme-foundation
Aug 13, 2026
Merged

Add Where multi-theme foundation#244
kyleve merged 10 commits into
mainfrom
codex/where-theme-foundation

Conversation

@kyleve

@kyleve kyleve commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Posted by an AI agent on kve's behalf.

Summary

  • add stable standard and alternate theme identities backed by device-local preferences and Broadway
  • add neutral theme selectors to onboarding and Appearance Settings while keeping both choices visually identical to the current Liquid Glass UI
  • propagate the selected identity atomically to App Intent snippets and widgets, with widget presentation stored independently from aggregated widget data
  • publish Standard explicitly during reset so extensions cannot retain the prior theme through onboarding

The two themes are intentionally identical scaffolding in this PR. Standard is the default and preserves the current app, widget, and snippet presentation. Theme-only widget changes write a small widget-presentation.json value and reload timelines without rebuilding the annual data snapshot.

Testing

  • ./swiftformat --lint
  • ./sf-symbols --lint
  • ./xcstrings --lint
  • ./test WhereCoreTests (551 tests passed)
  • ./test WhereUITests (420 tests passed)
  • ./test WhereIntentsTests (30 tests passed)
  • ./test --snapshots (42 snapshot tests passed)
  • ./test --everything (1,850 unit tests and 42 snapshot tests passed)

Stack

@kyleve
kyleve force-pushed the codex/where-theme-foundation branch from 688d736 to ff051b1 Compare August 12, 2026 21:15
get {
guard
let rawValue = store.object(forKey: Keys.theme.rawValue) as? String,
let theme = WhereTheme(rawValue: rawValue)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To check: Is this expensive? If so, we may want a read cache of these.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Checked this: UserDefaults serves these reads from its in-process representation, and WhereModel.theme is already the observable runtime cache used by SwiftUI, so I left the preference getter uncached.

// empty map rather than failing (the widget then uses default looks).
appearances = try container
.decodeIfPresent([Region: RegionAppearance].self, forKey: .appearances) ?? [:]
theme = try container.decodeIfPresent(WhereTheme.self, forKey: .theme) ?? .standard

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this should be written and read separately; I think the data JSON and the theme info should be separate so changing one doesn't require rewriting the other.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Addressed in f18d16f. Widget presentation now lives in a separate widget-presentation.json; WidgetSnapshot is data-only again, and a theme change writes only the small presentation value.

public func configureTheme(_ theme: WhereTheme) {
guard theme != self.theme else { return }
self.theme = theme
presentationRevision &+= 1

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a smell to me; I don't know why we'd need it if we're just poking data on change.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Addressed in f18d16f. Theme changes no longer invalidate or rebuild the data snapshot. A dedicated WidgetPresentationPublisher writes the separate theme file and reloads WidgetKit.

/// Serialize writes through a task chain. A theme change cancels a queued
/// superseded write; a write already in flight finishes before its
/// replacement, so the newest selection is always the final external state.
private func enqueue(_ snapshot: WidgetSnapshot, revision: UInt64) async {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit complicated; shouldn't we just finish in progress writes? Also, I think if we split presentation info from widget data it's easier? Maybe?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Addressed in f18d16f. Removing presentation from WidgetSnapshotPublisher eliminated its revision/task-chain machinery. Actor-serialized presentation writes finish in order, and the model's cancellable task suppresses superseded queued selections.

}

/// Preview a theme without writing device preferences.
public func previewTheme(_ newTheme: WhereTheme) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To check: How does this actually kick the theme I the app root to update it? And if it doesn't, what is the value of this method?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Added the requested inline comment in previewTheme(_:) documenting the observation path: RootView reads the observable theme property into whereBroadwayRoot, so the assignment immediately re-resolves the live presentation tree without persisting the preview. Commit e5407c0.

}

/// A compact rendering of the current Liquid Glass location-card language.
private struct WhereThemePreviewArtwork: View {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't love rebuilding the card; could we make the card more responsive to smaller rendering sizes perhaps? Eg, hiding content based on some passed in render size/purpose.

(Another option would be to always show the previews vertically stacked, and always include a full size card; which might be nice given the card is the primary UI you'll change!)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kyleve's behalf.

Removed the duplicate WhereThemePreviewArtwork implementation. Each option now renders the production RegionSummaryCard under its own theme, using the existing compact card size plus a theme-specimen purpose that only omits the provenance stamp at narrow width. This retains the real rosette, material, typography, count, and progress behavior; the full light/dark/contrast/RTL/Dynamic Type snapshot matrix was re-recorded and reviewed. Commit e4af1ce.

kyleve added 7 commits August 12, 2026 15:54
…ndation

# Conflicts:
#	Where/WhereCore/README.md
#	Where/WhereCore/Sources/Preferences/WherePreferences.swift
#	Where/WhereCore/Tests/WherePreferencesTests.swift
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png
#	Where/WhereUI/Sources/Resources/Localizable.xcstrings
…ndation

# Conflicts:
#	Where/WhereCore/README.md
#	Where/WhereCore/Sources/Preferences/WherePreferences.swift
#	Where/WhereCore/Tests/WherePreferencesTests.swift
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPad_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/AppearanceSettingsViewSnapshotTests/appearance.Default_iPhone_dark.png
#	Where/WhereUI/Sources/Settings/AppearanceSettingsView.swift
@kyleve
kyleve merged commit 4a3b56b into main Aug 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant