Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/poor-pants-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zenml-io/react-component-library": minor
---

Initialize Sidebar Component
12 changes: 12 additions & 0 deletions .storybook/assets/Appshell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React, { ReactNode } from "react";

export function AppShell({ children }: { children: ReactNode }) {
return (
<div style={{ minHeight: "100vh", display: "flex", flexDirection: "column", width: "100%" }}>
<div
style={{ height: "64px", backgroundColor: "white", borderBottom: "solid lightgrey 1px" }}
></div>
{children}
</div>
);
}
27 changes: 27 additions & 0 deletions .storybook/assets/CPU.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from "react";
export default function CPU({ className = "" }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
className={className}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<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>
<path d="M9 9h6v6h-6z"></path>
<path d="M3 10h2"></path>
<path d="M3 14h2"></path>
<path d="M10 3v2"></path>
<path d="M14 3v2"></path>
<path d="M21 10h-2"></path>
<path d="M21 14h-2"></path>
<path d="M14 21v-2"></path>
<path d="M10 21v-2"></path>
</svg>
);
}
44 changes: 44 additions & 0 deletions .storybook/assets/icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react";
export function CPU({ className = "" }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
className={className}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<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>
<path d="M9 9h6v6h-6z"></path>
<path d="M3 10h2"></path>
<path d="M3 14h2"></path>
<path d="M10 3v2"></path>
<path d="M14 3v2"></path>
<path d="M21 10h-2"></path>
<path d="M21 14h-2"></path>
<path d="M14 21v-2"></path>
<path d="M10 21v-2"></path>
</svg>
);
}

export function CloseButton({ className = "" }: { className?: string }) {
return (
<svg
viewBox="0 0 32 32"
fill="#6B7280"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
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"
/>
</svg>
);
}
7 changes: 7 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import "../src/index.css";

const preview: Preview = {
parameters: {
backgrounds: {
default: "light",
values: [
{ name: "light", value: "#F9FAFB" },
{ name: "dark", value: "#1F2937" }
]
},
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
Expand Down
83 changes: 83 additions & 0 deletions src/components/Sidebar/Sidebar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Meta } from "@storybook/react";
import React from "react";
import {
Sidebar,
SidebarHeader,
SidebarItem,
SidebarItemContent,
SidebarBody,
SidebarHeaderImage,
SidebarHeaderTitle,
SidebarList
} from "./index";
import { CPU, CloseButton } from "../../../.storybook/assets/icons";
import { StoryObj } from "@storybook/react";
import { AppShell } from "../../../.storybook/assets/Appshell";

const meta = {
title: "UI/Sidebar",
component: Sidebar,
parameters: {
layout: "fullscreen"
},
decorators: [
(Story) => (
<AppShell>
<Story />
</AppShell>
)
],
tags: ["autodocs"]
} satisfies Meta<typeof Sidebar>;

export default meta;

type Story = StoryObj<typeof meta>;

export const defaultStory: Story = {
name: "Sidebar",
args: {
children: (
<>
<SidebarHeader icon={<CloseButton />} title="ZenML Tenant">
<SidebarHeaderImage>
<img src={`https://avatar.vercel.sh/ZenMLTenant?size=32`} />
</SidebarHeaderImage>
<SidebarHeaderTitle>My Tenant</SidebarHeaderTitle>
</SidebarHeader>
<SidebarBody>
<SidebarList>
<li className="w-full">
<SidebarItem isActive={true}>
<div>
<SidebarItemContent isActive={true} icon={<CPU />} label="Models" />
</div>
</SidebarItem>
</li>
<li className="w-full">
<SidebarItem isActive={false}>
<div>
<SidebarItemContent isActive={false} icon={<CPU />} label="Models" />
</div>
</SidebarItem>
</li>
<li className="w-full">
<SidebarItem isActive={false}>
<div>
<SidebarItemContent isActive={false} icon={<CPU />} label="Models" />
</div>
</SidebarItem>
</li>
</SidebarList>
<div style={{ marginTop: "auto" }}>
<SidebarItem>
<div>
<SidebarItemContent isActive={false} icon={<CPU />} label="Models" />
</div>
</SidebarItem>
</div>
</SidebarBody>
</>
)
}
};
146 changes: 146 additions & 0 deletions src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, {
HTMLAttributes,
HTMLProps,
PropsWithChildren,
ReactElement,
ReactNode,
cloneElement,
forwardRef,
isValidElement
} from "react";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "../../utilities/index";

export const Sidebar = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
({ className, children, ...rest }, ref) => {
return (
<nav
ref={ref}
className={cn(
"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",
className
)}
{...rest}
>
<div className="flex flex-col h-full flex-1 w-full">{children}</div>
</nav>
);
}
);

Sidebar.displayName = "Sidebar";

export function SidebarHeaderImage({ children }: PropsWithChildren) {
return <Slot className="w-6 h-6 rounded-sm shrink-0">{children}</Slot>;
}

export const SidebarHeaderTitle = forwardRef<
HTMLParagraphElement,
HTMLAttributes<HTMLParagraphElement>
>(({ children, className, ...rest }, ref) => (
<p
ref={ref}
{...rest}
className={cn(
"opacity-0 group-hover:opacity-100 ml-1 truncate duration-300 transition-all",
className
)}
>
{children}
</p>
));

SidebarHeaderTitle.displayName = "SidebarHeaderTitle";

export type SidebarHeaderProps = HTMLAttributes<HTMLDivElement> & {
title: string;
icon?: ReactNode;
};

export const SidebarHeader = forwardRef<HTMLDivElement, SidebarHeaderProps>(
({ title, icon, children, className, ...rest }, ref) => {
const existingIconClasses = isValidElement(icon) ? icon.props.className || "" : "";

const iconClasses = cn(
existingIconClasses,
"w-6 ml-auto shrink-0 h-6 opacity-0 group-hover:opacity-100 duration-300 transition-all"
);

return (
<div
ref={ref}
{...rest}
className={cn(
"bg-theme-surface-primary flex items-center whitespace-nowrap p-3 border-b border-theme-border-moderate",
className
)}
>
{children}

{icon && cloneElement(icon as ReactElement, { className: iconClasses })}
</div>
);
}
);

SidebarHeader.displayName = "SidebarHeader";

export const SidebarBody = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...rest }, ref) => {
return (
<div ref={ref} className={cn(`flex-1 flex px-1 py-2 flex-col w-full`, className)} {...rest} />
);
}
);
export const SidebarList = forwardRef<HTMLUListElement, HTMLAttributes<HTMLUListElement>>(
({ className, ...rest }, ref) => {
return (
<ul
ref={ref}
className={cn("flex gap-0.5 w-full flex-col items-center", className)}
{...rest}
/>
);
}
);

SidebarList.displayName = "SidebarList";

export type SidebarItemProps = HTMLAttributes<HTMLLIElement> & {
isActive?: boolean;
};

export function SidebarItem({
isActive = false,
...rest
}: PropsWithChildren<{ isActive?: boolean }>) {
return (
<Slot
className={`flex p-2 items-center gap-2 rounded-md w-full ${
isActive ? "bg-theme-surface-primary" : "hover:bg-neutral-200 active:bg-neutral-300"
}`}
{...rest}
></Slot>
);
}

type SidebarItemContentProps = {
icon: ReactNode;
label: string;
isActive?: boolean;
};

export function SidebarItemContent({ icon, label, isActive }: SidebarItemContentProps) {
const existingIconClasses = isValidElement(icon) ? icon.props.className || "" : "";

const iconClasses = cn(
existingIconClasses,
`w-5 h-5 shrink-0 ${isActive ? "stroke-primary-400" : "stroke-theme-text-tertiary"} `
);
return (
<>
{cloneElement(icon as ReactElement, { className: iconClasses })}
<div className="opacity-0 group-hover:opacity-100 duration-300 transition-all">{label}</div>
</>
);
}
35 changes: 35 additions & 0 deletions src/components/Sidebar/SidebarHeader.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Meta } from "@storybook/react";
import React from "react";
import { SidebarHeader, SidebarHeaderImage } from "./Sidebar";
import { CloseButton } from "../../../.storybook/assets/icons";
import { StoryObj } from "@storybook/react";

const meta = {
title: "UI/Sidebar",
component: SidebarHeader,
decorators: [
(Story) => (
<div className="group">
<Story />
</div>
)
],
tags: ["autodocs"]
} satisfies Meta<typeof SidebarHeader>;

export default meta;

type Story = StoryObj<typeof meta>;

export const sidebarHeader: Story = {
name: "Sidebar Header",
args: {
title: "ZenML Tenant",
icon: <CloseButton />,
children: (
<SidebarHeaderImage>
<img src={`https://avatar.vercel.sh/ZenMLTenant?size=32`} />
</SidebarHeaderImage>
)
}
};
Loading