diff --git a/lib/screens/contribute_screen.dart b/lib/screens/contribute_screen.dart index 8f632b1..7618629 100644 --- a/lib/screens/contribute_screen.dart +++ b/lib/screens/contribute_screen.dart @@ -230,65 +230,130 @@ class _ContributeScreenState extends State with SingleTickerPr ); } + /// Bouncy spring curve matching web CSS: cubic-bezier(0.37, 1.95, 0.66, 0.56) + static const Curve _bouncyCurve = Cubic(0.37, 1.95, 0.66, 0.56); + + /// Tab icons for each tab + static const List _tabIcons = [ + Icons.edit_outlined, + Icons.emoji_events_outlined, + Icons.workspace_premium_outlined, + ]; + Widget _buildSegmentedTabs(BuildContext context) { final tabs = ['Contribute', 'Leaderboard', 'Achievements']; return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.06), - borderRadius: BorderRadius.circular(_cornerRadius + 6), - border: Border.all(color: Colors.white.withOpacity(0.10)), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 6))], - ), - child: Row( - children: List.generate(tabs.length, (i) { - final active = i == _activeTab; - return Expanded( - child: GestureDetector( - onTap: () => setState(() => _activeTab = i), - child: AnimatedContainer( - duration: const Duration(milliseconds: 220), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 10), - margin: EdgeInsets.only(right: i == tabs.length - 1 ? 0 : 8), - decoration: BoxDecoration( - color: active ? Colors.white : Colors.transparent, - borderRadius: BorderRadius.circular(_cornerRadius - 4), - boxShadow: active ? [BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 8, offset: const Offset(0, 4))] : null, + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 10), + child: LayoutBuilder( + builder: (context, constraints) { + final containerWidth = constraints.maxWidth; + // 6px padding on each side of the container + const double containerPadding = 6.0; + final innerWidth = containerWidth - (containerPadding * 2); + final tabWidth = innerWidth / tabs.length; + + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Container( + height: 57, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: Stack( + children: [ + // ── Sliding glider ── + AnimatedPositioned( + duration: const Duration(milliseconds: 500), + curve: _bouncyCurve, + left: containerPadding + (_activeTab * tabWidth), + top: containerPadding, + width: tabWidth, + height: 57 - (containerPadding * 2), + child: AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.95), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.10), + blurRadius: 15, + offset: const Offset(0, 4), + ), + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // show a small icon only for active tab - if (active) ...[ - Icon(Icons.edit, size: 14, color: _primary), - const SizedBox(width: 8), - ], - // FittedBox ensures the whole word is visible (scales down if necessary) - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.center, - child: Text( - tabs[i], - textAlign: TextAlign.center, - style: TextStyle( - color: active ? _primary : Colors.white.withOpacity(0.95), - fontWeight: active ? FontWeight.w800 : FontWeight.w600, - fontSize: 16, // preferred size; FittedBox will shrink if needed + + // ── Tab labels ── + Padding( + padding: const EdgeInsets.all(containerPadding), + child: Row( + children: List.generate(tabs.length, (i) { + final active = i == _activeTab; + return Expanded( + child: GestureDetector( + onTap: () => setState(() => _activeTab = i), + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: double.infinity, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Animated icon: only shows for active tab + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: active + ? Padding( + padding: const EdgeInsets.only(right: 6), + child: Icon( + _tabIcons[i], + size: 15, + color: _primary, + ), + ) + : const SizedBox.shrink(), + ), + Flexible( + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + style: TextStyle( + color: active ? _primary : Colors.white.withOpacity(0.7), + fontWeight: FontWeight.w600, + fontSize: 14, + fontFamily: Theme.of(context).textTheme.bodyMedium?.fontFamily, + ), + child: Text( + tabs[i], + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), ), ), - ), - ), - ], + ); + }), + ), ), - ), + ], ), - ); - }), - ), + ), + ); + }, ), ); }