diff --git a/.changeset/poor-pants-tell.md b/.changeset/poor-pants-tell.md new file mode 100644 index 0000000..c0dc48b --- /dev/null +++ b/.changeset/poor-pants-tell.md @@ -0,0 +1,5 @@ +--- +"@zenml-io/react-component-library": minor +--- + +Initialize Sidebar Component diff --git a/.storybook/assets/Appshell.tsx b/.storybook/assets/Appshell.tsx new file mode 100644 index 0000000..37ff9cf --- /dev/null +++ b/.storybook/assets/Appshell.tsx @@ -0,0 +1,12 @@ +import React, { ReactNode } from "react"; + +export function AppShell({ children }: { children: ReactNode }) { + return ( +
+
+ {children} +
+ ); +} diff --git a/.storybook/assets/CPU.tsx b/.storybook/assets/CPU.tsx new file mode 100644 index 0000000..c5f958e --- /dev/null +++ b/.storybook/assets/CPU.tsx @@ -0,0 +1,27 @@ +import React from "react"; +export default function CPU({ className = "" }: { className?: string }) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/.storybook/assets/icons.tsx b/.storybook/assets/icons.tsx new file mode 100644 index 0000000..92f6fb3 --- /dev/null +++ b/.storybook/assets/icons.tsx @@ -0,0 +1,44 @@ +import React from "react"; +export function CPU({ className = "" }: { className?: string }) { + return ( + + + + + + + + + + + + + + ); +} + +export function CloseButton({ className = "" }: { className?: string }) { + return ( + + + + ); +} diff --git a/.storybook/preview.ts b/.storybook/preview.ts index cd70a32..4be460a 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -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: { diff --git a/src/components/Sidebar/Sidebar.stories.tsx b/src/components/Sidebar/Sidebar.stories.tsx new file mode 100644 index 0000000..28481ab --- /dev/null +++ b/src/components/Sidebar/Sidebar.stories.tsx @@ -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) => ( + + + + ) + ], + tags: ["autodocs"] +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const defaultStory: Story = { + name: "Sidebar", + args: { + children: ( + <> + } title="ZenML Tenant"> + + + + My Tenant + + + +
  • + +
    + } label="Models" /> +
    +
    +
  • +
  • + +
    + } label="Models" /> +
    +
    +
  • +
  • + +
    + } label="Models" /> +
    +
    +
  • +
    +
    + +
    + } label="Models" /> +
    +
    +
    +
    + + ) + } +}; diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..de1141c --- /dev/null +++ b/src/components/Sidebar/Sidebar.tsx @@ -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>( + ({ className, children, ...rest }, ref) => { + return ( + + ); + } +); + +Sidebar.displayName = "Sidebar"; + +export function SidebarHeaderImage({ children }: PropsWithChildren) { + return {children}; +} + +export const SidebarHeaderTitle = forwardRef< + HTMLParagraphElement, + HTMLAttributes +>(({ children, className, ...rest }, ref) => ( +

    + {children} +

    +)); + +SidebarHeaderTitle.displayName = "SidebarHeaderTitle"; + +export type SidebarHeaderProps = HTMLAttributes & { + title: string; + icon?: ReactNode; +}; + +export const SidebarHeader = forwardRef( + ({ 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 ( +
    + {children} + + {icon && cloneElement(icon as ReactElement, { className: iconClasses })} +
    + ); + } +); + +SidebarHeader.displayName = "SidebarHeader"; + +export const SidebarBody = forwardRef>( + ({ className, ...rest }, ref) => { + return ( +
    + ); + } +); +export const SidebarList = forwardRef>( + ({ className, ...rest }, ref) => { + return ( +
      + ); + } +); + +SidebarList.displayName = "SidebarList"; + +export type SidebarItemProps = HTMLAttributes & { + isActive?: boolean; +}; + +export function SidebarItem({ + isActive = false, + ...rest +}: PropsWithChildren<{ isActive?: boolean }>) { + return ( + + ); +} + +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 })} +
      {label}
      + + ); +} diff --git a/src/components/Sidebar/SidebarHeader.stories.tsx b/src/components/Sidebar/SidebarHeader.stories.tsx new file mode 100644 index 0000000..0e5df48 --- /dev/null +++ b/src/components/Sidebar/SidebarHeader.stories.tsx @@ -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) => ( +
      + +
      + ) + ], + tags: ["autodocs"] +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const sidebarHeader: Story = { + name: "Sidebar Header", + args: { + title: "ZenML Tenant", + icon: , + children: ( + + + + ) + } +}; diff --git a/src/components/Sidebar/SidebarItem.stories.tsx b/src/components/Sidebar/SidebarItem.stories.tsx new file mode 100644 index 0000000..c440112 --- /dev/null +++ b/src/components/Sidebar/SidebarItem.stories.tsx @@ -0,0 +1,46 @@ +import { Meta } from "@storybook/react"; +import React from "react"; +import { SidebarItem, SidebarItemContent } from "./Sidebar"; +import { CPU } from "../../../.storybook/assets/icons"; +import { StoryObj } from "@storybook/react"; + +const meta = { + title: "UI/Sidebar", + component: SidebarItem, + decorators: [ + (Story) => ( +
        + +
      + ) + ], + tags: ["autodocs"] +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const inactive: Story = { + name: "Inactive Sidebar Item", + args: { + isActive: false, + children: ( +
      + } label="Models" /> +
      + ) + } +}; + +export const active: Story = { + name: "Active Sidebar Item", + args: { + isActive: true, + children: ( +
      + } label="Models" /> +
      + ) + } +}; diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx new file mode 100644 index 0000000..d678998 --- /dev/null +++ b/src/components/Sidebar/index.tsx @@ -0,0 +1 @@ +export * from "./Sidebar"; diff --git a/src/components/index.ts b/src/components/index.ts index e22c29a..f063219 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1 +1,2 @@ export * from "./Button"; +export * from "./Sidebar"; diff --git a/src/tailwind/index.ts b/src/tailwind/index.ts index 3d684b8..d5583c4 100644 --- a/src/tailwind/index.ts +++ b/src/tailwind/index.ts @@ -73,7 +73,7 @@ export const zenmlPlugin = plugin( "--color-blue-025": "208 88% 90%", "--color-text-primary": "var(--color-primary-900)", "--color-text-secondary": "var(--color-neutral-500)", - "--color-text-tertiary": "var(--color-neutral-300)", + "--color-text-tertiary": "var(--color-neutral-400)", "--color-text-negative": "var(--color-neutral-000)", "--color-text-brand": "var(--color-primary-500)", "--color-text-error": "var(--color-error-500)",