Skip to content

Commit ab3189e

Browse files
feat: init sidebar component (#28)
1 parent 6c1c338 commit ab3189e

File tree

12 files changed

+408
-1
lines changed

12 files changed

+408
-1
lines changed

.changeset/poor-pants-tell.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": minor
3+
---
4+
5+
Initialize Sidebar Component

.storybook/assets/Appshell.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React, { ReactNode } from "react";
2+
3+
export function AppShell({ children }: { children: ReactNode }) {
4+
return (
5+
<div style={{ minHeight: "100vh", display: "flex", flexDirection: "column", width: "100%" }}>
6+
<div
7+
style={{ height: "64px", backgroundColor: "white", borderBottom: "solid lightgrey 1px" }}
8+
></div>
9+
{children}
10+
</div>
11+
);
12+
}

.storybook/assets/CPU.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from "react";
2+
export default function CPU({ className = "" }: { className?: string }) {
3+
return (
4+
<svg
5+
xmlns="http://www.w3.org/2000/svg"
6+
viewBox="0 0 24 24"
7+
stroke-width="2"
8+
stroke="currentColor"
9+
fill="none"
10+
stroke-linecap="round"
11+
stroke-linejoin="round"
12+
className={className}
13+
>
14+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
15+
<path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path>
16+
<path d="M9 9h6v6h-6z"></path>
17+
<path d="M3 10h2"></path>
18+
<path d="M3 14h2"></path>
19+
<path d="M10 3v2"></path>
20+
<path d="M14 3v2"></path>
21+
<path d="M21 10h-2"></path>
22+
<path d="M21 14h-2"></path>
23+
<path d="M14 21v-2"></path>
24+
<path d="M10 21v-2"></path>
25+
</svg>
26+
);
27+
}

.storybook/assets/icons.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from "react";
2+
export function CPU({ className = "" }: { className?: string }) {
3+
return (
4+
<svg
5+
xmlns="http://www.w3.org/2000/svg"
6+
viewBox="0 0 24 24"
7+
stroke-width="2"
8+
stroke="currentColor"
9+
fill="none"
10+
stroke-linecap="round"
11+
stroke-linejoin="round"
12+
className={className}
13+
>
14+
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
15+
<path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path>
16+
<path d="M9 9h6v6h-6z"></path>
17+
<path d="M3 10h2"></path>
18+
<path d="M3 14h2"></path>
19+
<path d="M10 3v2"></path>
20+
<path d="M14 3v2"></path>
21+
<path d="M21 10h-2"></path>
22+
<path d="M21 14h-2"></path>
23+
<path d="M14 21v-2"></path>
24+
<path d="M10 21v-2"></path>
25+
</svg>
26+
);
27+
}
28+
29+
export function CloseButton({ className = "" }: { className?: string }) {
30+
return (
31+
<svg
32+
viewBox="0 0 32 32"
33+
fill="#6B7280"
34+
xmlns="http://www.w3.org/2000/svg"
35+
className={className}
36+
>
37+
<path
38+
fill-rule="evenodd"
39+
clip-rule="evenodd"
40+
d="M7 6C7.55228 6 8 6.44772 8 7V25C8 25.5523 7.55228 26 7 26C6.44772 26 6 25.5523 6 25V7C6 6.44772 6.44772 6 7 6ZM18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L13.4142 15H25C25.5523 15 26 15.4477 26 16C26 16.5523 25.5523 17 25 17H13.4142L18.7071 22.2929C19.0976 22.6834 19.0976 23.3166 18.7071 23.7071C18.3166 24.0976 17.6834 24.0976 17.2929 23.7071L10.2929 16.7071C9.90237 16.3166 9.90237 15.6834 10.2929 15.2929L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289Z"
41+
/>
42+
</svg>
43+
);
44+
}

.storybook/preview.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import "../src/index.css";
77

88
const preview: Preview = {
99
parameters: {
10+
backgrounds: {
11+
default: "light",
12+
values: [
13+
{ name: "light", value: "#F9FAFB" },
14+
{ name: "dark", value: "#1F2937" }
15+
]
16+
},
1017
actions: { argTypesRegex: "^on[A-Z].*" },
1118
controls: {
1219
matchers: {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Meta } from "@storybook/react";
2+
import React from "react";
3+
import {
4+
Sidebar,
5+
SidebarHeader,
6+
SidebarItem,
7+
SidebarItemContent,
8+
SidebarBody,
9+
SidebarHeaderImage,
10+
SidebarHeaderTitle,
11+
SidebarList
12+
} from "./index";
13+
import { CPU, CloseButton } from "../../../.storybook/assets/icons";
14+
import { StoryObj } from "@storybook/react";
15+
import { AppShell } from "../../../.storybook/assets/Appshell";
16+
17+
const meta = {
18+
title: "UI/Sidebar",
19+
component: Sidebar,
20+
parameters: {
21+
layout: "fullscreen"
22+
},
23+
decorators: [
24+
(Story) => (
25+
<AppShell>
26+
<Story />
27+
</AppShell>
28+
)
29+
],
30+
tags: ["autodocs"]
31+
} satisfies Meta<typeof Sidebar>;
32+
33+
export default meta;
34+
35+
type Story = StoryObj<typeof meta>;
36+
37+
export const defaultStory: Story = {
38+
name: "Sidebar",
39+
args: {
40+
children: (
41+
<>
42+
<SidebarHeader icon={<CloseButton />} title="ZenML Tenant">
43+
<SidebarHeaderImage>
44+
<img src={`https://avatar.vercel.sh/ZenMLTenant?size=32`} />
45+
</SidebarHeaderImage>
46+
<SidebarHeaderTitle>My Tenant</SidebarHeaderTitle>
47+
</SidebarHeader>
48+
<SidebarBody>
49+
<SidebarList>
50+
<li className="w-full">
51+
<SidebarItem isActive={true}>
52+
<div>
53+
<SidebarItemContent isActive={true} icon={<CPU />} label="Models" />
54+
</div>
55+
</SidebarItem>
56+
</li>
57+
<li className="w-full">
58+
<SidebarItem isActive={false}>
59+
<div>
60+
<SidebarItemContent isActive={false} icon={<CPU />} label="Models" />
61+
</div>
62+
</SidebarItem>
63+
</li>
64+
<li className="w-full">
65+
<SidebarItem isActive={false}>
66+
<div>
67+
<SidebarItemContent isActive={false} icon={<CPU />} label="Models" />
68+
</div>
69+
</SidebarItem>
70+
</li>
71+
</SidebarList>
72+
<div style={{ marginTop: "auto" }}>
73+
<SidebarItem>
74+
<div>
75+
<SidebarItemContent isActive={false} icon={<CPU />} label="Models" />
76+
</div>
77+
</SidebarItem>
78+
</div>
79+
</SidebarBody>
80+
</>
81+
)
82+
}
83+
};

src/components/Sidebar/Sidebar.tsx

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import React, {
2+
HTMLAttributes,
3+
HTMLProps,
4+
PropsWithChildren,
5+
ReactElement,
6+
ReactNode,
7+
cloneElement,
8+
forwardRef,
9+
isValidElement
10+
} from "react";
11+
import { Slot } from "@radix-ui/react-slot";
12+
import { cn } from "../../utilities/index";
13+
14+
export const Sidebar = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
15+
({ className, children, ...rest }, ref) => {
16+
return (
17+
<nav
18+
ref={ref}
19+
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",
21+
className
22+
)}
23+
{...rest}
24+
>
25+
<div className="flex flex-col h-full flex-1 w-full">{children}</div>
26+
</nav>
27+
);
28+
}
29+
);
30+
31+
Sidebar.displayName = "Sidebar";
32+
33+
export function SidebarHeaderImage({ children }: PropsWithChildren) {
34+
return <Slot className="w-6 h-6 rounded-sm shrink-0">{children}</Slot>;
35+
}
36+
37+
export const SidebarHeaderTitle = forwardRef<
38+
HTMLParagraphElement,
39+
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+
));
52+
53+
SidebarHeaderTitle.displayName = "SidebarHeaderTitle";
54+
55+
export type SidebarHeaderProps = HTMLAttributes<HTMLDivElement> & {
56+
title: string;
57+
icon?: ReactNode;
58+
};
59+
60+
export const SidebarHeader = forwardRef<HTMLDivElement, SidebarHeaderProps>(
61+
({ title, icon, children, className, ...rest }, ref) => {
62+
const existingIconClasses = isValidElement(icon) ? icon.props.className || "" : "";
63+
64+
const iconClasses = cn(
65+
existingIconClasses,
66+
"w-6 ml-auto shrink-0 h-6 opacity-0 group-hover:opacity-100 duration-300 transition-all"
67+
);
68+
69+
return (
70+
<div
71+
ref={ref}
72+
{...rest}
73+
className={cn(
74+
"bg-theme-surface-primary flex items-center whitespace-nowrap p-3 border-b border-theme-border-moderate",
75+
className
76+
)}
77+
>
78+
{children}
79+
80+
{icon && cloneElement(icon as ReactElement, { className: iconClasses })}
81+
</div>
82+
);
83+
}
84+
);
85+
86+
SidebarHeader.displayName = "SidebarHeader";
87+
88+
export const SidebarBody = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
89+
({ className, ...rest }, ref) => {
90+
return (
91+
<div ref={ref} className={cn(`flex-1 flex px-1 py-2 flex-col w-full`, className)} {...rest} />
92+
);
93+
}
94+
);
95+
export const SidebarList = forwardRef<HTMLUListElement, HTMLAttributes<HTMLUListElement>>(
96+
({ className, ...rest }, ref) => {
97+
return (
98+
<ul
99+
ref={ref}
100+
className={cn("flex gap-0.5 w-full flex-col items-center", className)}
101+
{...rest}
102+
/>
103+
);
104+
}
105+
);
106+
107+
SidebarList.displayName = "SidebarList";
108+
109+
export type SidebarItemProps = HTMLAttributes<HTMLLIElement> & {
110+
isActive?: boolean;
111+
};
112+
113+
export function SidebarItem({
114+
isActive = false,
115+
...rest
116+
}: PropsWithChildren<{ isActive?: boolean }>) {
117+
return (
118+
<Slot
119+
className={`flex p-2 items-center gap-2 rounded-md w-full ${
120+
isActive ? "bg-theme-surface-primary" : "hover:bg-neutral-200 active:bg-neutral-300"
121+
}`}
122+
{...rest}
123+
></Slot>
124+
);
125+
}
126+
127+
type SidebarItemContentProps = {
128+
icon: ReactNode;
129+
label: string;
130+
isActive?: boolean;
131+
};
132+
133+
export function SidebarItemContent({ icon, label, isActive }: SidebarItemContentProps) {
134+
const existingIconClasses = isValidElement(icon) ? icon.props.className || "" : "";
135+
136+
const iconClasses = cn(
137+
existingIconClasses,
138+
`w-5 h-5 shrink-0 ${isActive ? "stroke-primary-400" : "stroke-theme-text-tertiary"} `
139+
);
140+
return (
141+
<>
142+
{cloneElement(icon as ReactElement, { className: iconClasses })}
143+
<div className="opacity-0 group-hover:opacity-100 duration-300 transition-all">{label}</div>
144+
</>
145+
);
146+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Meta } from "@storybook/react";
2+
import React from "react";
3+
import { SidebarHeader, SidebarHeaderImage } from "./Sidebar";
4+
import { CloseButton } from "../../../.storybook/assets/icons";
5+
import { StoryObj } from "@storybook/react";
6+
7+
const meta = {
8+
title: "UI/Sidebar",
9+
component: SidebarHeader,
10+
decorators: [
11+
(Story) => (
12+
<div className="group">
13+
<Story />
14+
</div>
15+
)
16+
],
17+
tags: ["autodocs"]
18+
} satisfies Meta<typeof SidebarHeader>;
19+
20+
export default meta;
21+
22+
type Story = StoryObj<typeof meta>;
23+
24+
export const sidebarHeader: Story = {
25+
name: "Sidebar Header",
26+
args: {
27+
title: "ZenML Tenant",
28+
icon: <CloseButton />,
29+
children: (
30+
<SidebarHeaderImage>
31+
<img src={`https://avatar.vercel.sh/ZenMLTenant?size=32`} />
32+
</SidebarHeaderImage>
33+
)
34+
}
35+
};

0 commit comments

Comments
 (0)