Skip to content

feat: adaptive Warm Meadow palette (dark mode) and no placeholder identity - #38

Merged
Ivan-LB merged 8 commits into
devfrom
feat/adaptive-palette
Aug 16, 2026
Merged

Ivan-LB merged 8 commits into
devfrom
feat/adaptive-palette

Conversation

@Ivan-LB

@Ivan-LB Ivan-LB commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Summary

Executes docs/plans/pr-d2-adaptive-palette.md, in the plan's commit order:

  1. Placeholder identity removed (owner's request in feat(profile): real two-way reminder settings and a profile with content #37): the "User" monogram card on Profile and the person.fill avatar in the Dashboard greeting are gone. "Saving since " moved into the lifetime income card. All display-name plumbing deleted (EditProfileSheet, ProfileViewModel.displayName, AppViewModel.currentUserName, the "displayName" UserDefaults key, orphaned InnerHeightPreferenceKey, Strings.Profile.editNameHint + catalog entry).
  2. Trait-aware palette. Every Color.warm* token now resolves per appearance through a UIColor dynamic provider — zero call-site changes for the 18 existing tokens. Dark is "the notebook under a lamp": same hues, low chroma, one green. New role tokens: warmGreenFill (green as a surface — stays deep in dark so white content keeps ≥5:1), warmOnGreen, warmOnInk (ink flips light in dark, so its content flips to the ground), warmTrack, warmShadow/warmShellShadow, warmLilac(Soft), warmAmberDeep.
  3. The sweep. All 97 hardcoded color sites (Color(red:), .white, .black, legacy Color("primaryGreen")) now go through tokens, by semantics (fill vs ink, on-green vs on-ink) — not search-and-replace. GoalColor's 12 accents + tracks got dark variants (accents a step lighter, ≥3:1 vs dark surface). Legacy ColorPalette asset catalog deleted; four dead files that pointed at it or at nothing removed (NotificationTimePicker, InsightRow, QuickActionButton, PrimaryButton); legacy shadows and UIConstants.UIShadow gone — only the hero-card lifts and the "+" shell keep a shadow, per DESIGN.md. The remaining literals are deliberate and commented (splash frame, sprout mark, camera-overlay controls, the PDF).
  4. Dark Mode switch ON (FeatureFlags.darkModeEnabled = true). Kept as an absolute override (off = light, on = dark), not system-following — owner's call.
  5. Two light-mode AA corrections (owner accepted): warmInkMuted #8c8880 → #746f66 (3.21 → 4.54:1 — it carries every 12–13pt metadata line, body text under WCAG) and warmAmber #c48a2a → #a8741c for glyphs on amber-soft (2.55 → 3.45:1). Both imperceptible; the light look is otherwise unchanged.
  6. DESIGN.md frontmatter now lists light and dark for every token; prose updated.

Contrast (computed from the final hex in Color+Warm.swift, WCAG relative luminance)

pair light dark need
ink / bg · surface 15.9 · 17.4 15.9 · 13.5 4.5
ink-soft / bg · surface 7.1 · 7.8 10.2 · 8.6 4.5
ink-muted / bg · surface 4.5 · 5.0 6.3 · 5.3 4.5
green (ink) / bg · surface 5.8 · 6.3 7.7 · 6.5 4.5
on-green / green-fill 6.3 5.0 4.5
on-ink / ink 17.4 15.9 4.5
green-deep / green-soft 8.7 8.4 4.5
amber / amber-soft (glyph) 3.5 6.0 3.0
amber-deep / amber-soft (text) 5.6 7.8 4.5
clay / clay-soft · surface 3.6 · 4.5 4.9 · 5.5 3.0 · 4.5
sky / sky-soft · lilac / lilac-soft 3.6 · 3.8 5.7 · 6.3 3.0
green / track (ring) 5.1 5.0 3.0
green-fill / surface (boundary) 6.3 3.1 3.0

All pass.

Test plan

  • xcodebuild build — BUILD SUCCEEDED
  • xcodebuild test -skip-testing:SavelyUITests — TEST SUCCEEDED
  • swiftlint lint --strict — 0
  • grep -rn "Color(red:\|\.white�\|\.black�" Savely --include=*.swift | grep -v Color+Warm → only the commented deliberate exceptions
  • Dashboard screenshotted in both schemes on iPhone 17 Pro via simctl (dark: warm charcoal ground, hairline-separated surface, light ink, inverted "+" shell; light: unchanged)
  • Remaining screens in dark NOT walked by me — the Claude Code simulator panel is down for this session (no taps possible). Please flip Dark Mode in Profile and page through: Goals + AddGoalFlow (4 steps + Planted), Money (both tabs, keypad sheets), Profile, onboarding. The most likely soft spots: warmGreenSoft chips, the auto-move banner, the Planted screen, goal tiles' white initials on lighter dark accents (decorative; goal name sits next to them).
  • App uninstalled from the simulator afterwards

Risks

  • warmGreen was split into ink vs fill by hand at ~25 sites; a fill mistaken for ink would show as a too-bright button in dark. Reviewed each; worth an eyeball.
  • Deleting the ColorPalette assets and four dead files touches project.pbxproj (plutil -lint OK; build green).
  • Two light values changed on purpose (AA). Nothing else in light should differ.

Checklist

  • feat/ branch, Conventional Commits, no AI attribution
  • No secrets staged
  • DESIGN.md re-documented with the dark values
  • No new user-facing strings (one removed)

There is no account, so the monogram + "User" card on Profile and the
person.fill circle in the Dashboard greeting stood in for an identity the
app deliberately does not have.

- Profile: identity card removed; its one useful line ("Saving since
  <month>" / "Just getting started") is now the lifetime income card's
  subtitle.
- Dashboard: avatar circle removed from the greeting.
- Display-name plumbing deleted since nothing reaches it anymore:
  EditProfileSheet, ProfileViewModel.displayName /
  updatePersonalInformation(), AppViewModel.currentUserName /
  updateDisplayName(), the "displayName" UserDefaults key, and the
  orphaned InnerHeightPreferenceKey. Strings.Profile.editNameHint and its
  catalog entry go with them.
Every Color.warm* token now resolves per trait collection through a
UIColor dynamic provider, so call sites never branch on colorScheme.
Light values are unchanged; dark values are the same hues at low chroma
("the notebook under a lamp"), verified against WCAG AA in
docs/plans/pr-d2-adaptive-palette.md.

New role tokens for what call sites used to hardcode: warmGreenFill (green
as a surface, stays deep in dark so white content keeps 5:1), warmOnGreen,
warmOnInk (ink flips light in dark, so its content flips to the ground),
warmTrack, warmShadow, warmLilac/warmLilacSoft. No call sites change in
this commit.
The sweep that makes the adaptive palette actually reach the screen. Of
the 97 hardcoded color sites outside Color+Warm.swift, every one now
resolves through a token; the handful that stay literal are deliberate
and say so in a comment (the splash frame, the sprout brand mark, white
controls drawn over the live camera feed, and the PDF, which is paper).

Semantics, not search-and-replace:
- Green *surfaces* (CTAs, the income card, the Planted screen, the
  selected radio and calendar day) use warmGreenFill; green *as ink*
  (text, glyphs, rings, bars, toggles, page dots) keeps warmGreen.
- White content on those surfaces is warmOnGreen; white on an ink fill
  (the "+" shell, the scan banner, the Goals "+" buttons) is warmOnInk,
  which flips to the ground in dark because ink flips light.
- The goal ring track is warmTrack; the "New goal" quick action is
  warmLilac/warmLilacSoft; the amber banner copy is warmAmberDeep.
- GoalColor: each of the twelve goal accents and their tracks now carries
  a dark variant (accents a step lighter, ≥3:1 against the dark surface;
  tracks a deep tint of the same hue). Light values unchanged.
- Shadows: the hero-card lifts and the "+" shell keep theirs through
  warmShadow / warmShellShadow; the rest were legacy and are gone.
- Legacy: the ColorPalette asset catalog (primaryGreen, cardBackground…)
  is deleted, along with the four unreferenced files that still pointed at
  it or at nothing (NotificationTimePicker, InsightRow, QuickActionButton,
  PrimaryButton — dead since the auth removal / PR A) and the unused
  UIConstants.UIShadow. NetworkErrorView, ReportsView and the camera
  confirmation card are moved onto the tokens rather than deleted.
The palette now resolves per appearance, so the switch does what it
says. Kept as an absolute override (off = light, on = dark) rather than
following the system when off — owner's call.
warmInkMuted #8c8880 -> #746f66: 3.21:1 -> 4.54:1 on the ground and
3.53 -> 4.99 on paper. It carries every 12-13pt metadata line, which is
body text under WCAG, and it failed AA. Same warm-ink family, one step
darker.

warmAmber #c48a2a -> #a8741c: 2.55:1 -> 3.45:1 as a glyph on
amber-soft, clearing 1.4.11 for graphical objects. Dark values untouched.
Owner accepted both (2026-08-16).
…67c56

Frontmatter now lists every token's light and dark value (light stays
normative for the look). Prose updated for the ink-muted and amber AA
corrections, the green ink/fill split, and the on-green/on-ink roles.

warmGreenFill dark #357a55 -> #367c56: its ratio against the dark
surface rounded to 3.00 but sat at 2.9995; the nudge clears 3:1
(3.08) with white content still at 5.03:1.
…ances

On iOS 26+ the Home Screen takes its appearance variants from an Icon
Composer document, not from the PNGs in a legacy .appiconset. Savely
shipped dark/tinted PNGs that the system no longer reads, so the icon
stayed light whatever the Home Screen's icon appearance was.

- Savely/AppIcon.icon: the sprout as three SVG layers (light, dark, mono)
  drawn from the same 120-unit geometry as SproutMark.swift, on a warm
  cream gradient in light and the green-ink #27382d in dark; the sprout
  is opaque (the mark is flat) with a soft neutral shadow. Validated by
  rendering Default, Dark and TintedDark with Icon Composer's ictool; the
  built Assets.car carries an IconImageStack per appearance.
- The legacy AppIcon.appiconset is removed (it would collide on the
  "AppIcon" name, and the deployment floor is iOS 26).
- SproutMark's shaded leaf becomes trait-aware (#37795a light / #5d9c7c
  dark) so the in-app mark reads on the dark ground too.
@Ivan-LB

Ivan-LB commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

Added: Icon Composer app icon (6303711)

What you saw: simulator in dark, Savely's Home Screen icon still light.

What I found: the dark/tinted PNGs in AppIcon.appiconset were compiled into the bundle (assetutil showed UIAppearanceDark), but on iOS 26+/27 the Home Screen takes appearance variants from an Icon Composer .icon document, not from a legacy appiconset. So the app never had a dark icon the system would use.

Fix: Savely/AppIcon.icon — the sprout as three SVG layers (light / dark / mono) generated from the exact SproutMark.swift geometry (120-unit design space, stem 5.5 round-cap, seed r=9 at (60,92)); light rides the warm cream gradient, dark uses green-ink #27382d; opaque sprout (flat mark), soft neutral shadow. Validated by rendering Default / Dark / TintedDark with Icon Composer's own ictool; the built Assets.car now carries an IconImageStack per appearance. Legacy appiconset removed (name collision on "AppIcon"; floor is iOS 26). SproutMark's shaded leaf is trait-aware now too (#37795a / #5d9c7c), so the in-app mark reads on the dark ground.

One thing to check on your side: iOS only shows dark app icons when the Home Screen icon appearance is Dark or Automatic (long-press Home → Edit → Customize). On my simulator that setting was Light — Files also rendered white after a full reboot in dark mode — so a light Savely icon there is expected. I couldn't flip that setting without taps. If yours is on Automatic and Savely still renders light after installing this build, tell me and I'll dig further.

Renders from ictool (Default / Dark / TintedDark) are what I checked against; the SVGs are in the PR if you want to open the .icon in Icon Composer.

@Ivan-LB
Ivan-LB merged commit 25e72ba into dev Aug 16, 2026
1 check passed
@Ivan-LB
Ivan-LB deleted the feat/adaptive-palette branch August 16, 2026 05:48
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