diff --git a/CHANGE_LOG.md b/CHANGE_LOG.md index 44ffa9e..2631741 100644 --- a/CHANGE_LOG.md +++ b/CHANGE_LOG.md @@ -7,7 +7,7 @@ ### Changed - Adjust global CSS and use as many tailwind built-in CSS values as possible to optimize some color matching effects. -- Replace React.PropsWithChildren with React.HTMLAttributes +- Refactor all common components with radix-ui. ## v0.0.5 diff --git a/components/Button/index.tsx b/components/Button/index.tsx deleted file mode 100644 index 4320346..0000000 --- a/components/Button/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from "react"; -import classNames from "classnames"; - -interface ButtonProps extends React.PropsWithChildren { - onClick?: (...args: any) => void; - type?: "default" | "primary" | "danger"; -} - -const Button = React.forwardRef( - ({ children, onClick, type = "default" }, forwardedRef) => { - return ( - - ); - } -); - -Button.displayName = "Button"; - -export default Button; diff --git a/components/ChatSection/ChatFooter/index.tsx b/components/ChatSection/ChatFooter/index.tsx index a07e6de..26107fd 100644 --- a/components/ChatSection/ChatFooter/index.tsx +++ b/components/ChatSection/ChatFooter/index.tsx @@ -9,7 +9,7 @@ import { BsStop } from "react-icons/bs"; import { useDebounceFn } from "ahooks"; import toast from "react-hot-toast"; import { useChannel, useOpenAIKey, useProxy, useStreamDecoder } from "@/hooks"; -import { useScrollToBottom, Input, Confirm } from "@/components"; +import { useScrollToBottom, Confirm, Button, Textarea } from "@/components"; import { isMobile } from "@/utils"; const ChatFooter: React.FC = () => { @@ -239,25 +239,16 @@ const ChatFooter: React.FC = () => { > {!!findChannel?.chat_list?.length && (
-
: + } > - {loadingFinish ? ( - <> - {t("stop-generate")} - - ) : ( - <> - - {t("re-generate")} - - )} -
+ {loadingFinish ? t("stop-generate") : t("re-generate")} +
)} @@ -267,14 +258,20 @@ const ChatFooter: React.FC = () => { title={tMenu("clear-all-conversation")} content={t("clear-current-conversation")} trigger={ -
+
} onOk={clearNowConversation} />
- { )} {item.role === "user" && ( -
+
)}
-
+
{format(Number(item.time), "MM-DD HH:mm:ss")}
{
-
+
onDelete(item)} - className="hover:text-icon-hover dark:hover:text-blue-500 transition-colors h-6 w-6 flex justify-center items-center cursor-pointer" + className={classNames( + "transition-colors h-6 w-6 flex justify-center items-center cursor-pointer", + "hover:text-icon-hover dark:hover:text-sky-400/90" + )} >
@@ -126,7 +138,7 @@ const ChatList: React.FC = () => { {loadingStart && ( )}
diff --git a/components/ChatSection/index.tsx b/components/ChatSection/index.tsx index cc2f866..3ae5cde 100644 --- a/components/ChatSection/index.tsx +++ b/components/ChatSection/index.tsx @@ -8,7 +8,6 @@ const ChatSection: React.FC = () => { const [openAIKey, , envOpenAIKey] = useOpenAIKey(); // only the OpenAI Key or env OpenAI Key Configuration is not empty, show the chat section - // if (!openAIKey) return null; if (!openAIKey && !envOpenAIKey) return null; return ( diff --git a/components/Input/index.ts b/components/Input/index.ts deleted file mode 100644 index 4c2e298..0000000 --- a/components/Input/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type * as React from "react"; -import LInput, { InputProps, InputRef } from "./input"; -import TextArea from "./textarea"; - -export type { InputProps, InputRef } from "./input"; -export type { TextAreaProps, TextAreaRef } from "./textarea"; - -type Component = React.ForwardRefExoticComponent< - InputProps & React.RefAttributes -> & { - TextArea: typeof TextArea; -}; - -const Input = LInput as Component; - -Input.TextArea = TextArea; - -export default Input; diff --git a/components/Input/input.tsx b/components/Input/input.tsx deleted file mode 100644 index c3c8810..0000000 --- a/components/Input/input.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as React from "react"; -import classNames from "classnames"; - -type HTMLInputTypeAttribute = - | "button" - | "checkbox" - | "color" - | "date" - | "datetime-local" - | "email" - | "file" - | "hidden" - | "image" - | "month" - | "number" - | "password" - | "radio" - | "range" - | "reset" - | "search" - | "submit" - | "tel" - | "text" - | "time" - | "url" - | "week" - | (string & {}); - -export interface InputProps { - className?: string; - maxLength?: number; - onChange: (value: any) => void; - placeholder?: string; - type?: HTMLInputTypeAttribute; - value: any; -} - -export interface InputRef { - focus: () => void; - blur: () => void; -} - -const Input = React.forwardRef( - ( - { className, maxLength, onChange, placeholder, type = "text", value }, - forwardedRef - ) => { - const inputRef = React.useRef(null); - - const [isFocus, setIsFocus] = React.useState(false); - - React.useImperativeHandle(forwardedRef, () => ({ - focus() { - inputRef.current?.focus(); - }, - blur() { - inputRef.current?.blur(); - }, - })); - - return ( -
- setIsFocus(true)} - onBlur={() => setIsFocus(false)} - value={value} - onChange={(e) => onChange(e.target.value)} - maxLength={maxLength} - /> -
- ); - } -); - -Input.displayName = "Input"; - -export default Input; diff --git a/components/Menu/Mobile/index.tsx b/components/Menu/Mobile/index.tsx index 383c1b2..3abef2b 100644 --- a/components/Menu/Mobile/index.tsx +++ b/components/Menu/Mobile/index.tsx @@ -6,7 +6,7 @@ import { useDateFormat } from "l-hooks"; import { AiOutlineDelete, AiFillGithub } from "react-icons/ai"; import { BsChatSquareText } from "react-icons/bs"; import { v4 as uuidv4 } from "uuid"; -import { Drawer, Confirm, NewButton } from "@/components"; +import { Drawer, Confirm, Button } from "@/components"; import { useChannel, initChannelList } from "@/hooks"; import { useMobileMenuOpen } from "@/state"; @@ -81,7 +81,7 @@ const MobileMenu: React.FC = () => { onClose={onClose} >
- { onClick={onAddChannel} > {t("new-chat")} - +
{channel.list.map((item) => (
{ const { t } = useTranslation("menu"); @@ -72,7 +72,7 @@ const Menu: React.FC = () => { "dark:bg-slate-800" )} > - { onClick={onAddChannel} > {t("new-chat")} - +
{channel.list.map((item) => (
{
{t("title")}
{ {
void; - options: Options[]; - placeholder?: string; - value?: any; -} - -const LSelect: React.FC = ({ - className, - contentClassName, - disabled = false, - onChange, - options, - placeholder, - value, -}) => { - const triggerRef = React.useRef(null); - const [isOpen, setIsOpen] = React.useState(false); - - return ( - - - - - - - - - - - - - - {options.map((item) => ( - - {item.label} - - ))} - - - - - - - - ); -}; - -const SelectItem = React.forwardRef( - ({ children, className, ...props }, forwardedRef) => { - return ( - - {children} - - ); - } -); - -SelectItem.displayName = "SelectItem"; - -export default LSelect; diff --git a/components/Welcome/index.tsx b/components/Welcome/index.tsx index a7c4e4e..f4d60cb 100644 --- a/components/Welcome/index.tsx +++ b/components/Welcome/index.tsx @@ -17,7 +17,7 @@ const Welcome: React.FC = () => {
{t("welcome")}
{t("desc")}
{t("set-openai-key")}
-
+
{t("apply-openai-key")} ( { "w-full": block }, // default { - "bg-slate-300 hover:bg-slate-400/60 active:bg-slate-400/80 text-neutral-600/90": + "bg-gray-100 hover:bg-gray-200 active:bg-gray-300 text-slate-600/80": type === "default", }, { @@ -66,6 +66,16 @@ const Button = React.forwardRef( "dark:bg-rose-400/90 dark:hover:bg-rose-500/90 dark:active:bg-rose-600/90": type === "danger", }, + + // outline + { + "border border-sky-400 text-sky-400 bg-white/80 backdrop-blur-sm hover:bg-sky-100/80 active:bg-sky-200/60": + type === "outline", + }, + { + "dark:bg-neutral-900/80 dark:border-sky-400/90 dark:text-sky-400/90 dark:hover:bg-sky-50/10 dark:active:bg-sky-50/20": + type === "outline", + }, className )} > diff --git a/components/ui/Confirm/index.tsx b/components/ui/Confirm/index.tsx index e860a82..d0a87c3 100644 --- a/components/ui/Confirm/index.tsx +++ b/components/ui/Confirm/index.tsx @@ -3,7 +3,7 @@ import * as AlertDialog from "@radix-ui/react-alert-dialog"; import classNames from "classnames"; import { useTranslation } from "next-i18next"; import { AiFillExclamationCircle } from "react-icons/ai"; -import { NewButton } from "@/components"; +import { Button } from "@/components"; interface ConfirmProps { /** The AlertDialog's title */ @@ -84,17 +84,17 @@ const Confirm = React.forwardRef(
- event.stopPropagation()} > {t("cancel")} - + - +
diff --git a/components/ui/Input/index.tsx b/components/ui/Input/index.tsx new file mode 100644 index 0000000..6c050f0 --- /dev/null +++ b/components/ui/Input/index.tsx @@ -0,0 +1,118 @@ +import * as React from "react"; +import classNames from "classnames"; +import { twMerge } from "tailwind-merge"; +import { AiOutlineClose } from "react-icons/ai"; + +type InputType = "text" | "password"; + +interface InputProps + extends Omit, "onChange"> { + type?: InputType; + value: any; + allowClear?: boolean; + maxLength?: number; + onChange?: (value: any) => void; +} + +const Input = React.forwardRef( + ( + { + className, + type = "text", + placeholder, + value, + allowClear, + maxLength, + onChange, + }, + forwardedRef + ) => { + const inputRef = React.useRef(null); + const forceUpdate = React.useRef(false); + + const [isFocus, setIsFocus] = React.useState(false); + + const onClear = () => { + forceUpdate.current = false; + onChange?.(""); + inputRef.current?.focus(); + }; + + const onBlur = (event: any) => { + setIsFocus(false); + event.preventDefault(); + setTimeout(() => { + if (forceUpdate.current) inputRef.current?.focus(); + }, 0); + }; + + const onMouseEnter = () => { + forceUpdate.current = true; + }; + + const onMouseLeave = () => { + forceUpdate.current = false; + }; + + React.useImperativeHandle(forwardedRef, () => ({ + focus() { + inputRef.current?.focus(); + }, + blur() { + inputRef.current?.blur(); + }, + })); + + return ( +
+ setIsFocus(true)} + onBlur={onBlur} + value={value} + onChange={(e) => onChange?.(e.target.value)} + /> + {!!value && allowClear && ( + + + + )} +
+ ); + } +); + +Input.displayName = "Input"; + +export default Input; diff --git a/components/ui/Modal/index.tsx b/components/ui/Modal/index.tsx index 4771a05..e8ad469 100644 --- a/components/ui/Modal/index.tsx +++ b/components/ui/Modal/index.tsx @@ -3,7 +3,7 @@ import * as Dialog from "@radix-ui/react-dialog"; import classNames from "classnames"; import { AiOutlineClose } from "react-icons/ai"; import { useTranslation } from "next-i18next"; -import { NewButton } from "@/components"; +import { Button } from "@/components"; interface ModalProps extends Omit, "title"> { /** The Drawer is open or not */ @@ -101,10 +101,10 @@ const Modal = React.forwardRef( footer ) : (
- {t("cancel")} - + +
)} diff --git a/components/ui/Select/index.tsx b/components/ui/Select/index.tsx new file mode 100644 index 0000000..de05b73 --- /dev/null +++ b/components/ui/Select/index.tsx @@ -0,0 +1,83 @@ +import * as React from "react"; +import * as Select from "@radix-ui/react-select"; +import classnames from "classnames"; +import { twMerge } from "tailwind-merge"; +import { BsChevronDown } from "react-icons/bs"; +import Item from "./item"; +import classNames from "classnames"; + +type Options = { + label: string; + value: any; +}; + +interface LSelectProps extends React.HTMLAttributes { + contentClassName?: string; + options: Options[]; + value?: any; + onChange?: (value: any) => void; +} + +const LSelect = React.forwardRef( + ( + { className, contentClassName, placeholder, options, onChange, value }, + forwardedRef + ) => { + const [isOpen, setIsOpen] = React.useState(false); + + return ( + + + + + + + + + + + + {options.map((item) => ( + + {item.label} + + ))} + + + + + ); + } +); + +LSelect.displayName = "Select"; + +export default LSelect; diff --git a/components/ui/Select/item.tsx b/components/ui/Select/item.tsx new file mode 100644 index 0000000..e448769 --- /dev/null +++ b/components/ui/Select/item.tsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import * as Select from "@radix-ui/react-select"; +import classnames from "classnames"; + +interface SelectItemProps extends React.PropsWithChildren { + className?: string | undefined; + value: any; + disabled?: boolean; +} + +const SelectItem = React.forwardRef( + ({ children, className, ...props }, forwardedRef) => { + return ( + + {children} + + ); + } +); + +SelectItem.displayName = "SelectItem"; + +export default SelectItem; diff --git a/components/Input/textarea.tsx b/components/ui/Textarea/index.tsx similarity index 74% rename from components/Input/textarea.tsx rename to components/ui/Textarea/index.tsx index 5e5b7dd..d11aed0 100644 --- a/components/Input/textarea.tsx +++ b/components/ui/Textarea/index.tsx @@ -65,12 +65,12 @@ const TextArea = React.forwardRef( return (