Skip to content

Extract colors and apply cozy theme - #8

Draft
gsbernstein wants to merge 18 commits into
masterfrom
cursor/extract-cozy-colors-b65c
Draft

Extract colors and apply cozy theme#8
gsbernstein wants to merge 18 commits into
masterfrom
cursor/extract-cozy-colors-b65c

Conversation

@gsbernstein

@gsbernstein gsbernstein commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Centralizes app theme colors, updates the palette to a warm "cozy" theme inspired by the app icon (including a true-black background/cards in dark mode), and makes theme a Settings option with two choices: System (the original default iOS colors) and Cozy (the warm palette, with a true-black dark mode).
  • Colors are modeled as a ThemeColorPalette protocol with one conforming struct per theme (SystemThemeColors, CozyThemeColors), rather than a set of functions that switch on the theme. AppTheme.colors hands back the palette for the active case, and call sites read colors.accent etc., or resolve a KeyPath<any ThemeColorPalette, Color> when the choice itself depends on some other value (e.g. sleep stage colors, sleep-goal status colors).
  • The cozy palette's colors live in the asset catalog (Assets.xcassets/Cozy*.colorset) instead of inline RGB values, so they can be tweaked visually in Xcode's color picker.

Changes

Color extraction & cozy theme

  • New ThemeColorPalette protocol (Utils/ThemeColorPalette.swift) with named colors for accents, card icons, status indicators, sleep stages, backgrounds, and primary/secondary (so "grace"/fallback colors can also be expressed as a keypath).
  • Two conforming structs:
    • SystemThemeColors — the original default iOS colors (blue/purple/red/green/orange/indigo/gray, secondarySystemBackground/systemBackground/tertiarySystemBackground), recovered from before the cozy palette existed. Intentionally mirrors the OS's own semantic colors rather than the asset catalog, since there's nothing there to make tunable.
    • CozyThemeColors — reads every color from a generated Color.cozy* extension backed by an asset-catalog colorset (CozyAccent, CozyBackground, CozySleepDeep, etc.), each with its own light/dark variant. Dark mode uses a true-black background with a near-black card color.
  • All views updated to read colors through the active palette instead of .blue, .purple, .red, etc.

Theme as a setting

  • New AppTheme enum (system, cozy) threaded through the view hierarchy via EnvironmentValues.appTheme, with a colors: any ThemeColorPalette computed property.
  • Where a color choice depends on some other value (sleep stage, sleep-goal status), that logic returns a KeyPath<any ThemeColorPalette, Color> (or nil for a fallback) instead of switching on the theme itself — e.g. HKCategoryValueSleepAnalysis.paletteColor, Constants.sleepGoalColor(difference:), SleepBank.statusColor. The call site resolves the keypath against the active palette (colors[keyPath:]).
  • UserPreferences gains a persisted theme property (stored as a raw string, defaulting to cozy, falling back to cozy on an unrecognized value so future app versions can add themes without breaking older data).
  • SettingsView adds an "Appearance" section with a theme picker; it resolves preferences.theme.colors directly rather than through the appTheme environment key, since it's presented as a sheet/inspector.
  • ContentView injects the selected theme into the environment and applies it to the background and the app-wide tint, so default-tinted system controls (buttons, toggles, nav bar) follow the theme too.

Test plan

  • Build and run on iPhone simulator
  • In Settings, switch between System / Cozy and confirm accents, card icons, sleep stage colors, and backgrounds update immediately
  • Toggle dark mode under each theme and confirm: System uses default iOS backgrounds, Cozy uses true black with near-black cards
  • Confirm the sleep balance waterfall chart and sleep insights card follow the selected theme
  • Quit and relaunch the app to confirm the selected theme persists
  • Open Assets.xcassets in Xcode and confirm the new Cozy* colorsets show up correctly and are editable/previewable
Open in Web Open in Cursor 

cursoragent and others added 18 commits May 29, 2026 16:27
Centralize theme color definitions in a single AppColors enum and
update views to reference named colors instead of inline system colors.
No visual changes in this commit.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Replace the default system blues and purples with a warm palette
inspired by the app icon: coral accents, cream backgrounds, dusty
mauve and brown sleep-stage colors, and sage/terracotta status tones.
Asset catalog colors now use adaptive light/dark cozy values.

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Reconcile the cozy color palette with upstream changes that landed since
this branch was created (editable wake time/goal, sleep balance
waterfall chart, sleep insights card, earliest-bedtime setting, etc.):

- Constants.sleepGoalColor now uses AppColors.positive/negative instead
  of hardcoded .green/.red, so every consumer (LastNightCard,
  SleepDayGroup, SleepBankCard) picks up the cozy palette automatically.
- Card headers use CardHeader with cozy AppColors icon tints instead of
  the old inline HStack layout and system blue/red/purple colors.
- SleepBankCard and LastNightCard keep master's grace-period-aware
  status color logic (sleepBank.statusColor / durationColor).
- SettingsView keeps master's earliest-bedtime DatePicker, dropping the
  now-removed max/min sleep hour sliders.
- Extended the cozy theme to new views added upstream:
  SleepInsightsCard (green/orange -> AppColors.positive/warning) and
  BalanceWaterfallChart (green/red -> AppColors.positive/negative).

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Add an AppTheme enum (system / cozy / cozyBlack) and thread it through the app via EnvironmentValues.appTheme, so every color decision reacts to the selected theme instead of a fixed constant:

- AppColors.swift: every color accessor now takes an AppTheme and returns either the original system colors (blue/purple/red/green/orange/indigo/gray, as they were before the cozy palette existed) or the cozy warm palette. Background/card colors move here too (previously baked into the AccentColor/BackgroundBehindCards/CardBackground asset catalog entries), so they can vary by theme: cozy keeps the existing warm dark-brown dark mode, cozyBlack swaps it for a true black background with a near-black card color for OLED/maximum-contrast displays.
- UserPreferences gains a persisted theme property (stored as a raw string, defaulting to cozy, falling back to cozy on an unrecognized value).
- SettingsView adds an Appearance section with a theme picker.
- ContentView injects the selected theme into the environment and applies it to the background and the app-wide tint.
- Every view/model that previously read a fixed AppColors constant now takes an AppTheme parameter (via the appTheme environment key, or for SettingsView directly from the model to avoid depending on environment propagation into its sheet/inspector presentation).
Replace the theme-parameterized AppColors functions with a ThemeColorPalette protocol and one conforming struct per theme (SystemThemeColors, CozyThemeColors, CozyBlackThemeColors). AppTheme.colors hands back the palette for the active case, so call sites read colors.accent / theme.colors.background etc. instead of passing the theme into a switch on every access.

CozyBlackThemeColors composes CozyThemeColors and only overrides background/cardBackground, so the two palettes can never drift apart on the colors they share.

Where a color choice itself varies per case of some other type (sleep stage colors), that type now returns a KeyPath<any ThemeColorPalette, Color> instead of switching on the theme, and the call site resolves it against the active palette (HKCategoryValueSleepAnalysis.color(in:), replacing color(for:)). Verified with a standalone Swift 6 snippet that keypaths rooted in an existential protocol resolve correctly against different conforming struct instances, including under strict concurrency checking.

Constants.sleepGoalColor/sleepDurationColor and SleepBank.statusColor now take the resolved palette directly (positive/negative colors, not a keypath, since they also need to fall back to a caller-supplied grace color that isn't part of the palette).

SettingsView keeps resolving preferences.theme.colors directly rather than through the appTheme environment key, since it's presented as a sheet/inspector and this avoids depending on environment propagation into that presentation.
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
Add one colorset per cozy color (Assets.xcassets/Cozy*.colorset) with the
same light/dark RGB values that were previously inline in
CozyThemeColors.swift, and switch CozyThemeColors to read the generated
Color extensions (Color.cozyAccent, Color.cozyBackground, etc.) instead of
building UIColor dynamic providers from hardcoded tuples.

This only touches the cozy theme: SystemThemeColors intentionally mirrors
the OS's own semantic colors (.blue, .secondarySystemBackground, etc.), so
there's nothing there to make tunable via an asset. The generated symbols
already work today because the project has
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS enabled, and
the .xcodeproj uses a file-system-synchronized group, so the new colorsets
don't need any project.pbxproj changes to be picked up.

Removed the now-unused RGB-tuple dynamicColor(light:dark:) helper from
ThemeColorPalette.swift; only the UIColor-pair overload (used by
SystemThemeColors) is still needed.

Net effect: tweaking the cozy palette (e.g. lightening a sleep-stage color,
or adjusting how dark the near-black card is) is now a matter of picking a
new color in Xcode's asset catalog editor, with live light/dark preview,
rather than editing RGB tuples in code.
…ency with SystemThemeColors

Co-authored-by: Greg <gsbernstein@users.noreply.github.com>
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.

2 participants