[WEB-4328] feat: sidebar revamp (#7217)

* chore: sidebar peek state added to theme store

* chore: extended sidebar wrapper added

* chore: resizeable sidebar component added

* chore: appsidebar root component

* chore: updated sidebar and applied necessary changes across codebase

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: breadcrumb changes

* chore: sidebar improvements and fixes

* chore: enhancements and fixes

* fix: peek sidebar leave

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: icons added

* chore: add dock variable and toggle function to theme store

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: theme and workspace store updated

* chore: workspace content wrapper and apprail context

* chore: workspace and project wrapper updated

* chore: app rail component

* chore: content wrapper

* chore: sidebar component updated

* chore: layout changes and code refactoring

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: appsidebar toggle button

* chore: code refactor

* chore: workspace menu improvements

* chore: sidebar spacing and padding improvements

* chore: settings layout improvement

* chore: enhancements

* chore: extended sidebar code refactor

* chore: code refactor

* fix: merge conflict

* fix: merge conflict

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor
This commit is contained in:
Anmol Singh Bhatia 2025-07-08 20:18:39 +05:30 committed by GitHub
parent fd9da3164e
commit 0225d806cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
69 changed files with 2126 additions and 1143 deletions

View file

@ -0,0 +1,63 @@
"use client";
import { FC, useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { SIDEBAR_WIDTH } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
// hooks
import { ResizableSidebar } from "@/components/sidebar";
import { useAppTheme } from "@/hooks/store";
import { useAppRail } from "@/hooks/use-app-rail";
// local imports
import { ExtendedAppSidebar } from "./extended-sidebar";
import { AppSidebar } from "./sidebar";
export const ProjectAppSidebar: FC = observer(() => {
// store hooks
const {
sidebarCollapsed,
toggleSidebar,
sidebarPeek,
toggleSidebarPeek,
isExtendedSidebarOpened,
isAnySidebarDropdownOpen,
} = useAppTheme();
const { storedValue, setValue } = useLocalStorage("sidebarWidth", SIDEBAR_WIDTH);
// states
const [sidebarWidth, setSidebarWidth] = useState<number>(storedValue ?? SIDEBAR_WIDTH);
// hooks
const { shouldRenderAppRail } = useAppRail();
// derived values
const isAnyExtendedSidebarOpen = isExtendedSidebarOpened;
// handlers
const handleWidthChange = (width: number) => setValue(width);
return (
<>
<ResizableSidebar
showPeek={sidebarPeek}
defaultWidth={storedValue ?? 250}
width={sidebarWidth}
setWidth={setSidebarWidth}
defaultCollapsed={sidebarCollapsed}
peekDuration={1500}
onWidthChange={handleWidthChange}
onCollapsedChange={toggleSidebar}
isCollapsed={sidebarCollapsed}
toggleCollapsed={toggleSidebar}
togglePeek={toggleSidebarPeek}
extendedSidebar={
<>
<ExtendedAppSidebar />
</>
}
isAnyExtendedSidebarExpanded={isAnyExtendedSidebarOpen}
isAnySidebarDropdownOpen={isAnySidebarDropdownOpen}
disablePeekTrigger={shouldRenderAppRail}
>
<AppSidebar />
</ResizableSidebar>
</>
);
});

View file

@ -8,14 +8,14 @@ import { Plus, Search } from "lucide-react";
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
import { cn, copyUrlToClipboard, orderJoinedProjects } from "@plane/utils";
import { copyUrlToClipboard, orderJoinedProjects } from "@plane/utils";
// components
import { CreateProjectModal } from "@/components/project";
import { SidebarProjectsListItem } from "@/components/workspace";
// hooks
import { useAppTheme, useProject, useUserPermissions } from "@/hooks/store";
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
import { TProject } from "@/plane-web/types";
import { ExtendedSidebarWrapper } from "./extended-sidebar-wrapper";
export const ExtendedProjectSidebar = observer(() => {
// refs
@ -27,7 +27,7 @@ export const ExtendedProjectSidebar = observer(() => {
const { workspaceSlug } = useParams();
// store hooks
const { t } = useTranslation();
const { sidebarCollapsed, extendedProjectSidebarCollapsed, toggleExtendedProjectSidebar } = useAppTheme();
const { isExtendedProjectSidebarOpened, toggleExtendedProjectSidebar } = useAppTheme();
const { getPartialProjectById, joinedProjectIds: joinedProjects, updateProjectView } = useProject();
const { allowPermissions } = useUserPermissions();
@ -74,15 +74,7 @@ export const ExtendedProjectSidebar = observer(() => {
EUserPermissionsLevel.WORKSPACE
);
useExtendedSidebarOutsideClickDetector(
extendedProjectSidebarRef,
() => {
if (!isProjectModalOpen) {
toggleExtendedProjectSidebar(false);
}
},
"extended-project-sidebar-toggle"
);
const handleClose = () => toggleExtendedProjectSidebar(false);
const handleCopyText = (projectId: string) => {
copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/issues`).then(() => {
@ -103,17 +95,11 @@ export const ExtendedProjectSidebar = observer(() => {
workspaceSlug={workspaceSlug.toString()}
/>
)}
<div
ref={extendedProjectSidebarRef}
className={cn(
"absolute top-0 h-full z-[19] flex flex-col gap-2 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 shadow-md",
{
"translate-x-0 opacity-100 pointer-events-auto": extendedProjectSidebarCollapsed,
"-translate-x-full opacity-0 pointer-events-none": !extendedProjectSidebarCollapsed,
"left-[70px]": sidebarCollapsed,
"left-[250px]": !sidebarCollapsed,
}
)}
<ExtendedSidebarWrapper
isExtendedSidebarOpened={!!isExtendedProjectSidebarOpened}
extendedSidebarRef={extendedProjectSidebarRef}
handleClose={handleClose}
excludedElementId="extended-project-sidebar-toggle"
>
<div className="flex flex-col gap-1 w-full sticky top-4 pt-0 px-4">
<div className="flex items-center justify-between">
@ -159,7 +145,7 @@ export const ExtendedProjectSidebar = observer(() => {
/>
))}
</div>
</div>
</ExtendedSidebarWrapper>
</>
);
});

View file

@ -0,0 +1,46 @@
"use client";
import React, { FC } from "react";
import { observer } from "mobx-react";
// plane imports
import { EXTENDED_SIDEBAR_WIDTH, SIDEBAR_WIDTH } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { cn } from "@plane/utils";
// hooks
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
type Props = {
children: React.ReactNode;
extendedSidebarRef: React.RefObject<HTMLDivElement>;
isExtendedSidebarOpened: boolean;
handleClose: () => void;
excludedElementId: string;
};
export const ExtendedSidebarWrapper: FC<Props> = observer((props) => {
const { children, extendedSidebarRef, isExtendedSidebarOpened, handleClose, excludedElementId } = props;
// store hooks
const { storedValue } = useLocalStorage("sidebarWidth", SIDEBAR_WIDTH);
useExtendedSidebarOutsideClickDetector(extendedSidebarRef, handleClose, excludedElementId);
return (
<div
id={excludedElementId}
ref={extendedSidebarRef}
className={cn(
`absolute h-full z-[19] flex flex-col py-2 transform transition-all duration-300 ease-in-out bg-custom-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-sm`,
{
"translate-x-0 opacity-100": isExtendedSidebarOpened,
[`-translate-x-[${EXTENDED_SIDEBAR_WIDTH}px] opacity-0 hidden`]: !isExtendedSidebarOpened,
}
)}
style={{
left: `${storedValue ?? SIDEBAR_WIDTH}px`,
width: `${isExtendedSidebarOpened ? EXTENDED_SIDEBAR_WIDTH : 0}px`,
}}
>
{children}
</div>
);
});

View file

@ -6,12 +6,11 @@ import { useParams } from "next/navigation";
// plane imports
import { WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS } from "@plane/constants";
import { EUserWorkspaceRoles } from "@plane/types";
import { cn } from "@plane/utils";
// hooks
import { useAppTheme, useWorkspace } from "@/hooks/store";
import useExtendedSidebarOutsideClickDetector from "@/hooks/use-extended-sidebar-overview-outside-click";
// plane-web imports
import { ExtendedSidebarItem } from "@/plane-web/components/workspace/sidebar";
import { ExtendedSidebarWrapper } from "./extended-sidebar-wrapper";
export const ExtendedAppSidebar = observer(() => {
// refs
@ -19,7 +18,7 @@ export const ExtendedAppSidebar = observer(() => {
// routers
const { workspaceSlug } = useParams();
// store hooks
const { sidebarCollapsed, extendedSidebarCollapsed, toggleExtendedSidebar } = useAppTheme();
const { isExtendedSidebarOpened, toggleExtendedSidebar } = useAppTheme();
const { updateSidebarPreference, getNavigationPreferences } = useWorkspace();
// derived values
@ -95,24 +94,14 @@ export const ExtendedAppSidebar = observer(() => {
});
};
useExtendedSidebarOutsideClickDetector(
extendedSidebarRef,
() => toggleExtendedSidebar(true),
"extended-sidebar-toggle"
);
const handleClose = () => toggleExtendedSidebar(false);
return (
<div
ref={extendedSidebarRef}
className={cn(
"absolute top-0 h-full z-[19] flex flex-col gap-0.5 w-[300px] transform transition-all duration-300 ease-in-out bg-custom-sidebar-background-100 border-r border-custom-sidebar-border-200 p-4 shadow-md pb-6",
{
"-translate-x-full opacity-0 pointer-events-none": extendedSidebarCollapsed,
"translate-x-0 opacity-100 pointer-events-auto": !extendedSidebarCollapsed,
"left-[70px]": sidebarCollapsed,
"left-[250px]": !sidebarCollapsed,
}
)}
<ExtendedSidebarWrapper
isExtendedSidebarOpened={!!isExtendedSidebarOpened}
extendedSidebarRef={extendedSidebarRef}
handleClose={handleClose}
excludedElementId="extended-sidebar-toggle"
>
{sortedNavigationItems.map((item, index) => (
<ExtendedSidebarItem
@ -122,6 +111,6 @@ export const ExtendedAppSidebar = observer(() => {
handleOnNavigationItemDrop={handleOnNavigationItemDrop}
/>
))}
</div>
</ExtendedSidebarWrapper>
);
});

View file

@ -1,5 +1,6 @@
"use client";
import { observer } from "mobx-react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Home } from "lucide-react";
@ -16,16 +17,17 @@ import { BreadcrumbLink } from "@/components/common";
// hooks
import { captureElementAndEvent } from "@/helpers/event-tracker.helper";
export const WorkspaceDashboardHeader = () => {
export const WorkspaceDashboardHeader = observer(() => {
// hooks
const { resolvedTheme } = useTheme();
const { t } = useTranslation();
return (
<>
<Header>
<Header.LeftItem>
<div>
<div className="flex items-center gap-2">
<Breadcrumbs>
<Breadcrumbs.Item
component={
@ -65,4 +67,4 @@ export const WorkspaceDashboardHeader = () => {
</Header>
</>
);
};
});

View file

@ -2,20 +2,21 @@
import { CommandPalette } from "@/components/command-palette";
import { AuthenticationWrapper } from "@/lib/wrappers";
// plane web components
import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper";
import { AppSidebar } from "./sidebar";
import { ProjectAppSidebar } from "./_sidebar";
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
return (
<AuthenticationWrapper>
<CommandPalette />
<WorkspaceAuthWrapper>
<div className="relative flex h-screen w-full overflow-hidden">
<AppSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
{children}
</main>
<div className="relative flex flex-col h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
<div className="relative flex size-full overflow-hidden">
<ProjectAppSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
{children}
</main>
</div>
</div>
</WorkspaceAuthWrapper>
</AuthenticationWrapper>

View file

@ -5,25 +5,25 @@ import { observer } from "mobx-react";
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
// components
import { cn } from "@plane/utils";
import { SidebarDropdown, SidebarHelpSection, SidebarProjectsList, SidebarQuickActions } from "@/components/workspace";
import { AppSidebarToggleButton } from "@/components/sidebar";
import { SidebarDropdown, SidebarProjectsList, SidebarQuickActions } from "@/components/workspace";
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
import { HelpMenu } from "@/components/workspace/sidebar/help-menu";
import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items";
// helpers
// hooks
import { useAppTheme, useUserPermissions } from "@/hooks/store";
import { useFavorite } from "@/hooks/store/use-favorite";
import { useAppRail } from "@/hooks/use-app-rail";
import useSize from "@/hooks/use-window-size";
// plane web components
import { SidebarAppSwitcher } from "@/plane-web/components/sidebar";
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list";
import { ExtendedProjectSidebar } from "./extended-project-sidebar";
import { ExtendedAppSidebar } from "./extended-sidebar";
export const AppSidebar: FC = observer(() => {
// store hooks
const { allowPermissions } = useUserPermissions();
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
const { groupedFavorites } = useFavorite();
const windowSize = useSize();
// refs
@ -52,60 +52,38 @@ export const AppSidebar: FC = observer(() => {
return (
<>
<div
className={cn(
"fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300 w-[250px] md:relative md:ml-0",
{
"w-[70px] -ml-[250px]": sidebarCollapsed,
}
<div className="flex flex-col gap-3 px-3">
{/* Workspace switcher and settings */}
{!shouldRenderAppRail && <SidebarDropdown />}
{isAppRailEnabled && (
<div className="flex items-center justify-between gap-2">
<span className="text-md text-custom-text-200 font-medium pt-1">Projects</span>
<div className="flex items-center gap-2">
<AppSidebarToggleButton />
</div>
</div>
)}
>
<div
ref={ref}
className={cn("size-full flex flex-col flex-1 pt-4 pb-0", {
"p-2 pt-4": sidebarCollapsed,
})}
>
<div
className={cn("px-2", {
"px-4": !sidebarCollapsed,
})}
>
{/* Workspace switcher and settings */}
<SidebarDropdown />
<div className="flex-shrink-0 h-4" />
{/* App switcher */}
{canPerformWorkspaceMemberActions && <SidebarAppSwitcher />}
{/* Quick actions */}
<SidebarQuickActions />
</div>
<hr
className={cn("flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1", {
"opacity-0": !sidebarCollapsed,
})}
/>
<div
className={cn("overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto px-2 py-0.5", {
"vertical-scrollbar px-4": !sidebarCollapsed,
})}
>
<SidebarMenuItems />
{sidebarCollapsed && (
<hr className="flex-shrink-0 border-custom-sidebar-border-300 h-[0.5px] w-3/5 mx-auto my-1" />
)}
{/* Favorites Menu */}
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
{/* Teams List */}
<SidebarTeamsList />
{/* Projects List */}
<SidebarProjectsList />
</div>
{/* Help Section */}
<SidebarHelpSection />
{/* Quick actions */}
<SidebarQuickActions />
</div>
<div className="flex flex-col gap-3 overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto vertical-scrollbar px-3 pt-3 pb-0.5">
<SidebarMenuItems />
{/* Favorites Menu */}
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
{/* Teams List */}
<SidebarTeamsList />
{/* Projects List */}
<SidebarProjectsList />
</div>
{/* Help Section */}
<div className="flex items-center justify-between p-3 border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12">
<WorkspaceEditionBadge />
<div className="flex items-center gap-2">
{!shouldRenderAppRail && <HelpMenu />}
{!isAppRailEnabled && <AppSidebarToggleButton />}
</div>
</div>
<ExtendedAppSidebar />
<ExtendedProjectSidebar />
</>
);
});

View file

@ -11,14 +11,16 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
<AuthenticationWrapper>
<WorkspaceAuthWrapper>
<CommandPalette />
<main className="relative flex h-screen w-full flex-col overflow-hidden bg-custom-background-100">
{/* Header */}
<SettingsHeader />
{/* Content */}
<ContentWrapper className="px-4 md:pl-12 md:flex w-full">
<div className="w-full h-full overflow-hidden">{children}</div>
</ContentWrapper>
</main>
<div className="relative flex h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
{/* Header */}
<SettingsHeader />
{/* Content */}
<ContentWrapper className="p-page-x md:flex w-full">
<div className="w-full h-full overflow-hidden">{children}</div>
</ContentWrapper>
</main>
</div>
</WorkspaceAuthWrapper>
</AuthenticationWrapper>
);

View file

@ -0,0 +1,12 @@
"use client";
import { AppRailProvider } from "@/hooks/context/app-rail-context";
import { WorkspaceContentWrapper } from "@/plane-web/components/workspace";
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
return (
<AppRailProvider>
<WorkspaceContentWrapper>{children}</WorkspaceContentWrapper>
</AppRailProvider>
);
}

View file

@ -19,7 +19,7 @@ export default function ProfileSettingsLayout(props: Props) {
<>
<CommandPalette />
<AuthenticationWrapper>
<div className="relative flex h-full w-full overflow-hidden">
<div className="relative flex h-full w-full overflow-hidden rounded-lg border border-custom-border-200">
<ProfileLayoutSidebar />
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
<div className="h-full w-full overflow-hidden">{children}</div>