Skip to content

Commit 25a4d83

Browse files
committed
Merge branch 'main' into chore/update-dependencies
2 parents 326e88a + 08a7e84 commit 25a4d83

File tree

10 files changed

+1008
-512
lines changed

10 files changed

+1008
-512
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# @zenml-io/react-component-library
22

3+
## 0.20.0
4+
5+
### Minor Changes
6+
7+
- 279169d: add command component
8+
9+
## 0.19.2
10+
11+
### Patch Changes
12+
13+
- bad93d1: fix: wrap function with useCallback
14+
15+
## 0.19.1
16+
17+
### Patch Changes
18+
19+
- a6ba4d3: sidebar: start closing immediately when "close" button clicked, ignoring hover
20+
321
## 0.19.0
422

523
### Minor Changes

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenml-io/react-component-library",
3-
"version": "0.19.0",
3+
"version": "0.20.0",
44
"description": "React Component Library from Zenml.io",
55
"keywords": [
66
"ZenML",
@@ -72,6 +72,7 @@
7272
"@tanstack/react-table": "^8.20.6",
7373
"class-variance-authority": "^0.7.1",
7474
"clsx": "^2.1.1",
75+
"cmdk": "^1.0.4",
7576
"tailwind-merge": "^2.6.0"
7677
},
7778
"devDependencies": {

pnpm-lock.yaml

Lines changed: 743 additions & 507 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Meta } from "@storybook/react";
2+
import React from "react";
3+
import {
4+
Command,
5+
CommandEmpty,
6+
CommandGroup,
7+
CommandInput,
8+
CommandItem,
9+
CommandList,
10+
CommandSeparator,
11+
CommandShortcut
12+
} from "./index";
13+
import { StoryObj } from "@storybook/react";
14+
import { Input } from "../Input";
15+
16+
const meta = {
17+
title: "Elements/Command",
18+
component: Command,
19+
argTypes: {},
20+
parameters: {
21+
layout: "centered"
22+
},
23+
24+
tags: ["autodocs"]
25+
} satisfies Meta<typeof Command>;
26+
27+
export default meta;
28+
29+
type Story = StoryObj<typeof meta>;
30+
31+
export const DefaultVariant: Story = {
32+
name: "Default",
33+
render: () => (
34+
<Command className="rounded-md border border-theme-border-moderate shadow-md md:min-w-[450px]">
35+
<CommandInput placeholder="Type a command or search...">
36+
<Input className="w-full" inputSize="sm" />
37+
</CommandInput>
38+
<CommandList>
39+
<CommandEmpty>No results found.</CommandEmpty>
40+
<CommandGroup heading="Suggestions">
41+
<CommandItem>
42+
<span>Calendar</span>
43+
</CommandItem>
44+
<CommandItem>
45+
<span>Search Emoji</span>
46+
</CommandItem>
47+
<CommandItem disabled>
48+
<span>Calculator</span>
49+
</CommandItem>
50+
</CommandGroup>
51+
<CommandSeparator />
52+
<CommandGroup heading="Settings">
53+
<CommandItem>
54+
<span>Profile</span>
55+
<CommandShortcut>⌘P</CommandShortcut>
56+
</CommandItem>
57+
<CommandItem>
58+
<span>Billing</span>
59+
<CommandShortcut>⌘B</CommandShortcut>
60+
</CommandItem>
61+
<CommandItem>
62+
<span>Settings</span>
63+
<CommandShortcut>⌘S</CommandShortcut>
64+
</CommandItem>
65+
</CommandGroup>
66+
</CommandList>
67+
</Command>
68+
)
69+
};

src/components/Command/Command.tsx

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { type DialogProps } from "@radix-ui/react-dialog";
2+
import { Command as CommandPrimitive } from "cmdk";
3+
import * as React from "react";
4+
import { cn } from "../../utilities";
5+
import { Dialog, DialogContent } from "../Dialog";
6+
7+
const Command = React.forwardRef<
8+
React.ElementRef<typeof CommandPrimitive>,
9+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
10+
>(({ className, ...props }, ref) => (
11+
<CommandPrimitive
12+
ref={ref}
13+
className={cn(
14+
"bg-theme-surface-primary text-theme-text-primary flex h-full w-full flex-col overflow-hidden rounded-md",
15+
className
16+
)}
17+
{...props}
18+
/>
19+
));
20+
Command.displayName = CommandPrimitive.displayName;
21+
22+
const CommandDialog = ({ children, ...props }: DialogProps) => {
23+
return (
24+
<Dialog {...props}>
25+
<DialogContent className="overflow-hidden p-0">
26+
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
27+
{children}
28+
</Command>
29+
</DialogContent>
30+
</Dialog>
31+
);
32+
};
33+
34+
const CommandInput = React.forwardRef<
35+
React.ElementRef<typeof CommandPrimitive.Input>,
36+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
37+
>(({ ...props }, ref) => (
38+
<div className="flex items-center p-2 [&>div]:w-full" cmdk-input-wrapper="">
39+
<CommandPrimitive.Input asChild ref={ref} {...props}></CommandPrimitive.Input>
40+
</div>
41+
));
42+
43+
CommandInput.displayName = CommandPrimitive.Input.displayName;
44+
45+
const CommandList = React.forwardRef<
46+
React.ElementRef<typeof CommandPrimitive.List>,
47+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
48+
>(({ className, ...props }, ref) => (
49+
<CommandPrimitive.List
50+
ref={ref}
51+
className={cn("overflow-y-auto overflow-x-hidden", className)}
52+
{...props}
53+
/>
54+
));
55+
56+
CommandList.displayName = CommandPrimitive.List.displayName;
57+
58+
const CommandEmpty = React.forwardRef<
59+
React.ElementRef<typeof CommandPrimitive.Empty>,
60+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
61+
>((props, ref) => (
62+
<CommandPrimitive.Empty
63+
ref={ref}
64+
className="py-2 text-center text-text-sm text-theme-text-secondary"
65+
{...props}
66+
/>
67+
));
68+
69+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
70+
71+
const CommandGroup = React.forwardRef<
72+
React.ElementRef<typeof CommandPrimitive.Group>,
73+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
74+
>(({ className, ...props }, ref) => (
75+
<CommandPrimitive.Group
76+
ref={ref}
77+
className={cn(
78+
"text-theme-text-primary [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-text-sm [&_[cmdk-group-heading]]:text-theme-text-secondary overflow-hidden [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-semibold",
79+
className
80+
)}
81+
{...props}
82+
/>
83+
));
84+
85+
CommandGroup.displayName = CommandPrimitive.Group.displayName;
86+
87+
const CommandSeparator = React.forwardRef<
88+
React.ElementRef<typeof CommandPrimitive.Separator>,
89+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
90+
>(({ className, ...props }, ref) => (
91+
<CommandPrimitive.Separator
92+
ref={ref}
93+
className={cn("-mx-1 h-[1px] bg-theme-border-moderate", className)}
94+
{...props}
95+
/>
96+
));
97+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
98+
99+
const CommandItem = React.forwardRef<
100+
React.ElementRef<typeof CommandPrimitive.Item>,
101+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
102+
>(({ className, ...props }, ref) => (
103+
<CommandPrimitive.Item
104+
ref={ref}
105+
className={cn(
106+
"text-text-sm relative flex cursor-default select-none items-center gap-2 rounded-sm py-2 pl-2 pr-3 outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-primary-25 data-[selected=true]:text-theme-text-brand data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
107+
className
108+
)}
109+
{...props}
110+
/>
111+
));
112+
113+
CommandItem.displayName = CommandPrimitive.Item.displayName;
114+
115+
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
116+
return (
117+
<span
118+
className={cn("text-text- text-muted-foreground ml-auto tracking-widest", className)}
119+
{...props}
120+
/>
121+
);
122+
};
123+
CommandShortcut.displayName = "CommandShortcut";
124+
125+
export {
126+
Command,
127+
CommandDialog,
128+
CommandEmpty,
129+
CommandGroup,
130+
CommandInput,
131+
CommandItem,
132+
CommandList,
133+
CommandSeparator,
134+
CommandShortcut
135+
};

src/components/Command/index.tsx

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

src/components/Sidebar/Sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import { useSidebarContext } from "./SidebarContext";
1414

1515
export const Sidebar = forwardRef<HTMLDivElement, HTMLProps<HTMLDivElement>>(
1616
({ className, children, ...rest }, ref) => {
17-
const { isOpen } = useSidebarContext();
17+
const { isOpen, isClosing } = useSidebarContext();
1818

1919
return (
2020
<nav
2121
ref={ref}
2222
className={cn(
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`,
23+
`group flex-1 h-full flex w-9 ${isOpen ? "w-[220px]" : isClosing ? "" : "hover:w-[220px]"} bg-neutral-100 transition-all overflow-x-hidden duration-300 flex-col items-center border-r border-theme-border-moderate`,
2424
className
2525
)}
2626
{...rest}

src/components/Sidebar/SidebarContext.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import React, {
33
PropsWithChildren,
44
SetStateAction,
55
createContext,
6+
useCallback,
67
useContext,
78
useState
89
} from "react";
910

11+
import { Setter, getValueFromSetter } from "../../utilities";
12+
1013
type SidebarContextProps = {
1114
isOpen: boolean;
15+
isClosing: boolean;
1216
setIsOpen: Dispatch<SetStateAction<boolean>>;
1317
};
1418

@@ -18,10 +22,35 @@ export function SidebarProvider({
1822
children,
1923
initialOpen = false
2024
}: PropsWithChildren<{ initialOpen?: boolean }>) {
21-
const [isOpen, setIsOpen] = useState(initialOpen);
25+
const [isOpen, _setIsOpen] = useState(initialOpen);
26+
const [isClosing, setIsClosing] = useState(false);
27+
28+
/**
29+
* wrapper around `_setIsOpen`, to control `isClosing`.
30+
* we should instead have a single "status" variable, but refactoring might suck..
31+
*
32+
* the purpose of `isClosing` is single:
33+
* when sidebar is opened, and user hovers over it and clicks the button to close it,
34+
* then the sidebar gets automatically closed, instead of waiting for the user
35+
* to hover away from it before closing.
36+
*/
37+
const setIsOpen = useCallback((newValue: Setter<boolean>): void => {
38+
_setIsOpen((currIsOpen) => {
39+
const value: boolean = getValueFromSetter(newValue, currIsOpen);
40+
41+
if (value === false) {
42+
setIsClosing(true);
43+
setTimeout(() => setIsClosing(false), 300);
44+
}
45+
46+
return value;
47+
});
48+
}, []);
2249

2350
return (
24-
<SidebarContext.Provider value={{ isOpen, setIsOpen }}>{children}</SidebarContext.Provider>
51+
<SidebarContext.Provider value={{ isOpen, setIsOpen, isClosing }}>
52+
{children}
53+
</SidebarContext.Provider>
2554
);
2655
}
2756

src/components/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export * from "./AlertDialog";
1717
export * from "./ScrollArea";
1818
export * from "./HoverCard";
1919
export * from "./RadioGroup";
20+
export * from "./Command";

src/utilities/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,9 @@ const customTwMerge = extendTailwindMerge({
2828
export function cn(...inputs: ClassValue[]) {
2929
return customTwMerge(clsx(inputs));
3030
}
31+
32+
export type Setter<T> = T | ((prevValue: T) => T);
33+
34+
export function getValueFromSetter<T>(newValue: Setter<T>, currValue: T): T {
35+
return newValue instanceof Function ? newValue(currValue) : newValue;
36+
}

0 commit comments

Comments
 (0)