Skip to content

Commit b0eed25

Browse files
feat: add option to keep sidebar open (#68)
1 parent 77368cb commit b0eed25

File tree

8 files changed

+106
-24
lines changed

8 files changed

+106
-24
lines changed

.changeset/hot-elephants-beam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@zenml-io/react-component-library": patch
3+
---
4+
5+
add option to keep sidebar open

.storybook/assets/Appshell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { ReactNode } from "react";
22

33
export function AppShell({ children }: { children: ReactNode }) {
44
return (
5-
<div style={{ minHeight: "100vh", display: "flex", flexDirection: "column", width: "100%" }}>
5+
<div style={{ minHeight: "1000px", display: "flex", flexDirection: "column", width: "100%" }}>
66
<div
77
style={{ height: "64px", backgroundColor: "white", borderBottom: "solid lightgrey 1px" }}
88
></div>

src/components/Sidebar/Sidebar.stories.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import {
1010
SidebarHeaderTitle,
1111
SidebarList
1212
} from "./index";
13+
import { Button } from "../Button";
1314
import { CPU, CloseButton } from "../../../.storybook/assets/icons";
1415
import { StoryObj } from "@storybook/react";
1516
import { AppShell } from "../../../.storybook/assets/Appshell";
17+
import { SidebarProvider, useSidebarContext } from "./SidebarContext";
1618

1719
const meta = {
1820
title: "UI/Sidebar",
@@ -22,9 +24,11 @@ const meta = {
2224
},
2325
decorators: [
2426
(Story) => (
25-
<AppShell>
26-
<Story />
27-
</AppShell>
27+
<SidebarProvider initialOpen>
28+
<AppShell>
29+
<Story />
30+
</AppShell>
31+
</SidebarProvider>
2832
)
2933
],
3034
tags: ["autodocs"]
@@ -39,7 +43,14 @@ export const defaultStory: Story = {
3943
args: {
4044
children: (
4145
<>
42-
<SidebarHeader icon={<CloseButton className="w-6 h-6" />} title="ZenML Tenant">
46+
<SidebarHeader
47+
icon={
48+
<div>
49+
<SidebarButton />
50+
</div>
51+
}
52+
title="ZenML Tenant"
53+
>
4354
<SidebarHeaderImage>
4455
<img src={`https://avatar.vercel.sh/ZenMLTenant?size=32`} />
4556
</SidebarHeaderImage>
@@ -81,3 +92,16 @@ export const defaultStory: Story = {
8192
)
8293
}
8394
};
95+
96+
function SidebarButton() {
97+
const { setIsOpen, isOpen } = useSidebarContext();
98+
return (
99+
<Button
100+
onClick={() => setIsOpen((prev) => !prev)}
101+
className={`w-6 bg-transparent h-6 p-0 flex items-center justify-center`}
102+
intent="secondary"
103+
>
104+
<CloseButton className={`w-4 h-4 aspect-square ${!isOpen && "rotate-180"}`} />
105+
</Button>
106+
);
107+
}

src/components/Sidebar/Sidebar.tsx

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ import React, {
1010
} from "react";
1111
import { Slot } from "@radix-ui/react-slot";
1212
import { cn } from "../../utilities/index";
13+
import { useSidebarContext } from "./SidebarContext";
1314

1415
export const Sidebar = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
1516
({ className, children, ...rest }, ref) => {
17+
const { isOpen } = useSidebarContext();
18+
1619
return (
1720
<nav
1821
ref={ref}
1922
className={cn(
20-
"group flex-1 h-full flex w-9 hover:w-[220px] bg-neutral-100 transition-all overflow-x-hidden duration-300 flex-col items-center border-r border-theme-border-moderate",
23+
`group flex-1 h-full flex w-9 ${isOpen ? "w-[220px]" : "hover:w-[220px]"} bg-neutral-100 transition-all overflow-x-hidden duration-300 flex-col items-center border-r border-theme-border-moderate`,
2124
className
2225
)}
2326
{...rest}
@@ -37,18 +40,22 @@ export function SidebarHeaderImage({ children }: PropsWithChildren) {
3740
export const SidebarHeaderTitle = forwardRef<
3841
HTMLParagraphElement,
3942
HTMLAttributes<HTMLParagraphElement>
40-
>(({ children, className, ...rest }, ref) => (
41-
<p
42-
ref={ref}
43-
{...rest}
44-
className={cn(
45-
"opacity-0 group-hover:opacity-100 ml-1 truncate duration-300 transition-all",
46-
className
47-
)}
48-
>
49-
{children}
50-
</p>
51-
));
43+
>(({ children, className, ...rest }, ref) => {
44+
const { isOpen } = useSidebarContext();
45+
46+
return (
47+
<p
48+
ref={ref}
49+
{...rest}
50+
className={cn(
51+
`${!isOpen && "opacity-0 group-hover:opacity-100"} ml-1 truncate duration-300 transition-all`,
52+
className
53+
)}
54+
>
55+
{children}
56+
</p>
57+
);
58+
});
5259

5360
SidebarHeaderTitle.displayName = "SidebarHeaderTitle";
5461

@@ -58,11 +65,13 @@ export type SidebarHeaderProps = HTMLAttributes<HTMLDivElement> & {
5865

5966
export const SidebarHeader = forwardRef<HTMLDivElement, SidebarHeaderProps>(
6067
({ icon, children, className, ...rest }, ref) => {
68+
const { isOpen } = useSidebarContext();
69+
6170
const existingIconClasses = isValidElement(icon) ? icon.props.className || "" : "";
6271

6372
const iconClasses = cn(
6473
existingIconClasses,
65-
"ml-auto shrink-0 opacity-0 group-hover:opacity-100 duration-300 transition-all"
74+
`${!isOpen && "opacity-0 group-hover:opacity-100"} ml-auto shrink-0 duration-300 transition-all`
6675
);
6776

6877
return (
@@ -139,6 +148,7 @@ export function SidebarItemContent({
139148
isActive,
140149
svgStroke = false
141150
}: SidebarItemContentProps) {
151+
const { isOpen } = useSidebarContext();
142152
const existingIconClasses = isValidElement(icon) ? icon.props.className || "" : "";
143153

144154
const iconClasses = cn(
@@ -156,7 +166,11 @@ export function SidebarItemContent({
156166
return (
157167
<>
158168
{cloneElement(icon as ReactElement, { className: iconClasses })}
159-
<div className="opacity-0 group-hover:opacity-100 duration-300 transition-all">{label}</div>
169+
<div
170+
className={`${!isOpen && "opacity-0 group-hover:opacity-100"} duration-300 transition-all`}
171+
>
172+
{label}
173+
</div>
160174
</>
161175
);
162176
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React, {
2+
Dispatch,
3+
PropsWithChildren,
4+
SetStateAction,
5+
createContext,
6+
useContext,
7+
useState
8+
} from "react";
9+
10+
type SidebarContextProps = {
11+
isOpen: boolean;
12+
setIsOpen: Dispatch<SetStateAction<boolean>>;
13+
};
14+
15+
const SidebarContext = createContext<SidebarContextProps | null>(null);
16+
17+
export function SidebarProvider({
18+
children,
19+
initialOpen = false
20+
}: PropsWithChildren<{ initialOpen?: boolean }>) {
21+
const [isOpen, setIsOpen] = useState(initialOpen);
22+
23+
return (
24+
<SidebarContext.Provider value={{ isOpen, setIsOpen }}>{children}</SidebarContext.Provider>
25+
);
26+
}
27+
28+
export function useSidebarContext() {
29+
const context = useContext(SidebarContext);
30+
if (!context) throw new Error("useSidebarContext must be used within a SidebarProvider");
31+
return context;
32+
}

src/components/Sidebar/SidebarHeader.stories.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from "react";
33
import { SidebarHeader, SidebarHeaderImage } from "./Sidebar";
44
import { CloseButton } from "../../../.storybook/assets/icons";
55
import { StoryObj } from "@storybook/react";
6+
import { SidebarProvider } from "./SidebarContext";
67

78
const meta = {
89
title: "UI/Sidebar",
@@ -27,9 +28,11 @@ export const sidebarHeader: Story = {
2728
title: "ZenML Tenant",
2829
icon: <CloseButton className="w-6 h-6" />,
2930
children: (
30-
<SidebarHeaderImage>
31-
<img src={`https://avatar.vercel.sh/ZenMLTenant?size=32`} />
32-
</SidebarHeaderImage>
31+
<SidebarProvider initialOpen={false}>
32+
<SidebarHeaderImage>
33+
<img src={`https://avatar.vercel.sh/ZenMLTenant?size=32`} />
34+
</SidebarHeaderImage>
35+
</SidebarProvider>
3336
)
3437
}
3538
};

src/components/Sidebar/SidebarItem.stories.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ import React from "react";
33
import { SidebarItem, SidebarItemContent } from "./Sidebar";
44
import { CPU } from "../../../.storybook/assets/icons";
55
import { StoryObj } from "@storybook/react";
6+
import { SidebarProvider } from "./SidebarContext";
67

78
const meta = {
89
title: "UI/Sidebar",
910
component: SidebarItem,
1011
decorators: [
1112
(Story) => (
1213
<ul style={{ listStyle: "none" }} className="group">
13-
<Story />
14+
<SidebarProvider initialOpen={false}>
15+
<Story />
16+
</SidebarProvider>
1417
</ul>
1518
)
1619
],

src/components/Sidebar/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from "./Sidebar";
2+
export * from "./SidebarContext";

0 commit comments

Comments
 (0)