diff --git a/LIGHT_MODE_COLOR_PARITY.md b/LIGHT_MODE_COLOR_PARITY.md new file mode 100644 index 00000000..a4b6b198 --- /dev/null +++ b/LIGHT_MODE_COLOR_PARITY.md @@ -0,0 +1,175 @@ +# Light-Mode Color Parity + +Companion to `WALLET_PARITY.md`. This doc captures the light-mode color-palette +decisions landed on Android in `fix/light-mode-and-icon` so the iOS agent can +mirror them and keep the two apps visually identical. + +iOS is the long-running reference, but light-mode polish was an Android-first +pass — the Android code is the source of truth for the contract below until +iOS lands matching changes. + +--- + +## 1. Why two adjustments instead of one + +iOS and Android both ship the same per-theme `primary` / `zapColor` / +`repostColor` / etc. swatches. Two specific problems showed up only in light +mode and have separate fixes: + +1. **Dark-mode primary reads as too bright on near-white surfaces.** The + default Spark orange `#FF9800` doesn't have enough contrast against the + light-mode background, so buttons look washed out. **Fix:** light mode + uses a deeper, hand-tuned `customLightPrimary` (default `#D9730D`). +2. **The plain zap color reads as muddy when used for the celebratory + in-flight bolt animation.** The HSL lightness drops too low after the + primary darkening. **Fix:** the animation uses a separate + `zapAnimationColor` derived from `zapColor` with floored lightness + (`vividZapColor()`). + +Everywhere *other* than the in-flight animation, the zap color and the +Material primary should be the **same** light-mode hue (`#D9730D` for the +default theme), so the post-zap icon, count text, top zap indicators, wallet +accents, and buttons all read as one consistent orange. + +--- + +## 2. The contract (locked values) + +### 2.1 `primary` — the throughout-the-theme accent + +| Theme | Dark | Light | +| --- | --- | --- | +| default (`custom`) | `#FF9800` | **`#D9730D`** | +| user-picked custom accent | user value (`accentColor`) | `darkenColor(accentColor, fraction = 0.18f)` | + +`darkenColor` is `hsl[2] *= (1 - fraction)` then `HSLToColor(hsl)`. Floor +caps optional (Android doesn't currently floor here; iOS can match). + +### 2.2 `zapColor` — every zap surface *except* the in-flight animation + +**Matches `primary` exactly in light mode.** Used by: post-zap icon tint +when `hasUserZapped`, post-zap count text, reaction count text after +react, top zap indicators in `NotificationsScreen`, wallet accent +(`WalletScreen`), Lightning QR icon, `PostCard` zap-bar tint, etc. + +| Theme | Dark | Light | +| --- | --- | --- | +| default (`custom`) | `#FF9800` | **`#D9730D`** (same as `primary.light`) | +| user-picked custom accent | user value | `customLightPrimary` (same as `primary.light`) | + +### 2.3 `zapAnimationColor` — *only* the celebratory in-flight bolt + +Derived from `zapColor` via: + +``` +val hsl = colorToHSL(zapColor) +hsl[1] = (hsl[1] * 1.15f).coerceIn(0f, 1f) // bump saturation 15% +hsl[2] = hsl[2].coerceAtLeast(0.5f) // floor lightness at 0.5 +return HSLToColor(hsl) +``` + +Used only by `LightningAnimation` (the three-shadow halo on the post action +bar's bolt during a zap in flight). Everywhere else uses `zapColor`. + +### 2.4 Default theme other tokens (light) + +| Token | Light value | Notes | +| --- | --- | --- | +| `repostColor` | `#2E7D32` | Dark green for contrast | +| `bookmarkColor` | `#D9730D` | Tracks `primary.light` | +| `paidColor` | `#C9A000` | Slightly muted gold | +| `secondary` | `#FFB74D` | Lighter accent | +| `background` | `#D8D8D8` | | +| `surface` | `#E8E8E8` | | +| `surfaceVariant` | `#CDCDCD` | | +| `onBackground` / `onSurface` | `#1C1B1F` | | +| `onSurfaceVariant` | `#333333` | | +| `outline` | `#999999` | | + +--- + +## 3. Android implementation references + +| Concern | File | Symbol | +| --- | --- | --- | +| `primary` darkening for custom accents | `app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt` | `customLightPrimary` | +| `darkenColor()` / `lightenColor()` helpers | `app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt` | `darkenColor`, `lightenColor` | +| `vividZapColor()` for the animation | `app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt` | `vividZapColor` | +| `zapAnimationColor` accessor | `app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt` | `WispThemeColors.zapAnimationColor` | +| `zapColor` aligned to `customLightPrimary` | `app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt` | `WispColors` custom-light branch | +| Default theme light swatches | `app/src/main/kotlin/com/wisp/app/ui/theme/Themes.kt` | `Themes.themes[0].light` | +| In-flight bolt consumes `zapAnimationColor` | `app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt` | `LightningAnimation` | +| In-flight bolt overlay also uses it | `app/src/main/kotlin/com/wisp/app/ui/component/LightningOverlay.kt` | top-level `zapColor` lookup | + +Search hint on Android: `git grep -n "WispThemeColors.zap"` lists every +consumer. Every site uses `zapColor` *except* the two animation files +which use `zapAnimationColor`. + +--- + +## 4. iOS port checklist + +Mirror the contract above. Specific items the iOS agent should action: + +- [ ] Default-theme `light.primary` is `#D9730D` (was `#CC7000` on the + original light branch — bump it to match). +- [ ] Custom-accent light variant darkens by 18% in HSL space + (`darkenColor(accent, 0.18)` equivalent). +- [ ] Add a `zapAnimationColor` computed property derived from `zapColor` + with `saturation *= 1.15` (cap 1.0) and `lightness ≥ 0.5`. +- [ ] In-flight bolt animations (the white-core glow pulse, the trailing + shadow halos) consume `zapAnimationColor`. +- [ ] **Every other zap-tinted UI element consumes plain `zapColor`** — + post-zap icon, post-zap count, reaction count after react, top zap + indicators, wallet zap accents, etc. Don't accidentally use + `zapAnimationColor` anywhere except the celebratory animation. +- [ ] Light-mode `zapColor` and `bookmarkColor` track `primary.light` for + both the default theme and custom-accent themes (so the throughout- + the-theme orange is one consistent hue). +- [ ] Non-default themes (Nord, Dracula, Gruvbox, etc.) keep their own + curated `light.zapColor` — those weren't part of the mismatch and + are intentionally different from `primary` for hue contrast. + +### 4.1 Quick visual test + +In light mode, with the default theme: + +1. Open a post you've already zapped. The bolt icon + count should be + the same orange as the **Save** / **Connect** / **Confirm** buttons + elsewhere in the app. +2. Open the Notifications tab and find a zap row. The bolt + sat count + should be that same orange. +3. Open Wallet → main dashboard. The Send/Receive button rings should be + the same orange. +4. Trigger a fresh zap. The in-flight bolt animation should pulse a + **noticeably brighter** orange (the `zapAnimationColor` vivid variant + — never muddy or dark). + +If any of those 4 surfaces reads as a different shade, the contract is +violated — go back to §2 and audit which property the site is reading. + +--- + +## 5. App icon + +Separate change shipped in the same PR but unrelated to the color contract. +Android replaced the Android-Studio default launcher icon with the iOS +artwork (orange wisp on black square) as an adaptive icon. iOS already +has this artwork — no action needed on the iOS side. + +Reference: `app/src/main/res/drawable/ic_launcher_foreground.xml` +(vector), `app/src/main/res/drawable/ic_launcher_background.xml` +(solid black). + +--- + +## 6. Locked decisions + +| Decision | Choice | Why | +| --- | --- | --- | +| Light-mode `primary` for default theme | **`#D9730D`** (deeper than dark `#FF9800`) | Better contrast on near-white surfaces. | +| Light-mode custom-accent derivation | `darkenColor(accent, 0.18)` | Generic darken for any user-picked color. | +| `zapColor` vs `primary` in light mode | **Same value** | Throughout-the-theme orange should be one hue. | +| `zapAnimationColor` is separate | **Yes — vivid variant of `zapColor`** | Plain zap color reads muddy at animation time on light surfaces. | +| Scope of `zapAnimationColor` | **Animation only** | Static UI elements stay on `zapColor` to match `primary`. | +| Non-default themes | Their own curated palettes | Each preset has hue-contrast reasoning; don't blanket-align across all themes. | diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt b/app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt index 22a68f35..eef5a7b6 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/ActionBar.kt @@ -296,7 +296,7 @@ internal fun LightningAnimation(modifier: Modifier = Modifier) { label = "scale" ) - val zapColor = WispThemeColors.zapColor + val zapColor = WispThemeColors.zapAnimationColor Canvas(modifier = modifier) { val w = size.width diff --git a/app/src/main/kotlin/com/wisp/app/ui/component/LightningOverlay.kt b/app/src/main/kotlin/com/wisp/app/ui/component/LightningOverlay.kt index 4ce6d6ad..4e98e957 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/component/LightningOverlay.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/component/LightningOverlay.kt @@ -51,7 +51,7 @@ fun ZapBurstEffect( var sparks by remember { mutableStateOf>(emptyList()) } val progress = remember { Animatable(0f) } - val zapColor = WispThemeColors.zapColor + val zapColor = WispThemeColors.zapAnimationColor if (!isActive && progress.value <= 0f) return diff --git a/app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt b/app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt index 2ce58a40..e2ee349c 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/theme/Theme.kt @@ -37,6 +37,7 @@ data class WispColors( object WispThemeColors { val backgroundColor: Color @Composable get() = LocalWispColors.current.backgroundColor val zapColor: Color @Composable get() = LocalWispColors.current.zapColor + val zapAnimationColor: Color @Composable get() = vividZapColor(LocalWispColors.current.zapColor) val repostColor: Color @Composable get() = LocalWispColors.current.repostColor val bookmarkColor: Color @Composable get() = LocalWispColors.current.bookmarkColor val paidColor: Color @Composable get() = LocalWispColors.current.paidColor @@ -82,6 +83,19 @@ private fun darkenColor(color: Color, fraction: Float = 0.6f): Color { return Color(ColorUtils.HSLToColor(hsl)) } +/** + * Vivid variant of a zap color for the celebratory bolt animations. Floors + * lightness so the burst never reads muddy or dark on light backgrounds, + * while leaving already-bright dark-mode colors untouched. + */ +private fun vividZapColor(color: Color): Color { + val hsl = FloatArray(3) + ColorUtils.colorToHSL(color.toArgb(), hsl) + hsl[1] = (hsl[1] * 1.15f).coerceIn(0f, 1f) + hsl[2] = hsl[2].coerceAtLeast(0.5f) + return Color(ColorUtils.HSLToColor(hsl)) +} + @Composable fun WispTheme( isDarkTheme: Boolean = true, @@ -98,6 +112,14 @@ fun WispTheme( val primaryContainerDark = remember(primary) { darkenColor(primary, 0.6f) } val primaryContainerLight = remember(primary) { lightenColor(primary, 0.7f) } + // Light mode needs a deeper primary than the bright dark-mode accent so + // buttons keep enough contrast against the near-white surfaces. The default + // accent maps to a curated value shared with iOS; custom accents are darkened. + val customLightPrimary = remember(accentColor, themePreset) { + if (accentColor == Color(0xFFFF9800)) themePreset.light.primary + else darkenColor(accentColor, 0.18f) + } + val colorScheme = if (isDarkTheme) { if (isCustomTheme) { darkColorScheme( @@ -135,7 +157,7 @@ fun WispTheme( } else { if (isCustomTheme) { lightColorScheme( - primary = accentColor, + primary = customLightPrimary, onPrimary = Color.White, primaryContainer = primaryContainerLight, onPrimaryContainer = darkenColor(accentColor, 0.4f), @@ -189,11 +211,17 @@ fun WispTheme( } } else { if (isCustomTheme) { + // zap + bookmark colors track `customLightPrimary` so the + // zap icon, post-success count, and bookmark glyph all + // render the same orange the rest of the light-mode theme + // uses for buttons + accents. Previously these were pinned + // to a darker `#B85C00`, which read as a noticeably + // different shade against `#D9730D` buttons. WispColors( backgroundColor = Color(0xFFECECEC), - zapColor = Color(0xFFB85C00), + zapColor = customLightPrimary, repostColor = Color(0xFF2E7D32), - bookmarkColor = Color(0xFFB85C00), + bookmarkColor = customLightPrimary, paidColor = Color(0xFFC9A000) ) } else { diff --git a/app/src/main/kotlin/com/wisp/app/ui/theme/Themes.kt b/app/src/main/kotlin/com/wisp/app/ui/theme/Themes.kt index 900d5de2..8b50973a 100644 --- a/app/src/main/kotlin/com/wisp/app/ui/theme/Themes.kt +++ b/app/src/main/kotlin/com/wisp/app/ui/theme/Themes.kt @@ -23,7 +23,7 @@ object Themes { paidColor = Color(0xFFFFD54F) ), light = ThemeColors( - primary = Color(0xFFCC7000), + primary = Color(0xFFD9730D), secondary = Color(0xFFFFB74D), background = Color(0xFFD8D8D8), surface = Color(0xFFE8E8E8), @@ -32,9 +32,13 @@ object Themes { onSurface = Color(0xFF1C1B1F), onSurfaceVariant = Color(0xFF333333), outline = Color(0xFF999999), - zapColor = Color(0xFFB85C00), + // zap + bookmark match `primary` so the throughout-the- + // theme orange is consistent — final zap icon, post + // count text, and the top zap indicator all land on + // the same shade as buttons. + zapColor = Color(0xFFD9730D), repostColor = Color(0xFF2E7D32), - bookmarkColor = Color(0xFFB85C00), + bookmarkColor = Color(0xFFD9730D), paidColor = Color(0xFFC9A000) ) ), diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index ca3826a4..fe87a50e 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,74 +1,17 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..0183e7c7 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index c4a603d4..d378acd7 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index c4a603d4..bbd3e021 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 00000000..5af92fe0 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index 960cf907..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp deleted file mode 100644 index 9ab75f3f..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100755 index 00000000..5af92fe0 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index d60d4db3..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 00000000..fa981a35 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index a6fb326c..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp deleted file mode 100644 index 0560276b..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100755 index 00000000..fa981a35 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index df116daa..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 00000000..eb4415b1 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 026c261b..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 1b91cc19..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100755 index 00000000..eb4415b1 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1c7b61e1..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 00000000..20bd835d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 0d9d8b48..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 6ae28e01..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100755 index 00000000..20bd835d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index a77ac035..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 00000000..fcc6615b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index 7d1ba708..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp deleted file mode 100644 index 1aa2f81f..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100755 index 00000000..fcc6615b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 2d79ee18..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 1c58b8a4..47e2c1c3 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -8,7 +8,7 @@