snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
This commit is contained in:
28
apps/web/core/components/sidebar/add-button.tsx
Normal file
28
apps/web/core/components/sidebar/add-button.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { Button } from "@plane/propel/button";
|
||||
|
||||
type Props = React.ComponentProps<"button"> & {
|
||||
label: React.ReactNode;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export function SidebarAddButton(props: Props) {
|
||||
const { label, onClick, disabled, ...rest } = props;
|
||||
return (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xl"}
|
||||
className="w-full justify-start"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
269
apps/web/core/components/sidebar/resizable-sidebar.tsx
Normal file
269
apps/web/core/components/sidebar/resizable-sidebar.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { Dispatch, ReactElement, SetStateAction } from "react";
|
||||
import React, { useCallback, useEffect, useState, useRef } from "react";
|
||||
// helpers
|
||||
import { usePlatformOS } from "@plane/hooks";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
interface ResizableSidebarProps {
|
||||
showPeek?: boolean;
|
||||
togglePeek: (value?: boolean) => void;
|
||||
isCollapsed?: boolean;
|
||||
width: number;
|
||||
setWidth: Dispatch<SetStateAction<number>>;
|
||||
defaultWidth?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
defaultCollapsed?: boolean;
|
||||
peekDuration?: number;
|
||||
toggleCollapsed: (value?: boolean) => void;
|
||||
onWidthChange?: (width: number) => void;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
className?: string;
|
||||
children?: ReactElement;
|
||||
extendedSidebar?: ReactElement;
|
||||
isAnyExtendedSidebarExpanded?: boolean;
|
||||
isAnySidebarDropdownOpen?: boolean;
|
||||
}
|
||||
|
||||
export function ResizableSidebar({
|
||||
showPeek = false,
|
||||
togglePeek,
|
||||
peekDuration = 500,
|
||||
isCollapsed = false,
|
||||
toggleCollapsed: toggleCollapsedProp,
|
||||
onCollapsedChange,
|
||||
width,
|
||||
setWidth,
|
||||
onWidthChange,
|
||||
minWidth = 236,
|
||||
maxWidth = 350,
|
||||
className = "",
|
||||
children,
|
||||
extendedSidebar,
|
||||
isAnyExtendedSidebarExpanded = false,
|
||||
isAnySidebarDropdownOpen = false,
|
||||
}: ResizableSidebarProps) {
|
||||
// states
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [isHoveringTrigger, setIsHoveringTrigger] = useState(false);
|
||||
// refs
|
||||
const peekTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
const initialWidthRef = useRef<number>(0);
|
||||
const initialMouseXRef = useRef<number>(0);
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
// handlers
|
||||
const setShowPeek = useCallback(
|
||||
(value: boolean) => {
|
||||
togglePeek(value);
|
||||
},
|
||||
[togglePeek]
|
||||
);
|
||||
|
||||
const handleResize = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isResizing) return;
|
||||
|
||||
const deltaX = e.clientX - initialMouseXRef.current;
|
||||
const newWidth = Math.min(Math.max(initialWidthRef.current + deltaX, minWidth), maxWidth);
|
||||
setWidth(newWidth);
|
||||
},
|
||||
[isResizing, minWidth, maxWidth, setWidth]
|
||||
);
|
||||
|
||||
const startResizing = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
setIsResizing(true);
|
||||
initialWidthRef.current = width;
|
||||
initialMouseXRef.current = e.clientX;
|
||||
},
|
||||
[width]
|
||||
);
|
||||
|
||||
const stopResizing = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
toggleCollapsedProp();
|
||||
setShowPeek(false);
|
||||
setIsHoveringTrigger(false);
|
||||
if (peekTimeoutRef.current) {
|
||||
clearTimeout(peekTimeoutRef.current);
|
||||
}
|
||||
}, [toggleCollapsedProp, setShowPeek]);
|
||||
|
||||
const handlePeekEnter = useCallback(() => {
|
||||
if (isCollapsed && showPeek) {
|
||||
if (peekTimeoutRef.current) {
|
||||
clearTimeout(peekTimeoutRef.current);
|
||||
}
|
||||
}
|
||||
}, [isCollapsed, showPeek]);
|
||||
|
||||
const handlePeekLeave = useCallback(() => {
|
||||
if (isCollapsed && !isAnyExtendedSidebarExpanded && !isAnySidebarDropdownOpen) {
|
||||
peekTimeoutRef.current = setTimeout(() => {
|
||||
setShowPeek(false);
|
||||
}, peekDuration);
|
||||
}
|
||||
}, [isCollapsed, peekDuration, setShowPeek, isAnyExtendedSidebarExpanded, isAnySidebarDropdownOpen]);
|
||||
|
||||
// Set up event listeners for resizing
|
||||
useEffect(() => {
|
||||
if (isResizing) {
|
||||
document.addEventListener("mousemove", handleResize);
|
||||
document.addEventListener("mouseup", stopResizing);
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousemove", handleResize);
|
||||
document.removeEventListener("mouseup", stopResizing);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
}, [isResizing, handleResize, stopResizing]);
|
||||
|
||||
// Clean up timeout on unmount
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (peekTimeoutRef.current) {
|
||||
clearTimeout(peekTimeoutRef.current);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnySidebarDropdownOpen && isCollapsed && isHoveringTrigger) {
|
||||
handlePeekLeave();
|
||||
}
|
||||
}, [isAnySidebarDropdownOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnyExtendedSidebarExpanded && isCollapsed && isHoveringTrigger) {
|
||||
handlePeekLeave();
|
||||
}
|
||||
}, [isAnyExtendedSidebarExpanded]);
|
||||
|
||||
// Reset peek when sidebar is expanded
|
||||
useEffect(() => {
|
||||
if (!isCollapsed) {
|
||||
setShowPeek(false);
|
||||
setIsHoveringTrigger(false);
|
||||
if (peekTimeoutRef.current) {
|
||||
clearTimeout(peekTimeoutRef.current);
|
||||
}
|
||||
}
|
||||
}, [isCollapsed, setShowPeek]);
|
||||
|
||||
// Call external handlers when state changes
|
||||
useEffect(() => {
|
||||
onWidthChange?.(width);
|
||||
}, [width, onWidthChange]);
|
||||
|
||||
useEffect(() => {
|
||||
onCollapsedChange?.(isCollapsed);
|
||||
}, [isCollapsed, onCollapsedChange]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Main Sidebar */}
|
||||
<div
|
||||
id="main-sidebar"
|
||||
className={cn(
|
||||
"z-20 h-full border-r border-subtle bg-surface-1",
|
||||
!isResizing && "transition-all duration-300 ease-in-out",
|
||||
isCollapsed ? "w-0 translate-x-[-100%] opacity-0" : "translate-x-0 opacity-100",
|
||||
isMobile && "absolute",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
width: `${isCollapsed ? 0 : width}px`,
|
||||
minWidth: `${isCollapsed ? 0 : width}px`,
|
||||
maxWidth: `${isCollapsed ? 0 : width}px`,
|
||||
}}
|
||||
role="complementary"
|
||||
aria-label="Main sidebar"
|
||||
data-prevent-outside-click={isMobile}
|
||||
>
|
||||
<aside
|
||||
className={cn(
|
||||
"group/sidebar relative flex h-full w-full flex-col overflow-hidden bg-surface-1 pt-3",
|
||||
isAnyExtendedSidebarExpanded && "rounded-none"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
{/* Resize Handle */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute z-[20] h-full w-1 cursor-ew-resize transition-all duration-200",
|
||||
!isResizing && "hover:bg-surface-2",
|
||||
isResizing && "w-1.5 bg-layer-1",
|
||||
"top-0 right-0"
|
||||
)}
|
||||
// onDoubleClick toggle sidebar
|
||||
onDoubleClick={() => toggleCollapsed()}
|
||||
onMouseDown={(e) => startResizing(e)}
|
||||
role="separator"
|
||||
aria-label="Resize sidebar"
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
{/* Peek View */}
|
||||
<div
|
||||
className={cn(
|
||||
"shadow-sm absolute left-0 z-20 h-full bg-surface-1",
|
||||
!isResizing && "transition-all duration-300 ease-in-out",
|
||||
isCollapsed && showPeek ? "translate-x-0 opacity-100" : "translate-x-[-100%] opacity-0",
|
||||
"pointer-events-none",
|
||||
isCollapsed && showPeek && "pointer-events-auto",
|
||||
!showPeek ? "w-0" : "w-full"
|
||||
)}
|
||||
style={{
|
||||
width: `${width}px`,
|
||||
}}
|
||||
onMouseEnter={handlePeekEnter}
|
||||
onMouseLeave={handlePeekLeave}
|
||||
role="complementary"
|
||||
aria-label="Sidebar peek view"
|
||||
>
|
||||
<aside
|
||||
className={cn(
|
||||
"group/sidebar relative z-20 flex h-full w-full flex-col overflow-hidden bg-surface-1 pt-4",
|
||||
"self-center rounded-md rounded-tl-none rounded-bl-none border-r border-subtle",
|
||||
isAnyExtendedSidebarExpanded && "rounded-none"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
{/* Resize Handle */}
|
||||
<div
|
||||
className={cn(
|
||||
"absolute z-[20] h-full w-1 cursor-ew-resize transition-all duration-200",
|
||||
!isResizing && "hover:bg-surface-2",
|
||||
isResizing && "bg-layer-1",
|
||||
"top-0 right-0"
|
||||
)}
|
||||
// onDoubleClick toggle sidebar
|
||||
onDoubleClick={() => toggleCollapsed()}
|
||||
onMouseDown={(e) => startResizing(e)}
|
||||
role="separator"
|
||||
aria-label="Resize sidebar"
|
||||
/>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{/* Extended Sidebar */}
|
||||
{extendedSidebar && extendedSidebar}
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
apps/web/core/components/sidebar/search-button.tsx
Normal file
32
apps/web/core/components/sidebar/search-button.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { SearchOutline } from "@makeplane/propel/icons";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export function SidebarSearchButton(props: Props) {
|
||||
const { isActive } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"shadow-sm grid aspect-square size-8 flex-shrink-0 place-items-center rounded-md border-[0.5px] border-strong outline-none hover:bg-surface-2",
|
||||
{
|
||||
"border-accent-strong-200 bg-accent-primary/10 hover:bg-accent-primary/10": isActive,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<SearchOutline
|
||||
className={cn("size-4 text-tertiary", {
|
||||
"text-accent-secondary": isActive,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
162
apps/web/core/components/sidebar/sidebar-item.tsx
Normal file
162
apps/web/core/components/sidebar/sidebar-item.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import Link from "next/link";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
interface AppSidebarItemData {
|
||||
href?: string;
|
||||
label?: string;
|
||||
icon?: React.ReactNode;
|
||||
isActive?: boolean;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
interface AppSidebarItemProps {
|
||||
variant?: "link" | "button";
|
||||
item?: AppSidebarItemData;
|
||||
}
|
||||
|
||||
interface AppSidebarItemLabelProps {
|
||||
highlight?: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface AppSidebarItemIconProps {
|
||||
icon?: React.ReactNode;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
interface AppSidebarLinkItemProps {
|
||||
href?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface AppSidebarButtonItemProps {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = {
|
||||
base: "group flex flex-col gap-0.5 items-center justify-center text-tertiary",
|
||||
icon: "flex items-center justify-center gap-2 size-8 rounded-md text-tertiary",
|
||||
iconActive: "bg-layer-transparent-selected text-secondary !text-icon-primary",
|
||||
iconInactive: "group-hover:text-icon-secondary group-hover:bg-layer-transparent-hover !text-icon-tertiary",
|
||||
label: "text-11 font-medium",
|
||||
labelActive: "text-secondary",
|
||||
labelInactive: "group-hover:text-secondary text-tertiary",
|
||||
} as const;
|
||||
|
||||
// ============================================================================
|
||||
// SUB-COMPONENTS
|
||||
// ============================================================================
|
||||
|
||||
function AppSidebarItemLabel({ highlight = false, label }: AppSidebarItemLabelProps) {
|
||||
if (!label) return null;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cn(styles.label, {
|
||||
[styles.labelActive]: highlight,
|
||||
[styles.labelInactive]: !highlight,
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function AppSidebarItemIcon({ icon, highlight }: AppSidebarItemIconProps) {
|
||||
if (!icon) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(styles.icon, {
|
||||
[styles.iconActive]: highlight,
|
||||
[styles.iconInactive]: !highlight,
|
||||
})}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AppSidebarLinkItem({ href, children, className }: AppSidebarLinkItemProps) {
|
||||
if (!href) return null;
|
||||
|
||||
return (
|
||||
<Link href={href} className={cn(styles.base, className)}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function AppSidebarButtonItem({ children, onClick, disabled = false, className }: AppSidebarButtonItemProps) {
|
||||
return (
|
||||
<button className={cn(styles.base, className)} onClick={onClick} disabled={disabled} type="button">
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
export type AppSidebarItemComponent = React.FC<AppSidebarItemProps> & {
|
||||
Label: React.FC<AppSidebarItemLabelProps>;
|
||||
Icon: React.FC<AppSidebarItemIconProps>;
|
||||
Link: React.FC<AppSidebarLinkItemProps>;
|
||||
Button: React.FC<AppSidebarButtonItemProps>;
|
||||
};
|
||||
|
||||
function AppSidebarItem({ variant = "link", item }: AppSidebarItemProps) {
|
||||
if (!item) return null;
|
||||
|
||||
const { icon, isActive, label, href, onClick, disabled, showLabel = true } = item;
|
||||
|
||||
const commonItems = (
|
||||
<>
|
||||
<AppSidebarItemIcon icon={icon} highlight={isActive} />
|
||||
{showLabel && <AppSidebarItemLabel highlight={isActive} label={label} />}
|
||||
</>
|
||||
);
|
||||
|
||||
if (variant === "link") {
|
||||
return <AppSidebarLinkItem href={href}>{commonItems}</AppSidebarLinkItem>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppSidebarButtonItem onClick={onClick} disabled={disabled}>
|
||||
{commonItems}
|
||||
</AppSidebarButtonItem>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMPOUND COMPONENT ASSIGNMENT
|
||||
// ============================================================================
|
||||
|
||||
AppSidebarItem.Label = AppSidebarItemLabel;
|
||||
AppSidebarItem.Icon = AppSidebarItemIcon;
|
||||
AppSidebarItem.Link = AppSidebarLinkItem;
|
||||
AppSidebarItem.Button = AppSidebarButtonItem;
|
||||
|
||||
export { AppSidebarItem };
|
||||
export type { AppSidebarItemData, AppSidebarItemProps };
|
||||
31
apps/web/core/components/sidebar/sidebar-navigation.tsx
Normal file
31
apps/web/core/components/sidebar/sidebar-navigation.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type TSidebarNavItem = {
|
||||
className?: string;
|
||||
isActive?: boolean;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function SidebarNavItem(props: TSidebarNavItem) {
|
||||
const { className, isActive, children } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative flex w-full cursor-pointer items-center justify-between gap-1.5 rounded-md px-2 py-1 outline-none",
|
||||
{
|
||||
"!bg-layer-transparent-active text-primary": isActive,
|
||||
"text-secondary hover:bg-layer-transparent-hover active:bg-layer-transparent-active": !isActive,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
apps/web/core/components/sidebar/sidebar-toggle-button.tsx
Normal file
28
apps/web/core/components/sidebar/sidebar-toggle-button.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { LeftSidePaneOutline } from "@makeplane/propel/icons";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { IconButton } from "@plane/propel/icon-button";
|
||||
|
||||
export const AppSidebarToggleButton = observer(function AppSidebarToggleButton() {
|
||||
// store hooks
|
||||
const { toggleSidebar, sidebarPeek, toggleSidebarPeek } = useAppTheme();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="base"
|
||||
variant="ghost"
|
||||
icon={LeftSidePaneOutline}
|
||||
onClick={() => {
|
||||
if (sidebarPeek) toggleSidebarPeek(false);
|
||||
toggleSidebar();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
96
apps/web/core/components/sidebar/sidebar-wrapper.tsx
Normal file
96
apps/web/core/components/sidebar/sidebar-wrapper.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import { PreferencesOutline } from "@makeplane/propel/icons";
|
||||
import { ScrollArea } from "@plane/propel/scrollarea";
|
||||
// components
|
||||
import { CustomizeNavigationDialog } from "@/components/navigation/customize-navigation-dialog";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// plane web components
|
||||
import { WorkspaceEditionBadge } from "@/components/workspace/edition-badge";
|
||||
import { AppSidebarToggleButton } from "./sidebar-toggle-button";
|
||||
import { IconButton } from "@plane/propel/icon-button";
|
||||
|
||||
type TSidebarWrapperProps = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
quickActions?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const SidebarWrapper = observer(function SidebarWrapper(props: TSidebarWrapperProps) {
|
||||
const { title, children, quickActions } = props;
|
||||
// state
|
||||
const [isCustomizeNavDialogOpen, setIsCustomizeNavDialogOpen] = useState(false);
|
||||
// store hooks
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const windowSize = useSize();
|
||||
// refs
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (sidebarCollapsed === false && window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [windowSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomizeNavigationDialog isOpen={isCustomizeNavDialogOpen} onClose={() => setIsCustomizeNavDialogOpen(false)} />
|
||||
<div ref={ref} className="flex h-full w-full animate-fade-in flex-col">
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
{/* Workspace switcher and settings */}
|
||||
|
||||
<div className="flex items-center justify-between gap-2 px-2">
|
||||
<span className="pt-1 text-16 font-medium text-primary">{title}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{title === "Projects" && (
|
||||
<IconButton
|
||||
size="base"
|
||||
variant="ghost"
|
||||
icon={PreferencesOutline}
|
||||
onClick={() => setIsCustomizeNavDialogOpen(true)}
|
||||
/>
|
||||
)}
|
||||
<AppSidebarToggleButton />
|
||||
</div>
|
||||
</div>
|
||||
{/* Quick actions */}
|
||||
{quickActions}
|
||||
</div>
|
||||
|
||||
<ScrollArea
|
||||
orientation="vertical"
|
||||
scrollType="hover"
|
||||
size="sm"
|
||||
rootClassName="size-full overflow-x-hidden overflow-y-auto"
|
||||
viewportClassName="flex flex-col gap-3 overflow-x-hidden h-full w-full overflow-y-auto px-3 pt-3 pb-0.5"
|
||||
>
|
||||
{children}
|
||||
</ScrollArea>
|
||||
{/* Help Section */}
|
||||
<div className="flex h-12 items-center justify-between border-t border-subtle bg-surface-1 p-3">
|
||||
<WorkspaceEditionBadge />
|
||||
{/* TODO: To be checked if we need this */}
|
||||
{/* <div className="flex items-center gap-2">
|
||||
{!shouldRenderAppRail && <HelpMenu />}
|
||||
{!isAppRailEnabled && <AppSidebarToggleButton />}
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user