Skip to content

feat(theme): iOS-parity dark-mode polish for Android#565

Merged
barrydeen merged 6 commits into
barrydeen:mainfrom
dmnyc:feat/android-dark-mode-backgrounds
May 25, 2026
Merged

feat(theme): iOS-parity dark-mode polish for Android#565
barrydeen merged 6 commits into
barrydeen:mainfrom
dmnyc:feat/android-dark-mode-backgrounds

Conversation

@dmnyc
Copy link
Copy Markdown
Contributor

@dmnyc dmnyc commented May 23, 2026

Summary

iOS-parity refinements layered on top of the previous d6d30ea "darken default dark-mode backgrounds" commit. Together they bring the Android dark-mode chrome, post separators, action bar metrics, and destructive-action red in line with the iOS design.

What's in

  1. Chrome color sweep (ee56c04) — Every screen's TopAppBar containerColor switches from colorScheme.surface (a step lighter than the body) to colorScheme.background. iOS uses one near-black across body + chrome and reserves the lighter surface tier for elevated controls (pills, cards); this makes Android match. 30 screens touched.

  2. Compact chrome heights + iOS-style bottom nav (3fc9d02, refined in 5db13e5) —

    • FeedScreen.CenterAlignedTopAppBar content area clamps to 48dp (was Material's 64dp).
    • BottomBar.NavigationBar content area clamps to 56dp (was Material's 80dp — most of which is wasted on a label slot we don't render).
    • Active-tab indicator pill is suppressed (indicatorColor = Color.Transparent); the orange tint on the selected icon is enough signal and matches iOS.
    • Notification dot uses iOS systemRed (#FF3B30) instead of the app's primary accent so it reads as "alert" rather than "branded highlight."
    • "All content types" filter icon switches from Icons.Outlined.Dashboard (1 large + 3 small panels) to Icons.Outlined.GridView (2×2 of equal squares) to match the iOS toolbar icon.
    • Height clamps use Modifier.windowInsetsPadding(insets).height(content) with windowInsets = WindowInsets(0) on the bar — layout-time inset resolution, no first-frame snap that an asPaddingValues().calculate*Padding() read at composition time would cause.
  3. Full-width post divider + hide zero-value counters (f1177fe) —

    • PostCard wraps content + the inter-post HorizontalDivider in an outer Column. Content keeps its 16dp horizontal padding; the divider sits outside it and runs edge-to-edge, matching iOS.
    • ActionBar gates each of the four counters (replyCount, likeCount, repostCount, zapSats) on > 0. No more "0" beside icons; counts reappear as soon as engagement crosses 1.
  4. iOS-red error color + darken profile tab strip (d7bae63) —

    • WispTheme sets error = #FF3B30 (and onError = white) explicitly on every color-scheme variant (custom dark/light + preset dark/light). Replaces Material 3's pinkish dark-mode default / brick-red light-mode default with the iOS systemRed already used by Disconnect Wallet, Switch, and the bell badge.
    • UserProfileScreen sticky-header tab strip + sort-pill row use background (#0A0A0B) instead of surface (#1C1C1E) so the chrome reads as part of the body and doesn't stack two grey tiers.

Test plan

  • Cold-start the app — neither the top app bar nor the bottom nav snaps to a new height after the first frame.
  • Bottom nav: active tab is just an orange icon (no rounded pill behind it).
  • Bell tab unread dot is iOS red, not orange.
  • Home feed top-left filter icon (next to avatar) is a 2×2 grid of equal squares.
  • A post with no engagement shows icons alone (no 0s); add one reply / like / zap, the corresponding count appears.
  • Post separators run from screen edge to screen edge.
  • Logout text in the drawer is iOS red (not pink).
  • Profile screen: tab strip + sort pill row sit on the same near-black as the body.

🤖 Generated with Claude Code

@dmnyc dmnyc force-pushed the feat/android-dark-mode-backgrounds branch from 5db13e5 to 7968da3 Compare May 23, 2026 20:45
dmnyc added 6 commits May 23, 2026 16:50
The default ("custom") Android dark theme rendered noticeably lighter
than iOS, which uses near-black backgrounds. Align with iOS HIG dark
system colors (slight off-black for the base, iOS secondary/tertiary
greys for elevated surfaces) so the two platforms feel like the same
app in dark mode.

- background: #131215 → #0A0A0B  (slight off-black, OLED-friendly
  without the harsh #000 step on LCD)
- surface:    #1F1E21 → #1C1C1E  (iOS secondarySystemBackground)
- surfaceVariant: #2B2A2E → #2C2C2E  (iOS tertiarySystemBackground)
- outline:    #343338 → #38383A  (iOS separator on dark)

Named presets (Nord, Dracula, Gruvbox, …) are left untouched — their
distinctive backgrounds are part of each preset's identity.
Default `TopAppBarDefaults.topAppBarColors` uses `MaterialTheme.color
Scheme.surface`, which sat noticeably lighter than the body after the
preceding dark-mode background darken. iOS uses one near-black across
body + chrome and reserves the lighter "surface" tone for elevated
controls (pills, cards). Switch every screen's TopAppBar container
to `background` so chrome reads as part of the page, not as a raised
layer above it.

30 screens touched; only `containerColor` lines inside
`TopAppBarDefaults.topAppBarColors(...)` blocks are changed, so other
surface usages (cards, dialogs, sheets, the elevated pills the home
top bar overlays) keep their existing tone.
iOS-style cleanup on the home screen's top + bottom chrome:

- `FeedScreen` `CenterAlignedTopAppBar` clamps to 48dp content +
  status-bar inset (was Material's default ~64dp + inset). Drops the
  gap below the icon row that pushed the feed down.
- `BottomBar` `NavigationBar` clamps to 56dp content + gesture inset
  (was Material's default 80dp). The 80dp slot reserves space for the
  label text we don't render — pure waste on small phones.
- Tab indicator pill is suppressed (`indicatorColor = Color.Transparent`).
  The selected-icon orange tint is enough signal; matches iOS where
  the tab bar has no rounded background on the active tab.
- Notification dot uses iOS systemRed (#FF3B30) instead of the app's
  primary accent so it reads as "alert" rather than "branded highlight"
  — same red iOS shows on the bell.
- Filter icon for "All" content types switches from
  `Icons.Outlined.Dashboard` (1 large + 3 small panels) to
  `Icons.Outlined.GridView` (2x2 of equal squares) to match the iOS
  toolbar icon.
Two post-card refinements that move the feed toward the iOS look:

- `PostCard` now wraps content + the inter-post `HorizontalDivider`
  in an outer Column. The content Column keeps its 16dp horizontal
  padding (so post body / action bar / metadata stay inset), but the
  divider sits outside that padding and runs edge-to-edge. Matches
  iOS where the separator spans the full viewport width.
- `ActionBar` gates each of the four counters (`replyCount`,
  `likeCount`, `repostCount`, `zapSats`) on `> 0`. Empty engagement
  no longer shows "0" beside the icon — matches iOS where the
  count text only appears when there's something to show. As soon
  as the count crosses 1, the number reappears.
Two more iOS-parity tweaks:

- `WispTheme` sets `error = #FF3B30` (and `onError = white`) explicitly
  on every color-scheme variant (custom dark/light + preset dark/light).
  Material 3's defaults for `error` render pinkish in dark mode and a
  muted brick red in light mode — neither matches the iOS systemRed used
  by the rest of the destructive UI in this app. With this override,
  every `MaterialTheme.colorScheme.error` consumer (logout button,
  destructive labels, error text) now matches the iOS counterpart and
  the existing #FF3B30 used directly on Disconnect/Switch wallet flows.
- `UserProfileScreen` sticky-header tab strip + the sort-pill row below
  it use `background` (#0A0A0B) instead of `surface` (#1C1C1E). The two
  grey tiers stacked above each other read as visually noisy on the
  profile; the iOS profile uses one near-black across both. Body posts
  below still render with the elevated tier where they need to.
Previous attempt computed total chrome height as
`Modifier.height(content + insetReadViaPaddingValues)`. The inset is
read at composition time and arrives as 0 on the very first frame
before the system-bar inset connection delivers its value — the bar
laid out at the shorter (no-inset) height, then re-measured once the
inset arrived. Visible as a one-frame snap on app cold start.

Switch to a layout-time pattern that subscribes to inset changes
correctly:

    Modifier
        .windowInsetsPadding(insets)   // reserves the inset via padding
        .height(contentHeight)         // content area only

`windowInsetsPadding` is a Modifier.Node that re-layouts (not re-
composes) on inset arrival, so the bar measures at the right total
height on the first frame. The bar's own `windowInsets` is set to
`WindowInsets(0)` so it doesn't double-pad.

Applied to:
- `BottomBar` NavigationBar — content height 56dp + navigation-bars inset
- `FeedScreen` CenterAlignedTopAppBar — content height 48dp + status-bars
  inset
@dmnyc dmnyc force-pushed the feat/android-dark-mode-backgrounds branch from 7968da3 to 6f97c42 Compare May 23, 2026 20:53
@barrydeen barrydeen merged commit bdef0b2 into barrydeen:main May 25, 2026
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