From fe6f0460de6918dc6528b5a6063c4009c194487f Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Mon, 17 Mar 2025 16:19:34 +0530 Subject: [PATCH 1/4] Initial commit Add tab bar to RequestEditor with left-aligned tabs Implement TabPane and TabRequestCard for request navigation Enhance tab design with elevation and selection indicator --- .../home_page/editor_pane/editor_request.dart | 16 ++-- .../home_page/editor_pane/tab_pane.dart | 58 +++++++++++++ lib/widgets/tab_request_card.dart | 85 +++++++++++++++++++ 3 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 lib/screens/home_page/editor_pane/tab_pane.dart create mode 100644 lib/widgets/tab_request_card.dart diff --git a/lib/screens/home_page/editor_pane/editor_request.dart b/lib/screens/home_page/editor_pane/editor_request.dart index 902e5151a..709381451 100644 --- a/lib/screens/home_page/editor_pane/editor_request.dart +++ b/lib/screens/home_page/editor_pane/editor_request.dart @@ -5,6 +5,7 @@ import 'details_card/details_card.dart'; import 'details_card/request_pane/request_pane.dart'; import 'request_editor_top_bar.dart'; import 'url_card.dart'; +import 'tab_pane.dart'; class RequestEditor extends StatelessWidget { const RequestEditor({super.key}); @@ -12,25 +13,28 @@ class RequestEditor extends StatelessWidget { @override Widget build(BuildContext context) { return context.isMediumWindow - ? const Padding( + ? Padding( padding: kPb10, child: Column( children: [ + const TabPane(), kVSpacer20, Expanded( - child: EditRequestPane(), + child: const EditRequestPane(), ), ], ), ) : Padding( padding: kIsMacOS || kIsWindows ? kPt28o8 : kP8, - child: const Column( + child: Column( children: [ - RequestEditorTopBar(), - EditorPaneRequestURLCard(), + const TabPane(), kVSpacer10, - Expanded( + const RequestEditorTopBar(), + const EditorPaneRequestURLCard(), + kVSpacer10, + const Expanded( child: EditorPaneRequestDetailsCard(), ), ], diff --git a/lib/screens/home_page/editor_pane/tab_pane.dart b/lib/screens/home_page/editor_pane/tab_pane.dart new file mode 100644 index 000000000..6c84cda26 --- /dev/null +++ b/lib/screens/home_page/editor_pane/tab_pane.dart @@ -0,0 +1,58 @@ +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/providers/collection_providers.dart'; +import 'package:apidash/models/models.dart'; +import 'package:apidash/widgets/tab_request_card.dart'; +import 'package:apidash/screens/home_page/collection_pane.dart'; +import 'package:apidash/utils/utils.dart'; + +class TabPane extends ConsumerWidget { + const TabPane({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final collection = ref.watch(collectionStateNotifierProvider); + final requestSequence = ref.watch(requestSequenceProvider); + final selectedId = ref.watch(selectedIdStateProvider); + + if (collection == null || requestSequence.isEmpty) { + return const SizedBox.shrink(); + } + + return Container( + color: Theme.of(context).colorScheme.surfaceContainerLowest, + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: MediaQuery.of(context).size.width, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: requestSequence.map((id) { + final request = collection[id]!; + final name = request.name.isNotEmpty + ? request.name + : getRequestTitleFromUrl(request.httpRequestModel?.url) ?? 'Untitled'; + return TabRequestCard( + apiType: request.apiType, + method: request.httpRequestModel!.method, + name: name, + isSelected: selectedId == id, + onTap: () { + ref.read(selectedIdStateProvider.notifier).state = id; + }, + onClose: () { + ref.read(collectionStateNotifierProvider.notifier).remove(id: id); + }, + ); + }).toList(), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/tab_request_card.dart b/lib/widgets/tab_request_card.dart new file mode 100644 index 000000000..a966e0afa --- /dev/null +++ b/lib/widgets/tab_request_card.dart @@ -0,0 +1,85 @@ + +import 'package:apidash_core/apidash_core.dart'; +import 'package:apidash_design_system/apidash_design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:apidash/consts.dart'; +import 'package:apidash/utils/utils.dart'; +import 'package:apidash/widgets/texts.dart'; + +class TabRequestCard extends StatelessWidget { + const TabRequestCard({ + super.key, + required this.apiType, + required this.method, + required this.name, + required this.isSelected, + this.onTap, + this.onClose, + }); + + final APIType apiType; + final HTTPVerb method; + final String name; + final bool isSelected; + final VoidCallback? onTap; + final VoidCallback? onClose; + + @override + Widget build(BuildContext context) { + return Card( + elevation: isSelected ? 1 : 0, + shape: const RoundedRectangleBorder( + borderRadius: kBorderRadius8, + ), + margin: const EdgeInsets.only(right: 4), + child: Stack( + children: [ + InkWell( + onTap: onTap, + borderRadius: kBorderRadius8, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SidebarRequestCardTextBox( + apiType: apiType, + method: method, + ), + kHSpacer8, + Text( + name, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + kHSpacer8, + GestureDetector( + onTap: onClose, + child: Icon( + Icons.close, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + if (isSelected) + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: 2, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ); + } +} \ No newline at end of file From 2c417f7de434c6df98edb6e7217fac97a6b31516 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 18 Mar 2025 00:29:24 +0530 Subject: [PATCH 2/4] This commit the fixes all identified bugs, ensuring: Tabs display in the correct order. Tabs display in the correct order. New or duplicated requests add to visible tabs without overwriting others. Selecting from the collection pane opens the corresponding tab. Selecting from the collection pane opens the corresponding tab. Signed-off-by: Balasubramaniam12007 --- lib/providers/collection_providers.dart | 47 ++++++++++++++--- lib/screens/home_page/collection_pane.dart | 3 ++ .../home_page/editor_pane/editor_request.dart | 3 +- .../home_page/editor_pane/tab_pane.dart | 51 +++++++++++-------- lib/widgets/tab_request_card.dart | 18 ++----- 5 files changed, 77 insertions(+), 45 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 35bc4aa0c..22d0f023c 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -32,6 +32,33 @@ final StateNotifierProvider?> hiveHandler, )); +final visibleTabsProvider = StateNotifierProvider>((ref) { + return VisibleTabsNotifier(ref); +}); + +class VisibleTabsNotifier extends StateNotifier> { + VisibleTabsNotifier(this.ref) : super({}); + + final Ref ref; + + void toggleVisibility(String id) { + state = Set.from(state); + if (state.contains(id)) { + state.remove(id); // Hide the tab + } else { + state.add(id); // Show the tab (if reopened) + } + } + + void showAll() { + state = ref.read(requestSequenceProvider).toSet(); + } + + void addSingleTab(String id) { + state = {id}; + } +} + class CollectionStateNotifier extends StateNotifier?> { CollectionStateNotifier( @@ -45,8 +72,6 @@ class CollectionStateNotifier state!.keys.first, ]; } - ref.read(selectedIdStateProvider.notifier).state = - ref.read(requestSequenceProvider)[0]; }); } @@ -77,6 +102,7 @@ class CollectionStateNotifier .read(requestSequenceProvider.notifier) .update((state) => [id, ...state]); ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id; + ref.read(visibleTabsProvider.notifier).toggleVisibility(id); unsave(); } @@ -97,6 +123,7 @@ class CollectionStateNotifier .read(requestSequenceProvider.notifier) .update((state) => [id, ...state]); ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id; + ref.read(visibleTabsProvider.notifier).toggleVisibility(id); unsave(); } @@ -110,21 +137,23 @@ class CollectionStateNotifier void remove({String? id}) { final rId = id ?? ref.read(selectedIdStateProvider); + if (rId == null) return; + var itemIds = ref.read(requestSequenceProvider); - int idx = itemIds.indexOf(rId!); + int idx = itemIds.indexOf(rId); cancelHttpRequest(rId); itemIds.remove(rId); ref.read(requestSequenceProvider.notifier).state = [...itemIds]; + ref.read(visibleTabsProvider.notifier).state = + ref.read(visibleTabsProvider).where((tabId) => tabId != rId).toSet(); + String? newId; - if (idx == 0 && itemIds.isNotEmpty) { - newId = itemIds[0]; - } else if (itemIds.length > 1) { - newId = itemIds[idx - 1]; + if (itemIds.isNotEmpty) { + newId = idx < itemIds.length ? itemIds[idx] : itemIds.last; } else { newId = null; } - ref.read(selectedIdStateProvider.notifier).state = newId; var map = {...state!}; @@ -175,6 +204,7 @@ class CollectionStateNotifier ref.read(requestSequenceProvider.notifier).state = [...itemIds]; ref.read(selectedIdStateProvider.notifier).state = newId; + ref.read(visibleTabsProvider.notifier).toggleVisibility(newId); unsave(); } @@ -201,6 +231,7 @@ class CollectionStateNotifier ref.read(requestSequenceProvider.notifier).state = [...itemIds]; ref.read(selectedIdStateProvider.notifier).state = newId; + ref.read(visibleTabsProvider.notifier).toggleVisibility(newId); unsave(); } diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index 4573b7eaa..cfc7435f0 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -194,6 +194,9 @@ class RequestItem extends ConsumerWidget { editRequestId: editRequestId, onTap: () { ref.read(selectedIdStateProvider.notifier).state = id; + if (!ref.read(visibleTabsProvider).contains(id)) { + ref.read(visibleTabsProvider.notifier).toggleVisibility(id); + } kHomeScaffoldKey.currentState?.closeDrawer(); }, onSecondaryTap: () { diff --git a/lib/screens/home_page/editor_pane/editor_request.dart b/lib/screens/home_page/editor_pane/editor_request.dart index 709381451..18b6ee4bf 100644 --- a/lib/screens/home_page/editor_pane/editor_request.dart +++ b/lib/screens/home_page/editor_pane/editor_request.dart @@ -17,7 +17,6 @@ class RequestEditor extends StatelessWidget { padding: kPb10, child: Column( children: [ - const TabPane(), kVSpacer20, Expanded( child: const EditRequestPane(), @@ -41,4 +40,4 @@ class RequestEditor extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/screens/home_page/editor_pane/tab_pane.dart b/lib/screens/home_page/editor_pane/tab_pane.dart index 6c84cda26..7e11c2366 100644 --- a/lib/screens/home_page/editor_pane/tab_pane.dart +++ b/lib/screens/home_page/editor_pane/tab_pane.dart @@ -5,7 +5,6 @@ import 'package:apidash/consts.dart'; import 'package:apidash/providers/collection_providers.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/widgets/tab_request_card.dart'; -import 'package:apidash/screens/home_page/collection_pane.dart'; import 'package:apidash/utils/utils.dart'; class TabPane extends ConsumerWidget { @@ -16,6 +15,7 @@ class TabPane extends ConsumerWidget { final collection = ref.watch(collectionStateNotifierProvider); final requestSequence = ref.watch(requestSequenceProvider); final selectedId = ref.watch(selectedIdStateProvider); + final visibleTabs = ref.watch(visibleTabsProvider); if (collection == null || requestSequence.isEmpty) { return const SizedBox.shrink(); @@ -27,29 +27,36 @@ class TabPane extends ConsumerWidget { child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: MediaQuery.of(context).size.width, - ), + constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width), child: Row( mainAxisAlignment: MainAxisAlignment.start, - children: requestSequence.map((id) { - final request = collection[id]!; - final name = request.name.isNotEmpty - ? request.name - : getRequestTitleFromUrl(request.httpRequestModel?.url) ?? 'Untitled'; - return TabRequestCard( - apiType: request.apiType, - method: request.httpRequestModel!.method, - name: name, - isSelected: selectedId == id, - onTap: () { - ref.read(selectedIdStateProvider.notifier).state = id; - }, - onClose: () { - ref.read(collectionStateNotifierProvider.notifier).remove(id: id); - }, - ); - }).toList(), + children: requestSequence + .where((id) => visibleTabs.contains(id)) + .map((id) { + final request = collection[id]!; + final name = request.name.isNotEmpty + ? request.name + : getRequestTitleFromUrl(request.httpRequestModel?.url) ?? 'Untitled'; + return TabRequestCard( + apiType: request.apiType, + method: request.httpRequestModel!.method, + name: name, + isSelected: selectedId == id, + onTap: () => ref.read(selectedIdStateProvider.notifier).state = id, + onClose: () { + ref.read(visibleTabsProvider.notifier).toggleVisibility(id); + if (selectedId == id) { + final remainingTabs = requestSequence.where((tabId) => visibleTabs.contains(tabId) && tabId != id).toList(); + if (remainingTabs.isNotEmpty) { + ref.read(selectedIdStateProvider.notifier).state = remainingTabs.first; + } else { + ref.read(selectedIdStateProvider.notifier).state = null; + } + } + }, + ); + }) + .toList(), ), ), ), diff --git a/lib/widgets/tab_request_card.dart b/lib/widgets/tab_request_card.dart index a966e0afa..021e8ec3c 100644 --- a/lib/widgets/tab_request_card.dart +++ b/lib/widgets/tab_request_card.dart @@ -1,4 +1,3 @@ - import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_design_system/apidash_design_system.dart'; import 'package:flutter/material.dart'; @@ -28,9 +27,7 @@ class TabRequestCard extends StatelessWidget { Widget build(BuildContext context) { return Card( elevation: isSelected ? 1 : 0, - shape: const RoundedRectangleBorder( - borderRadius: kBorderRadius8, - ), + shape: const RoundedRectangleBorder(borderRadius: kBorderRadius8), margin: const EdgeInsets.only(right: 4), child: Stack( children: [ @@ -42,18 +39,13 @@ class TabRequestCard extends StatelessWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - SidebarRequestCardTextBox( - apiType: apiType, - method: method, - ), + SidebarRequestCardTextBox(apiType: apiType, method: method), kHSpacer8, Text( name, overflow: TextOverflow.ellipsis, maxLines: 1, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), kHSpacer8, GestureDetector( @@ -74,8 +66,8 @@ class TabRequestCard extends StatelessWidget { right: 0, bottom: 0, child: Container( - height: 2, - color: Theme.of(context).colorScheme.primary, + height: 2, + color: Theme.of(context).colorScheme.primary, ), ), ], From e96677177482977ed496fcd2376a88c0767404f1 Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 18 Mar 2025 16:52:38 +0530 Subject: [PATCH 3/4] (feat)(style) Enhance the default request screen Show logo when no request is selected. Display a message with logo when no API requests exist. tested with flutter sample logo Signed-off-by: Balasubramaniam12007 --- .../home_page/editor_pane/editor_default.dart | 75 +++++++++++++------ .../home_page/editor_pane/editor_pane.dart | 12 ++- lib/widgets/logo_apidash.dart | 29 +++++++ lib/widgets/widgets.dart | 1 + 4 files changed, 91 insertions(+), 26 deletions(-) create mode 100644 lib/widgets/logo_apidash.dart diff --git a/lib/screens/home_page/editor_pane/editor_default.dart b/lib/screens/home_page/editor_pane/editor_default.dart index 757d1c300..fb73562e0 100644 --- a/lib/screens/home_page/editor_pane/editor_default.dart +++ b/lib/screens/home_page/editor_pane/editor_default.dart @@ -9,33 +9,62 @@ class RequestEditorDefault extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, + return Stack( children: [ - Text.rich( - TextSpan( - children: [ - TextSpan( - text: "Click ", - style: Theme.of(context).textTheme.titleMedium, - ), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: ElevatedButton( - onPressed: () { - ref.read(collectionStateNotifierProvider.notifier).add(); - }, - child: const Text( - kLabelPlusNew, - style: kTextStyleButton, - ), - ), + Align( + alignment: Alignment.topCenter, + child: Padding( + padding: const EdgeInsets.only(top: 80.0), + child: Opacity( + opacity: 0.1, + child: const FlutterLogo( + size: 400, ), + // TODO: Replace FlutterLogo with apidash_logo + // child: Image.asset( + // 'assets/apidash_logo.png', + // width: X, + // height: X, + // ), + // OR use SVG : + // child: SvgPicture.asset( + // 'assets/apidash_logo.svg', + // width: X, + // height: X, + // ), + ), + ), + ), + Center( + child: Padding( + padding: const EdgeInsets.only(top: 200.0), + child: Text.rich( TextSpan( - text: " to start drafting a new API request.", - style: Theme.of(context).textTheme.titleMedium, + children: [ + TextSpan( + text: "Click ", + style: Theme.of(context).textTheme.titleMedium, + ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: ElevatedButton( + onPressed: () { + ref.read(collectionStateNotifierProvider.notifier).add(); + }, + child: const Text( + kLabelPlusNew, + style: kTextStyleButton, + ), + ), + ), + TextSpan( + text: " to start drafting a new API request.", + style: Theme.of(context).textTheme.titleMedium, + ), + ], ), - ], + textAlign: TextAlign.center, + ), ), ), ], diff --git a/lib/screens/home_page/editor_pane/editor_pane.dart b/lib/screens/home_page/editor_pane/editor_pane.dart index 7ff67f5c1..d92b19821 100644 --- a/lib/screens/home_page/editor_pane/editor_pane.dart +++ b/lib/screens/home_page/editor_pane/editor_pane.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/providers/providers.dart'; import 'editor_default.dart'; import 'editor_request.dart'; +import 'package:apidash/widgets/logo_apidash.dart'; class RequestEditorPane extends ConsumerWidget { const RequestEditorPane({ @@ -12,10 +13,15 @@ class RequestEditorPane extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final selectedId = ref.watch(selectedIdStateProvider); - if (selectedId == null) { + final collection = ref.watch(collectionStateNotifierProvider); + + if (collection == null || collection.isEmpty) { //NO collection (empty or null) -> Show logo + text return const RequestEditorDefault(); - } else { - return const RequestEditor(); } + if (selectedId == null) { // No selectedId -> Show only the logo + return const LogoApidash(); + } + + return const RequestEditor(); } } diff --git a/lib/widgets/logo_apidash.dart b/lib/widgets/logo_apidash.dart new file mode 100644 index 000000000..8428172a2 --- /dev/null +++ b/lib/widgets/logo_apidash.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class LogoApidash extends StatelessWidget { + const LogoApidash({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Opacity( + opacity: 0.1, + child: const FlutterLogo( + size: 400, + ), + // TODO: Replace FlutterLogo with apidash_logo + // child: Image.asset( + // 'assets/apidash_logo.png', + // width: X, + // height: X, + // ), + // OR use SVG: + // child: SvgPicture.asset( + // 'assets/apidash_logo.svg', + // width: X, + // height: X, + // ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index f64276042..0d4667803 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -61,3 +61,4 @@ export 'texts.dart'; export 'uint8_audio_player.dart'; export 'window_caption.dart'; export 'workspace_selector.dart'; +export 'logo_apidash.dart'; \ No newline at end of file From 7da87a5d1ca7bf486d78b279cbd8f8c0c34b500e Mon Sep 17 00:00:00 2001 From: Balasubramaniam12007 Date: Tue, 18 Mar 2025 19:27:50 +0530 Subject: [PATCH 4/4] (style) :Updated tab component to display the close button only when hovered Signed-off-by: Balasubramaniam12007 --- lib/providers/collection_providers.dart | 3 + .../home_page/editor_pane/tab_pane.dart | 6 +- lib/widgets/tab_request_card.dart | 73 ++++++++++++------- 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index 22d0f023c..e03c36c46 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -32,6 +32,7 @@ final StateNotifierProvider?> hiveHandler, )); +// Manages the set of visible tab IDs final visibleTabsProvider = StateNotifierProvider>((ref) { return VisibleTabsNotifier(ref); }); @@ -41,6 +42,7 @@ class VisibleTabsNotifier extends StateNotifier> { final Ref ref; + // Toggles a tab’s visibility in the tab bar void toggleVisibility(String id) { state = Set.from(state); if (state.contains(id)) { @@ -154,6 +156,7 @@ class CollectionStateNotifier } else { newId = null; } + ref.read(selectedIdStateProvider.notifier).state = newId; var map = {...state!}; diff --git a/lib/screens/home_page/editor_pane/tab_pane.dart b/lib/screens/home_page/editor_pane/tab_pane.dart index 7e11c2366..13cdae7a9 100644 --- a/lib/screens/home_page/editor_pane/tab_pane.dart +++ b/lib/screens/home_page/editor_pane/tab_pane.dart @@ -17,6 +17,7 @@ class TabPane extends ConsumerWidget { final selectedId = ref.watch(selectedIdStateProvider); final visibleTabs = ref.watch(visibleTabsProvider); + // Prevents rendering if data isn’t ready if (collection == null || requestSequence.isEmpty) { return const SizedBox.shrink(); } @@ -24,12 +25,13 @@ class TabPane extends ConsumerWidget { return Container( color: Theme.of(context).colorScheme.surfaceContainerLowest, padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - child: SingleChildScrollView( + child: SingleChildScrollView( // horizontal scrolling for the tab bar scrollDirection: Axis.horizontal, child: ConstrainedBox( constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width), child: Row( mainAxisAlignment: MainAxisAlignment.start, + // Filters and maps only visible tabs for rendering children: requestSequence .where((id) => visibleTabs.contains(id)) .map((id) { @@ -37,12 +39,14 @@ class TabPane extends ConsumerWidget { final name = request.name.isNotEmpty ? request.name : getRequestTitleFromUrl(request.httpRequestModel?.url) ?? 'Untitled'; + return TabRequestCard( apiType: request.apiType, method: request.httpRequestModel!.method, name: name, isSelected: selectedId == id, onTap: () => ref.read(selectedIdStateProvider.notifier).state = id, + // Manages tab closure and selection adjustment onClose: () { ref.read(visibleTabsProvider.notifier).toggleVisibility(id); if (selectedId == id) { diff --git a/lib/widgets/tab_request_card.dart b/lib/widgets/tab_request_card.dart index 021e8ec3c..36acbeeb8 100644 --- a/lib/widgets/tab_request_card.dart +++ b/lib/widgets/tab_request_card.dart @@ -5,7 +5,7 @@ import 'package:apidash/consts.dart'; import 'package:apidash/utils/utils.dart'; import 'package:apidash/widgets/texts.dart'; -class TabRequestCard extends StatelessWidget { +class TabRequestCard extends StatefulWidget { const TabRequestCard({ super.key, required this.apiType, @@ -23,44 +23,61 @@ class TabRequestCard extends StatelessWidget { final VoidCallback? onTap; final VoidCallback? onClose; + @override + State createState() => _TabRequestCardState(); +} + +class _TabRequestCardState extends State { + bool _isHovering = false; + @override Widget build(BuildContext context) { return Card( - elevation: isSelected ? 1 : 0, + elevation: widget.isSelected ? 1 : 0, shape: const RoundedRectangleBorder(borderRadius: kBorderRadius8), margin: const EdgeInsets.only(right: 4), child: Stack( children: [ - InkWell( - onTap: onTap, - borderRadius: kBorderRadius8, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SidebarRequestCardTextBox(apiType: apiType, method: method), - kHSpacer8, - Text( - name, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: TextStyle(color: Theme.of(context).colorScheme.onSurface), - ), - kHSpacer8, - GestureDetector( - onTap: onClose, - child: Icon( - Icons.close, - size: 16, - color: Theme.of(context).colorScheme.onSurfaceVariant, + // MouseRegion to detect hover state + MouseRegion( + onEnter: (_) => setState(() => _isHovering = true), + onExit: (_) => setState(() => _isHovering = false), + child: InkWell( + onTap: widget.onTap, + borderRadius: kBorderRadius8, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SidebarRequestCardTextBox(apiType: widget.apiType, method: widget.method), + kHSpacer8, + Text( + widget.name, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), - ), - ], + kHSpacer8, + // Show close button only when hovering + if (_isHovering) + GestureDetector( + onTap: widget.onClose, + child: Icon( + Icons.close, + size: 16, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + // Add space even when close button is not visible + if (!_isHovering) + const SizedBox(width: 16), + ], + ), ), ), ), - if (isSelected) + if (widget.isSelected) // Adds a visual indicator for the selected tab as mentioned in wireframe Positioned( left: 0, right: 0,