feat: implement Dropdown kit component

This commit is contained in:
Steven
2023-04-14 08:10:25 +08:00
parent 81ad3408fb
commit d43fefc7f3
11 changed files with 163 additions and 45 deletions

4
next.config.js Normal file
View File

@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
module.exports = {
output: "standalone",
};

View File

@ -13,6 +13,7 @@
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@mui/material": "^5.11.14", "@mui/material": "^5.11.14",
"@mui/styled-engine-sc": "^5.11.11", "@mui/styled-engine-sc": "^5.11.11",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-popover": "^1.0.5", "@radix-ui/react-popover": "^1.0.5",
"@radix-ui/react-select": "^1.2.1", "@radix-ui/react-select": "^1.2.1",
"@radix-ui/react-tooltip": "^1.0.5", "@radix-ui/react-tooltip": "^1.0.5",

74
pnpm-lock.yaml generated
View File

@ -13,6 +13,9 @@ dependencies:
'@mui/styled-engine-sc': '@mui/styled-engine-sc':
specifier: ^5.11.11 specifier: ^5.11.11
version: 5.11.11(styled-components@5.3.9) version: 5.11.11(styled-components@5.3.9)
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.4
version: 2.0.4(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-popover': '@radix-ui/react-popover':
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0) version: 1.0.5(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
@ -901,6 +904,26 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/@radix-ui/react-dropdown-menu@2.0.4(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-y6AT9+MydyXcByivdK1+QpjWoKaC7MLjkS/cH1Q3keEyMvDkiY85m8o2Bi6+Z1PPUlCsMULopxagQOSfN0wahg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-id': 1.0.0(react@18.2.0)
'@radix-ui/react-menu': 2.0.4(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-focus-guards@1.0.0(react@18.2.0): /@radix-ui/react-focus-guards@1.0.0(react@18.2.0):
resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==} resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==}
peerDependencies: peerDependencies:
@ -934,6 +957,37 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/@radix-ui/react-menu@2.0.4(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-mzKR47tZ1t193trEqlQoJvzY4u9vYfVH16ryBrVrCAGZzkgyWnMQYEZdUkM7y8ak9mrkKtJiqB47TlEnubeOFQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-collection': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-direction': 1.0.0(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.3(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.0(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.0(react@18.2.0)
'@radix-ui/react-popper': 1.1.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-portal': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-roving-focus': 1.0.3(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.1(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.0.28)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-popover@1.0.5(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0): /@radix-ui/react-popover@1.0.5(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GRHZ8yD12MrN2NLobHPE8Rb5uHTxd9x372DE9PPNnBjpczAQHcZ5ne0KXG4xpf+RDdXSzdLv9ym6mYJCDTaUZg==} resolution: {integrity: sha512-GRHZ8yD12MrN2NLobHPE8Rb5uHTxd9x372DE9PPNnBjpczAQHcZ5ne0KXG4xpf+RDdXSzdLv9ym6mYJCDTaUZg==}
peerDependencies: peerDependencies:
@ -1022,6 +1076,26 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/@radix-ui/react-roving-focus@1.0.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-stjCkIoMe6h+1fWtXlA6cRfikdBzCLp3SnVk7c48cv/uy3DTGoXhN76YaOYUJuy3aEDvDIKwKR5KSmvrtPvQPQ==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-collection': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-direction': 1.0.0(react@18.2.0)
'@radix-ui/react-id': 1.0.0(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-select@1.2.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0): /@radix-ui/react-select@1.2.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GULRMITaOHNj79BZvQs3iZO0+f2IgI8g5HDhMi7Bnc13t7IlG86NFtOCfTLme4PNZdEtU+no+oGgcl6IFiphpQ==} resolution: {integrity: sha512-GULRMITaOHNj79BZvQs3iZO0+f2IgI8g5HDhMi7Bnc13t7IlG86NFtOCfTLme4PNZdEtU+no+oGgcl6IFiphpQ==}
peerDependencies: peerDependencies:

View File

@ -6,6 +6,7 @@ import { useConversationStore, useConnectionStore, useLayoutStore, ResponsiveWid
import { Conversation, Connection } from "@/types"; import { Conversation, Connection } from "@/types";
import Select from "./kit/Select"; import Select from "./kit/Select";
import Tooltip from "./kit/Tooltip"; import Tooltip from "./kit/Tooltip";
import Dropdown, { DropdownItem } from "./kit/Dropdown";
import Icon from "./Icon"; import Icon from "./Icon";
import EngineIcon from "./EngineIcon"; import EngineIcon from "./EngineIcon";
import LocaleSwitch from "./LocaleSwitch"; import LocaleSwitch from "./LocaleSwitch";
@ -243,7 +244,7 @@ const ConnectionSidebar = () => {
{conversationList.map((conversation) => ( {conversationList.map((conversation) => (
<div <div
key={conversation.id} key={conversation.id}
className={`w-full mt-2 first:mt-4 py-3 pl-4 pr-2 rounded-lg flex flex-row justify-start items-center cursor-pointer dark:text-gray-300 border border-transparent group hover:bg-gray-50 dark:hover:bg-zinc-800 ${ className={`w-full mt-2 first:mt-4 py-3 pl-4 pr-2 rounded-lg flex flex-row justify-start items-center cursor-pointer dark:text-gray-300 border border-transparent group hover:bg-white dark:hover:bg-zinc-800 ${
conversation.id === conversationStore.currentConversation?.id && "bg-white dark:bg-zinc-800 border-gray-200 font-medium" conversation.id === conversationStore.currentConversation?.id && "bg-white dark:bg-zinc-800 border-gray-200 font-medium"
}`} }`}
onClick={() => handleConversationSelect(conversation)} onClick={() => handleConversationSelect(conversation)}
@ -254,22 +255,30 @@ const ConnectionSidebar = () => {
<Icon.IoChatbubbleOutline className="w-5 h-auto mr-1.5 opacity-80 shrink-0" /> <Icon.IoChatbubbleOutline className="w-5 h-auto mr-1.5 opacity-80 shrink-0" />
)} )}
<span className="truncate grow">{conversation.title || "SQL Chat"}</span> <span className="truncate grow">{conversation.title || "SQL Chat"}</span>
<span className="ml-0.5 shrink-0 hidden group-hover:flex flex-row justify-end items-center space-x-1"> <Dropdown
<Icon.FiEdit3 tigger={
className="w-4 h-auto opacity-60 hover:opacity-80" <button className="w-4 h-4 shrink-0 group-hover:visible invisible flex justify-center items-center text-gray-400 hover:text-gray-500">
onClick={(e) => { <Icon.FiMoreHorizontal className="w-full h-auto" />
e.stopPropagation(); </button>
handleEditConversationTitle(conversation); }
}} >
/> <div className="p-1 flex flex-col justify-start items-start bg-white dark:bg-zinc-900 shadow-lg rounded-lg">
<Icon.IoClose <DropdownItem
className="w-5 h-auto opacity-60 hover:opacity-80" className="w-full p-1 px-2 flex flex-row justify-start items-center rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-800"
onClick={(e) => { onClick={() => handleEditConversationTitle(conversation)}
e.stopPropagation(); >
handleDeleteConversation(conversation); <Icon.FiEdit3 className="w-4 h-auto mr-1 opacity-70" />
}} {t("common.edit")}
/> </DropdownItem>
</span> <DropdownItem
className="w-full p-1 px-2 flex flex-row justify-start items-center rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-800"
onClick={() => handleDeleteConversation(conversation)}
>
<Icon.IoTrash className="w-4 h-auto mr-1 opacity-70" />
{t("common.delete")}
</DropdownItem>
</div>
</Dropdown>
</div> </div>
))} ))}
<button <button

View File

@ -1,4 +1,3 @@
import { Menu, MenuItem } from "@mui/material";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ReactElement, useState } from "react"; import { ReactElement, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -7,6 +6,7 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { useConversationStore, useConnectionStore, useMessageStore, useUserStore, useSettingStore } from "@/store"; import { useConversationStore, useConnectionStore, useMessageStore, useUserStore, useSettingStore } from "@/store";
import { Message } from "@/types"; import { Message } from "@/types";
import Dropdown, { DropdownItem } from "../kit/Dropdown";
import Icon from "../Icon"; import Icon from "../Icon";
import { CodeBlock } from "../CodeBlock"; import { CodeBlock } from "../CodeBlock";
import EngineIcon from "../EngineIcon"; import EngineIcon from "../EngineIcon";
@ -112,30 +112,30 @@ const MessageView = (props: Props) => {
</span> </span>
</div> </div>
<div className={`invisible group-hover:visible ${showMenu && "!visible"}`}> <div className={`invisible group-hover:visible ${showMenu && "!visible"}`}>
<button <Dropdown
className="w-6 h-6 ml-1 mt-2 flex justify-center items-center text-gray-400 hover:text-gray-500" tigger={
onClick={handleMoreMenuClick} <button className="w-6 h-6 ml-1 mt-2 flex justify-center items-center text-gray-400 hover:text-gray-500">
<Icon.IoMdMore className="w-5 h-auto" />
</button>
}
> >
<Icon.IoMdMore className="w-5 h-auto" /> <div className="p-1 flex flex-col justify-start items-start bg-white dark:bg-zinc-900 rounded-lg">
</button> <DropdownItem
<Menu className="w-full p-1 px-2 flex flex-row justify-start items-center rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-800"
className="mt-1" onClick={copyMessage}
anchorEl={menuAnchorEl} >
open={showMenu} <Icon.BiClipboard className="w-4 h-auto mr-2 opacity-70" />
onClose={() => setMenuAnchorEl(null)} {t("common.copy")}
MenuListProps={{ </DropdownItem>
"aria-labelledby": "basic-button", <DropdownItem
}} className="w-full p-1 px-2 flex flex-row justify-start items-center rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-800"
> onClick={() => deleteMessage(message)}
<MenuItem onClick={copyMessage}> >
<Icon.BiClipboard className="w-4 h-auto mr-2 opacity-70" /> <Icon.BiTrash className="w-4 h-auto mr-2 opacity-70" />
{t("common.copy")} {t("common.delete")}
</MenuItem> </DropdownItem>
<MenuItem onClick={() => deleteMessage(message)}> </div>
<Icon.BiTrash className="w-4 h-auto mr-2 opacity-70" /> </Dropdown>
{t("common.delete")}
</MenuItem>
</Menu>
</div> </div>
</> </>
)} )}

View File

@ -0,0 +1,28 @@
import * as DropdownUI from "@radix-ui/react-dropdown-menu";
import { ReactNode } from "react";
interface Props {
children: ReactNode;
tigger: ReactNode;
}
const Dropdown = (props: Props) => {
const { children, tigger } = props;
return (
<DropdownUI.Root modal={false}>
<DropdownUI.Trigger asChild onClick={(e) => e.stopPropagation()}>
{tigger}
</DropdownUI.Trigger>
<DropdownUI.Portal>
<DropdownUI.Content className="z-[999] drop-shadow" sideOffset={5}>
{children}
</DropdownUI.Content>
</DropdownUI.Portal>
</DropdownUI.Root>
);
};
export const DropdownItem = DropdownUI.Item;
export default Dropdown;

View File

@ -17,7 +17,7 @@ const Modal = (props: Props) => {
<div <div
className={`${ className={`${
className || "" className || ""
} flex flex-col bg-white dark:bg-zinc-800 rounded-xl p-4 fixed top-[50%] left-[50%] h-auto max-h-[85vh] w-[90vw] max-w-[90vw] sm:max-w-lg translate-x-[-50%] translate-y-[-50%] z-100`} } flex flex-col bg-white dark:bg-zinc-800 rounded-xl p-4 fixed top-[50%] left-[50%] h-auto max-h-[85vh] w-[90vw] max-w-[90vw] sm:max-w-lg translate-x-[-50%] translate-y-[-50%] z-100 outline-none`}
> >
<p className="text-lg pl-1 text-black dark:text-gray-300 font-medium mb-2">{title}</p> <p className="text-lg pl-1 text-black dark:text-gray-300 font-medium mb-2">{title}</p>
<button <button

View File

@ -16,7 +16,7 @@ const Popover = (props: Props) => {
<PopoverUI.Portal> <PopoverUI.Portal>
<PopoverUI.Content <PopoverUI.Content
asChild asChild
className={`${className || ""} z-[9999] p-2 bg-white dark:bg-zinc-700 shadow-lg rounded-lg`} className={`${className || ""} z-[999] p-2 bg-white dark:bg-zinc-700 drop-shadow rounded-lg`}
sideOffset={5} sideOffset={5}
> >
{children} {children}

View File

@ -30,13 +30,13 @@ const Select = (props: Props) => {
</SelectUI.Trigger> </SelectUI.Trigger>
<SelectUI.Portal> <SelectUI.Portal>
<SelectUI.Content <SelectUI.Content
className="z-[999] -mt-px" className="z-[999] mt-1"
style={{ style={{
width: "var(--radix-select-trigger-width)", width: "var(--radix-select-trigger-width)",
}} }}
position="popper" position="popper"
> >
<SelectUI.Viewport className="bg-white dark:bg-zinc-700 border dark:border-zinc-800 shadow p-1 rounded-lg"> <SelectUI.Viewport className="bg-white dark:bg-zinc-700 border dark:border-zinc-800 drop-shadow-lg p-1 rounded-lg">
<SelectUI.Group> <SelectUI.Group>
{placeholder && <SelectUI.Label className="w-full px-3 mt-2 mb-1 text-sm text-gray-400">{placeholder}</SelectUI.Label>} {placeholder && <SelectUI.Label className="w-full px-3 mt-2 mb-1 text-sm text-gray-400">{placeholder}</SelectUI.Label>}
{itemList.map((item) => ( {itemList.map((item) => (

View File

@ -4,6 +4,7 @@
"close": "Close", "close": "Close",
"confirm": "Confirm", "confirm": "Confirm",
"save": "Save", "save": "Save",
"edit": "Edit",
"loading": "Loading", "loading": "Loading",
"setting": "Setting", "setting": "Setting",
"copy": "Copy", "copy": "Copy",

View File

@ -4,6 +4,7 @@
"close": "关闭", "close": "关闭",
"confirm": "确认", "confirm": "确认",
"save": "保存", "save": "保存",
"edit": "编辑",
"loading": "加载中", "loading": "加载中",
"setting": "设置", "setting": "设置",
"copy": "复制", "copy": "复制",