import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../state/timecards_controller.dart'; import '../widgets/krow_nav_bar.dart'; /// The five primary tabs. Each keeps its own navigation stack. class HomeShell extends ConsumerWidget { const HomeShell({super.key, required this.navigationShell}); final StatefulNavigationShell navigationShell; @override Widget build(BuildContext context, WidgetRef ref) { final needsReview = ref.watch(timecardsNeedingReviewProvider).isNotEmpty; return Scaffold( body: _TabTransition( index: navigationShell.currentIndex, child: navigationShell, ), bottomNavigationBar: KrowNavBar( currentIndex: navigationShell.currentIndex, onSelect: (i) => navigationShell.goBranch( i, initialLocation: i == navigationShell.currentIndex, ), // Must stay in the same order as the shell branches in the router. items: [ const KrowNavItem(icon: LucideIcons.briefcase, label: 'Shifts'), const KrowNavItem(icon: LucideIcons.wallet, label: 'Pay'), const KrowNavItem(icon: LucideIcons.house, label: 'Today'), KrowNavItem( icon: LucideIcons.clock, label: 'Clock', showDot: needsReview, ), const KrowNavItem(icon: LucideIcons.user, label: 'Profile'), ], ), ); } } /// Fades and lifts the tab content each time the tab changes. /// /// Deliberately not an AnimatedSwitcher: that would hold two copies of the /// shell in the tree at once, and the branch navigators are keyed by /// GlobalKey, so duplicating them would throw. This keeps one subtree — the /// branches stay alive and keep their scroll positions — and animates it in /// place when the index changes. class _TabTransition extends StatefulWidget { const _TabTransition({required this.index, required this.child}); final int index; final Widget child; @override State<_TabTransition> createState() => _TabTransitionState(); } class _TabTransitionState extends State<_TabTransition> with SingleTickerProviderStateMixin { late final AnimationController _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 260), value: 1, ); late final Animation _curve = CurvedAnimation( parent: _controller, curve: Curves.easeOutCubic, ); @override void didUpdateWidget(_TabTransition oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.index != widget.index) _controller.forward(from: 0); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _curve, // The shell is passed through untouched so it is never rebuilt. child: widget.child, builder: (context, child) { final t = _curve.value; return Opacity( // Never fully transparent: a tab that blinks out reads as a glitch. opacity: 0.35 + 0.65 * t, child: Transform.translate( offset: Offset(0, (1 - t) * 10), child: child, ), ); }, ); } }