Fix five Android nav/shell UX issues

- Bottom bar: labels now sit below each icon instead of beside it, and
  the active-tab pill is one shared indicator that slides between tabs
  instead of popping in/out per item.
- Upload FAB stays mounted across every tab and collapses to an
  icon-only circle off Files/Photos, instead of the Scaffold popping it
  fully in/out on every tab switch.
- Tapping the top-bar avatar now opens the accounts sheet instead of
  jumping straight to Settings.
- Settings tab reordering only starts a drag from the grip icon now -
  previously the whole row claimed vertical drags, so scrolling the
  section could trigger an accidental reorder.
This commit is contained in:
2026-09-28 12:08:23 -04:00
parent d2c34b000f
commit 800e8ebf44
9 changed files with 488 additions and 117 deletions
+43 -5
View File
@@ -5,12 +5,24 @@ import '../../../theme/design_tokens.dart';
/// Android-only extended Upload pill, bottom-right of Files and Photos (Noo
/// Design System project, `components/core/FAB.jsx`). 56px tall, accent
/// fill - iOS uses a `plus` in the nav bar instead (not yet built).
///
/// [collapsed] shrinks it to an icon-only circle (no label) on tabs that
/// don't support upload, so the button stays in place and resizes as the
/// user switches tabs instead of the Scaffold popping it fully in/out -
/// the label's width animates away rather than the FAB disappearing.
class NooFab extends StatelessWidget {
final String label;
final IconData icon;
final VoidCallback? onTap;
final bool collapsed;
const NooFab({super.key, this.label = 'Upload', this.icon = LucideIcons.plus, this.onTap});
const NooFab({
super.key,
this.label = 'Upload',
this.icon = LucideIcons.plus,
this.onTap,
this.collapsed = false,
});
@override
Widget build(BuildContext context) {
@@ -21,9 +33,16 @@ class NooFab extends StatelessWidget {
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(NooRadii.pill),
child: Container(
child: AnimatedContainer(
duration: NooMotion.base,
curve: NooMotion.ease,
height: 56,
padding: const EdgeInsets.fromLTRB(16, 0, 20, 0),
padding: EdgeInsetsDirectional.fromSTEB(
16,
0,
collapsed ? 16 : 20,
0,
),
// No `alignment:` here - Container without an explicit width
// wraps its child in an Align when alignment is set, and Align
// EXPANDS to fill all available bounded space (not just the
@@ -36,8 +55,27 @@ class NooFab extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 24, color: Colors.white),
const SizedBox(width: 10),
Text(label, style: NooText.button.copyWith(color: Colors.white)),
ClipRect(
child: AnimatedSize(
duration: NooMotion.base,
curve: NooMotion.ease,
alignment: AlignmentDirectional.centerStart,
child: collapsed
? const SizedBox(height: 24)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 10),
Text(
label,
style: NooText.button.copyWith(
color: Colors.white,
),
),
],
),
),
),
],
),
),