Skip to content

Migrate to Material 3 (full color roles + dynamic color) + fix FAB#56

Merged
hawkff merged 6 commits into
mainfrom
feature/material3-migration
Jun 21, 2026
Merged

Migrate to Material 3 (full color roles + dynamic color) + fix FAB#56
hawkff merged 6 commits into
mainfrom
feature/material3-migration

Conversation

@hawkff

@hawkff hawkff commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Update — explicit M3 color roles for the hand-picked themes

Follow-up commit completing the color-role layer so M3 widgets resolve real roles instead of the library's generated grayscale fallback, while keeping the hand-picked themes' look.

  • Added the full M3 role set to the base theme (values, values-v26, values-night): colorPrimaryContainer/onPrimaryContainer, colorSecondary(+Container), colorTertiary(+Container), colorSurfaceVariant/onSurfaceVariant, colorOutline/outlineVariant. Each role derives from values the theme already owns, so the standard themes are visually stable.
  • Removed the remaining Material 2 widget styles: Chip.ChoiceWidget.Material3.Chip.Suggestion, MaterialAlertDialog overlay → ThemeOverlay.Material3.MaterialAlertDialog, dialog TextButtons → Widget.Material3.Button.TextButton.Dialog(.Flush), ActionBar.Solid → M3, plus the Chip.Filter/borderless-button/popup overlays in 6 layouts. No MaterialComponents/AppCompat widget styles remain.
  • Explicit role overrides for the special themes: Dracula (dark-canvas containers/surfaceVariant/outline) and Black (near-black containers, no gray). Black-on-light accents (Yellow/Lime/Amber) keep correct contrast via the base defaults.
  • New colors: m3_outline (light + night) and Dracula/Black container/surface-variant/outline swatches.

Verification

Throwaway debug build, installed on Android. Cycled the standard themes plus Dracula, Black, Yellow/Lime/Amber, and Dynamic in light and dark, covering the profile list, nav drawer, settings, switches, cards, FAB, and the theme-picker dialog. No regressions beyond the intended slight M3 container/outline tones on selected cards. CodeRabbit CLI run: the 4 findings (1 critical, 3 major) were all addressed (added the missing colorSurfaceVariant to all three base themes; confirmed the night m3_outline value is the correct colorOutline role).


Summary

Full Material 3 migration: theme stack + complete M3 color, plus a FAB visibility/touch fix.

Theme engine (M3)

  • All theme parents Theme.MaterialComponents.* -> Theme.Material3.* (base, dialog, Theme.Start, v26) and widget/overlay parents (CardView.Filled, Button, TextInputLayout, PopupMenu, BottomSheet, ShapeAppearance). Custom style names unchanged.
  • Material 1.12.0 keeps M2 attrs valid, so the ~21 color themes + Dracula keep working.

M3 color, to the bone

  • ThemedActivity applies Material 3 dynamic color. New Dynamic (Material You) theme option (id 24) seeds the palette from the wallpaper on Android 12+; every other theme seeds a content-based M3 palette from its own colorPrimary, so all M3 roles (primaryContainer / surfaceVariant / outline / tertiary…) are generated correctly on all API levels — no hand-authored per-theme palettes.
  • Added the Dynamic theme to Theme.kt (activity + dialog mappings) and a picker swatch.

Latent crash fixed (surfaced by M3)

M3 wraps a View's Context in a ContextThemeWrapper, breaking context as MainActivity / as LifecycleOwner casts (StatsBar, ServiceButton, Dialogs). Added Context.unwrap<T>() (walks the ContextWrapper chain).

FAB fix (invisible / untappable airplane when connected)

M3's BottomAppBar dropped the FAB cradle cutout, so the airplane (declared before the StatsBar) was drawn under + touch-intercepted by the bar when connected — you had to swipe the panel to reach it. Reordered the FAB to draw after the bar, and tinted it colorTertiaryContainer/onTertiaryContainer so it contrasts the colorPrimary bar.

Testing

  • CodeRabbit CLI: No findings. lintVitalOssRelease passes.
  • Verified on-device: launch / Settings / profile Edit + dialogs / QR generation / remove-confirm alert all render with no inflation or cast crashes; the airplane FAB is now visible and disconnects on tap while connected without swiping; connect/disconnect/reconnect round-trips cleanly; 0 crashes.

Greptile Summary

This PR migrates the entire theme stack from Material Components (M2) to Material 3, adds a new "Dynamic (Material You)" theme option backed by DynamicColors, and fixes a FAB visibility/touch bug caused by M3's removal of the BottomAppBar cradle cutout. It also patches latent ClassCastException crashes in StatsBar, ServiceButton, and Dialogs that M3's ContextThemeWrapper wrapping surfaced.

  • M3 theme migration: All parent styles updated (Theme.MaterialComponents.*Theme.Material3.*), per-theme M3 color roles (colorPrimaryContainer, colorTertiaryContainer, colorSurfaceVariant, colorOutline, etc.) hand-authored for every palette to avoid the library's gray-toned fallbacks; Dracula and Black get explicit night/day overrides.
  • Dynamic theme: ThemedActivity calls DynamicColors.applyToActivityIfAvailable before super.onCreate when theme 24 is selected; FAB recolored with ?attr/fabColorBackground / ?attr/fabStoppedColor so each theme controls its own background/tint without layout hacks.
  • FAB fix: FAB and its progress indicator moved after StatsBar in the layout to win z-order; Widget.SagerNet.BottomAppBar restores the M3 cradle cutout so the airplane icon stays visible and tappable while connected.

Confidence Score: 5/5

The change is safe to merge; the M3 migration is thorough, the FAB z-order fix is correct, and the ClassCastException patches are sound.

All three previously-flagged concerns have been addressed in the current revision: the Black theme's FAB background was changed from the gray accent (#9E9E9E) to the near-black primary (#2B2B2B), eliminating the contrast issue; applyStateTint uses colorOnPrimary which is the correct on-color for the ?colorPrimary FAB background used by all standard themes; and the double unwrap() call in iconConnecting was replaced with a single captured owner variable. The one remaining observation — that dialog-style activities are excluded from DynamicColors when the Dynamic theme is selected — is a UX inconsistency rather than a crash or data-loss path.

No files require special attention. ThemedActivity.kt is worth a second look if dialog-style activities are ever added, since they would silently show the static base theme when the Dynamic option is chosen.

Important Files Changed

Filename Overview
app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt Adds Context.unwrap<T>() extension that walks the ContextWrapper chain; clean implementation, correctly uses error() for impossible unwrap case.
app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt Adds DynamicColors integration for the new DYNAMIC theme; the !isDialog guard means dialog-style activities won't receive Material You colors, creating a theme inconsistency for that option.
app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt Fixes ClassCastException by replacing direct context as LifecycleOwner casts with context.unwrap<LifecycleOwner>(); applyStateTint correctly uses colorOnPrimary (the on-color for fabColorBackground=?colorPrimary).
app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt Replaces two direct context as MainActivity casts with context.unwrap<MainActivity>(); straightforward crash fix.
app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt Adds DYNAMIC = 24 constant and maps it to Theme.SagerNet / Theme.SagerNet_Dialog; straightforward addition.
app/src/main/res/layout/layout_main.xml Reorders FAB + progress indicator after StatsBar to fix z-order/touch interception; adds fabAnchorMode=cradle and changes backgroundTint to ?attr/fabColorBackground.
app/src/main/res/values/themes.xml Full M3 migration: parent themes, widget styles, and per-theme M3 role mappings. Black theme fabColorBackground changed from gray accent to near-black primary (fixes FAB contrast). Extensive but logically consistent.
app/src/main/res/values-night/themes.xml Adds M3 container/variant role overrides for Dracula in both activity and dialog themes for correct dark canvas rendering.
app/src/main/res/values-v26/themes.xml API 26+ variant migrated to Theme.Material3.DayNight.NoActionBar; M3 color roles added matching values/themes.xml.
app/src/main/res/values/colors.xml Adds M3 container/surface/outline color tokens for Dracula and Black themes; adds neutral Dynamic theme swatch and m3_outline base color.
app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt Single-line fix: replaces context as Activity cast with context.unwrap<Activity>() in tryToShow().

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ThemedActivity.onCreate] --> B{isDialog?}
    B -- No --> C[Theme.apply: setTheme activity style]
    B -- Yes --> D[Theme.applyDialog: setTheme dialog style]
    C --> E[Theme.applyNightTheme]
    D --> E
    E --> F{isDialog?}
    F -- No --> G{appTheme == DYNAMIC?}
    F -- Yes --> H[skip DynamicColors]
    G -- Yes --> I[DynamicColors.applyToActivityIfAvailable]
    G -- No --> J[keep hand-picked theme palette]
    I --> K[super.onCreate]
    J --> K
    H --> K
    K --> L[setContentView / inflate layout]
    L --> M[FAB background = fabColorBackground]
    M --> N{state change}
    N -- Connected --> O[applyStateTint: statusConnectedColor]
    N -- Stopped --> P[applyStateTint: fabStoppedColor]
    N -- Connecting/Stopping --> Q[applyStateTint: colorOnPrimary]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[ThemedActivity.onCreate] --> B{isDialog?}
    B -- No --> C[Theme.apply: setTheme activity style]
    B -- Yes --> D[Theme.applyDialog: setTheme dialog style]
    C --> E[Theme.applyNightTheme]
    D --> E
    E --> F{isDialog?}
    F -- No --> G{appTheme == DYNAMIC?}
    F -- Yes --> H[skip DynamicColors]
    G -- Yes --> I[DynamicColors.applyToActivityIfAvailable]
    G -- No --> J[keep hand-picked theme palette]
    I --> K[super.onCreate]
    J --> K
    H --> K
    K --> L[setContentView / inflate layout]
    L --> M[FAB background = fabColorBackground]
    M --> N{state change}
    N -- Connected --> O[applyStateTint: statusConnectedColor]
    N -- Stopped --> P[applyStateTint: fabStoppedColor]
    N -- Connecting/Stopping --> Q[applyStateTint: colorOnPrimary]
Loading

Comments Outside Diff (2)

  1. app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt, line 143-153 (link)

    P1 Icon tint mismatch with new FAB background color

    The FAB background was changed from ?colorPrimary to ?attr/colorTertiaryContainer in layout_main.xml, and app:tint="?attr/colorOnTertiaryContainer" was added to the XML for the initial render. However, applyStateTint() overrides that tint on every state change: for non-Dracula themes, all states (stopped, connecting, stopping) resolve to colorOnPrimary, which M3 only guarantees to contrast against colorPrimary — not colorTertiaryContainer. In a content-based generated palette, colorTertiaryContainer is typically a light/pastel surface and colorOnPrimary is often white, giving near-zero contrast. colorOnTertiaryContainer is the M3-correct on-color for this background and should be used as the fallback here (and the corresponding theme attributes statusConnectedColor/fabStoppedColor/speedTextColor should default to ?attr/colorOnTertiaryContainer for non-Dracula themes).

  2. app/src/main/res/values/themes.xml, line 644 (link)

    P1 Black theme FAB icon contrast regression

    Before this PR the FAB background was hardcoded to ?colorPrimary in the layout, so the Black theme's fabColorBackground = color_ng_black_accent (#9E9E9E, medium gray) was never applied. Now that the layout reads ?attr/fabColorBackground, that override kicks in. However, the icon tint for every state resolves to colorOnPrimary = white (inherited — Black theme doesn't override it), giving a white icon on #9E9E9E. That is roughly 2.6:1 contrast, below the 3:1 WCAG AA minimum for interactive UI icons.

    The transient states (Connecting/Stopping) are the hardest to fix because applyStateTint uses com.google.android.material.R.attr.colorOnPrimary directly and bypasses fabStoppedColor. The cleanest options are:

    • Add fabStoppedColor = @color/black and statusConnectedColor = @color/black to the Black theme, and change the else branch in applyStateTint to also resolve via R.attr.fabStoppedColor so all four states are theme-controllable.
    • Or remove this override so the FAB stays ?colorPrimary (#2B2B2B) and relies on the cradle cutout for visual separation from the bar.

Reviews (6): Last reviewed commit: "feat(theme): complete Material 3 color-r..." | Re-trigger Greptile

Move the theme stack from Theme.MaterialComponents.* to Theme.Material3.*:
- base Theme.SagerNet (values + values-v26), Theme.SagerNet.Dialog, Theme.Start
- widget parents: CardView -> Material3.CardView.Filled, Button, TextInputLayout
  OutlinedBox, PopupMenu(.Overflow), BottomSheet.Modal, BottomSheetDialog overlay,
  ShapeAppearance Small/Medium. Custom style *names* are unchanged (they're identifiers).
- Material 1.12.0 accepts the existing M2 attrs (colorAccent/colorPrimaryDark/etc.),
  and the ~25 per-theme color variants + Dracula keep working.

Fix a latent crash surfaced by M3: M3 wraps a View's Context in a ContextThemeWrapper,
so  /  in StatsBar, ServiceButton and
Dialogs threw ClassCastException on launch. Added Context.unwrap<T>() (walks the
ContextWrapper.baseContext chain) and used it at those sites.

Verified on-device: app launches; main list, Settings, profile Edit + input dialog, QR
generation (MaterialAlertDialog), and the remove-confirm alert all render with no inflation
or cast crashes; lintVitalOssRelease passes.
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a Context.unwrap<T>() extension function that walks ContextWrapper.baseContext to resolve typed instances, replacing direct casts in Dialogs.kt, ServiceButton.kt, and StatsBar.kt. Migrates all app theme and widget style parents from MaterialComponents to Material3 equivalents. Introduces Material3 dynamic color support via a new DYNAMIC theme constant, color helper methods in ThemedActivity, and updated layout and resource attributes.

Changes

Context.unwrap() helper and adoption

Layer / File(s) Summary
Context.unwrap<T>() implementation and call-site adoption
app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt, app/src/main/java/io/nekohasekai/sagernet/ktx/Dialogs.kt, app/src/main/java/io/nekohasekai/sagernet/widget/ServiceButton.kt, app/src/main/java/io/nekohasekai/sagernet/widget/StatsBar.kt
Adds inline reified Context.unwrap<T>() extension that traverses ContextWrapper.baseContext until finding a matching typed instance, throwing error(...) on failure. Replaces context as Activity in AlertDialog.tryToShow(), (context as LifecycleOwner) in ServiceButton delayed animation, and two context as MainActivity casts in StatsBar.changeState() and StatsBar.testConnection() with context.unwrap<T>().

Material Components → Material 3 migration and dynamic color support

Layer / File(s) Summary
Dynamic color theme constant and resolution
app/src/main/java/io/nekohasekai/sagernet/utils/Theme.kt
Theme.kt adds DYNAMIC = 24 constant mapped to base theme styles in getTheme() and getDialogTheme() resolution methods.
Dynamic color application in activity lifecycle
app/src/main/java/io/nekohasekai/sagernet/ui/ThemedActivity.kt
ThemedActivity.onCreate() conditionally calls applyDynamicColors() helper that applies wallpaper-based Material3 dynamic colors when DataStore.appTheme equals Theme.DYNAMIC, leaving colors unchanged for other themes.
Base theme parent and color role migration to Material3
app/src/main/res/values/themes.xml, app/src/main/res/values-v26/themes.xml
Theme.SagerNet, Theme.SagerNet.Dialog, and Theme.Start migrate parent attributes and itemShapeAppearance references from MaterialComponents to Material3 equivalents. Base themes add explicit colorOnPrimary attribute and Material3 semantic color-role items (primary/secondary/tertiary containers, surface variants, outline).
Widget and overlay style migration to Material3
app/src/main/res/values/themes.xml
Widget.SagerNet.CardView, TextInputLayout, PopupMenu variants, BottomSheet, BottomSheetDialog overlay, Button, BottomAppBar, FloatingActionButton, ShapeAppearance.SagerNet.NavItem, and material alert dialog overlays migrate to Material3 parent styles while preserving custom app-specific attributes.
Color variant theme updates and special M3 roles
app/src/main/res/values/themes.xml, app/src/main/res/values-night/themes.xml, app/src/main/res/values/colors.xml, app/src/main/res/values-night/colors.xml
Theme.SagerNet.Amber, Lime, Yellow and their Dialog variants add colorOnPrimary for proper contrast. Theme.SagerNet.Black and Dialog.Black define Material3 surface role overrides. Dracula variants in night mode add M3 container/variant role colors. New m3_outline color resources defined for light and night modes; color_dynamic_swatch added for dynamic theming.
Layout and resource updates for Material3 compatibility
app/src/main/res/layout/layout_main.xml, app/src/main/res/layout/layout_app_list.xml, app/src/main/res/layout/layout_appbar.xml, app/src/main/res/layout/layout_apps.xml, app/src/main/res/layout/layout_network.xml, app/src/main/res/layout/layout_stun.xml, app/src/main/res/layout/layout_webdav_settings.xml
layout_main.xml configures StatsBar FAB docking (fabAlignmentMode, fabAnchorMode) and reorders fabProgress/fab declarations with Material3-compatible tint/background attributes. Other layouts switch toolbar popup themes from AppCompat to ThemeOverlay.SagerNet.Toolbar.Popup and button/chip styles to Material3 equivalents.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • hawkff/NekoBoxForAndroid#38: Both PRs extend utils/Theme.kt's theme-resolution logic by adding new theme constants and getTheme()/getDialogTheme() branches (DYNAMIC in this PR vs DRACULA in the retrieved PR).
  • hawkff/NekoBoxForAndroid#41: Both PRs modify StatsBar.kt's changeState() and testConnection() methods, with this PR changing how MainActivity is retrieved via context.unwrap<>() and the retrieved PR modifying status/progress color rendering.

Poem

🐇 Hopping through wrappers, one by one,
No more raw casts—unwrap gets it done!
Material Three now paints the stage,
With dynamic colors on every page.
From ContextWrapper to dynamic hue,
This rabbit's theme is shiny new! 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the main changes: migration to Material 3 with full color roles, dynamic color support, and FAB visibility fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description provides detailed, substantive information about the Material 3 migration, including theme updates, dynamic color support, context unwrapping fixes, and FAB visibility improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

hawkff added 2 commits June 21, 2026 15:09
…fix invisible/untappable FAB

M3 color, to the bone:
- Apply Material 3 dynamic color in ThemedActivity. New DYNAMIC (Material You) theme
  option seeds the palette from the wallpaper on Android 12+; every other theme seeds a
  content-based M3 palette from its own colorPrimary, so all M3 roles (primaryContainer/
  surfaceVariant/outline/...) are generated correctly on all API levels without hand-
  authoring 21 palettes.
- Add DYNAMIC theme (id 24) to Theme.kt (activity + dialog mapping) and a picker swatch.

FAB fix (regressed by M3 BottomAppBar dropping the FAB cradle):
- The airplane FAB was declared BEFORE the StatsBar in the CoordinatorLayout, so the bar
  drew on top and intercepted its touches when connected (invisible + needed a swipe to
  reach). Reordered the FAB (and its progress) to draw AFTER the bar, and tint it with
  colorTertiaryContainer/onTertiaryContainer so it contrasts the colorPrimary bar.

Verified on-device: launch/connect/disconnect/reconnect clean; the FAB is visible and
disconnects on tap while connected without swiping; no crashes.
@hawkff hawkff changed the title Migrate theme stack to Material 3 (+ fix ContextThemeWrapper cast crash) Migrate to Material 3 (dynamic color + content-based palettes) + fix FAB Jun 21, 2026
hawkff added 3 commits June 21, 2026 16:45
Finish the M3 transition started on this branch. The base theme was
already Theme.Material3.DayNight; this fills in the full M3 color-role
model and removes the remaining Material 2 widget styles, without
breaking the hand-picked themes.

Color roles:
- Add colorPrimaryContainer/onPrimaryContainer, colorSecondary(+Container),
  colorTertiary(+Container), colorSurfaceVariant/onSurfaceVariant,
  colorOutline/outlineVariant to the base theme (values, values-v26,
  values-night). Each role derives from values the theme already owns so
  the standard themes keep their look and M3 widgets stop falling back to
  the library's generated grayscale tones.
- Explicit role overrides for the special themes: Dracula (dark-canvas
  containers/surfaceVariant/outline), Black (near-black containers, no
  gray). Black-on-light accents (Yellow/Lime/Amber) keep correct contrast
  via the base defaults.
- New colors: m3_outline (light + night), Dracula and Black container/
  surface-variant/outline swatches.

Widgets (Material 2 -> Material 3):
- themes.xml: Chip.Choice -> Widget.Material3.Chip.Suggestion;
  MaterialAlertDialog overlay -> ThemeOverlay.Material3.MaterialAlertDialog;
  dialog TextButtons -> Widget.Material3.Button.TextButton.Dialog(.Flush);
  ActionBar.Solid -> Widget.Material3.ActionBar.Solid; add a DayNight-aware
  toolbar popup overlay.
- 6 layouts: Chip.Filter, borderless buttons, and AppCompat/
  MaterialComponents popup overlays -> their Material 3 equivalents.

Verified on Android across the standard themes plus Dracula, Black, the
black-on-light accents, and Dynamic, in light and dark, covering the
profile list, nav drawer, settings, switches, cards, FAB, and the
theme-picker dialog. No regressions beyond the intended slight M3
container/outline tones on selected cards.
@hawkff hawkff changed the title Migrate to Material 3 (dynamic color + content-based palettes) + fix FAB Migrate to Material 3 (full color roles + dynamic color) + fix FAB Jun 21, 2026
@hawkff

hawkff commented Jun 21, 2026

Copy link
Copy Markdown
Owner Author

Both Greptile "Comments Outside Diff" reference earlier commits in this PR and are resolved in the current tree (392517b):

  1. FAB tint vs colorTertiaryContainer — stale. The intermediate colorTertiaryContainer FAB background was reverted; the main FAB now uses ?attr/fabColorBackground (= ?colorPrimary) with ?attr/fabStoppedColor (= ?colorOnPrimary), so the icon on-color matches the background role.
  2. Black theme FAB icon contrast — resolved by 3287546. fabColorBackground is color_ng_black_primary (#2B2B2B), giving a white icon ~14:1 contrast (well above WCAG AA), not the old #9E9E9E.

Verified on-device across all themes (incl. Black day/night): FABs render with legible icons. CodeRabbit CLI on the latest commit reports no actionable findings.

@hawkff hawkff merged commit 6b9c51d into main Jun 21, 2026
5 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