Fix status-bar overlap on scroll, and reposition/elevate the avatar menu

- Every tab now wraps its CustomScrollView in SafeArea(top: true) - the
  pinned sort/filter row was riding up underneath the status bar once
  the floating top bar above it fully collapsed, since that bar's own
  internal SafeArea only reserved the inset while it had some height
  left to put it in. Reserving it outside the scrolling region entirely
  fixes this regardless of the floating header's state. Caught live on
  device and backed by a new regression test.
- The avatar menu now covers the top bar (title included) instead of
  sitting below it - positioned off just the status-bar inset rather
  than the top bar's full height on top of that.
- The avatar menu's card now carries a second, tighter contact shadow
  alongside nooDialogShadow - that shadow alone was essentially
  invisible on a small card over a dark theme's near-black background.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 22:59:19 -04:00
co-authored by Claude Sonnet 5
parent e6d96a6f9f
commit 9f47d0a3e3
12 changed files with 280 additions and 75 deletions
+20 -3
View File
@@ -133,14 +133,31 @@ List<Widget> tabBottomInsetSlivers(BuildContext context) => [
/// [SliverFloatingHeader] sizes itself from [topBar]'s own natural layout
/// (like `SliverToBoxAdapter`) rather than a fixed extent declared up
/// front - so [topBar]'s own internal `SafeArea` (see `NooTopBar`'s doc
/// comment) already accounts for the status-bar inset correctly, with no
/// extra height math needed here (unlike building this on the general-
/// purpose `SliverPersistentHeader` would have required).
/// comment) already accounts for the status-bar inset correctly while
/// [topBar] itself is visible, with no extra height math needed here
/// (unlike building this on the general-purpose `SliverPersistentHeader`
/// would have required).
///
/// Sits above a tab's own pinned in-content header (built with
/// [StickyHeaderDelegate] - the sort/filter controls row, or the selection
/// bar that replaces it) - put this sliver first in `contentSlivers` so
/// that header stays exactly where it already is, independent of whether
/// [topBar] is currently shown or scrolled away.
///
/// That pinned header needs its OWN protection from the status bar too,
/// though: [topBar]'s `SafeArea` only reserves space while [topBar] has
/// some height to put it in - once it's fully collapsed (0 height, [topBar]
/// scrolled all the way away), that reservation disappears with it, and
/// the pinned header would ride up underneath the status bar instead of
/// stopping below it (a real bug this shipped with once already - caught
/// by `tab_state_slivers_test.dart`'s regression test for it). Every tab
/// view wraps its whole `CustomScrollView` (this sliver, the pinned header,
/// and everything else) in `SafeArea(top: true, bottom: false, ...)` to
/// fix this - that reserves the inset outside the scrolling/collapsing
/// region entirely, so it's never implicated in this sliver's own
/// collapse math regardless of [topBar]'s current state. Flutter's
/// `SafeArea` nesting means this doesn't double the inset: the outer one
/// zeroes `MediaQuery.padding.top` for everything below it, so [topBar]'s
/// own inner `SafeArea` sees nothing left to add.
Widget topBarSliver(PreferredSizeWidget topBar) =>
SliverFloatingHeader(child: topBar);