mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-02 22:32:27 +08:00
initialize project with Next.js, Tailwind CSS, and essential configurations
This commit is contained in:
132
components/assistant-ui/markdown-text.tsx
Normal file
132
components/assistant-ui/markdown-text.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import "@assistant-ui/react-markdown/styles/dot.css";
|
||||
|
||||
import {
|
||||
CodeHeaderProps,
|
||||
MarkdownTextPrimitive,
|
||||
unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
|
||||
useIsMarkdownCodeBlock,
|
||||
} from "@assistant-ui/react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { FC, memo, useState } from "react";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const MarkdownTextImpl = () => {
|
||||
return (
|
||||
<MarkdownTextPrimitive
|
||||
remarkPlugins={[remarkGfm]}
|
||||
className="aui-md"
|
||||
components={defaultComponents}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MarkdownText = memo(MarkdownTextImpl);
|
||||
|
||||
const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
|
||||
const { isCopied, copyToClipboard } = useCopyToClipboard();
|
||||
const onCopy = () => {
|
||||
if (!code || isCopied) return;
|
||||
copyToClipboard(code);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 rounded-t-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white">
|
||||
<span className="lowercase [&>span]:text-xs">{language}</span>
|
||||
<TooltipIconButton tooltip="Copy" onClick={onCopy}>
|
||||
{!isCopied && <CopyIcon />}
|
||||
{isCopied && <CheckIcon />}
|
||||
</TooltipIconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useCopyToClipboard = ({
|
||||
copiedDuration = 3000,
|
||||
}: {
|
||||
copiedDuration?: number;
|
||||
} = {}) => {
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
|
||||
const copyToClipboard = (value: string) => {
|
||||
if (!value) return;
|
||||
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), copiedDuration);
|
||||
});
|
||||
};
|
||||
|
||||
return { isCopied, copyToClipboard };
|
||||
};
|
||||
|
||||
const defaultComponents = memoizeMarkdownComponents({
|
||||
h1: ({ className, ...props }) => (
|
||||
<h1 className={cn("mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0", className)} {...props} />
|
||||
),
|
||||
h2: ({ className, ...props }) => (
|
||||
<h2 className={cn("mb-4 mt-8 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0", className)} {...props} />
|
||||
),
|
||||
h3: ({ className, ...props }) => (
|
||||
<h3 className={cn("mb-4 mt-6 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0", className)} {...props} />
|
||||
),
|
||||
h4: ({ className, ...props }) => (
|
||||
<h4 className={cn("mb-4 mt-6 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0", className)} {...props} />
|
||||
),
|
||||
h5: ({ className, ...props }) => (
|
||||
<h5 className={cn("my-4 text-lg font-semibold first:mt-0 last:mb-0", className)} {...props} />
|
||||
),
|
||||
h6: ({ className, ...props }) => (
|
||||
<h6 className={cn("my-4 font-semibold first:mt-0 last:mb-0", className)} {...props} />
|
||||
),
|
||||
p: ({ className, ...props }) => (
|
||||
<p className={cn("mb-5 mt-5 leading-7 first:mt-0 last:mb-0", className)} {...props} />
|
||||
),
|
||||
a: ({ className, ...props }) => (
|
||||
<a className={cn("text-primary font-medium underline underline-offset-4", className)} {...props} />
|
||||
),
|
||||
blockquote: ({ className, ...props }) => (
|
||||
<blockquote className={cn("border-l-2 pl-6 italic", className)} {...props} />
|
||||
),
|
||||
ul: ({ className, ...props }) => (
|
||||
<ul className={cn("my-5 ml-6 list-disc [&>li]:mt-2", className)} {...props} />
|
||||
),
|
||||
ol: ({ className, ...props }) => (
|
||||
<ol className={cn("my-5 ml-6 list-decimal [&>li]:mt-2", className)} {...props} />
|
||||
),
|
||||
hr: ({ className, ...props }) => (
|
||||
<hr className={cn("my-5 border-b", className)} {...props} />
|
||||
),
|
||||
table: ({ className, ...props }) => (
|
||||
<table className={cn("my-5 w-full border-separate border-spacing-0 overflow-y-auto", className)} {...props} />
|
||||
),
|
||||
th: ({ className, ...props }) => (
|
||||
<th className={cn("bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [&[align=center]]:text-center [&[align=right]]:text-right", className)} {...props} />
|
||||
),
|
||||
td: ({ className, ...props }) => (
|
||||
<td className={cn("border-b border-l px-4 py-2 text-left last:border-r [&[align=center]]:text-center [&[align=right]]:text-right", className)} {...props} />
|
||||
),
|
||||
tr: ({ className, ...props }) => (
|
||||
<tr className={cn("m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg", className)} {...props} />
|
||||
),
|
||||
sup: ({ className, ...props }) => (
|
||||
<sup className={cn("[&>a]:text-xs [&>a]:no-underline", className)} {...props} />
|
||||
),
|
||||
pre: ({ className, ...props }) => (
|
||||
<pre className={cn("overflow-x-auto rounded-b-lg bg-black p-4 text-white", className)} {...props} />
|
||||
),
|
||||
code: function Code({ className, ...props }) {
|
||||
const isCodeBlock = useIsMarkdownCodeBlock();
|
||||
return (
|
||||
<code
|
||||
className={cn(!isCodeBlock && "bg-muted rounded border font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
CodeHeader,
|
||||
});
|
||||
66
components/assistant-ui/thread-list.tsx
Normal file
66
components/assistant-ui/thread-list.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
ThreadListItemPrimitive,
|
||||
ThreadListPrimitive,
|
||||
} from "@assistant-ui/react";
|
||||
import { ArchiveIcon, PlusIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
|
||||
export const ThreadList: FC = () => {
|
||||
return (
|
||||
<ThreadListPrimitive.Root className="flex flex-col items-stretch gap-1.5">
|
||||
<ThreadListNew />
|
||||
<ThreadListItems />
|
||||
</ThreadListPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadListNew: FC = () => {
|
||||
return (
|
||||
<ThreadListPrimitive.New asChild>
|
||||
<Button className="data-[active]:bg-muted hover:bg-muted flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start" variant="ghost">
|
||||
<PlusIcon />
|
||||
New Thread
|
||||
</Button>
|
||||
</ThreadListPrimitive.New>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadListItems: FC = () => {
|
||||
return <ThreadListPrimitive.Items components={{ ThreadListItem }} />;
|
||||
};
|
||||
|
||||
const ThreadListItem: FC = () => {
|
||||
return (
|
||||
<ThreadListItemPrimitive.Root className="data-[active]:bg-muted hover:bg-muted focus-visible:bg-muted focus-visible:ring-ring flex items-center gap-2 rounded-lg transition-all focus-visible:outline-none focus-visible:ring-2">
|
||||
<ThreadListItemPrimitive.Trigger className="flex-grow px-3 py-2 text-start">
|
||||
<ThreadListItemTitle />
|
||||
</ThreadListItemPrimitive.Trigger>
|
||||
<ThreadListItemArchive />
|
||||
</ThreadListItemPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadListItemTitle: FC = () => {
|
||||
return (
|
||||
<p className="text-sm">
|
||||
<ThreadListItemPrimitive.Title fallback="New Chat" />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadListItemArchive: FC = () => {
|
||||
return (
|
||||
<ThreadListItemPrimitive.Archive asChild>
|
||||
<TooltipIconButton
|
||||
className="hover:text-primary text-foreground ml-auto mr-3 size-4 p-0"
|
||||
variant="ghost"
|
||||
tooltip="Archive thread"
|
||||
>
|
||||
<ArchiveIcon />
|
||||
</TooltipIconButton>
|
||||
</ThreadListItemPrimitive.Archive>
|
||||
);
|
||||
};
|
||||
287
components/assistant-ui/thread.tsx
Normal file
287
components/assistant-ui/thread.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
import {
|
||||
ActionBarPrimitive,
|
||||
BranchPickerPrimitive,
|
||||
ComposerPrimitive,
|
||||
MessagePrimitive,
|
||||
ThreadPrimitive,
|
||||
} from "@assistant-ui/react";
|
||||
import type { FC } from "react";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
CheckIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
CopyIcon,
|
||||
PencilIcon,
|
||||
RefreshCwIcon,
|
||||
SendHorizontalIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
||||
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
||||
|
||||
export const Thread: FC = () => {
|
||||
return (
|
||||
<ThreadPrimitive.Root
|
||||
className="bg-background box-border flex h-full flex-col overflow-hidden"
|
||||
style={{
|
||||
["--thread-max-width" as string]: "42rem",
|
||||
}}
|
||||
>
|
||||
<ThreadPrimitive.Viewport className="flex h-full flex-col items-center overflow-y-scroll scroll-smooth bg-inherit px-4 pt-8">
|
||||
<ThreadWelcome />
|
||||
|
||||
<ThreadPrimitive.Messages
|
||||
components={{
|
||||
UserMessage: UserMessage,
|
||||
EditComposer: EditComposer,
|
||||
AssistantMessage: AssistantMessage,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ThreadPrimitive.If empty={false}>
|
||||
<div className="min-h-8 flex-grow" />
|
||||
</ThreadPrimitive.If>
|
||||
|
||||
<div className="sticky bottom-0 mt-3 flex w-full max-w-[var(--thread-max-width)] flex-col items-center justify-end rounded-t-lg bg-inherit pb-4">
|
||||
<ThreadScrollToBottom />
|
||||
<Composer />
|
||||
</div>
|
||||
</ThreadPrimitive.Viewport>
|
||||
</ThreadPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadScrollToBottom: FC = () => {
|
||||
return (
|
||||
<ThreadPrimitive.ScrollToBottom asChild>
|
||||
<TooltipIconButton
|
||||
tooltip="Scroll to bottom"
|
||||
variant="outline"
|
||||
className="absolute -top-8 rounded-full disabled:invisible"
|
||||
>
|
||||
<ArrowDownIcon />
|
||||
</TooltipIconButton>
|
||||
</ThreadPrimitive.ScrollToBottom>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadWelcome: FC = () => {
|
||||
return (
|
||||
<ThreadPrimitive.Empty>
|
||||
<div className="flex w-full max-w-[var(--thread-max-width)] flex-grow flex-col">
|
||||
<div className="flex w-full flex-grow flex-col items-center justify-center">
|
||||
<p className="mt-4 font-medium">
|
||||
How can I help you today?
|
||||
</p>
|
||||
</div>
|
||||
{/* <ThreadWelcomeSuggestions /> */}
|
||||
</div>
|
||||
</ThreadPrimitive.Empty>
|
||||
);
|
||||
};
|
||||
|
||||
const ThreadWelcomeSuggestions: FC = () => {
|
||||
return (
|
||||
<div className="mt-3 flex w-full items-stretch justify-center gap-4">
|
||||
<ThreadPrimitive.Suggestion
|
||||
className="hover:bg-muted/80 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-lg border p-3 transition-colors ease-in"
|
||||
prompt="What is the weather in Tokyo?"
|
||||
method="replace"
|
||||
autoSend
|
||||
>
|
||||
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">
|
||||
What is the weather in Tokyo?
|
||||
</span>
|
||||
</ThreadPrimitive.Suggestion>
|
||||
<ThreadPrimitive.Suggestion
|
||||
className="hover:bg-muted/80 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-lg border p-3 transition-colors ease-in"
|
||||
prompt="What is assistant-ui?"
|
||||
method="replace"
|
||||
autoSend
|
||||
>
|
||||
<span className="line-clamp-2 text-ellipsis text-sm font-semibold">
|
||||
What is assistant-ui?
|
||||
</span>
|
||||
</ThreadPrimitive.Suggestion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Composer: FC = () => {
|
||||
return (
|
||||
<ComposerPrimitive.Root className="focus-within:border-ring/20 flex w-full flex-wrap items-end rounded-lg border bg-inherit px-2.5 shadow-sm transition-colors ease-in">
|
||||
<ComposerPrimitive.Input
|
||||
rows={1}
|
||||
autoFocus
|
||||
placeholder="Write a message..."
|
||||
className="placeholder:text-muted-foreground max-h-40 flex-grow resize-none border-none bg-transparent px-2 py-4 text-sm outline-none focus:ring-0 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<ComposerAction />
|
||||
</ComposerPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const ComposerAction: FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ThreadPrimitive.If running={false}>
|
||||
<ComposerPrimitive.Send asChild>
|
||||
<TooltipIconButton
|
||||
tooltip="Send"
|
||||
variant="default"
|
||||
className="my-2.5 size-8 p-2 transition-opacity ease-in"
|
||||
>
|
||||
<SendHorizontalIcon />
|
||||
</TooltipIconButton>
|
||||
</ComposerPrimitive.Send>
|
||||
</ThreadPrimitive.If>
|
||||
<ThreadPrimitive.If running>
|
||||
<ComposerPrimitive.Cancel asChild>
|
||||
<TooltipIconButton
|
||||
tooltip="Cancel"
|
||||
variant="default"
|
||||
className="my-2.5 size-8 p-2 transition-opacity ease-in"
|
||||
>
|
||||
<CircleStopIcon />
|
||||
</TooltipIconButton>
|
||||
</ComposerPrimitive.Cancel>
|
||||
</ThreadPrimitive.If>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UserMessage: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Root className="grid auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] gap-y-2 [&:where(>*)]:col-start-2 w-full max-w-[var(--thread-max-width)] py-4">
|
||||
<UserActionBar />
|
||||
|
||||
<div className="bg-muted text-foreground max-w-[calc(var(--thread-max-width)*0.8)] break-words rounded-3xl px-5 py-2.5 col-start-2 row-start-2">
|
||||
<MessagePrimitive.Content />
|
||||
</div>
|
||||
|
||||
<BranchPicker className="col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const UserActionBar: FC = () => {
|
||||
return (
|
||||
<ActionBarPrimitive.Root
|
||||
hideWhenRunning
|
||||
autohide="not-last"
|
||||
className="flex flex-col items-end col-start-1 row-start-2 mr-3 mt-2.5"
|
||||
>
|
||||
<ActionBarPrimitive.Edit asChild>
|
||||
<TooltipIconButton tooltip="Edit">
|
||||
<PencilIcon />
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Edit>
|
||||
</ActionBarPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const EditComposer: FC = () => {
|
||||
return (
|
||||
<ComposerPrimitive.Root className="bg-muted my-4 flex w-full max-w-[var(--thread-max-width)] flex-col gap-2 rounded-xl">
|
||||
<ComposerPrimitive.Input className="text-foreground flex h-8 w-full resize-none bg-transparent p-4 pb-0 outline-none" />
|
||||
|
||||
<div className="mx-3 mb-3 flex items-center justify-center gap-2 self-end">
|
||||
<ComposerPrimitive.Cancel asChild>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</ComposerPrimitive.Cancel>
|
||||
<ComposerPrimitive.Send asChild>
|
||||
<Button>Send</Button>
|
||||
</ComposerPrimitive.Send>
|
||||
</div>
|
||||
</ComposerPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const AssistantMessage: FC = () => {
|
||||
return (
|
||||
<MessagePrimitive.Root className="grid grid-cols-[auto_auto_1fr] grid-rows-[auto_1fr] relative w-full max-w-[var(--thread-max-width)] py-4">
|
||||
<div className="text-foreground max-w-[calc(var(--thread-max-width)*0.8)] break-words leading-7 col-span-2 col-start-2 row-start-1 my-1.5">
|
||||
<MessagePrimitive.Content components={{ Text: MarkdownText }} />
|
||||
</div>
|
||||
|
||||
<AssistantActionBar />
|
||||
|
||||
<BranchPicker className="col-start-2 row-start-2 -ml-2 mr-2" />
|
||||
</MessagePrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const AssistantActionBar: FC = () => {
|
||||
return (
|
||||
<ActionBarPrimitive.Root
|
||||
hideWhenRunning
|
||||
autohide="not-last"
|
||||
autohideFloat="single-branch"
|
||||
className="text-muted-foreground flex gap-1 col-start-3 row-start-2 -ml-1 data-[floating]:bg-background data-[floating]:absolute data-[floating]:rounded-md data-[floating]:border data-[floating]:p-1 data-[floating]:shadow-sm"
|
||||
>
|
||||
<ActionBarPrimitive.Copy asChild>
|
||||
<TooltipIconButton tooltip="Copy">
|
||||
<MessagePrimitive.If copied>
|
||||
<CheckIcon />
|
||||
</MessagePrimitive.If>
|
||||
<MessagePrimitive.If copied={false}>
|
||||
<CopyIcon />
|
||||
</MessagePrimitive.If>
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Copy>
|
||||
<ActionBarPrimitive.Reload asChild>
|
||||
<TooltipIconButton tooltip="Refresh">
|
||||
<RefreshCwIcon />
|
||||
</TooltipIconButton>
|
||||
</ActionBarPrimitive.Reload>
|
||||
</ActionBarPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
|
||||
className,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<BranchPickerPrimitive.Root
|
||||
hideWhenSingleBranch
|
||||
className={cn(
|
||||
"text-muted-foreground inline-flex items-center text-xs",
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<BranchPickerPrimitive.Previous asChild>
|
||||
<TooltipIconButton tooltip="Previous">
|
||||
<ChevronLeftIcon />
|
||||
</TooltipIconButton>
|
||||
</BranchPickerPrimitive.Previous>
|
||||
<span className="font-medium">
|
||||
<BranchPickerPrimitive.Number /> /{" "}
|
||||
<BranchPickerPrimitive.Count />
|
||||
</span>
|
||||
<BranchPickerPrimitive.Next asChild>
|
||||
<TooltipIconButton tooltip="Next">
|
||||
<ChevronRightIcon />
|
||||
</TooltipIconButton>
|
||||
</BranchPickerPrimitive.Next>
|
||||
</BranchPickerPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const CircleStopIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<rect width="10" height="10" x="3" y="3" rx="2" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
44
components/assistant-ui/tooltip-icon-button.tsx
Normal file
44
components/assistant-ui/tooltip-icon-button.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { ComponentPropsWithoutRef, forwardRef } from "react";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type TooltipIconButtonProps = ComponentPropsWithoutRef<typeof Button> & {
|
||||
tooltip: string;
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
};
|
||||
|
||||
export const TooltipIconButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
TooltipIconButtonProps
|
||||
>(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
{...rest}
|
||||
className={cn("size-6 p-1", className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
<span className="sr-only">{tooltip}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side={side}>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
});
|
||||
|
||||
TooltipIconButton.displayName = "TooltipIconButton";
|
||||
59
components/ui/button.tsx
Normal file
59
components/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
61
components/ui/tooltip.tsx
Normal file
61
components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
Reference in New Issue
Block a user