From 4a8d787f31e6f84d751de2a85f9d56e61c43eec7 Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Sat, 6 Dec 2025 02:07:05 +0530 Subject: [PATCH] feat: Moved Workspace Selector to the Titlebar of the window. (#6319) * refactor: update sidebar components and styles, replace TitleBar with SidebarHeader, and enhance collections search functionality * refactor: improve event listener management in AppTitleBar and clean up SidebarHeader styles * fix: ensure safe access to layout preferences in AppTitleBar and set default order in SidebarHeader * refactor: centralize toTitleCase utility and remove redundant implementations in AppTitleBar and WorkspaceSelector * feat: enhance accessibility and testing for sidebar and devtools toggle buttons in AppTitleBar * chore: quick fix on a flaky test --------- Co-authored-by: Bijin A B --- .../components/AppTitleBar/StyledWrapper.js | 248 +++++++++++++ .../src/components/AppTitleBar/index.js | 244 +++++++++++++ .../ClientCertSettings/index.js | 2 +- .../Icons/IconBottombarToggle/index.js | 16 + .../RequestTabs/CollectionToolBar/index.js | 4 +- .../Collections/Collection/StyledWrapper.js | 2 +- .../CollectionSearch/StyledWrapper.js | 65 ++++ .../Collections/CollectionSearch/index.js | 25 +- .../CollectionsHeader/StyledWrapper.js | 24 -- .../Collections/CollectionsHeader/index.js | 86 ----- .../Sidebar/Collections/StyledWrapper.js | 33 +- .../components/Sidebar/Collections/index.js | 16 +- .../CloseWorkspace/index.js | 0 .../Sidebar/SidebarHeader/StyledWrapper.js | 110 ++++++ .../WorkspaceSelector/index.js | 10 +- .../components/Sidebar/SidebarHeader/index.js | 337 ++++++++++++++++++ .../src/components/Sidebar/StyledWrapper.js | 21 +- .../Sidebar/TitleBar/StyledWrapper.js | 154 -------- .../src/components/Sidebar/TitleBar/index.js | 181 ---------- .../bruno-app/src/components/Sidebar/index.js | 24 +- .../src/components/StatusBar/index.js | 13 +- .../src/components/WorkspaceHome/index.js | 2 +- packages/bruno-app/src/pages/Bruno/index.js | 4 +- packages/bruno-app/src/providers/App/index.js | 19 +- .../src/providers/ReduxStore/slices/app.js | 2 +- packages/bruno-app/src/utils/common/index.js | 20 ++ packages/bruno-electron/src/index.js | 17 +- .../collection/draft/draft-indicator.spec.ts | 2 +- .../sidebar-toggle/sidebar-toggle.spec.js | 2 +- tests/response/response-actions.spec.ts | 2 +- 30 files changed, 1158 insertions(+), 527 deletions(-) create mode 100644 packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/AppTitleBar/index.js create mode 100644 packages/bruno-app/src/components/Icons/IconBottombarToggle/index.js create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/StyledWrapper.js delete mode 100644 packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/StyledWrapper.js delete mode 100644 packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/index.js rename packages/bruno-app/src/components/Sidebar/{TitleBar => SidebarHeader}/CloseWorkspace/index.js (100%) create mode 100644 packages/bruno-app/src/components/Sidebar/SidebarHeader/StyledWrapper.js rename packages/bruno-app/src/components/Sidebar/{TitleBar => SidebarHeader}/WorkspaceSelector/index.js (95%) create mode 100644 packages/bruno-app/src/components/Sidebar/SidebarHeader/index.js delete mode 100644 packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js delete mode 100644 packages/bruno-app/src/components/Sidebar/TitleBar/index.js diff --git a/packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js b/packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js new file mode 100644 index 000000000..b625d59be --- /dev/null +++ b/packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js @@ -0,0 +1,248 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + height: 36px; + display: flex; + align-items: center; + background: ${(props) => props.theme.sidebar.bg}; + border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg}; + -webkit-app-region: drag; + user-select: none; + + .titlebar-content { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 100%; + padding: 0 12px; + padding-left: 70px; /* Space for macOS window controls */ + transition: padding-left 0.15s ease; + } + + /* When in full screen, no traffic lights so reduce padding */ + &.fullscreen .titlebar-content { + padding-left: 4px; + } + + /* Remove drag region from interactive elements */ + .workspace-name-container, + .dropdown-item, + .home-button, + .env-selector-trigger, + .dropdown, + button { + -webkit-app-region: no-drag; + } + + /* Left section */ + .titlebar-left { + display: flex; + align-items: center; + flex-shrink: 0; + margin-left: 10px; + -webkit-app-region: no-drag; + } + + /* When in full screen, no traffic lights so remove margin-left */ + &.fullscreen .titlebar-left { + margin-left: 0px; + } + + /* Home button */ + .home-button { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + background: transparent; + border-radius: 6px; + cursor: pointer; + color: ${(props) => props.theme.sidebar.color}; + transition: background 0.15s ease; + + &:hover { + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + } + + /* Workspace Name Dropdown Trigger */ + .workspace-name-container { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 10px; + border-radius: 6px; + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + + .workspace-name { + font-size: 13px; + font-weight: 500; + color: ${(props) => props.theme.sidebar.color}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 180px; + } + + .chevron-icon { + flex-shrink: 0; + color: ${(props) => props.theme.sidebar.muted}; + transition: transform 0.2s ease; + } + } + + /* Center section - Bruno branding */ + .titlebar-center { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 6px; + pointer-events: none; + + .bruno-text { + font-size: 13px; + font-weight: 600; + color: ${(props) => props.theme.sidebar.muted}; + letter-spacing: 0.5px; + } + } + + /* Right section */ + .titlebar-right { + display: flex; + align-items: center; + justify-content: flex-end; + flex-shrink: 0; + } + + /* Action buttons in right section */ + .titlebar-action-button { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + background: transparent; + border-radius: 6px; + cursor: pointer; + color: ${(props) => props.theme.sidebar.color}; + transition: background 0.15s ease; + + &:hover { + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + + svg { + color: ${(props) => props.theme.sidebar.color}; + } + } + + /* Draggable region */ + .drag-region { + flex: 1; + height: 100%; + -webkit-app-region: drag; + } + + /* Workspace Dropdown Styles */ + .workspace-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 10px !important; + margin: 0 !important; + + &.active { + .check-icon { + opacity: 1; + } + } + + &:hover { + .pin-btn:not(.pinned) { + opacity: 1; + } + } + + .workspace-name { + flex: 1; + min-width: 0; + font-size: 13px; + font-weight: 400; + color: ${(props) => props.theme.dropdown.color}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .workspace-actions { + display: flex; + align-items: center; + gap: 4px; + margin-left: 8px; + flex-shrink: 0; + pointer-events: none; + + > * { + pointer-events: auto; + } + } + + .check-icon { + color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow}; + flex-shrink: 0; + } + + .pin-btn { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: none; + background: transparent; + border-radius: 4px; + cursor: pointer; + color: ${(props) => props.theme.dropdown.mutedText}; + transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease; + opacity: 0; + + &.pinned { + opacity: 1; + } + + &:hover { + background: ${(props) => props.theme.dropdown.hoverBg}; + color: ${(props) => props.theme.dropdown.mutedText}; + } + } + } + + /* Adjust for non-macOS platforms */ + body:not(.os-mac) & { + .titlebar-content { + padding-left: 12px; + } + } + + /* Leave room for Windows caption buttons when the overlay is enabled */ + body.os-windows & { + .titlebar-content { + padding-right: 120px; + } + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/AppTitleBar/index.js b/packages/bruno-app/src/components/AppTitleBar/index.js new file mode 100644 index 000000000..5f3dce40c --- /dev/null +++ b/packages/bruno-app/src/components/AppTitleBar/index.js @@ -0,0 +1,244 @@ +import { IconCheck, IconChevronDown, IconFolder, IconHome, IconLayoutColumns, IconLayoutRows, IconPin, IconPinned, IconPlus } from '@tabler/icons'; +import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import toast from 'react-hot-toast'; +import { useDispatch, useSelector } from 'react-redux'; + +import { savePreferences, showHomePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app'; +import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs'; +import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; +import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces'; + +import Bruno from 'components/Bruno'; +import Dropdown from 'components/Dropdown'; +import IconSidebarToggle from 'components/Icons/IconSidebarToggle'; +import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace'; + +import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index'; +import StyledWrapper from './StyledWrapper'; +import { toTitleCase } from 'utils/common/index'; + +const AppTitleBar = () => { + const dispatch = useDispatch(); + const [isFullScreen, setIsFullScreen] = useState(false); + + // Listen for fullscreen changes + useEffect(() => { + const { ipcRenderer } = window; + if (!ipcRenderer) return; + + const removeEnterFullScreenListener = ipcRenderer.on('main:enter-full-screen', () => { + setIsFullScreen(true); + }); + + const removeLeaveFullScreenListener = ipcRenderer.on('main:leave-full-screen', () => { + setIsFullScreen(false); + }); + + return () => { + removeEnterFullScreenListener(); + removeLeaveFullScreenListener(); + }; + }, []); + + // Get workspace info + const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); + const preferences = useSelector((state) => state.app.preferences); + const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed); + const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen); + const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); + + // Sort workspaces according to preferences + const sortedWorkspaces = useMemo(() => { + return sortWorkspaces(workspaces, preferences); + }, [workspaces, preferences]); + + const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false); + const [showWorkspaceDropdown, setShowWorkspaceDropdown] = useState(false); + const workspaceDropdownTippyRef = useRef(); + const onWorkspaceDropdownCreate = (ref) => (workspaceDropdownTippyRef.current = ref); + + const WorkspaceName = forwardRef((props, ref) => { + return ( +
setShowWorkspaceDropdown(!showWorkspaceDropdown)}> + {toTitleCase(activeWorkspace?.name) || 'Default Workspace'} + +
+ ); + }); + + const handleHomeClick = () => { + dispatch(showHomePage()); + }; + + const handleWorkspaceSwitch = (workspaceUid) => { + dispatch(switchWorkspace(workspaceUid)); + setShowWorkspaceDropdown(false); + toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`); + }; + + const handleOpenWorkspace = async () => { + setShowWorkspaceDropdown(false); + try { + await dispatch(openWorkspaceDialog()); + toast.success('Workspace opened successfully'); + } catch (error) { + toast.error(error.message || 'Failed to open workspace'); + } + }; + + const handleCreateWorkspace = () => { + setShowWorkspaceDropdown(false); + setCreateWorkspaceModalOpen(true); + }; + + const handlePinWorkspace = useCallback((workspaceUid, e) => { + e.preventDefault(); + e.stopPropagation(); + const newPreferences = toggleWorkspacePin(workspaceUid, preferences); + dispatch(savePreferences(newPreferences)); + }, [dispatch, preferences]); + + const orientation = preferences?.layout?.responsePaneOrientation || 'horizontal'; + + const handleToggleSidebar = () => { + dispatch(toggleSidebarCollapse()); + }; + + const handleToggleDevtools = () => { + if (isConsoleOpen) { + dispatch(closeConsole()); + } else { + dispatch(openConsole()); + } + }; + + const handleToggleVerticalLayout = () => { + const newOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal'; + const updatedPreferences = { + ...preferences, + layout: { + ...preferences?.layout || {}, + responsePaneOrientation: newOrientation + } + }; + dispatch(savePreferences(updatedPreferences)); + }; + + return ( + + {createWorkspaceModalOpen && ( + setCreateWorkspaceModalOpen(false)} /> + )} + +
+ {/* Left section: Home + Workspace */} +
+ + + {/* Workspace Dropdown */} + } + placement="bottom-start" + style="new" + visible={showWorkspaceDropdown} + onClickOutside={() => setShowWorkspaceDropdown(false)} + > + {sortedWorkspaces.map((workspace) => { + const isActive = workspace.uid === activeWorkspaceUid; + const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid); + + return ( +
handleWorkspaceSwitch(workspace.uid)} + > + {toTitleCase(workspace.name)} +
+ {workspace.type !== 'default' && ( + + )} + {isActive && } +
+
+ ); + })} + +
Workspaces
+ +
+ + Create workspace +
+
+ + Open workspace +
+
+
+ + {/* Center section: Bruno logo + text */} +
+ + Bruno +
+ + {/* Right section: Action buttons */} +
+ {/* Toggle sidebar */} + + + {/* Toggle devtools */} + + + {/* Toggle vertical layout */} + + +
+
+
+ ); +}; + +export default AppTitleBar; diff --git a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js index b871be3e3..2b13f529e 100644 --- a/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js @@ -373,7 +373,7 @@ const ClientCertSettings = ({ collection }) => { ) : null}
-
diff --git a/packages/bruno-app/src/components/Icons/IconBottombarToggle/index.js b/packages/bruno-app/src/components/Icons/IconBottombarToggle/index.js new file mode 100644 index 000000000..cde4dc298 --- /dev/null +++ b/packages/bruno-app/src/components/Icons/IconBottombarToggle/index.js @@ -0,0 +1,16 @@ +import React from 'react'; + +const IconBottombarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, className = '', ...rest }) => { + return ( + + + + + {!collapsed && ( + + )} + + ); +}; + +export default IconBottombarToggle; diff --git a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js index 396b5cbd2..fe88ee9cc 100644 --- a/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js +++ b/packages/bruno-app/src/components/RequestTabs/CollectionToolBar/index.js @@ -1,6 +1,6 @@ import React from 'react'; import { uuid } from 'utils/common'; -import { IconFiles, IconRun, IconEye, IconSettings } from '@tabler/icons'; +import { IconBox, IconRun, IconEye, IconSettings } from '@tabler/icons'; import EnvironmentSelector from 'components/Environments/EnvironmentSelector'; import { addTab } from 'providers/ReduxStore/slices/tabs'; import { useDispatch } from 'react-redux'; @@ -45,7 +45,7 @@ const CollectionToolBar = ({ collection }) => {
- + {collection?.name}
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js index bcce56e5e..839522f7a 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js @@ -5,7 +5,7 @@ const Wrapper = styled.div` height: 1.75rem; cursor: pointer; user-select: none; - padding-left: 8px; + padding-left: 4px; border: ${(props) => props.theme.dragAndDrop.borderStyle} transparent; .rotate-90 { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/StyledWrapper.js new file mode 100644 index 000000000..98f839ffe --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/StyledWrapper.js @@ -0,0 +1,65 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + margin: 4px 10px 8px 10px; + position: relative; + + .search-icon { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + color: ${(props) => props.theme.sidebar.muted}; + pointer-events: none; + } + + input { + width: 100%; + height: 32px; + padding: 0 32px 0 32px; + font-size: 12px; + color: ${(props) => props.theme.sidebar.color}; + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + border: 1px solid transparent; + border-radius: 6px; + outline: none; + transition: all 0.15s ease; + + &::placeholder { + color: ${(props) => props.theme.sidebar.muted}; + } + + &:hover { + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + border-color: ${(props) => props.theme.sidebar.muted}40; + } + + &:focus { + background: ${(props) => props.theme.sidebar.bg}; + border-color: ${(props) => props.theme.sidebar.muted}80; + } + } + + .clear-icon { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 4px; + color: ${(props) => props.theme.sidebar.muted}; + cursor: pointer; + transition: all 0.15s ease; + + &:hover { + color: ${(props) => props.theme.sidebar.color}; + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js b/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js index db7499c75..21150c473 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js @@ -1,39 +1,28 @@ import { IconSearch, IconX } from '@tabler/icons'; +import StyledWrapper from './StyledWrapper'; const CollectionSearch = ({ searchText, setSearchText }) => { return ( -
-
- - - -
+ + setSearchText(e.target.value.toLowerCase())} /> {searchText !== '' && ( -
- { - setSearchText(''); - }} - > - - +
setSearchText('')}> +
)} -
+
); }; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/StyledWrapper.js deleted file mode 100644 index ef11bec64..000000000 --- a/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/StyledWrapper.js +++ /dev/null @@ -1,24 +0,0 @@ -import styled from 'styled-components'; - -const Wrapper = styled.div` - .collections-badge { - margin-inline: 0.5rem; - background-color: ${(props) => props.theme.sidebar.badge.bg}; - border-radius: 5px; - - .caret { - margin-left: 0.25rem; - color: rgb(140, 140, 140); - fill: rgb(140, 140, 140); - } - - .collections-header-actions { - .collection-action-button { - opacity: 0; - transition: opacity 0.2s ease-in-out; - } - } - } -`; - -export default Wrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/index.js b/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/index.js deleted file mode 100644 index 935239723..000000000 --- a/packages/bruno-app/src/components/Sidebar/Collections/CollectionsHeader/index.js +++ /dev/null @@ -1,86 +0,0 @@ -import { useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { IconArrowsSort, IconFolders, IconSortAscendingLetters, IconSortDescendingLetters } from '@tabler/icons'; -import CloseAllIcon from 'components/Icons/CloseAll'; -import { sortCollections } from 'providers/ReduxStore/slices/collections/index'; -import RemoveCollectionsModal from '../RemoveCollectionsModal'; -import StyledWrapper from './StyledWrapper'; - -const CollectionsHeader = () => { - const dispatch = useDispatch(); - const { collections } = useSelector((state) => state.collections); - const { collectionSortOrder } = useSelector((state) => state.collections); - const [collectionsToClose, setCollectionsToClose] = useState([]); - - const sortCollectionOrder = () => { - let order; - switch (collectionSortOrder) { - case 'default': - order = 'alphabetical'; - break; - case 'alphabetical': - order = 'reverseAlphabetical'; - break; - case 'reverseAlphabetical': - order = 'default'; - break; - } - dispatch(sortCollections({ order })); - }; - - let sortIcon; - if (collectionSortOrder === 'default') { - sortIcon = ; - } else if (collectionSortOrder === 'alphabetical') { - sortIcon = ; - } else { - sortIcon = ; - } - - const selectAllCollectionsToClose = () => { - setCollectionsToClose(collections.map((c) => c.uid)); - }; - - const clearCollectionsToClose = () => { - setCollectionsToClose([]); - }; - - return ( - -
-
- - - - Collections -
- {collections.length >= 1 && ( -
- - - {collectionsToClose.length > 0 && ( - - )} -
- )} -
-
- ); -}; - -export default CollectionsHeader; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js index 2854eeb09..53a7906de 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js @@ -1,12 +1,35 @@ import styled from 'styled-components'; const Wrapper = styled.div` - span.close-icon { - color: ${(props) => props.theme.colors.text.muted}; - } + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; + padding-top: 4px; - &:hover .collections-badge .collections-header-actions .collection-action-button { - opacity: 1; + .collections-list { + min-height: 0; + padding: 0 4px; + padding-top: 4px; + overflow-y: auto; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: ${(props) => props.theme.scrollbar.color}; + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb:hover { + background: ${(props) => props.theme.scrollbar.color}; + } } `; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/index.js b/packages/bruno-app/src/components/Sidebar/Collections/index.js index 31bb9ac91..c0ba1175c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/index.js @@ -5,6 +5,7 @@ import CreateCollection from '../CreateCollection'; import StyledWrapper from './StyledWrapper'; import CreateOrOpenCollection from './CreateOrOpenCollection'; import CollectionSearch from './CollectionSearch/index'; +import { useMemo } from 'react'; const Collections = ({ showSearch }) => { const [searchText, setSearchText] = useState(''); @@ -14,13 +15,12 @@ const Collections = ({ showSearch }) => { const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid) || workspaces.find((w) => w.type === 'default'); - let workspaceCollections = []; - - if (activeWorkspace?.collections?.length) { - workspaceCollections = activeWorkspace.collections.map((wc) => { - return collections.find((c) => c.pathname === wc.path); - }).filter(Boolean); - } + const workspaceCollections = useMemo(() => { + if (!activeWorkspace) return []; + return collections.filter((c) => + activeWorkspace.collections?.some((wc) => wc.path === c.pathname) + ); + }, [activeWorkspace, collections]); if (!workspaceCollections || !workspaceCollections.length) { return ( @@ -42,7 +42,7 @@ const Collections = ({ showSearch }) => { )} -
+
{workspaceCollections && workspaceCollections.length ? workspaceCollections.map((c) => { return ( diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/CloseWorkspace/index.js b/packages/bruno-app/src/components/Sidebar/SidebarHeader/CloseWorkspace/index.js similarity index 100% rename from packages/bruno-app/src/components/Sidebar/TitleBar/CloseWorkspace/index.js rename to packages/bruno-app/src/components/Sidebar/SidebarHeader/CloseWorkspace/index.js diff --git a/packages/bruno-app/src/components/Sidebar/SidebarHeader/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/SidebarHeader/StyledWrapper.js new file mode 100644 index 000000000..457c9b826 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/SidebarHeader/StyledWrapper.js @@ -0,0 +1,110 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + padding: 8px 4px 6px 10px; + + .sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + + /* Section Title (single view mode) - with separator */ + &.single-view { + border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + + /* Section Title (single view mode) */ + .section-title { + display: flex; + align-items: center; + gap: 6px; + color: ${(props) => props.theme.sidebar.color}; + font-size: 12px; + font-weight: 600; + padding: 2px 0; + + svg { + color: ${(props) => props.theme.sidebar.muted}; + } + } + + /* View Tabs (multi-view mode) */ + .view-tabs { + display: flex; + align-items: center; + gap: 2px; + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + border-radius: 6px; + padding: 2px; + } + + .view-tab { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border: none; + background: transparent; + border-radius: 4px; + cursor: pointer; + color: ${(props) => props.theme.sidebar.muted}; + font-size: 11px; + font-weight: 500; + transition: all 0.15s ease; + white-space: nowrap; + + &:hover { + color: ${(props) => props.theme.sidebar.color}; + } + + &.active { + background: ${(props) => props.theme.sidebar.bg}; + color: ${(props) => props.theme.sidebar.color}; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + svg { + flex-shrink: 0; + } + + span { + display: none; + } + + @media (min-width: 280px) { + span { + display: inline; + } + } + } + + /* Header Actions */ + .header-actions { + display: flex; + align-items: center; + gap: 1px; + } + + .action-button { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + background: transparent; + border-radius: 4px; + cursor: pointer; + color: ${(props) => props.theme.sidebar.muted}; + transition: all 0.15s ease; + + &:hover { + background: ${(props) => props.theme.dropdown?.hoverBg || props.theme.sidebar?.collection?.item?.hoverBg}; + color: ${(props) => props.theme.dropdown?.mutedText || props.theme.text?.muted || '#888'}; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/WorkspaceSelector/index.js b/packages/bruno-app/src/components/Sidebar/SidebarHeader/WorkspaceSelector/index.js similarity index 95% rename from packages/bruno-app/src/components/Sidebar/TitleBar/WorkspaceSelector/index.js rename to packages/bruno-app/src/components/Sidebar/SidebarHeader/WorkspaceSelector/index.js index 86d279bc7..aeaed8079 100644 --- a/packages/bruno-app/src/components/Sidebar/TitleBar/WorkspaceSelector/index.js +++ b/packages/bruno-app/src/components/Sidebar/SidebarHeader/WorkspaceSelector/index.js @@ -9,6 +9,7 @@ import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces'; import Dropdown from 'components/Dropdown'; import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace'; +import { toTitleCase } from 'utils/common/index'; const WorkspaceSelector = () => { const dispatch = useDispatch(); @@ -27,15 +28,6 @@ const WorkspaceSelector = () => { const dropdownTippyRef = useRef(); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); - const toTitleCase = (str) => { - if (!str) return ''; - if (str === 'default') return 'Default'; - return str - .split(/[\s-_]+/) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) - .join(' '); - }; - const WorkspaceName = forwardRef((props, ref) => { return (
setShowDropdown(!showDropdown)}> diff --git a/packages/bruno-app/src/components/Sidebar/SidebarHeader/index.js b/packages/bruno-app/src/components/Sidebar/SidebarHeader/index.js new file mode 100644 index 000000000..747501828 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/SidebarHeader/index.js @@ -0,0 +1,337 @@ +import { + IconArrowsSort, + IconBox, + IconDeviceDesktop, + IconDotsVertical, + IconDownload, + IconFolder, + IconPlus, + IconSearch, + IconSortAscendingLetters, + IconSortDescendingLetters, + IconSquareX, + IconTrash +} from '@tabler/icons'; +import { useRef, useState } from 'react'; +import toast from 'react-hot-toast'; +import { useDispatch, useSelector } from 'react-redux'; + +import { importCollection, openCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { sortCollections } from 'providers/ReduxStore/slices/collections/index'; +import { importCollectionInWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; + +import Dropdown from 'components/Dropdown'; +import ImportCollection from 'components/Sidebar/ImportCollection'; +import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation'; + +import RemoveCollectionsModal from '../Collections/RemoveCollectionsModal/index'; +import CreateCollection from '../CreateCollection'; +import StyledWrapper from './StyledWrapper'; + +const VIEW_TABS = [ + { id: 'collections', label: 'Collections', icon: IconBox } +]; + +const SidebarHeader = ({ setShowSearch, activeView = 'collections', onViewChange }) => { + const dispatch = useDispatch(); + const { ipcRenderer } = window; + + const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); + const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); + + // Get collection sort order + const { collections } = useSelector((state) => state.collections); + const { collectionSortOrder } = useSelector((state) => state.collections); + const [collectionsToClose, setCollectionsToClose] = useState([]); + + const [importData, setImportData] = useState(null); + const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); + const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false); + const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false); + + const handleImportCollection = ({ rawData, type }) => { + setImportCollectionModalOpen(false); + + if (activeWorkspace && activeWorkspace.type !== 'default') { + dispatch(importCollectionInWorkspace(rawData, activeWorkspace.uid, undefined, type)) + .catch((err) => { + toast.error('An error occurred while importing the collection'); + }); + } else { + setImportData({ rawData, type }); + setImportCollectionLocationModalOpen(true); + } + }; + + const handleImportCollectionLocation = (convertedCollection, collectionLocation) => { + dispatch(importCollection(convertedCollection, collectionLocation)) + .then(() => { + setImportCollectionLocationModalOpen(false); + setImportData(null); + toast.success('Collection imported successfully'); + }) + .catch((err) => { + console.error(err); + toast.error('An error occurred while importing the collection'); + }); + }; + + const addDropdownTippyRef = useRef(); + const onAddDropdownCreate = (ref) => (addDropdownTippyRef.current = ref); + + const actionsDropdownTippyRef = useRef(); + const onActionsDropdownCreate = (ref) => (actionsDropdownTippyRef.current = ref); + + const handleToggleSearch = () => { + if (setShowSearch) { + setShowSearch((prev) => !prev); + } + }; + + const handleSortCollections = () => { + let order; + switch (collectionSortOrder) { + case 'default': + order = 'alphabetical'; + break; + case 'alphabetical': + order = 'reverseAlphabetical'; + break; + case 'reverseAlphabetical': + order = 'default'; + break; + default: + order = 'default'; + break; + } + dispatch(sortCollections({ order })); + }; + + const getSortIcon = () => { + switch (collectionSortOrder) { + case 'alphabetical': + return IconSortDescendingLetters; + case 'reverseAlphabetical': + return IconArrowsSort; + default: + return IconSortAscendingLetters; + } + }; + + const getSortLabel = () => { + switch (collectionSortOrder) { + case 'alphabetical': + return 'Sort Z-A'; + case 'reverseAlphabetical': + return 'Clear sort'; + default: + return 'Sort A-Z'; + } + }; + + const selectAllCollectionsToClose = () => { + setCollectionsToClose(collections.map((c) => c.uid)); + }; + + const clearCollectionsToClose = () => { + setCollectionsToClose([]); + }; + + const handleOpenCollection = () => { + const options = {}; + if (activeWorkspace?.pathname) { + options.workspaceId = activeWorkspace.pathname; + } + + dispatch(openCollection(options)).catch((err) => { + toast.error('An error occurred while opening the collection'); + }); + }; + + const renderModals = () => ( + <> + {createCollectionModalOpen && ( + setCreateCollectionModalOpen(false)} + /> + )} + {importCollectionModalOpen && ( + setImportCollectionModalOpen(false)} + handleSubmit={handleImportCollection} + /> + )} + {importCollectionLocationModalOpen && importData && ( + setImportCollectionLocationModalOpen(false)} + handleSubmit={handleImportCollectionLocation} + /> + )} + + ); + + const isSingleView = VIEW_TABS.length === 1; + + // Render Collections-specific actions + const renderCollectionsActions = () => ( + <> + + {/* Add/Create dropdown */} + + + + )} + placement="bottom-end" + style="new" + > +
Collections
+
{ + setCreateCollectionModalOpen(true); + addDropdownTippyRef.current?.hide(); + }} + > + + Create collection +
+
{ + addDropdownTippyRef.current?.hide(); + setImportCollectionModalOpen(true); + }} + > + + Import collection +
+
{ + handleOpenCollection(); + addDropdownTippyRef.current?.hide(); + }} + > + + Open collection +
+ +
+ + {/* Actions dropdown (sort, close all, etc.) */} + + + + )} + placement="bottom-end" + style="new" + > +
{ + handleSortCollections(); + actionsDropdownTippyRef.current?.hide(); + }} + aria-label="Sort collections" + title="Sort collections" + data-testid="sort-collections-button" + > + {(() => { + const SortIcon = getSortIcon(); + return ; + })()} + {getSortLabel()} +
+
{ + selectAllCollectionsToClose(); + actionsDropdownTippyRef.current?.hide(); + }} + aria-label="Close all collections" + title="Close all collections" + data-testid="close-all-collections-button" + > + + Close all +
+
+ + {collectionsToClose.length > 0 && ( + + )} + + ); + + // Render Second Tab-specific actions + const renderSecondTabActions = () => ( + <> + {/* Add second tab actions here */} + + ); + + // Render the view switcher - either tabs or single title + const renderViewSwitcher = () => { + if (isSingleView) { + // Single view - just show the title + const tab = VIEW_TABS[0]; + const TabIcon = tab.icon; + return ( +
+ + {tab.label} +
+ ); + } + + // Multiple views - show segmented tabs + return ( +
+ {VIEW_TABS.map((tab) => { + const TabIcon = tab.icon; + return ( + + ); + })} +
+ ); + }; + + return ( + + {renderModals()} +
+ {renderViewSwitcher()} + + {/* Action Buttons - Context Sensitive */} +
+ {activeView === 'collections' ? renderCollectionsActions() : renderSecondTabActions()} +
+
+
+ ); +}; + +export default SidebarHeader; diff --git a/packages/bruno-app/src/components/Sidebar/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/StyledWrapper.js index 5cda45b32..40ab5a299 100644 --- a/packages/bruno-app/src/components/Sidebar/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/StyledWrapper.js @@ -28,18 +28,6 @@ const Wrapper = styled.div` top: -0.625rem; } } - - .collection-filter { - input { - border: ${(props) => props.theme.sidebar.search.border}; - border-radius: 2px; - background-color: ${(props) => props.theme.sidebar.search.bg}; - - &:focus { - outline: none; - } - } - } } div.sidebar-drag-handle { @@ -65,6 +53,15 @@ const Wrapper = styled.div` border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.activeBorder}; } } + + .second-tab-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + color: ${(props) => props.theme.sidebar.muted}; + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js deleted file mode 100644 index 288a02e98..000000000 --- a/packages/bruno-app/src/components/Sidebar/TitleBar/StyledWrapper.js +++ /dev/null @@ -1,154 +0,0 @@ -import styled from 'styled-components'; - -const StyledWrapper = styled.div` - .titlebar-container { - display: flex; - align-items: center; - } - - .workspace-name-container { - display: flex; - align-items: center; - gap: 4px; - padding: 6px 10px; - margin-left: 0px; - border-radius: ${(props) => props.theme.border.radius.base}; - cursor: pointer; - transition: all 0.2s ease; - min-width: 0; - flex: 1; - max-width: 120px; - - &:hover { - background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; - } - - .workspace-name { - font-size: ${(props) => props.theme.font.size.base}; - font-weight: 600; - color: ${(props) => props.theme.sidebar.color}; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .chevron-icon { - flex-shrink: 0; - color: ${(props) => props.theme.sidebar.muted}; - transition: transform 0.2s ease; - } - } - - /* Actions Button */ - .actions-container { - margin-left: auto; - display: flex; - align-items: center; - } - - .home-icon-button, - .search-icon-button, - .plus-icon-button { - display: flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: none; - background: transparent; - border-radius: 4px; - cursor: pointer; - transition: background 0.15s ease; - color: ${(props) => props.theme.text}; - } - - .workspace-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 4px 10px !important; - margin: 0 !important; - - &.active { - .check-icon { - opacity: 1; - } - } - - &:hover { - .pin-btn:not(.pinned) { - opacity: 1; - } - } - - .workspace-name { - flex: 1; - min-width: 0; - font-size: ${(props) => props.theme.font.size.base}; - font-weight: 400; - color: ${(props) => props.theme.dropdown.color}; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .workspace-actions { - display: flex; - align-items: center; - gap: 4px; - margin-left: 8px; - flex-shrink: 0; - pointer-events: none; - - > * { - pointer-events: auto; - } - } - - .check-icon { - color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow || '#f0c674'}; - flex-shrink: 0; - } - - .pin-btn { - display: flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - padding: 0; - border: none; - background: transparent; - border-radius: 4px; - cursor: pointer; - color: ${(props) => props.theme.dropdown?.mutedText || props.theme.text?.muted || '#888'}; - transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease; - opacity: 0; - - &.pinned { - opacity: 1; - } - - &:hover { - background: ${(props) => props.theme.dropdown?.hoverBg || props.theme.sidebar?.collection?.item?.hoverBg}; - color: ${(props) => props.theme.dropdown?.mutedText || props.theme.text?.muted || '#888'}; - } - } - } - - .collection-dropdown { - color: ${(props) => props.theme.sidebar.dropdownIcon.color}; - - &:hover { - color: inherit; - } - - .tippy-box { - top: -0.5rem; - position: relative; - user-select: none; - } - } -`; - -export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js b/packages/bruno-app/src/components/Sidebar/TitleBar/index.js deleted file mode 100644 index f2777eb2b..000000000 --- a/packages/bruno-app/src/components/Sidebar/TitleBar/index.js +++ /dev/null @@ -1,181 +0,0 @@ -import { useState, useRef } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import toast from 'react-hot-toast'; -import { IconPlus, IconFolder, IconDownload, IconHome, IconSearch, IconDeviceDesktop } from '@tabler/icons'; - -import { showHomePage } from 'providers/ReduxStore/slices/app'; -import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions'; -import { importCollectionInWorkspace } from 'providers/ReduxStore/slices/workspaces/actions'; - -import Dropdown from 'components/Dropdown'; -import ImportCollection from 'components/Sidebar/ImportCollection'; -import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation'; - -import CreateCollection from '../CreateCollection'; -import WorkspaceSelector from './WorkspaceSelector'; -import StyledWrapper from './StyledWrapper'; - -const TitleBar = ({ showSearch, setShowSearch }) => { - const dispatch = useDispatch(); - const { ipcRenderer } = window; - - const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); - const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid); - - const [importData, setImportData] = useState(null); - const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); - const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false); - const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false); - - const actionsDropdownTippyRef = useRef(); - const onActionsDropdownCreate = (ref) => (actionsDropdownTippyRef.current = ref); - - const handleImportCollection = ({ rawData, type }) => { - setImportCollectionModalOpen(false); - - if (activeWorkspace && activeWorkspace.type !== 'default') { - dispatch(importCollectionInWorkspace(rawData, activeWorkspace.uid, undefined, type)) - .catch((err) => { - toast.error('An error occurred while importing the collection'); - }); - } else { - setImportData({ rawData, type }); - setImportCollectionLocationModalOpen(true); - } - }; - - const handleImportCollectionLocation = (convertedCollection, collectionLocation) => { - dispatch(importCollection(convertedCollection, collectionLocation)) - .then(() => { - setImportCollectionLocationModalOpen(false); - setImportData(null); - toast.success('Collection imported successfully'); - }) - .catch((err) => { - console.error(err); - toast.error('An error occurred while importing the collection'); - }); - }; - - const handleToggleSearch = () => { - if (setShowSearch) { - setShowSearch((prev) => !prev); - } - }; - - const handleOpenCollection = () => { - const options = {}; - if (activeWorkspace?.pathname) { - options.workspaceId = activeWorkspace.pathname; - } - - dispatch(openCollection(options)).catch((err) => { - toast.error('An error occurred while opening the collection'); - }); - }; - - const openDevTools = () => { - ipcRenderer.invoke('renderer:open-devtools'); - }; - - const renderModals = () => ( - <> - {createCollectionModalOpen && ( - setCreateCollectionModalOpen(false)} - /> - )} - {importCollectionModalOpen && ( - setImportCollectionModalOpen(false)} - handleSubmit={handleImportCollection} - /> - )} - {importCollectionLocationModalOpen && importData && ( - setImportCollectionLocationModalOpen(false)} - handleSubmit={handleImportCollectionLocation} - /> - )} - - ); - - return ( - - {renderModals()} -
- - -
- - - {setShowSearch && ( - - )} - - - - - )} - placement="bottom-end" - style="new" - > -
Collections
-
{ - setCreateCollectionModalOpen(true); - actionsDropdownTippyRef.current?.hide(); - }} - > - - Create collection -
-
{ - actionsDropdownTippyRef.current?.hide(); - setImportCollectionModalOpen(true); - }} - > - - Import collection -
-
{ - handleOpenCollection(); - actionsDropdownTippyRef.current?.hide(); - }} - > - - Open collection -
-
-
{ - actionsDropdownTippyRef.current?.hide(); - openDevTools(); - }} - > - - Devtools -
-
-
-
-
- ); -}; - -export default TitleBar; diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index bd8b319c3..60dc98a24 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -1,4 +1,4 @@ -import TitleBar from './TitleBar'; +import SidebarHeader from './SidebarHeader'; import Collections from './Collections'; import StyledWrapper from './StyledWrapper'; @@ -6,7 +6,7 @@ import { useState, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app'; -const MIN_LEFT_SIDEBAR_WIDTH = 221; +const MIN_LEFT_SIDEBAR_WIDTH = 220; const MAX_LEFT_SIDEBAR_WIDTH = 600; const Sidebar = () => { @@ -14,6 +14,8 @@ const Sidebar = () => { const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed); const [asideWidth, setAsideWidth] = useState(leftSidebarWidth); const lastWidthRef = useRef(leftSidebarWidth); + const [showSearch, setShowSearch] = useState(false); + const [activeView, setActiveView] = useState('collections'); // 'collections' or any other future tab const dispatch = useDispatch(); const [dragging, setDragging] = useState(false); @@ -80,9 +82,21 @@ const Sidebar = () => {