Skip to content

Commit 97893e8

Browse files
update sidebar collapse feature
1 parent 36fbacc commit 97893e8

File tree

16 files changed

+232
-154
lines changed

16 files changed

+232
-154
lines changed

apps/gitness/src/components-v2/breadcrumbs/breadcrumbs.tsx

+2-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
11
import { Link, useMatches } from 'react-router-dom'
22

3-
import { Breadcrumb, Separator, Sidebar, Topbar } from '@harnessio/ui/components'
3+
import { Breadcrumb, Topbar } from '@harnessio/ui/components'
44

5-
import { useIsMFE } from '../../framework/hooks/useIsMFE'
65
import { CustomHandle } from '../../framework/routing/types'
76

87
function Breadcrumbs() {
98
const matches = useMatches()
109
const matchesWithBreadcrumb = matches.filter(match => (match.handle as CustomHandle)?.breadcrumb)
11-
const isMFE = useIsMFE()
1210

1311
return (
1412
<Topbar.Root>
1513
<Topbar.Left>
16-
{!isMFE ? (
17-
<>
18-
<Sidebar.Trigger className="text-topbar-foreground-2 hover:text-topbar-foreground-1 hover:bg-topbar-background-1 -ml-1" />
19-
<Separator orientation="vertical" className="bg-topbar-background-1 ml-1 mr-2 h-4" />
20-
</>
21-
) : null}
2214
<Breadcrumb.Root className="select-none">
2315
<Breadcrumb.List>
2416
{matchesWithBreadcrumb.map((match, index) => {
@@ -29,7 +21,7 @@ function Breadcrumbs() {
2921

3022
return (
3123
<Breadcrumb.Item key={match.pathname}>
32-
{!isFirst ? <Breadcrumb.Separator className="text-topbar-foreground-3" /> : null}
24+
{!isFirst && <Breadcrumb.Separator className="text-topbar-foreground-3" />}
3325
{isLast || !asLink ? (
3426
<Breadcrumb.Page className={isLast ? 'text-topbar-foreground-4' : 'text-topbar-foreground-3'}>
3527
{breadcrumbContent}

packages/ui/locales/en/component.json

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"recent": "Recent",
55
"user-management": "User Management",
66
"settings": "Settings",
7+
"sidebarToggle": {
8+
"expand": "Expand",
9+
"collapse": "Collapse"
10+
},
711
"pin": "Pin",
812
"remove": "Remove",
913
"reorder": "Reorder",

packages/ui/locales/fr/component.json

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"recent": "Récent",
55
"user-management": "Gestion des utilisateurs",
66
"settings": "Paramètres",
7+
"sidebarToggle": {
8+
"expand": "Développer",
9+
"collapse": "Réduire"
10+
},
711
"pin": "Épingler",
812
"remove": "Supprimer",
913
"reorder": "Réorganiser",

packages/ui/src/components/app-sidebar/app-sidebar.tsx

+60-30
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import {
77
LanguageInterface,
88
languages,
99
Sidebar,
10-
Spacer,
11-
ThemeDialog
10+
ThemeDialog,
11+
useSidebar
1212
} from '@/components'
1313
import { ContentStyleType, useRouterContext, useTheme } from '@/context'
1414
import { TypesUser } from '@/types'
1515
import { TranslationStore } from '@/views'
16+
import { cn } from '@utils/cn'
1617

1718
import { SidebarItem } from './sidebar-item'
1819
import { SidebarSearchLegacy } from './sidebar-search-legacy'
@@ -35,6 +36,7 @@ interface SidebarProps {
3536
handleRemoveRecentMenuItem: (item: NavbarItemType) => void
3637
useTranslationStore: () => TranslationStore
3738
showNewSearch?: boolean
39+
hasToggle?: boolean
3840
}
3941

4042
export const AppSidebar = ({
@@ -48,11 +50,13 @@ export const AppSidebar = ({
4850
handleSettingsMenu,
4951
handleCustomNav,
5052
handleLogOut,
53+
hasToggle = true,
5154
showNewSearch
5255
}: SidebarProps) => {
5356
const { t, i18n, changeLanguage } = useTranslationStore()
5457
const { theme, setTheme, setInset, isInset } = useTheme()
5558
const { Link, navigate } = useRouterContext()
59+
const { collapsed, toggleSidebar } = useSidebar()
5660

5761
const [openThemeDialog, setOpenThemeDialog] = useState(false)
5862
const [openLanguageDialog, setOpenLanguageDialog] = useState(false)
@@ -72,6 +76,19 @@ export const AppSidebar = ({
7276

7377
const onInsetChange = (style: ContentStyleType) => setInset(style === ContentStyleType.Inset)
7478

79+
const HarnessLogo = ({ className }: { className?: string }) => (
80+
<Link to="/" className={cn('flex items-center gap-0.5 overflow-hidden', className)}>
81+
<Icon name="harness" size={18} className="text-sidebar-foreground-accent" />
82+
<div
83+
className={cn('overflow-hidden max-w-20 mb-px opacity-100 transition-[max-width,opacity] ease-linear', {
84+
'max-w-0 opacity-0': collapsed
85+
})}
86+
>
87+
<Icon name="harness-logo-text" width={65} height={15} className="text-sidebar-foreground-1" />
88+
</div>
89+
</Link>
90+
)
91+
7592
return (
7693
<>
7794
<Sidebar.Root className="z-20">
@@ -81,24 +98,11 @@ export const AppSidebar = ({
8198
<SidebarSearch
8299
className="pb-3 pt-1.5"
83100
t={t}
84-
logo={
85-
<Link to="/" className="flex h-[58px] items-center justify-start gap-0.5 pl-1">
86-
<Icon name="harness" size={18} className="text-sidebar-foreground-accent" />
87-
<Icon name="harness-logo-text" width={65} height={15} className="text-sidebar-foreground-1 mb-px" />
88-
</Link>
89-
}
101+
logo={<HarnessLogo className="h-[58px] justify-start pl-1" />}
90102
/>
91103
</SearchProvider>
92104
) : (
93-
<SidebarSearchLegacy
94-
t={t}
95-
logo={
96-
<Link className="flex items-center gap-0.5" to="/">
97-
<Icon name="harness" size={18} className="text-sidebar-foreground-accent" />
98-
<Icon name="harness-logo-text" width={65} height={15} className="text-sidebar-foreground-1 mb-px" />
99-
</Link>
100-
}
101-
/>
105+
<SidebarSearchLegacy t={t} logo={<HarnessLogo />} />
102106
)}
103107
</Sidebar.Header>
104108
<Sidebar.Content>
@@ -131,7 +135,6 @@ export const AppSidebar = ({
131135
{!!recentMenuItems.length && (
132136
<Sidebar.Group title={t('component:navbar.recent', 'Recent')} className="border-t pt-2.5">
133137
<Sidebar.GroupLabel>{t('component:navbar.recent', 'Recent')}</Sidebar.GroupLabel>
134-
<Spacer size={2} />
135138
<Sidebar.GroupContent>
136139
<Sidebar.Menu>
137140
{recentMenuItems.map(item => (
@@ -155,26 +158,53 @@ export const AppSidebar = ({
155158
<Sidebar.Menu>
156159
{!!currentUser?.admin && (
157160
<Sidebar.MenuItem>
158-
<Sidebar.MenuButton asChild onClick={() => navigate('/admin/default-settings')}>
159-
<Sidebar.MenuItemText
160-
text={t('component:navbar.user-management', 'User Management')}
161-
icon={<Icon name="account" size={14} />}
162-
/>
163-
</Sidebar.MenuButton>
161+
<button className="w-full" onClick={() => navigate('/admin/default-settings')}>
162+
<Sidebar.MenuButton asChild>
163+
<Sidebar.MenuItemText
164+
text={t('component:navbar.user-management', 'User Management')}
165+
icon={<Icon name="account" size={14} />}
166+
/>
167+
</Sidebar.MenuButton>
168+
</button>
164169
</Sidebar.MenuItem>
165170
)}
166171
<Sidebar.MenuItem>
167-
<Sidebar.MenuButton asChild onClick={handleSettingsMenu}>
168-
<Sidebar.MenuItemText
169-
text={t('component:navbar.settings', 'Settings')}
170-
icon={<Icon name="settings-1" size={14} />}
171-
/>
172-
</Sidebar.MenuButton>
172+
<button className="w-full" onClick={handleSettingsMenu}>
173+
<Sidebar.MenuButton asChild>
174+
<Sidebar.MenuItemText
175+
text={t('component:navbar.settings', 'Settings')}
176+
icon={<Icon name="settings-1" size={14} />}
177+
/>
178+
</Sidebar.MenuButton>
179+
</button>
173180
</Sidebar.MenuItem>
174181
</Sidebar.Menu>
175182
</Sidebar.GroupContent>
176183
</Sidebar.Group>
177184
</Sidebar.Content>
185+
186+
{hasToggle && (
187+
<Sidebar.Group>
188+
<Sidebar.Menu>
189+
<Sidebar.MenuItem>
190+
<button className="w-full" onClick={toggleSidebar}>
191+
<Sidebar.MenuButton asChild>
192+
<Sidebar.MenuItemText
193+
aria-label={
194+
collapsed
195+
? t('component:navbar.sidebarToggle.expand', 'Expand')
196+
: t('component:navbar.sidebarToggle.collapse', 'Collapse')
197+
}
198+
text={t('component:navbar.sidebarToggle.collapse', 'Collapse')}
199+
icon={<Icon name={collapsed ? 'sidebar-right' : 'sidebar-left'} size={14} />}
200+
/>
201+
</Sidebar.MenuButton>
202+
</button>
203+
</Sidebar.MenuItem>
204+
</Sidebar.Menu>
205+
</Sidebar.Group>
206+
)}
207+
178208
<Sidebar.Footer className="border-sidebar-border-1 border-t">
179209
<User
180210
user={currentUser}

packages/ui/src/components/app-sidebar/sidebar-item.tsx

+27-35
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DropdownMenu, Icon, IconProps, Sidebar, Text } from '@/components'
1+
import { DropdownMenu, Icon, IconProps, Sidebar, Text, useSidebar } from '@/components'
22
import { useRouterContext } from '@/context'
33
import { TFunction } from 'i18next'
44

@@ -29,6 +29,7 @@ export const SidebarItem = ({
2929
t
3030
}: NavbarItemProps) => {
3131
const { NavLink } = useRouterContext()
32+
const { collapsed } = useSidebar()
3233

3334
const iconName = item.iconName && (item.iconName.replace('-gradient', '') as IconProps['name'])
3435

@@ -40,46 +41,37 @@ export const SidebarItem = ({
4041
handleRemoveRecentMenuItem(item)
4142
}
4243

44+
const dropdownItemClassNames =
45+
'text-sidebar-foreground-6 data-[highlighted]:bg-sidebar-background-2 data-[highlighted]:text-sidebar-foreground-1'
46+
4347
const dropdownItems = isRecent ? (
4448
<>
45-
<DropdownMenu.Item
46-
className="text-sidebar-foreground-6 data-[highlighted]:bg-sidebar-background-2 data-[highlighted]:text-sidebar-foreground-1"
47-
onSelect={handlePin}
48-
>
49+
<DropdownMenu.Item className={dropdownItemClassNames} onSelect={handlePin}>
4950
<Text size={2} truncate color="inherit">
5051
{t('component:navbar.pin', 'Pin')}
5152
</Text>
5253
</DropdownMenu.Item>
53-
<DropdownMenu.Item
54-
className="text-sidebar-foreground-6 data-[highlighted]:bg-sidebar-background-2 data-[highlighted]:text-sidebar-foreground-1"
55-
onSelect={handleRemoveRecent}
56-
>
54+
<DropdownMenu.Item className={dropdownItemClassNames} onSelect={handleRemoveRecent}>
5755
<Text size={2} truncate color="inherit">
5856
{t('component:navbar.remove', 'Remove')}
5957
</Text>
6058
</DropdownMenu.Item>
6159
</>
6260
) : (
6361
<>
64-
<DropdownMenu.Item
65-
className="text-sidebar-foreground-6 data-[highlighted]:bg-sidebar-background-2 data-[highlighted]:text-sidebar-foreground-1"
66-
onSelect={handleCustomNav}
67-
>
62+
<DropdownMenu.Item className={dropdownItemClassNames} onSelect={handleCustomNav}>
6863
<Text size={2} truncate color="inherit">
6964
{t('component:navbar.reorder', 'Reorder')}
7065
</Text>
7166
</DropdownMenu.Item>
7267

73-
{!item.permanentlyPinned ? (
74-
<DropdownMenu.Item
75-
className="text-sidebar-foreground-6 data-[highlighted]:bg-sidebar-background-2 data-[highlighted]:text-sidebar-foreground-1"
76-
onSelect={handlePin}
77-
>
68+
{!item.permanentlyPinned && (
69+
<DropdownMenu.Item className={dropdownItemClassNames} onSelect={handlePin}>
7870
<Text size={2} truncate color="inherit">
7971
{t('component:navbar.unpin', 'Unpin')}
8072
</Text>
8173
</DropdownMenu.Item>
82-
) : null}
74+
)}
8375
</>
8476
)
8577

@@ -97,23 +89,23 @@ export const SidebarItem = ({
9789
)}
9890
</NavLink>
9991

100-
<DropdownMenu.Root>
101-
<DropdownMenu.Trigger asChild>
102-
<Sidebar.MenuAction className="text-sidebar-icon-3 hover:text-sidebar-icon-1 right-0" showOnHover>
103-
<span>
92+
{!collapsed && (
93+
<DropdownMenu.Root>
94+
<DropdownMenu.Trigger asChild>
95+
<Sidebar.MenuAction className="text-sidebar-icon-3 hover:text-sidebar-icon-1 right-0" showOnHover>
10496
<Icon name="menu-dots" size={12} />
105-
</span>
106-
</Sidebar.MenuAction>
107-
</DropdownMenu.Trigger>
108-
<DropdownMenu.Content
109-
className="border-sidebar-border-3 bg-sidebar-background-4 w-[128px]"
110-
align="end"
111-
sideOffset={3}
112-
alignOffset={4}
113-
>
114-
{dropdownItems}
115-
</DropdownMenu.Content>
116-
</DropdownMenu.Root>
97+
</Sidebar.MenuAction>
98+
</DropdownMenu.Trigger>
99+
<DropdownMenu.Content
100+
className="border-sidebar-border-3 bg-sidebar-background-4 w-[128px]"
101+
align="end"
102+
sideOffset={3}
103+
alignOffset={4}
104+
>
105+
{dropdownItems}
106+
</DropdownMenu.Content>
107+
</DropdownMenu.Root>
108+
)}
117109
</Sidebar.MenuItem>
118110
)
119111
}

packages/ui/src/components/app-sidebar/sidebar-search-legacy.tsx

+37-16
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { FormEvent, ReactNode, useState } from 'react'
1+
import { FormEvent, MouseEvent, ReactNode, useState } from 'react'
22

3+
import { Button, Dialog, Icon, SearchBox, Spacer, useSidebar } from '@/components'
4+
import { cn } from '@/utils'
35
import { TFunction } from 'i18next'
46

5-
import { Dialog, SearchBox, Spacer } from '..'
6-
77
interface ProjectProps {
88
logo: ReactNode
99
t: TFunction
@@ -12,7 +12,9 @@ interface ProjectProps {
1212
function SidebarSearchLegacy({ logo, t }: ProjectProps) {
1313
const [isSearchDialogOpen, setSearchDialogOpen] = useState(false)
1414

15-
const openSearchDialog = (e?: FormEvent<HTMLInputElement>) => {
15+
const { collapsed } = useSidebar()
16+
17+
const openSearchDialog = (e?: FormEvent<HTMLInputElement> | MouseEvent<HTMLButtonElement>) => {
1618
e?.preventDefault()
1719
e?.stopPropagation()
1820

@@ -25,18 +27,37 @@ function SidebarSearchLegacy({ logo, t }: ProjectProps) {
2527

2628
return (
2729
<div className="flex w-full flex-col place-items-start pb-3 pt-1.5">
28-
<div className="mb-5 mt-5 flex items-center pl-2">{logo}</div>
29-
<SearchBox.Root
30-
width="full"
31-
placeholder={`${t('component:navbar.search', 'Search')}...`}
32-
hasShortcut
33-
shortcutLetter="K"
34-
shortcutModifier="cmd"
35-
value=""
36-
onSearch={openSearchDialog}
37-
handleChange={openSearchDialog}
38-
theme="sidebar"
39-
/>
30+
<div className="my-5 flex max-w-full items-center overflow-hidden pl-2">{logo}</div>
31+
<div className="relative w-full">
32+
<Button
33+
variant="ghost"
34+
tabIndex={collapsed ? 0 : -1}
35+
aria-label="Open search dialog"
36+
className={cn(
37+
'absolute opacity-0 -z-10 left-0 top-0 px-2.5 py-[9px] bg-sidebar-background-1 pointer-events-none transition-[opacity] duration-150 ease-linear',
38+
{ 'z-10 opacity-100 pointer-events-auto': collapsed }
39+
)}
40+
onClick={openSearchDialog}
41+
>
42+
<Icon className="text-sidebar-foreground-4" name="search" size={12} />
43+
</Button>
44+
45+
<SearchBox.Root
46+
className={cn('overflow-hidden opacity-100 transition-[width,opacity] duration-150 ease-linear', {
47+
'w-8 opacity-0': collapsed
48+
})}
49+
width="full"
50+
placeholder={`${t('component:navbar.search', 'Search')}...`}
51+
hasShortcut
52+
shortcutLetter="K"
53+
shortcutModifier="cmd"
54+
value=""
55+
onSearch={openSearchDialog}
56+
handleChange={openSearchDialog}
57+
theme="sidebar"
58+
tabIndex={collapsed ? -1 : 0}
59+
/>
60+
</div>
4061
<Dialog.Root open={isSearchDialogOpen} onOpenChange={closeSearchDialog}>
4162
<Dialog.Content className="h-[600px] max-w-[800px]">
4263
<Dialog.Header>

0 commit comments

Comments
 (0)