@oppulence/design-system 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/use-resize-observer.ts +24 -0
- package/lib/ai.ts +31 -0
- package/package.json +19 -1
- package/src/components/atoms/animated-size-container.tsx +59 -0
- package/src/components/atoms/currency-input.tsx +16 -0
- package/src/components/atoms/icons.tsx +840 -0
- package/src/components/atoms/image.tsx +23 -0
- package/src/components/atoms/index.ts +10 -0
- package/src/components/atoms/loader.tsx +92 -0
- package/src/components/atoms/quantity-input.tsx +103 -0
- package/src/components/atoms/record-button.tsx +178 -0
- package/src/components/atoms/submit-button.tsx +26 -0
- package/src/components/atoms/text-effect.tsx +251 -0
- package/src/components/atoms/text-shimmer.tsx +74 -0
- package/src/components/molecules/actions.tsx +53 -0
- package/src/components/molecules/branch.tsx +192 -0
- package/src/components/molecules/code-block.tsx +151 -0
- package/src/components/molecules/form.tsx +177 -0
- package/src/components/molecules/index.ts +12 -0
- package/src/components/molecules/inline-citation.tsx +295 -0
- package/src/components/molecules/message.tsx +64 -0
- package/src/components/molecules/sources.tsx +116 -0
- package/src/components/molecules/suggestion.tsx +53 -0
- package/src/components/molecules/task.tsx +74 -0
- package/src/components/molecules/time-range-input.tsx +73 -0
- package/src/components/molecules/tool-call-indicator.tsx +42 -0
- package/src/components/molecules/tool.tsx +130 -0
- package/src/components/organisms/combobox-dropdown.tsx +171 -0
- package/src/components/organisms/conversation.tsx +98 -0
- package/src/components/organisms/date-range-picker.tsx +53 -0
- package/src/components/organisms/editor/extentions/bubble-menu/bubble-item.tsx +30 -0
- package/src/components/organisms/editor/extentions/bubble-menu/bubble-menu-button.tsx +27 -0
- package/src/components/organisms/editor/extentions/bubble-menu/index.tsx +63 -0
- package/src/components/organisms/editor/extentions/bubble-menu/link-item.tsx +104 -0
- package/src/components/organisms/editor/extentions/register.ts +22 -0
- package/src/components/organisms/editor/index.tsx +50 -0
- package/src/components/organisms/editor/styles.css +31 -0
- package/src/components/organisms/editor/utils.ts +19 -0
- package/src/components/organisms/index.ts +11 -0
- package/src/components/organisms/multiple-selector.tsx +632 -0
- package/src/components/organisms/prompt-input.tsx +747 -0
- package/src/components/organisms/reasoning.tsx +170 -0
- package/src/components/organisms/response.tsx +121 -0
- package/src/components/organisms/toast-toaster.tsx +84 -0
- package/src/components/organisms/toast.tsx +124 -0
- package/src/components/organisms/use-toast.tsx +206 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Icons } from "../atoms/icons";
|
|
4
|
+
import { TextShimmer } from "../atoms/text-shimmer";
|
|
5
|
+
|
|
6
|
+
export const toolDisplayConfig = {
|
|
7
|
+
getBurnRate: {
|
|
8
|
+
displayText: "Getting Burn Rate Data",
|
|
9
|
+
icon: Icons.TrendingUp,
|
|
10
|
+
},
|
|
11
|
+
web_search: {
|
|
12
|
+
displayText: "Searching the Web",
|
|
13
|
+
icon: Icons.Search,
|
|
14
|
+
},
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export type SupportedToolName = keyof typeof toolDisplayConfig;
|
|
18
|
+
|
|
19
|
+
export interface ToolCallIndicatorProps {
|
|
20
|
+
toolName: SupportedToolName;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ToolCallIndicator({ toolName }: ToolCallIndicatorProps) {
|
|
24
|
+
const config = toolDisplayConfig[toolName];
|
|
25
|
+
|
|
26
|
+
if (!config) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex justify-start mt-3 animate-fade-in">
|
|
32
|
+
<div className="border px-3 py-1 flex items-center gap-2 w-fit">
|
|
33
|
+
<div className="flex items-center justify-center size-3.5">
|
|
34
|
+
<config.icon size={14} />
|
|
35
|
+
</div>
|
|
36
|
+
<TextShimmer size="xs" baseColor="#707070" gradientColor="#111111" duration={1}>
|
|
37
|
+
{config.displayText}
|
|
38
|
+
</TextShimmer>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ToolUIPart } from "../../../lib/ai";
|
|
4
|
+
import { cn } from "../../../lib/utils";
|
|
5
|
+
import {
|
|
6
|
+
CheckCircleIcon,
|
|
7
|
+
ChevronDownIcon,
|
|
8
|
+
CircleIcon,
|
|
9
|
+
ClockIcon,
|
|
10
|
+
WrenchIcon,
|
|
11
|
+
XCircleIcon,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
14
|
+
|
|
15
|
+
import { badgeVariants } from "../atoms/badge";
|
|
16
|
+
import { CodeBlock } from "./code-block";
|
|
17
|
+
import {
|
|
18
|
+
Collapsible,
|
|
19
|
+
CollapsibleContent,
|
|
20
|
+
CollapsibleTrigger,
|
|
21
|
+
} from "./collapsible";
|
|
22
|
+
|
|
23
|
+
export type ToolProps = Omit<ComponentProps<typeof Collapsible>, "className">;
|
|
24
|
+
|
|
25
|
+
export const Tool = ({ ...props }: ToolProps) => (
|
|
26
|
+
<Collapsible className="not-prose mb-4 w-full rounded-md border" {...props} />
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export type ToolHeaderProps = {
|
|
30
|
+
type: ToolUIPart["type"];
|
|
31
|
+
state: ToolUIPart["state"];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getStatusBadge = (status: ToolUIPart["state"]) => {
|
|
35
|
+
const labels = {
|
|
36
|
+
"input-streaming": "Pending",
|
|
37
|
+
"input-available": "Running",
|
|
38
|
+
"output-available": "Completed",
|
|
39
|
+
"output-error": "Error",
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
const icons = {
|
|
43
|
+
"input-streaming": <CircleIcon className="size-4" />,
|
|
44
|
+
"input-available": <ClockIcon className="size-4 animate-pulse" />,
|
|
45
|
+
"output-available": <CheckCircleIcon className="size-4 text-green-600" />,
|
|
46
|
+
"output-error": <XCircleIcon className="size-4 text-red-600" />,
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<span className={cn(badgeVariants({ variant: "secondary" }), "gap-1.5 rounded-full text-xs")}>
|
|
51
|
+
{icons[status]}
|
|
52
|
+
{labels[status]}
|
|
53
|
+
</span>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const ToolHeader = ({ type, state, ...props }: ToolHeaderProps) => (
|
|
58
|
+
<CollapsibleTrigger
|
|
59
|
+
className="flex w-full items-center justify-between gap-4 p-3"
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
<div className="flex items-center gap-2">
|
|
63
|
+
<WrenchIcon className="size-4 text-muted-foreground" />
|
|
64
|
+
<span className="font-medium text-sm">{type}</span>
|
|
65
|
+
{getStatusBadge(state)}
|
|
66
|
+
</div>
|
|
67
|
+
<ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
|
68
|
+
</CollapsibleTrigger>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export type ToolContentProps = Omit<
|
|
72
|
+
ComponentProps<typeof CollapsibleContent>,
|
|
73
|
+
"className"
|
|
74
|
+
>;
|
|
75
|
+
|
|
76
|
+
export const ToolContent = ({ ...props }: ToolContentProps) => (
|
|
77
|
+
<CollapsibleContent
|
|
78
|
+
className="data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
export type ToolInputProps = Omit<ComponentProps<"div">, "className"> & {
|
|
84
|
+
input: ToolUIPart["input"];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const ToolInput = ({ input, ...props }: ToolInputProps) => (
|
|
88
|
+
<div className="space-y-2 overflow-hidden p-4" {...props}>
|
|
89
|
+
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
|
90
|
+
Parameters
|
|
91
|
+
</h4>
|
|
92
|
+
<div className="rounded-md bg-muted/50">
|
|
93
|
+
<CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
export type ToolOutputProps = Omit<ComponentProps<"div">, "className"> & {
|
|
99
|
+
output: ReactNode;
|
|
100
|
+
errorText: ToolUIPart["errorText"];
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const ToolOutput = ({
|
|
104
|
+
output,
|
|
105
|
+
errorText,
|
|
106
|
+
...props
|
|
107
|
+
}: ToolOutputProps) => {
|
|
108
|
+
if (!(output || errorText)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className="space-y-2 p-4" {...props}>
|
|
114
|
+
<h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
|
|
115
|
+
{errorText ? "Error" : "Result"}
|
|
116
|
+
</h4>
|
|
117
|
+
<div
|
|
118
|
+
className={cn(
|
|
119
|
+
"overflow-x-auto rounded-md text-xs [&_table]:w-full",
|
|
120
|
+
errorText
|
|
121
|
+
? "bg-destructive/10 text-destructive"
|
|
122
|
+
: "bg-muted/50 text-foreground",
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
{errorText && <div>{errorText}</div>}
|
|
126
|
+
{output && <div>{output}</div>}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Check, ChevronsUpDown } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { Command } from "./command";
|
|
7
|
+
import { CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./command";
|
|
8
|
+
import { Button } from "../atoms/button";
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../molecules/popover";
|
|
10
|
+
|
|
11
|
+
export type ComboboxItem = {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Props<T> = {
|
|
18
|
+
placeholder?: React.ReactNode;
|
|
19
|
+
searchPlaceholder?: string;
|
|
20
|
+
items: T[];
|
|
21
|
+
onSelect: (item: T) => void;
|
|
22
|
+
selectedItem?: T;
|
|
23
|
+
renderSelectedItem?: (selectedItem: T) => React.ReactNode;
|
|
24
|
+
renderOnCreate?: (value: string) => React.ReactNode;
|
|
25
|
+
renderListItem?: (listItem: { isChecked: boolean; item: T }) => React.ReactNode;
|
|
26
|
+
emptyResults?: React.ReactNode;
|
|
27
|
+
popoverProps?: Omit<React.ComponentProps<typeof PopoverContent>, "className">;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
onCreate?: (value: string) => void;
|
|
30
|
+
headless?: boolean;
|
|
31
|
+
modal?: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function ComboboxDropdown<T extends ComboboxItem>({
|
|
35
|
+
headless,
|
|
36
|
+
placeholder,
|
|
37
|
+
searchPlaceholder,
|
|
38
|
+
items,
|
|
39
|
+
onSelect,
|
|
40
|
+
selectedItem: incomingSelectedItem,
|
|
41
|
+
renderSelectedItem = (item) => item.label,
|
|
42
|
+
renderListItem,
|
|
43
|
+
renderOnCreate,
|
|
44
|
+
emptyResults,
|
|
45
|
+
popoverProps,
|
|
46
|
+
disabled,
|
|
47
|
+
onCreate,
|
|
48
|
+
modal = true,
|
|
49
|
+
}: Props<T>) {
|
|
50
|
+
const [open, setOpen] = React.useState(false);
|
|
51
|
+
const [internalSelectedItem, setInternalSelectedItem] = React.useState<
|
|
52
|
+
T | undefined
|
|
53
|
+
>();
|
|
54
|
+
const [inputValue, setInputValue] = React.useState("");
|
|
55
|
+
|
|
56
|
+
const selectedItem = incomingSelectedItem ?? internalSelectedItem;
|
|
57
|
+
|
|
58
|
+
const filteredItems = items.filter((item) =>
|
|
59
|
+
item.label.toLowerCase().includes(inputValue.toLowerCase()),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const showCreate = onCreate && Boolean(inputValue) && !filteredItems.length;
|
|
63
|
+
|
|
64
|
+
const Component = (
|
|
65
|
+
<Command loop shouldFilter={false}>
|
|
66
|
+
<CommandInput
|
|
67
|
+
value={inputValue}
|
|
68
|
+
onValueChange={setInputValue}
|
|
69
|
+
placeholder={searchPlaceholder ?? "Search item..."}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<CommandGroup>
|
|
73
|
+
<CommandList>
|
|
74
|
+
{filteredItems.map((item) => {
|
|
75
|
+
const isChecked = selectedItem?.id === item.id;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<CommandItem
|
|
79
|
+
disabled={item.disabled}
|
|
80
|
+
key={item.id}
|
|
81
|
+
value={item.id}
|
|
82
|
+
onSelect={(id) => {
|
|
83
|
+
const foundItem = items.find((item) => item.id === id);
|
|
84
|
+
|
|
85
|
+
if (!foundItem) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onSelect(foundItem);
|
|
90
|
+
setInternalSelectedItem(foundItem);
|
|
91
|
+
setOpen(false);
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{renderListItem ? (
|
|
95
|
+
renderListItem({ isChecked, item })
|
|
96
|
+
) : (
|
|
97
|
+
<>
|
|
98
|
+
<Check
|
|
99
|
+
className={
|
|
100
|
+
isChecked ? "mr-2 h-4 w-4 opacity-100" : "mr-2 h-4 w-4 opacity-0"
|
|
101
|
+
}
|
|
102
|
+
/>
|
|
103
|
+
{item.label}
|
|
104
|
+
</>
|
|
105
|
+
)}
|
|
106
|
+
</CommandItem>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
|
|
110
|
+
<CommandEmpty>{emptyResults ?? "No item found"}</CommandEmpty>
|
|
111
|
+
|
|
112
|
+
{showCreate && (
|
|
113
|
+
<CommandItem
|
|
114
|
+
key={inputValue}
|
|
115
|
+
value={inputValue}
|
|
116
|
+
onSelect={() => {
|
|
117
|
+
onCreate(inputValue);
|
|
118
|
+
setOpen(false);
|
|
119
|
+
setInputValue("");
|
|
120
|
+
}}
|
|
121
|
+
onMouseDown={(event) => {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
event.stopPropagation();
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
{renderOnCreate ? renderOnCreate(inputValue) : null}
|
|
127
|
+
</CommandItem>
|
|
128
|
+
)}
|
|
129
|
+
</CommandList>
|
|
130
|
+
</CommandGroup>
|
|
131
|
+
</Command>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (headless) {
|
|
135
|
+
return Component;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Popover open={open} onOpenChange={setOpen} modal={modal}>
|
|
140
|
+
<PopoverTrigger
|
|
141
|
+
render={<Button variant="outline" width="full" aria-expanded={open} />}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
>
|
|
144
|
+
<span className="flex w-full items-center justify-between gap-2">
|
|
145
|
+
<span className="truncate text-ellipsis">
|
|
146
|
+
{selectedItem ? (
|
|
147
|
+
<span className="items-center overflow-hidden whitespace-nowrap text-ellipsis block">
|
|
148
|
+
{renderSelectedItem
|
|
149
|
+
? renderSelectedItem(selectedItem)
|
|
150
|
+
: selectedItem.label}
|
|
151
|
+
</span>
|
|
152
|
+
) : (
|
|
153
|
+
placeholder ?? "Select item..."
|
|
154
|
+
)}
|
|
155
|
+
</span>
|
|
156
|
+
<ChevronsUpDown className="size-4 opacity-50" />
|
|
157
|
+
</span>
|
|
158
|
+
</PopoverTrigger>
|
|
159
|
+
|
|
160
|
+
<PopoverContent
|
|
161
|
+
{...popoverProps}
|
|
162
|
+
style={{
|
|
163
|
+
width: "var(--anchor-width)",
|
|
164
|
+
...popoverProps?.style,
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{Component}
|
|
168
|
+
</PopoverContent>
|
|
169
|
+
</Popover>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowDownIcon } from "lucide-react";
|
|
4
|
+
import type { ButtonHTMLAttributes, ComponentProps, ReactNode } from "react";
|
|
5
|
+
import { useCallback } from "react";
|
|
6
|
+
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
|
7
|
+
|
|
8
|
+
import { cn } from "../../../lib/utils";
|
|
9
|
+
import { buttonVariants } from "../atoms/button";
|
|
10
|
+
|
|
11
|
+
export type ConversationProps = Omit<
|
|
12
|
+
ComponentProps<typeof StickToBottom>,
|
|
13
|
+
"className"
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
export const Conversation = ({ ...props }: ConversationProps) => (
|
|
17
|
+
<StickToBottom
|
|
18
|
+
className="relative flex-1 overflow-y-auto [&_div::-webkit-scrollbar]:hidden [&_div]:[scrollbar-width:none] [&_div]:[-ms-overflow-style:none]"
|
|
19
|
+
initial="smooth"
|
|
20
|
+
resize="smooth"
|
|
21
|
+
role="log"
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export type ConversationContentProps = Omit<
|
|
27
|
+
ComponentProps<typeof StickToBottom.Content>,
|
|
28
|
+
"className"
|
|
29
|
+
>;
|
|
30
|
+
|
|
31
|
+
export const ConversationContent = ({ ...props }: ConversationContentProps) => (
|
|
32
|
+
<StickToBottom.Content className="p-4" {...props} />
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export type ConversationEmptyStateProps = Omit<
|
|
36
|
+
ComponentProps<"div">,
|
|
37
|
+
"className"
|
|
38
|
+
> & {
|
|
39
|
+
title?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
icon?: ReactNode;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const ConversationEmptyState = ({
|
|
45
|
+
title = "No messages yet",
|
|
46
|
+
description = "Start a conversation to see messages here",
|
|
47
|
+
icon,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: ConversationEmptyStateProps) => (
|
|
51
|
+
<div
|
|
52
|
+
className="flex size-full flex-col items-center justify-center gap-3 p-8 text-center"
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{children ?? (
|
|
56
|
+
<>
|
|
57
|
+
{icon && <div className="text-muted-foreground">{icon}</div>}
|
|
58
|
+
<div className="space-y-1">
|
|
59
|
+
<h3 className="font-medium text-sm">{title}</h3>
|
|
60
|
+
{description && (
|
|
61
|
+
<p className="text-muted-foreground text-sm">{description}</p>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export type ConversationScrollButtonProps = Omit<
|
|
70
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
71
|
+
"className"
|
|
72
|
+
>;
|
|
73
|
+
|
|
74
|
+
export const ConversationScrollButton = ({
|
|
75
|
+
...props
|
|
76
|
+
}: ConversationScrollButtonProps) => {
|
|
77
|
+
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
|
78
|
+
|
|
79
|
+
const handleScrollToBottom = useCallback(() => {
|
|
80
|
+
scrollToBottom();
|
|
81
|
+
}, [scrollToBottom]);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
!isAtBottom && (
|
|
85
|
+
<button
|
|
86
|
+
className={cn(
|
|
87
|
+
buttonVariants({ variant: "outline", size: "icon" }),
|
|
88
|
+
"absolute bottom-[10.5rem] left-1/2 -translate-x-1/2 rounded-full backdrop-filter backdrop-blur-lg dark:bg-[#1A1A1A]/80 bg-[#F6F6F3]/80",
|
|
89
|
+
)}
|
|
90
|
+
onClick={handleScrollToBottom}
|
|
91
|
+
type="button"
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
<ArrowDownIcon className="size-4" />
|
|
95
|
+
</button>
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { HTMLAttributes } from "react";
|
|
4
|
+
import type { DateRange } from "react-day-picker";
|
|
5
|
+
|
|
6
|
+
import { ChevronDownIcon } from "lucide-react";
|
|
7
|
+
import { Button } from "../atoms/button";
|
|
8
|
+
import { Calendar } from "./calendar";
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../molecules/popover";
|
|
10
|
+
|
|
11
|
+
export type DateRangePickerProps = Omit<
|
|
12
|
+
HTMLAttributes<HTMLDivElement>,
|
|
13
|
+
"className"
|
|
14
|
+
> & {
|
|
15
|
+
range: DateRange;
|
|
16
|
+
onSelect: (range?: DateRange) => void;
|
|
17
|
+
placeholder: string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function DateRangePicker({
|
|
22
|
+
range,
|
|
23
|
+
disabled,
|
|
24
|
+
onSelect,
|
|
25
|
+
placeholder,
|
|
26
|
+
...props
|
|
27
|
+
}: DateRangePickerProps) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="grid gap-2" {...props}>
|
|
30
|
+
<Popover>
|
|
31
|
+
<PopoverTrigger
|
|
32
|
+
render={<Button variant="outline" width="full" />}
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
>
|
|
35
|
+
<span className="flex w-full items-center justify-between gap-2 font-medium">
|
|
36
|
+
<span>{placeholder}</span>
|
|
37
|
+
<ChevronDownIcon className="size-4" />
|
|
38
|
+
</span>
|
|
39
|
+
</PopoverTrigger>
|
|
40
|
+
<PopoverContent align="end" sideOffset={8}>
|
|
41
|
+
<Calendar
|
|
42
|
+
initialFocus
|
|
43
|
+
mode="range"
|
|
44
|
+
defaultMonth={range?.from}
|
|
45
|
+
selected={range}
|
|
46
|
+
onSelect={onSelect}
|
|
47
|
+
numberOfMonths={2}
|
|
48
|
+
/>
|
|
49
|
+
</PopoverContent>
|
|
50
|
+
</Popover>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { Editor } from "@tiptap/react";
|
|
4
|
+
import { BubbleMenuButton } from "./bubble-menu-button";
|
|
5
|
+
|
|
6
|
+
interface BubbleItemProps {
|
|
7
|
+
editor: Editor;
|
|
8
|
+
action: () => void;
|
|
9
|
+
isActive: boolean;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function BubbleMenuItem({
|
|
14
|
+
editor,
|
|
15
|
+
action,
|
|
16
|
+
isActive,
|
|
17
|
+
children,
|
|
18
|
+
}: BubbleItemProps) {
|
|
19
|
+
return (
|
|
20
|
+
<BubbleMenuButton
|
|
21
|
+
action={() => {
|
|
22
|
+
editor.chain().focus();
|
|
23
|
+
action();
|
|
24
|
+
}}
|
|
25
|
+
isActive={isActive}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</BubbleMenuButton>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
interface BubbleMenuButtonProps {
|
|
4
|
+
action: () => void;
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function BubbleMenuButton({
|
|
10
|
+
action,
|
|
11
|
+
isActive,
|
|
12
|
+
children,
|
|
13
|
+
}: BubbleMenuButtonProps) {
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
onClick={action}
|
|
18
|
+
className={
|
|
19
|
+
isActive
|
|
20
|
+
? "px-2.5 py-1.5 text-[11px] transition-colors bg-white dark:bg-stone-900 text-primary"
|
|
21
|
+
: "px-2.5 py-1.5 text-[11px] transition-colors bg-transparent hover:bg-muted"
|
|
22
|
+
}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</button>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type Editor, BubbleMenu as TiptapBubbleMenu } from "@tiptap/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
MdOutlineFormatBold,
|
|
5
|
+
MdOutlineFormatItalic,
|
|
6
|
+
MdOutlineFormatStrikethrough,
|
|
7
|
+
} from "react-icons/md";
|
|
8
|
+
import type { Props as TippyOptions } from "tippy.js";
|
|
9
|
+
import { BubbleMenuItem } from "./bubble-item";
|
|
10
|
+
import { LinkItem } from "./link-item";
|
|
11
|
+
|
|
12
|
+
export function BubbleMenu({
|
|
13
|
+
editor,
|
|
14
|
+
tippyOptions,
|
|
15
|
+
}: {
|
|
16
|
+
editor: Editor;
|
|
17
|
+
tippyOptions?: TippyOptions;
|
|
18
|
+
}) {
|
|
19
|
+
const [openLink, setOpenLink] = useState(false);
|
|
20
|
+
|
|
21
|
+
if (!editor) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div>
|
|
27
|
+
<TiptapBubbleMenu editor={editor} tippyOptions={tippyOptions}>
|
|
28
|
+
<div className="flex w-fit max-w-[90vw] overflow-hidden rounded-full border border-border bg-background text-mono font-regular">
|
|
29
|
+
<>
|
|
30
|
+
<BubbleMenuItem
|
|
31
|
+
editor={editor}
|
|
32
|
+
action={() => editor.chain().focus().toggleBold().run()}
|
|
33
|
+
isActive={editor.isActive("bold")}
|
|
34
|
+
>
|
|
35
|
+
<MdOutlineFormatBold className="size-4" />
|
|
36
|
+
<span className="sr-only">Bold</span>
|
|
37
|
+
</BubbleMenuItem>
|
|
38
|
+
|
|
39
|
+
<BubbleMenuItem
|
|
40
|
+
editor={editor}
|
|
41
|
+
action={() => editor.chain().focus().toggleItalic().run()}
|
|
42
|
+
isActive={editor.isActive("italic")}
|
|
43
|
+
>
|
|
44
|
+
<MdOutlineFormatItalic className="size-4" />
|
|
45
|
+
<span className="sr-only">Italic</span>
|
|
46
|
+
</BubbleMenuItem>
|
|
47
|
+
|
|
48
|
+
<BubbleMenuItem
|
|
49
|
+
editor={editor}
|
|
50
|
+
action={() => editor.chain().focus().toggleStrike().run()}
|
|
51
|
+
isActive={editor.isActive("strike")}
|
|
52
|
+
>
|
|
53
|
+
<MdOutlineFormatStrikethrough className="size-4" />
|
|
54
|
+
<span className="sr-only">Strike</span>
|
|
55
|
+
</BubbleMenuItem>
|
|
56
|
+
|
|
57
|
+
<LinkItem editor={editor} open={openLink} setOpen={setOpenLink} />
|
|
58
|
+
</>
|
|
59
|
+
</div>
|
|
60
|
+
</TiptapBubbleMenu>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|