import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../tokens.dart'; import 'account_screen.dart'; import 'home_screen.dart'; import 'orders_screen.dart'; /// Bottom-navigation shell: Home · Orders · Account. /// /// Booking and tracking screens are pushed on top, so the tab bar only ever /// shows on the three destinations that need it. class ShellScreen extends StatefulWidget { const ShellScreen({super.key, this.initialTab = 0}); final int initialTab; static ShellScreenState? of(BuildContext context) => context.findAncestorStateOfType(); @override State createState() => ShellScreenState(); } class ShellScreenState extends State { late int _index = widget.initialTab; void goToTab(int index) { if (index == _index) return; HapticFeedback.selectionClick(); setState(() => _index = index); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: DmColors.canvas, body: IndexedStack( index: _index, children: const [HomeScreen(), OrdersScreen(), AccountScreen()], ), bottomNavigationBar: _TabBar(index: _index, onChanged: goToTab), ); } } /// The tab bar, fixed to the bottom edge. /// /// ── Why it is no longer a floating pill-shaped bar ── /// /// It used to hover 10pt above the bottom with a 28pt radius and a shadow /// under it, which put a second rounded surface between the content and the /// gesture bar and made every screen end in two stacked edges. It now sits on /// the edge with a single hairline above it, like the footers in the rest of /// the app. /// /// The indicator does the work instead: a crimson pill that travels to the tab /// you pick, with the glyph turning white as it arrives. Selection reads as one /// object moving rather than three tabs changing colour in place — and the /// label under it stays crimson, so the tab is still identifiable in the /// half-second the pill is in flight. class _TabBar extends StatelessWidget { const _TabBar({required this.index, required this.onChanged}); final int index; final ValueChanged onChanged; /// Lucide, not Material. One 1.5px stroke family at one weight, so the three /// glyphs read as a set instead of three different drawing styles — and the /// same set the pushed screens use. static const _items = [ (label: 'Home', icon: LucideIcons.house), (label: 'Orders', icon: LucideIcons.package), (label: 'Account', icon: LucideIcons.userRound), ]; /// The travelling pill's size. Narrower than its slot so it reads as an /// object sitting in the row rather than as a selected cell. static const double _pillWidth = 62; static const double _pillHeight = 34; @override Widget build(BuildContext context) { return DecoratedBox( decoration: const BoxDecoration( color: DmColors.surface, border: Border(top: BorderSide(color: DmColors.border)), ), child: SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(6, 9, 6, 7), child: SizedBox( // The row is the pill plus the label, and the label is type: at // 1.8x accessibility text a fixed 58 clipped it by two points. The // bar gets taller instead, which is what a larger-text user asked // for. height: _pillHeight + 5 + MediaQuery.textScalerOf(context).scale(11.5) * 1.3, child: LayoutBuilder( builder: (context, constraints) { final slot = constraints.maxWidth / _items.length; return Stack( children: [ AnimatedPositioned( duration: DmMotion.slow, curve: DmMotion.glide, left: slot * index + (slot - _pillWidth) / 2, width: _pillWidth, top: 0, height: _pillHeight, child: DecoratedBox( decoration: BoxDecoration( color: DmColors.brand, borderRadius: DmRadius.all(DmRadius.pill), boxShadow: DmShadow.liftSm, ), ), ), Row( children: [ for (var i = 0; i < _items.length; i++) Expanded( child: _TabButton( label: _items[i].label, icon: _items[i].icon, selected: index == i, onTap: () => onChanged(i), ), ), ], ), ], ); }, ), ), ), ), ); } } class _TabButton extends StatelessWidget { const _TabButton({ required this.label, required this.icon, required this.selected, required this.onTap, }); final String label; final IconData icon; final bool selected; final VoidCallback onTap; @override Widget build(BuildContext context) { return Semantics( button: true, selected: selected, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox( height: _TabBar._pillHeight, child: Center( child: AnimatedSwitcher( duration: DmMotion.base, child: Icon( icon, key: ValueKey(selected), size: 22, color: selected ? Colors.white : DmColors.ink3, ), ), ), ), const SizedBox(height: 5), AnimatedDefaultTextStyle( duration: DmMotion.base, curve: DmMotion.ease, style: DmText.label.copyWith( fontSize: 11.5, height: 1.0, color: selected ? DmColors.brand : DmColors.ink3, fontWeight: selected ? FontWeight.w700 : FontWeight.w600, ), child: Text(label, maxLines: 1, overflow: TextOverflow.ellipsis), ), ], ), ), ); } }