Просмотр исходного кода

Add shadcn/ui dropdown menu component

cte 11 месяцев назад
Родитель
Сommit
a7101a9b94

+ 1 - 6
webview-ui/.storybook/main.ts

@@ -2,12 +2,7 @@ import type { StorybookConfig } from "@storybook/react-vite"
 
 const config: StorybookConfig = {
 	stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
-	addons: [
-		"@storybook/addon-onboarding",
-		"@storybook/addon-essentials",
-		"@chromatic-com/storybook",
-		"@storybook/addon-interactions",
-	],
+	addons: ["@storybook/addon-essentials", "@storybook/addon-interactions"],
 	framework: {
 		name: "@storybook/react-vite",
 		options: {},

Разница между файлами не показана из-за своего большого размера
+ 611 - 751
webview-ui/package-lock.json


+ 8 - 8
webview-ui/package.json

@@ -13,6 +13,8 @@
 		"build-storybook": "storybook build"
 	},
 	"dependencies": {
+		"@radix-ui/react-dropdown-menu": "^2.1.5",
+		"@radix-ui/react-icons": "^1.3.2",
 		"@radix-ui/react-slot": "^1.1.1",
 		"@tailwindcss/vite": "^4.0.0",
 		"@vscode/webview-ui-toolkit": "^1.4.0",
@@ -36,14 +38,12 @@
 		"vscrui": "^0.2.0"
 	},
 	"devDependencies": {
-		"@chromatic-com/storybook": "^3.2.4",
-		"@storybook/addon-essentials": "^8.5.2",
-		"@storybook/addon-interactions": "^8.5.2",
-		"@storybook/addon-onboarding": "^8.5.2",
+		"@storybook/addon-essentials": "^8.5.3",
+		"@storybook/addon-interactions": "^8.5.3",
 		"@storybook/blocks": "^8.5.2",
-		"@storybook/react": "^8.5.2",
-		"@storybook/react-vite": "^8.5.2",
-		"@storybook/test": "^8.5.2",
+		"@storybook/react": "^8.5.3",
+		"@storybook/react-vite": "^8.5.3",
+		"@storybook/test": "^8.5.3",
 		"@testing-library/jest-dom": "^5.17.0",
 		"@testing-library/react": "^13.4.0",
 		"@testing-library/user-event": "^13.5.0",
@@ -65,7 +65,7 @@
 		"jest": "^27.5.1",
 		"jest-environment-jsdom": "^27.5.1",
 		"jest-simple-dot-reporter": "^1.0.5",
-		"storybook": "^8.5.2",
+		"storybook": "^8.5.3",
 		"ts-jest": "^27.1.5",
 		"typescript": "^4.9.5",
 		"vite": "6.0.11"

+ 177 - 0
webview-ui/src/components/ui/dropdown-menu.tsx

@@ -0,0 +1,177 @@
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
+
+import { cn } from "@/lib/utils"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
+		inset?: boolean
+	}
+>(({ className, inset, children, ...props }, ref) => (
+	<DropdownMenuPrimitive.SubTrigger
+		ref={ref}
+		className={cn(
+			"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+			inset && "pl-8",
+			className,
+		)}
+		{...props}>
+		{children}
+		<ChevronRightIcon className="ml-auto" />
+	</DropdownMenuPrimitive.SubTrigger>
+))
+DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
+>(({ className, ...props }, ref) => (
+	<DropdownMenuPrimitive.SubContent
+		ref={ref}
+		className={cn(
+			"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+			className,
+		)}
+		{...props}
+	/>
+))
+DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Content>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+>(({ className, sideOffset = 4, ...props }, ref) => (
+	<DropdownMenuPrimitive.Portal>
+		<DropdownMenuPrimitive.Content
+			ref={ref}
+			sideOffset={sideOffset}
+			className={cn(
+				"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
+				"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+				className,
+			)}
+			{...props}
+		/>
+	</DropdownMenuPrimitive.Portal>
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Item>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
+		inset?: boolean
+	}
+>(({ className, inset, ...props }, ref) => (
+	<DropdownMenuPrimitive.Item
+		ref={ref}
+		className={cn(
+			"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+			inset && "pl-8",
+			className,
+		)}
+		{...props}
+	/>
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
+>(({ className, children, checked, ...props }, ref) => (
+	<DropdownMenuPrimitive.CheckboxItem
+		ref={ref}
+		className={cn(
+			"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+			className,
+		)}
+		checked={checked}
+		{...props}>
+		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+			<DropdownMenuPrimitive.ItemIndicator>
+				<CheckIcon className="h-4 w-4" />
+			</DropdownMenuPrimitive.ItemIndicator>
+		</span>
+		{children}
+	</DropdownMenuPrimitive.CheckboxItem>
+))
+DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
+>(({ className, children, ...props }, ref) => (
+	<DropdownMenuPrimitive.RadioItem
+		ref={ref}
+		className={cn(
+			"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
+			className,
+		)}
+		{...props}>
+		<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
+			<DropdownMenuPrimitive.ItemIndicator>
+				<DotFilledIcon className="h-2 w-2 fill-current" />
+			</DropdownMenuPrimitive.ItemIndicator>
+		</span>
+		{children}
+	</DropdownMenuPrimitive.RadioItem>
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Label>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
+		inset?: boolean
+	}
+>(({ className, inset, ...props }, ref) => (
+	<DropdownMenuPrimitive.Label
+		ref={ref}
+		className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
+		{...props}
+	/>
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+	React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
+>(({ className, ...props }, ref) => (
+	<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
+	return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+	DropdownMenu,
+	DropdownMenuTrigger,
+	DropdownMenuContent,
+	DropdownMenuItem,
+	DropdownMenuCheckboxItem,
+	DropdownMenuRadioItem,
+	DropdownMenuLabel,
+	DropdownMenuSeparator,
+	DropdownMenuShortcut,
+	DropdownMenuGroup,
+	DropdownMenuPortal,
+	DropdownMenuSub,
+	DropdownMenuSubContent,
+	DropdownMenuSubTrigger,
+	DropdownMenuRadioGroup,
+}

+ 2 - 0
webview-ui/src/components/ui/index.ts

@@ -0,0 +1,2 @@
+export * from "./button"
+export * from "./dropdown-menu"

+ 38 - 38
webview-ui/src/stories/Button.stories.ts

@@ -1,15 +1,47 @@
 import type { Meta, StoryObj } from "@storybook/react"
-import { fn } from "@storybook/test"
 
-import { Button } from "@/components/ui/button"
+import { Button } from "@/components/ui"
 
 const meta = {
-	title: "Example/Button",
+	title: "@shadcn/Button",
 	component: Button,
 	parameters: { layout: "centered" },
 	tags: ["autodocs"],
-	argTypes: {},
-	args: { onClick: fn(), children: "Button" },
+	argTypes: {
+		variant: {
+			control: "select",
+			options: ["default", "secondary", "outline", "ghost", "link", "destructive"],
+			type: "string",
+			table: {
+				defaultValue: {
+					summary: "default",
+				},
+			},
+		},
+		size: {
+			control: "select",
+			options: ["default", "sm", "lg", "icon"],
+			type: "string",
+			table: {
+				defaultValue: {
+					summary: "default",
+				},
+			},
+		},
+		children: {
+			table: {
+				disable: true,
+			},
+		},
+		asChild: {
+			table: {
+				disable: true,
+			},
+		},
+	},
+	args: {
+		children: "Button",
+	},
 } satisfies Meta<typeof Button>
 
 export default meta
@@ -17,37 +49,5 @@ export default meta
 type Story = StoryObj<typeof meta>
 
 export const Default: Story = {
-	args: {
-		variant: "default",
-	},
-}
-
-export const Secondary: Story = {
-	args: {
-		variant: "secondary",
-	},
-}
-
-export const Outline: Story = {
-	args: {
-		variant: "outline",
-	},
-}
-
-export const Ghost: Story = {
-	args: {
-		variant: "ghost",
-	},
-}
-
-export const Link: Story = {
-	args: {
-		variant: "link",
-	},
-}
-
-export const Destructive: Story = {
-	args: {
-		variant: "destructive",
-	},
+	name: "Button",
 }

+ 134 - 0
webview-ui/src/stories/DropdownMenu.stories.tsx

@@ -0,0 +1,134 @@
+import type { Meta, StoryObj } from "@storybook/react"
+import {
+	HamburgerMenuIcon,
+	BorderLeftIcon,
+	BorderRightIcon,
+	BorderBottomIcon,
+	BorderTopIcon,
+	TextAlignLeftIcon,
+	TextAlignCenterIcon,
+	TextAlignRightIcon,
+} from "@radix-ui/react-icons"
+
+import {
+	Button,
+	DropdownMenu,
+	DropdownMenuContent,
+	DropdownMenuGroup,
+	DropdownMenuItem,
+	DropdownMenuLabel,
+	DropdownMenuPortal,
+	DropdownMenuSeparator,
+	DropdownMenuShortcut,
+	DropdownMenuSub,
+	DropdownMenuSubContent,
+	DropdownMenuSubTrigger,
+	DropdownMenuTrigger,
+} from "@/components/ui"
+
+const meta = {
+	title: "@shadcn/DropdownMenu",
+	component: DropdownMenu,
+	parameters: { layout: "centered" },
+	tags: ["autodocs"],
+} satisfies Meta<typeof DropdownMenu>
+
+export default meta
+
+type Story = StoryObj<typeof meta>
+
+export const Default: Story = {
+	name: "DropdownMenu",
+	render: () => (
+		<DropdownMenu>
+			<DropdownMenuTrigger asChild>
+				<Button variant="ghost" size="icon">
+					<HamburgerMenuIcon />
+				</Button>
+			</DropdownMenuTrigger>
+			<DropdownMenuContent>
+				<DropdownMenuLabel>Label</DropdownMenuLabel>
+				<DropdownMenuSeparator />
+				<DropdownMenuGroup>
+					<DropdownMenuItem>Item 1</DropdownMenuItem>
+					<DropdownMenuItem>
+						Item 2<DropdownMenuShortcut>⌘2</DropdownMenuShortcut>
+					</DropdownMenuItem>
+				</DropdownMenuGroup>
+				<DropdownMenuSeparator />
+				<DropdownMenuGroup>
+					<DropdownMenuSub>
+						<DropdownMenuSubTrigger>Submenu</DropdownMenuSubTrigger>
+						<DropdownMenuPortal>
+							<DropdownMenuSubContent>
+								<DropdownMenuItem>Foo</DropdownMenuItem>
+								<DropdownMenuItem>
+									Bar
+									<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
+								</DropdownMenuItem>
+								<DropdownMenuSeparator />
+								<DropdownMenuItem>Baz</DropdownMenuItem>
+							</DropdownMenuSubContent>
+						</DropdownMenuPortal>
+					</DropdownMenuSub>
+				</DropdownMenuGroup>
+			</DropdownMenuContent>
+		</DropdownMenu>
+	),
+}
+
+type DropdownMenuVariantProps = {
+	side?: "top" | "bottom" | "left" | "right"
+	align?: "start" | "center" | "end"
+	children?: React.ReactNode
+}
+
+const DropdownMenuVariant = ({ side = "bottom", align = "center", children }: DropdownMenuVariantProps) => (
+	<DropdownMenu>
+		<DropdownMenuTrigger asChild>
+			<Button variant="ghost" size="icon">
+				{children}
+			</Button>
+		</DropdownMenuTrigger>
+		<DropdownMenuContent side={side} align={align}>
+			<DropdownMenuItem>Foo</DropdownMenuItem>
+			<DropdownMenuItem>Bar</DropdownMenuItem>
+			<DropdownMenuItem>Baz</DropdownMenuItem>
+		</DropdownMenuContent>
+	</DropdownMenu>
+)
+
+export const Placements: Story = {
+	render: () => (
+		<div className="flex gap-2">
+			<DropdownMenuVariant side="top">
+				<BorderTopIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant side="bottom">
+				<BorderBottomIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant side="left">
+				<BorderLeftIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant side="right">
+				<BorderRightIcon />
+			</DropdownMenuVariant>
+		</div>
+	),
+}
+
+export const Alignments: Story = {
+	render: () => (
+		<div className="flex gap-2">
+			<DropdownMenuVariant align="center">
+				<TextAlignCenterIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant align="end">
+				<TextAlignRightIcon />
+			</DropdownMenuVariant>
+			<DropdownMenuVariant align="start">
+				<TextAlignLeftIcon />
+			</DropdownMenuVariant>
+		</div>
+	),
+}

+ 47 - 0
webview-ui/src/stories/vscrui/Dropdown.stories.tsx

@@ -0,0 +1,47 @@
+import type { Meta, StoryObj } from "@storybook/react"
+
+import { Dropdown } from "vscrui"
+
+const meta = {
+	title: "@vscrui/Dropdown",
+	component: () => (
+		<Dropdown
+			value="foo"
+			role="combobox"
+			options={[
+				{ value: "foo", label: "Foo" },
+				{ value: "bar", label: "Bar" },
+				{ value: "baz", label: "Baz" },
+			]}
+		/>
+	),
+	parameters: { layout: "centered" },
+	tags: ["autodocs"],
+	argTypes: {},
+	args: {},
+} satisfies Meta<typeof Dropdown>
+
+export default meta
+
+type Story = StoryObj<typeof meta>
+
+export const Default: Story = {
+	args: {},
+	parameters: {
+		docs: {
+			source: {
+				code: `
+<Dropdown
+    value="foo"
+    role="combobox"
+    options={[
+        { value: "foo", label: "Foo" },
+        { value: "bar", label: "Bar" },
+        { value: "baz", label: "Baz" }
+    ]}
+/>`,
+				language: "tsx",
+			},
+		},
+	},
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов