From 0401af21630f4aac85facf2067a8655a94c1ab12 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 10 Mar 2026 22:11:26 +0000 Subject: [PATCH] Redesign temporary strategies UX/UI with color-coded session states - Add amber (Draft Copy) and cyan (Quick Board) accent color tokens to Settings - Create TemporarySessionBar widget: full-width colored bar between header and map showing session state, description, and contextual action buttons (Save to Original / Save as New / Save Board / Discard) - Redesign StrategyQuickSwitcher: show original name with glowing colored dot indicator and tinted border in temp mode; replace 'Temporary Copy' button with clearer 'Draft Copy' button with pen icon and amber hover state; remove ambiguous 'Finish' button (actions now in session bar) - Redesign temporary session dialogs with Lucide icons, color-coded action buttons, and clearer copy (e.g. 'Save Draft Changes?' with pen icon) - Fix Quick Board double-load bug: navigateWithLoading was re-calling loadFromHive with default saved sessionKind, overwriting the quickBoard state set by createQuickBoard; added skipLoad parameter - Update Quick Board button in folder navigator with cyan zap icon - Remove unused strategy_provider import from strategy_save_icon_button Co-authored-by: Dara Adedeji --- lib/const/settings.dart | 10 + lib/strategy_view.dart | 2 + .../strategy/temporary_session_flow.dart | 57 ++- lib/widgets/folder_navigator.dart | 23 +- lib/widgets/strategy_quick_switcher.dart | 135 +++++-- lib/widgets/strategy_save_icon_button.dart | 1 - lib/widgets/temporary_session_bar.dart | 341 ++++++++++++++++++ 7 files changed, 520 insertions(+), 49 deletions(-) create mode 100644 lib/widgets/temporary_session_bar.dart diff --git a/lib/const/settings.dart b/lib/const/settings.dart index b98f9d31..9ff2d73a 100644 --- a/lib/const/settings.dart +++ b/lib/const/settings.dart @@ -33,6 +33,16 @@ class Settings { static const Color sideBarColor = Color(0xFF141114); static const Color highlightColor = Color(0xff27272a); + static const Color tempCopyAccent = Color(0xFFF59E0B); + static const Color tempCopyAccentMuted = Color(0x14F59E0B); + static const Color tempCopyAccentSubtle = Color(0x33F59E0B); + static const Color tempCopyAccentForeground = Color(0xFFFEF3C7); + + static const Color quickBoardAccent = Color(0xFF06B6D4); + static const Color quickBoardAccentMuted = Color(0x1406B6D4); + static const Color quickBoardAccentSubtle = Color(0x3306B6D4); + static const Color quickBoardAccentForeground = Color(0xFFCFFAFE); + static List penColors = [ ColorOption(color: Colors.white, isSelected: true), ColorOption(color: Colors.red, isSelected: false), diff --git a/lib/strategy_view.dart b/lib/strategy_view.dart index 26696ad7..c2ce1760 100644 --- a/lib/strategy_view.dart +++ b/lib/strategy_view.dart @@ -14,6 +14,7 @@ import 'package:icarus/sidebar.dart'; import 'package:icarus/widgets/delete_capture.dart'; import 'package:icarus/widgets/demo_tag.dart'; import 'package:icarus/widgets/strategy_quick_switcher.dart'; +import 'package:icarus/widgets/temporary_session_bar.dart'; import 'package:icarus/widgets/map_selector.dart'; import 'package:icarus/widgets/pages_bar.dart'; import 'package:icarus/widgets/save_and_load_button.dart'; @@ -144,6 +145,7 @@ class _StrategyViewState extends ConsumerState ], ), ), + const TemporarySessionBar(), const Expanded( child: Stack( clipBehavior: Clip.none, diff --git a/lib/widgets/dialogs/strategy/temporary_session_flow.dart b/lib/widgets/dialogs/strategy/temporary_session_flow.dart index a1120af1..caff5e95 100644 --- a/lib/widgets/dialogs/strategy/temporary_session_flow.dart +++ b/lib/widgets/dialogs/strategy/temporary_session_flow.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:icarus/const/settings.dart'; import 'package:icarus/providers/strategy_provider.dart'; import 'package:icarus/widgets/dialogs/strategy/save_strategy_details_dialog.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; @@ -15,34 +16,49 @@ Future _showTemporaryCopyDialog( BuildContext context, { required bool includeDiscard, }) { + const accent = Settings.tempCopyAccent; + return showShadDialog( context: context, builder: (context) => ShadDialog( - title: const Text('Save Temporary Changes?'), + title: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.penLine, size: 18, color: accent), + SizedBox(width: 8), + Text('Save Draft Changes?'), + ], + ), description: const Text( - 'Choose where to apply this temporary copy before leaving.', + 'You have unsaved changes in your draft. Choose how to save before continuing.', ), actions: [ - ShadButton.secondary( + ShadButton.outline( onPressed: () => Navigator.of(context).pop(TemporarySaveIntent.cancel), child: const Text('Cancel'), ), if (includeDiscard) - ShadButton.secondary( + ShadButton.destructive( onPressed: () => Navigator.of(context).pop(TemporarySaveIntent.discard), + leading: const Icon(LucideIcons.trash2, size: 14), child: const Text('Discard'), ), ShadButton.secondary( onPressed: () => Navigator.of(context).pop(TemporarySaveIntent.saveAsNew), + leading: const Icon(LucideIcons.filePlus, size: 14), child: const Text('Save as New'), ), ShadButton( + backgroundColor: accent, + foregroundColor: const Color(0xFF1C1917), + hoverBackgroundColor: accent.withValues(alpha: 0.85), onPressed: () => Navigator.of(context).pop(TemporarySaveIntent.overwriteOriginal), - child: const Text('Overwrite Original'), + leading: const Icon(LucideIcons.save, size: 14), + child: const Text('Save to Original'), ), ], ), @@ -53,28 +69,43 @@ Future _showQuickBoardDialog( BuildContext context, { required bool includeDiscard, }) { + const accent = Settings.quickBoardAccent; + return showShadDialog( context: context, builder: (context) => ShadDialog( - title: const Text('Save Quick Board?'), + title: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(LucideIcons.zap, size: 18, color: accent), + SizedBox(width: 8), + Text('Save Quick Board?'), + ], + ), description: const Text( - 'Quick Boards are temporary. Save now or discard this session.', + 'Quick Boards are temporary workspaces. Save now to keep your work.', ), actions: [ - ShadButton.secondary( + ShadButton.outline( onPressed: () => Navigator.of(context).pop(TemporarySaveIntent.cancel), child: const Text('Cancel'), ), if (includeDiscard) - ShadButton.secondary( + ShadButton.destructive( onPressed: () => Navigator.of(context).pop(TemporarySaveIntent.discard), + leading: const Icon(LucideIcons.trash2, size: 14), child: const Text('Discard'), ), ShadButton( - onPressed: () => Navigator.of(context).pop(TemporarySaveIntent.saveAsNew), - child: const Text('Save'), + backgroundColor: accent, + foregroundColor: const Color(0xFF1C1917), + hoverBackgroundColor: accent.withValues(alpha: 0.85), + onPressed: () => + Navigator.of(context).pop(TemporarySaveIntent.saveAsNew), + leading: const Icon(LucideIcons.save, size: 14), + child: const Text('Save Board'), ), ], ), @@ -107,7 +138,7 @@ Future resolveTemporarySessionForNavigation({ final sourceStrategy = strategyNotifier.currentStrategyData(); final details = await showStrategySaveDetailsDialog( context: context, - title: strategyState.isQuickBoard ? 'Save Quick Board' : 'Save as New', + title: strategyState.isQuickBoard ? 'Save Quick Board' : 'Save as New Strategy', confirmLabel: 'Save', initialName: strategyState.isQuickBoard ? sourceName : '$sourceName (Copy)', initialFolderId: sourceStrategy?.folderID, @@ -147,7 +178,7 @@ Future resolveTemporarySessionForManualSave({ final sourceStrategy = strategyNotifier.currentStrategyData(); final details = await showStrategySaveDetailsDialog( context: context, - title: strategyState.isQuickBoard ? 'Save Quick Board' : 'Save as New', + title: strategyState.isQuickBoard ? 'Save Quick Board' : 'Save as New Strategy', confirmLabel: 'Save', initialName: strategyState.isQuickBoard ? sourceName : '$sourceName (Copy)', initialFolderId: sourceStrategy?.folderID, diff --git a/lib/widgets/folder_navigator.dart b/lib/widgets/folder_navigator.dart index 1720b299..6ea52d81 100644 --- a/lib/widgets/folder_navigator.dart +++ b/lib/widgets/folder_navigator.dart @@ -96,12 +96,14 @@ class _FolderNavigatorState extends ConsumerState { ? ref.read(folderProvider.notifier).findFolderByID(currentFolderId) : null; Future navigateWithLoading( - BuildContext context, String strategyId) async { - // Show loading overlay - // showLoadingOverlay(context); - + BuildContext context, + String strategyId, { + bool skipLoad = false, + }) async { try { - await ref.read(strategyProvider.notifier).loadFromHive(strategyId); + if (!skipLoad) { + await ref.read(strategyProvider.notifier).loadFromHive(strategyId); + } if (!context.mounted) return; @@ -110,7 +112,7 @@ class _FolderNavigatorState extends ConsumerState { PageRouteBuilder( transitionDuration: const Duration(milliseconds: 200), reverseTransitionDuration: - const Duration(milliseconds: 200), // pop duration + const Duration(milliseconds: 200), pageBuilder: (context, animation, secondaryAnimation) => const StrategyView(), transitionsBuilder: @@ -129,7 +131,6 @@ class _FolderNavigatorState extends ConsumerState { ); } catch (e) { // Handle errors - // Show error message } } @@ -209,9 +210,13 @@ class _FolderNavigatorState extends ConsumerState { .read(strategyProvider.notifier) .createQuickBoard(); if (!context.mounted) return; - await navigateWithLoading(context, strategyId); + await navigateWithLoading(context, strategyId, skipLoad: true); }, - leading: const Icon(Icons.bolt), + leading: const Icon( + LucideIcons.zap, + size: 16, + color: Settings.quickBoardAccent, + ), child: const Text('Quick Board'), ), ], diff --git a/lib/widgets/strategy_quick_switcher.dart b/lib/widgets/strategy_quick_switcher.dart index 763d2d70..cc42e7bb 100644 --- a/lib/widgets/strategy_quick_switcher.dart +++ b/lib/widgets/strategy_quick_switcher.dart @@ -262,16 +262,21 @@ class _StrategyQuickSwitcherState extends ConsumerState { return '$months month$plural ago'; } + Color? _sessionAccentColor(StrategyState strategy) { + if (strategy.isQuickBoard) return Settings.quickBoardAccent; + if (strategy.isTemporaryCopy) return Settings.tempCopyAccent; + return null; + } + @override Widget build(BuildContext context) { final currentStrategy = ref.watch(strategyProvider); final strategyName = currentStrategy.stratName ?? 'Untitled Strategy'; final displayName = currentStrategy.isQuickBoard ? 'Quick Board' - : currentStrategy.isTemporaryCopy - ? '$strategyName (Temporary Copy)' - : strategyName; + : strategyName; final strategiesBox = Hive.box(HiveBoxNames.strategiesBox); + final accentColor = _sessionAccentColor(currentStrategy); return Padding( padding: _displayMargin, @@ -365,17 +370,37 @@ class _StrategyQuickSwitcherState extends ConsumerState { }, child: Row( children: [ - Container( + AnimatedContainer( + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, width: _barWidth, decoration: BoxDecoration( color: Settings.tacticalVioletTheme.card, borderRadius: BorderRadius.circular(8), border: Border.all( - color: Settings.tacticalVioletTheme.border, + color: accentColor?.withValues(alpha: 0.5) + ?? Settings.tacticalVioletTheme.border, ), ), child: Row( children: [ + if (currentStrategy.isTemporarySession) ...[ + const SizedBox(width: 10), + Container( + width: 7, + height: 7, + decoration: BoxDecoration( + color: accentColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: accentColor!.withValues(alpha: 0.5), + blurRadius: 4, + ), + ], + ), + ), + ], Expanded( child: _isEditingName ? Padding( @@ -445,7 +470,7 @@ class _StrategyQuickSwitcherState extends ConsumerState { message: currentStrategy.stratName == null ? 'Load a strategy to rename it' : currentStrategy.isTemporarySession - ? 'Rename is disabled in temporary mode' + ? 'Rename is disabled in draft mode' : 'Rename strategy', child: Material( color: Colors.transparent, @@ -482,7 +507,8 @@ class _StrategyQuickSwitcherState extends ConsumerState { Container( width: 1, height: 30, - color: Settings.tacticalVioletTheme.border, + color: accentColor?.withValues(alpha: 0.3) + ?? Settings.tacticalVioletTheme.border, ), SizedBox( width: 38, @@ -499,36 +525,24 @@ class _StrategyQuickSwitcherState extends ConsumerState { ) : Icon( _isOpen - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down, + ? LucideIcons.chevronUp + : LucideIcons.chevronDown, color: Colors.white, - size: 18, + size: 16, ), ), ), ], ), ), - const SizedBox(width: 8), - if (currentStrategy.isTemporarySession) - ShadButton.secondary( - onPressed: _isSwitching || _isEditingName - ? null - : () async { - await resolveTemporarySessionForNavigation( - context: context, - ref: ref, - ); - }, - child: const Text('Finish'), - ) - else - ShadButton.secondary( + if (!currentStrategy.isTemporarySession) ...[ + const SizedBox(width: 8), + _DraftCopyButton( onPressed: currentStrategy.stratName == null ? null : _startTemporaryCopy, - child: const Text('Temporary Copy'), ), + ], ], ), ); @@ -539,6 +553,75 @@ class _StrategyQuickSwitcherState extends ConsumerState { } } +class _DraftCopyButton extends StatefulWidget { + const _DraftCopyButton({required this.onPressed}); + + final VoidCallback? onPressed; + + @override + State<_DraftCopyButton> createState() => _DraftCopyButtonState(); +} + +class _DraftCopyButtonState extends State<_DraftCopyButton> { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + final isEnabled = widget.onPressed != null; + const accent = Settings.tempCopyAccent; + + return MouseRegion( + cursor: isEnabled ? SystemMouseCursors.click : SystemMouseCursors.basic, + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: GestureDetector( + onTap: widget.onPressed, + child: ShadTooltip( + builder: (context) => + const Text('Create an editable draft copy of this strategy'), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: _isHovered && isEnabled + ? accent.withValues(alpha: 0.12) + : Settings.tacticalVioletTheme.secondary, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _isHovered && isEnabled + ? accent.withValues(alpha: 0.4) + : Settings.tacticalVioletTheme.border, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + LucideIcons.penLine, + size: 14, + color: _isHovered && isEnabled + ? accent + : Colors.white.withValues(alpha: 0.7), + ), + const SizedBox(width: 6), + Text( + 'Draft Copy', + style: ShadTheme.of(context).textTheme.small.copyWith( + color: _isHovered && isEnabled + ? accent + : Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + ); + } +} + class _StrategyQuickSwitchItem extends StatefulWidget { const _StrategyQuickSwitchItem({ required this.strategyName, diff --git a/lib/widgets/strategy_save_icon_button.dart b/lib/widgets/strategy_save_icon_button.dart index f758b9ba..264b9f68 100644 --- a/lib/widgets/strategy_save_icon_button.dart +++ b/lib/widgets/strategy_save_icon_button.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:icarus/const/settings.dart'; import 'package:icarus/providers/auto_save_notifier.dart'; -import 'package:icarus/providers/strategy_provider.dart'; import 'package:icarus/widgets/dialogs/strategy/temporary_session_flow.dart'; import 'package:shadcn_ui/shadcn_ui.dart'; import 'package:toastification/toastification.dart'; diff --git a/lib/widgets/temporary_session_bar.dart b/lib/widgets/temporary_session_bar.dart new file mode 100644 index 00000000..021e2a62 --- /dev/null +++ b/lib/widgets/temporary_session_bar.dart @@ -0,0 +1,341 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:icarus/const/settings.dart'; +import 'package:icarus/providers/strategy_provider.dart'; +import 'package:icarus/widgets/dialogs/strategy/save_strategy_details_dialog.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +class TemporarySessionBar extends ConsumerStatefulWidget { + const TemporarySessionBar({super.key}); + + @override + ConsumerState createState() => + _TemporarySessionBarState(); +} + +class _TemporarySessionBarState extends ConsumerState + with SingleTickerProviderStateMixin { + late final AnimationController _animController; + late final Animation _fadeAnim; + late final Animation _slideAnim; + bool _wasTemporary = false; + + @override + void initState() { + super.initState(); + _animController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 250), + ); + _fadeAnim = CurvedAnimation( + parent: _animController, + curve: Curves.easeOut, + ); + _slideAnim = Tween(begin: -1.0, end: 0.0).animate( + CurvedAnimation(parent: _animController, curve: Curves.easeOutCubic), + ); + } + + @override + void dispose() { + _animController.dispose(); + super.dispose(); + } + + Future _saveToOriginal() async { + await ref + .read(strategyProvider.notifier) + .overwriteOriginalFromTemporaryCopy(); + } + + Future _saveAsNew() async { + final strategy = ref.read(strategyProvider); + final notifier = ref.read(strategyProvider.notifier); + final sourceName = strategy.stratName ?? 'Strategy'; + final sourceStrategy = notifier.currentStrategyData(); + final details = await showStrategySaveDetailsDialog( + context: context, + title: strategy.isQuickBoard ? 'Save Quick Board' : 'Save as New Strategy', + confirmLabel: 'Save', + initialName: + strategy.isQuickBoard ? sourceName : '$sourceName (Copy)', + initialFolderId: sourceStrategy?.folderID, + ); + if (details == null) return; + await notifier.saveTemporarySessionAsNewStrategy( + name: details.name, + folderID: details.folderId, + ); + } + + Future _exitDraft() async { + final strategy = ref.read(strategyProvider); + final confirmed = await _showExitConfirmation( + isQuickBoard: strategy.isQuickBoard, + ); + if (confirmed) { + await ref.read(strategyProvider.notifier).discardTemporarySession(); + } + } + + Future _showExitConfirmation({required bool isQuickBoard}) async { + final accentColor = isQuickBoard + ? Settings.quickBoardAccent + : Settings.tempCopyAccent; + + final result = await showShadDialog( + context: context, + builder: (context) => ShadDialog( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isQuickBoard ? LucideIcons.zap : LucideIcons.penLine, + size: 18, + color: accentColor, + ), + const SizedBox(width: 8), + Text(isQuickBoard + ? 'Discard Quick Board?' + : 'Discard Draft Changes?'), + ], + ), + description: Text( + isQuickBoard + ? 'This Quick Board hasn\'t been saved. All changes will be lost.' + : 'Unsaved changes to this draft will be lost. The original strategy will remain unchanged.', + ), + actions: [ + ShadButton.outline( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + ShadButton.destructive( + onPressed: () => Navigator.of(context).pop(true), + child: Text(isQuickBoard ? 'Discard Board' : 'Discard Draft'), + ), + ], + ), + ); + return result ?? false; + } + + @override + Widget build(BuildContext context) { + final strategy = ref.watch(strategyProvider); + final isTemp = strategy.isTemporarySession; + + if (isTemp && !_wasTemporary) { + _wasTemporary = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _animController.forward(); + }); + } else if (!isTemp && _wasTemporary) { + _wasTemporary = false; + _animController.reverse(); + } else if (isTemp && !_animController.isCompleted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && !_animController.isCompleted) { + _animController.forward(); + } + }); + } + + if (!isTemp && !_animController.isAnimating) { + return const SizedBox.shrink(); + } + + final isQuickBoard = strategy.isQuickBoard; + final accentColor = + isQuickBoard ? Settings.quickBoardAccent : Settings.tempCopyAccent; + final bgColor = isQuickBoard + ? Settings.quickBoardAccentMuted + : Settings.tempCopyAccentMuted; + final fgColor = isQuickBoard + ? Settings.quickBoardAccentForeground + : Settings.tempCopyAccentForeground; + + final label = isQuickBoard ? 'Quick Board' : 'Draft Mode'; + final description = isQuickBoard + ? 'Temporary workspace — save to keep your work' + : 'Editing a copy of "${strategy.stratName ?? "Untitled"}"'; + final icon = isQuickBoard ? LucideIcons.zap : LucideIcons.penLine; + + return FadeTransition( + opacity: _fadeAnim, + child: AnimatedBuilder( + animation: _slideAnim, + builder: (context, child) => ClipRect( + child: FractionalTranslation( + translation: Offset(0, _slideAnim.value), + child: child, + ), + ), + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: bgColor, + border: Border( + bottom: BorderSide( + color: accentColor.withValues(alpha: 0.2), + width: 1, + ), + ), + ), + child: Row( + children: [ + Container( + width: 3, + height: 36, + color: accentColor, + ), + const SizedBox(width: 12), + Icon(icon, size: 15, color: accentColor), + const SizedBox(width: 8), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: accentColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(4), + ), + child: Text( + label, + style: ShadTheme.of(context).textTheme.small.copyWith( + color: accentColor, + fontWeight: FontWeight.w600, + fontSize: 11, + letterSpacing: 0.3, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: ShadTheme.of(context).textTheme.small.copyWith( + color: fgColor.withValues(alpha: 0.6), + fontSize: 12, + ), + ), + ), + const SizedBox(width: 8), + if (strategy.isTemporaryCopy) ...[ + _SessionBarButton( + onPressed: _saveToOriginal, + icon: LucideIcons.save, + label: 'Save to Original', + accentColor: accentColor, + isPrimary: true, + ), + const SizedBox(width: 6), + _SessionBarButton( + onPressed: _saveAsNew, + icon: LucideIcons.filePlus, + label: 'Save as New', + accentColor: accentColor, + isPrimary: false, + ), + ] else ...[ + _SessionBarButton( + onPressed: _saveAsNew, + icon: LucideIcons.save, + label: 'Save Board', + accentColor: accentColor, + isPrimary: true, + ), + ], + const SizedBox(width: 6), + ShadTooltip( + builder: (context) => Text( + isQuickBoard ? 'Discard board' : 'Discard draft', + ), + child: ShadIconButton.ghost( + onPressed: _exitDraft, + width: 28, + height: 28, + icon: Icon(LucideIcons.x, size: 14, color: fgColor.withValues(alpha: 0.5)), + ), + ), + const SizedBox(width: 8), + ], + ), + ), + ), + ); + } +} + +class _SessionBarButton extends StatefulWidget { + const _SessionBarButton({ + required this.onPressed, + required this.icon, + required this.label, + required this.accentColor, + required this.isPrimary, + }); + + final VoidCallback onPressed; + final IconData icon; + final String label; + final Color accentColor; + final bool isPrimary; + + @override + State<_SessionBarButton> createState() => _SessionBarButtonState(); +} + +class _SessionBarButtonState extends State<_SessionBarButton> { + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + final bg = widget.isPrimary + ? widget.accentColor.withValues(alpha: _isHovered ? 0.25 : 0.15) + : Colors.white.withValues(alpha: _isHovered ? 0.08 : 0.04); + final fg = widget.isPrimary + ? widget.accentColor + : Colors.white.withValues(alpha: 0.7); + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: GestureDetector( + onTap: widget.onPressed, + child: AnimatedContainer( + duration: const Duration(milliseconds: 120), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(6), + border: widget.isPrimary + ? Border.all( + color: widget.accentColor.withValues(alpha: 0.3), + width: 1, + ) + : null, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(widget.icon, size: 13, color: fg), + const SizedBox(width: 5), + Text( + widget.label, + style: ShadTheme.of(context).textTheme.small.copyWith( + color: fg, + fontWeight: + widget.isPrimary ? FontWeight.w600 : FontWeight.w400, + fontSize: 12, + ), + ), + ], + ), + ), + ), + ); + } +}