@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,295 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
type ComponentProps,
|
|
6
|
+
createContext,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
|
|
13
|
+
import { cn } from "../../../lib/utils";
|
|
14
|
+
import { badgeVariants } from "../atoms/badge";
|
|
15
|
+
import {
|
|
16
|
+
Carousel,
|
|
17
|
+
type CarouselApi,
|
|
18
|
+
CarouselContent,
|
|
19
|
+
CarouselItem,
|
|
20
|
+
} from "../organisms/carousel";
|
|
21
|
+
import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card";
|
|
22
|
+
|
|
23
|
+
export type InlineCitationProps = Omit<
|
|
24
|
+
ComponentProps<"span">,
|
|
25
|
+
"className"
|
|
26
|
+
>;
|
|
27
|
+
|
|
28
|
+
export const InlineCitation = ({ ...props }: InlineCitationProps) => (
|
|
29
|
+
<span className="group inline items-center gap-1" {...props} />
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export type InlineCitationTextProps = Omit<
|
|
33
|
+
ComponentProps<"span">,
|
|
34
|
+
"className"
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
export const InlineCitationText = ({ ...props }: InlineCitationTextProps) => (
|
|
38
|
+
<span className="transition-colors group-hover:bg-accent" {...props} />
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
|
|
42
|
+
|
|
43
|
+
export const InlineCitationCard = (props: InlineCitationCardProps) => (
|
|
44
|
+
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export type InlineCitationCardTriggerProps =
|
|
48
|
+
Omit<ComponentProps<"span">, "className"> & {
|
|
49
|
+
sources: string[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const InlineCitationCardTrigger = ({
|
|
53
|
+
sources,
|
|
54
|
+
...props
|
|
55
|
+
}: InlineCitationCardTriggerProps) => (
|
|
56
|
+
<HoverCardTrigger
|
|
57
|
+
render={
|
|
58
|
+
<span
|
|
59
|
+
className={cn(
|
|
60
|
+
badgeVariants({ variant: "secondary" }),
|
|
61
|
+
"ml-1 rounded-full",
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
{sources.length ? (
|
|
68
|
+
<>
|
|
69
|
+
{new URL(sources[0]!).hostname} {sources.length > 1 && `+${sources.length - 1}`}
|
|
70
|
+
</>
|
|
71
|
+
) : (
|
|
72
|
+
"unknown"
|
|
73
|
+
)}
|
|
74
|
+
</HoverCardTrigger>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export type InlineCitationCardBodyProps = Omit<
|
|
78
|
+
ComponentProps<"div">,
|
|
79
|
+
"className"
|
|
80
|
+
>;
|
|
81
|
+
|
|
82
|
+
export const InlineCitationCardBody = ({
|
|
83
|
+
children,
|
|
84
|
+
...props
|
|
85
|
+
}: InlineCitationCardBodyProps) => (
|
|
86
|
+
<HoverCardContent>
|
|
87
|
+
<div className="relative w-80 p-0" {...props}>
|
|
88
|
+
{children}
|
|
89
|
+
</div>
|
|
90
|
+
</HoverCardContent>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
|
|
94
|
+
|
|
95
|
+
const useCarouselApi = () => {
|
|
96
|
+
const context = useContext(CarouselApiContext);
|
|
97
|
+
return context;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;
|
|
101
|
+
|
|
102
|
+
export const InlineCitationCarousel = ({
|
|
103
|
+
children,
|
|
104
|
+
...props
|
|
105
|
+
}: InlineCitationCarouselProps) => {
|
|
106
|
+
const [api, setApi] = useState<CarouselApi>();
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<CarouselApiContext.Provider value={api}>
|
|
110
|
+
<div className="w-full">
|
|
111
|
+
<Carousel setApi={setApi} {...props}>
|
|
112
|
+
{children}
|
|
113
|
+
</Carousel>
|
|
114
|
+
</div>
|
|
115
|
+
</CarouselApiContext.Provider>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type InlineCitationCarouselContentProps = ComponentProps<
|
|
120
|
+
typeof CarouselContent
|
|
121
|
+
>;
|
|
122
|
+
|
|
123
|
+
export const InlineCitationCarouselContent = (
|
|
124
|
+
props: InlineCitationCarouselContentProps,
|
|
125
|
+
) => <CarouselContent {...props} />;
|
|
126
|
+
|
|
127
|
+
export type InlineCitationCarouselItemProps = ComponentProps<
|
|
128
|
+
typeof CarouselItem
|
|
129
|
+
>;
|
|
130
|
+
|
|
131
|
+
export const InlineCitationCarouselItem = ({
|
|
132
|
+
children,
|
|
133
|
+
...props
|
|
134
|
+
}: InlineCitationCarouselItemProps) => (
|
|
135
|
+
<CarouselItem {...props}>
|
|
136
|
+
<div className="w-full space-y-2 p-4 pl-8">{children}</div>
|
|
137
|
+
</CarouselItem>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
export type InlineCitationCarouselHeaderProps = Omit<
|
|
141
|
+
ComponentProps<"div">,
|
|
142
|
+
"className"
|
|
143
|
+
>;
|
|
144
|
+
|
|
145
|
+
export const InlineCitationCarouselHeader = ({
|
|
146
|
+
...props
|
|
147
|
+
}: InlineCitationCarouselHeaderProps) => (
|
|
148
|
+
<div
|
|
149
|
+
className="flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2"
|
|
150
|
+
{...props}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
export type InlineCitationCarouselIndexProps = Omit<
|
|
155
|
+
ComponentProps<"div">,
|
|
156
|
+
"className"
|
|
157
|
+
>;
|
|
158
|
+
|
|
159
|
+
export const InlineCitationCarouselIndex = ({
|
|
160
|
+
children,
|
|
161
|
+
...props
|
|
162
|
+
}: InlineCitationCarouselIndexProps) => {
|
|
163
|
+
const api = useCarouselApi();
|
|
164
|
+
const [current, setCurrent] = useState(0);
|
|
165
|
+
const [count, setCount] = useState(0);
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!api) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setCount(api.scrollSnapList().length);
|
|
173
|
+
setCurrent(api.selectedScrollSnap() + 1);
|
|
174
|
+
|
|
175
|
+
api.on("select", () => {
|
|
176
|
+
setCurrent(api.selectedScrollSnap() + 1);
|
|
177
|
+
});
|
|
178
|
+
}, [api]);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div
|
|
182
|
+
className="flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs"
|
|
183
|
+
{...props}
|
|
184
|
+
>
|
|
185
|
+
{children ?? `${current}/${count}`}
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type InlineCitationCarouselPrevProps = Omit<
|
|
191
|
+
ComponentProps<"button">,
|
|
192
|
+
"className"
|
|
193
|
+
>;
|
|
194
|
+
|
|
195
|
+
export const InlineCitationCarouselPrev = ({
|
|
196
|
+
...props
|
|
197
|
+
}: InlineCitationCarouselPrevProps) => {
|
|
198
|
+
const api = useCarouselApi();
|
|
199
|
+
|
|
200
|
+
const handleClick = useCallback(() => {
|
|
201
|
+
if (api) {
|
|
202
|
+
api.scrollPrev();
|
|
203
|
+
}
|
|
204
|
+
}, [api]);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<button
|
|
208
|
+
aria-label="Previous"
|
|
209
|
+
className="shrink-0"
|
|
210
|
+
onClick={handleClick}
|
|
211
|
+
type="button"
|
|
212
|
+
{...props}
|
|
213
|
+
>
|
|
214
|
+
<ArrowLeftIcon className="size-4 text-muted-foreground" />
|
|
215
|
+
</button>
|
|
216
|
+
);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export type InlineCitationCarouselNextProps = Omit<
|
|
220
|
+
ComponentProps<"button">,
|
|
221
|
+
"className"
|
|
222
|
+
>;
|
|
223
|
+
|
|
224
|
+
export const InlineCitationCarouselNext = ({
|
|
225
|
+
...props
|
|
226
|
+
}: InlineCitationCarouselNextProps) => {
|
|
227
|
+
const api = useCarouselApi();
|
|
228
|
+
|
|
229
|
+
const handleClick = useCallback(() => {
|
|
230
|
+
if (api) {
|
|
231
|
+
api.scrollNext();
|
|
232
|
+
}
|
|
233
|
+
}, [api]);
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<button
|
|
237
|
+
aria-label="Next"
|
|
238
|
+
className="shrink-0"
|
|
239
|
+
onClick={handleClick}
|
|
240
|
+
type="button"
|
|
241
|
+
{...props}
|
|
242
|
+
>
|
|
243
|
+
<ArrowRightIcon className="size-4 text-muted-foreground" />
|
|
244
|
+
</button>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export type InlineCitationSourceProps = Omit<
|
|
249
|
+
ComponentProps<"div">,
|
|
250
|
+
"className"
|
|
251
|
+
> & {
|
|
252
|
+
title?: string;
|
|
253
|
+
url?: string;
|
|
254
|
+
description?: string;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export const InlineCitationSource = ({
|
|
258
|
+
title,
|
|
259
|
+
url,
|
|
260
|
+
description,
|
|
261
|
+
children,
|
|
262
|
+
...props
|
|
263
|
+
}: InlineCitationSourceProps) => (
|
|
264
|
+
<div className="space-y-1" {...props}>
|
|
265
|
+
{title && (
|
|
266
|
+
<h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
|
|
267
|
+
)}
|
|
268
|
+
{url && (
|
|
269
|
+
<p className="truncate break-all text-muted-foreground text-xs">{url}</p>
|
|
270
|
+
)}
|
|
271
|
+
{description && (
|
|
272
|
+
<p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">
|
|
273
|
+
{description}
|
|
274
|
+
</p>
|
|
275
|
+
)}
|
|
276
|
+
{children}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
export type InlineCitationQuoteProps = Omit<
|
|
281
|
+
ComponentProps<"blockquote">,
|
|
282
|
+
"className"
|
|
283
|
+
>;
|
|
284
|
+
|
|
285
|
+
export const InlineCitationQuote = ({
|
|
286
|
+
children,
|
|
287
|
+
...props
|
|
288
|
+
}: InlineCitationQuoteProps) => (
|
|
289
|
+
<blockquote
|
|
290
|
+
className="border-muted border-l-2 pl-3 text-muted-foreground text-sm italic"
|
|
291
|
+
{...props}
|
|
292
|
+
>
|
|
293
|
+
{children}
|
|
294
|
+
</blockquote>
|
|
295
|
+
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ChatRole } from "../../../lib/ai";
|
|
2
|
+
import { cn } from "../../../lib/utils";
|
|
3
|
+
import type { ComponentProps, HTMLAttributes } from "react";
|
|
4
|
+
|
|
5
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../atoms/avatar";
|
|
6
|
+
|
|
7
|
+
export type MessageProps = Omit<HTMLAttributes<HTMLDivElement>, "className"> & {
|
|
8
|
+
from: ChatRole;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const Message = ({ from, ...props }: MessageProps) => (
|
|
12
|
+
<div
|
|
13
|
+
className={cn(
|
|
14
|
+
"group flex w-full items-end justify-end gap-2 py-4",
|
|
15
|
+
from === "user" ? "is-user" : "is-assistant flex-row-reverse justify-end",
|
|
16
|
+
"[&>div]:max-w-[80%]",
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export type MessageContentProps = Omit<
|
|
23
|
+
HTMLAttributes<HTMLDivElement>,
|
|
24
|
+
"className"
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
export const MessageContent = ({
|
|
28
|
+
children,
|
|
29
|
+
...props
|
|
30
|
+
}: MessageContentProps) => (
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
"flex flex-col gap-2 overflow-hidden rounded-lg px-4 py-3 text-foreground text-sm",
|
|
34
|
+
"group-[.is-user]:!bg-[#F7F7F7] dark:group-[.is-user]:!bg-[#131313] group-[.is-user]:!text-primary group-[.is-user]:!px-4 group-[.is-user]:!py-2 group-[.is-user]:max-w-fit group-[.is-user]:rounded-2xl group-[.is-user]:rounded-br-none",
|
|
35
|
+
"group-[.is-assistant]:!bg-transparent group-[.is-assistant]:!shadow-none group-[.is-assistant]:!border-none group-[.is-assistant]:!px-0 group-[.is-assistant]:!py-0 group-[.is-assistant]:!rounded-none group-[.is-assistant]:!text-[#666666]",
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export type MessageAvatarProps = Omit<
|
|
44
|
+
ComponentProps<typeof Avatar>,
|
|
45
|
+
"className"
|
|
46
|
+
> & {
|
|
47
|
+
src: string;
|
|
48
|
+
name?: string;
|
|
49
|
+
size?: "xs" | "sm" | "default" | "lg" | "xl";
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const MessageAvatar = ({
|
|
53
|
+
src,
|
|
54
|
+
name,
|
|
55
|
+
size = "xs",
|
|
56
|
+
...props
|
|
57
|
+
}: MessageAvatarProps) => (
|
|
58
|
+
<Avatar size={size} {...props}>
|
|
59
|
+
<AvatarImage alt="" src={src} />
|
|
60
|
+
<AvatarFallback>
|
|
61
|
+
{name?.slice(0, 1) || "M"}
|
|
62
|
+
</AvatarFallback>
|
|
63
|
+
</Avatar>
|
|
64
|
+
);
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { BookIcon, ChevronDownIcon } from "lucide-react";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
|
|
6
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../atoms/avatar";
|
|
7
|
+
import {
|
|
8
|
+
Collapsible,
|
|
9
|
+
CollapsibleContent,
|
|
10
|
+
CollapsibleTrigger,
|
|
11
|
+
} from "./collapsible";
|
|
12
|
+
|
|
13
|
+
export type SourcesProps = Omit<ComponentProps<typeof Collapsible>, "className">;
|
|
14
|
+
|
|
15
|
+
export const Sources = ({ ...props }: SourcesProps) => (
|
|
16
|
+
<Collapsible className="not-prose mb-4 text-primary text-xs" {...props} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export type SourcesTriggerProps = Omit<
|
|
20
|
+
ComponentProps<typeof CollapsibleTrigger>,
|
|
21
|
+
"className"
|
|
22
|
+
> & {
|
|
23
|
+
count: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const SourcesTrigger = ({
|
|
27
|
+
count,
|
|
28
|
+
children,
|
|
29
|
+
...props
|
|
30
|
+
}: SourcesTriggerProps) => (
|
|
31
|
+
<CollapsibleTrigger className="flex items-center gap-2" {...props}>
|
|
32
|
+
{children ?? (
|
|
33
|
+
<>
|
|
34
|
+
<p className="font-medium">Used {count} sources</p>
|
|
35
|
+
<ChevronDownIcon className="h-4 w-4" />
|
|
36
|
+
</>
|
|
37
|
+
)}
|
|
38
|
+
</CollapsibleTrigger>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export type SourcesContentProps = Omit<
|
|
42
|
+
ComponentProps<typeof CollapsibleContent>,
|
|
43
|
+
"className"
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
export const SourcesContent = ({ ...props }: SourcesContentProps) => (
|
|
47
|
+
<CollapsibleContent
|
|
48
|
+
className="mt-3 flex w-fit flex-col gap-2 data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export type SourceProps = Omit<ComponentProps<"a">, "className"> & {
|
|
54
|
+
domain?: string;
|
|
55
|
+
showAvatar?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function extractDomainFromUrl(url: string): string {
|
|
59
|
+
try {
|
|
60
|
+
const urlObj = new URL(url);
|
|
61
|
+
return urlObj.hostname.replace(/^www\./, "");
|
|
62
|
+
} catch {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Source = ({
|
|
68
|
+
href,
|
|
69
|
+
title,
|
|
70
|
+
domain,
|
|
71
|
+
showAvatar = true,
|
|
72
|
+
children,
|
|
73
|
+
...props
|
|
74
|
+
}: SourceProps) => {
|
|
75
|
+
const sourceDomain = domain || (href ? extractDomainFromUrl(href) : "");
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<a
|
|
79
|
+
className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors"
|
|
80
|
+
href={href}
|
|
81
|
+
rel="noreferrer"
|
|
82
|
+
target="_blank"
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
{children ?? (
|
|
86
|
+
<>
|
|
87
|
+
{showAvatar && sourceDomain ? (
|
|
88
|
+
<Avatar size="sm">
|
|
89
|
+
<AvatarImage
|
|
90
|
+
src={`https://img.logo.dev/${sourceDomain}?token=pk_BQw8Qo2gQeGk5LGKGGMUxA&format=png&size=24&theme=light`}
|
|
91
|
+
alt={`${sourceDomain} logo`}
|
|
92
|
+
onError={(e) => {
|
|
93
|
+
(e.target as HTMLImageElement).src =
|
|
94
|
+
`https://${sourceDomain}/favicon.ico`;
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
<AvatarFallback>
|
|
98
|
+
{sourceDomain.split(".")[0]?.charAt(0).toUpperCase() || "?"}
|
|
99
|
+
</AvatarFallback>
|
|
100
|
+
</Avatar>
|
|
101
|
+
) : (
|
|
102
|
+
<BookIcon className="h-4 w-4 text-muted-foreground" />
|
|
103
|
+
)}
|
|
104
|
+
<div className="flex-1 min-w-0">
|
|
105
|
+
<span className="block font-medium text-sm truncate">{title}</span>
|
|
106
|
+
{sourceDomain && (
|
|
107
|
+
<span className="block text-xs text-muted-foreground truncate">
|
|
108
|
+
{sourceDomain}
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</>
|
|
113
|
+
)}
|
|
114
|
+
</a>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ButtonHTMLAttributes, ComponentProps } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { type ButtonProps, buttonVariants } from "../atoms/button";
|
|
7
|
+
|
|
8
|
+
export type SuggestionsProps = Omit<ComponentProps<"div">, "className">;
|
|
9
|
+
|
|
10
|
+
export const Suggestions = ({ children, ...props }: SuggestionsProps) => (
|
|
11
|
+
<div className="w-full overflow-x-auto whitespace-nowrap" {...props}>
|
|
12
|
+
<div className="flex w-max flex-nowrap items-center gap-2">
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export type SuggestionProps = Omit<
|
|
19
|
+
ButtonHTMLAttributes<HTMLButtonElement>,
|
|
20
|
+
"className" | "onClick"
|
|
21
|
+
> & {
|
|
22
|
+
suggestion: string;
|
|
23
|
+
onClick?: (suggestion: string) => void;
|
|
24
|
+
variant?: ButtonProps["variant"];
|
|
25
|
+
size?: ButtonProps["size"];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const Suggestion = ({
|
|
29
|
+
suggestion,
|
|
30
|
+
onClick,
|
|
31
|
+
variant = "outline",
|
|
32
|
+
size = "sm",
|
|
33
|
+
children,
|
|
34
|
+
...props
|
|
35
|
+
}: SuggestionProps) => {
|
|
36
|
+
const handleClick = () => {
|
|
37
|
+
onClick?.(suggestion);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
className={cn(
|
|
43
|
+
buttonVariants({ variant, size }),
|
|
44
|
+
"cursor-pointer rounded-full px-4",
|
|
45
|
+
)}
|
|
46
|
+
onClick={handleClick}
|
|
47
|
+
type="button"
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{children || suggestion}
|
|
51
|
+
</button>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronDownIcon, SearchIcon } from "lucide-react";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Collapsible,
|
|
8
|
+
CollapsibleContent,
|
|
9
|
+
CollapsibleTrigger,
|
|
10
|
+
} from "./collapsible";
|
|
11
|
+
|
|
12
|
+
export type TaskItemFileProps = Omit<ComponentProps<"div">, "className">;
|
|
13
|
+
|
|
14
|
+
export const TaskItemFile = ({ children, ...props }: TaskItemFileProps) => (
|
|
15
|
+
<div
|
|
16
|
+
className="inline-flex items-center gap-1 rounded-md border bg-secondary px-1.5 py-0.5 text-foreground text-xs"
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export type TaskItemProps = Omit<ComponentProps<"div">, "className">;
|
|
24
|
+
|
|
25
|
+
export const TaskItem = ({ children, ...props }: TaskItemProps) => (
|
|
26
|
+
<div className="text-muted-foreground text-sm" {...props}>
|
|
27
|
+
{children}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export type TaskProps = Omit<ComponentProps<typeof Collapsible>, "className">;
|
|
32
|
+
|
|
33
|
+
export const Task = ({ defaultOpen = true, ...props }: TaskProps) => (
|
|
34
|
+
<Collapsible
|
|
35
|
+
className="data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 data-[state=closed]:animate-out data-[state=open]:animate-in"
|
|
36
|
+
defaultOpen={defaultOpen}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export type TaskTriggerProps = Omit<
|
|
42
|
+
ComponentProps<typeof CollapsibleTrigger>,
|
|
43
|
+
"className"
|
|
44
|
+
> & {
|
|
45
|
+
title: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const TaskTrigger = ({ children, title, ...props }: TaskTriggerProps) => (
|
|
49
|
+
<CollapsibleTrigger className="group" {...props}>
|
|
50
|
+
{children ?? (
|
|
51
|
+
<div className="flex cursor-pointer items-center gap-2 text-muted-foreground hover:text-foreground">
|
|
52
|
+
<SearchIcon className="size-4" />
|
|
53
|
+
<p className="text-sm">{title}</p>
|
|
54
|
+
<ChevronDownIcon className="size-4 transition-transform group-data-[state=open]:rotate-180" />
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</CollapsibleTrigger>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export type TaskContentProps = Omit<
|
|
61
|
+
ComponentProps<typeof CollapsibleContent>,
|
|
62
|
+
"className"
|
|
63
|
+
>;
|
|
64
|
+
|
|
65
|
+
export const TaskContent = ({ children, ...props }: TaskContentProps) => (
|
|
66
|
+
<CollapsibleContent
|
|
67
|
+
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"
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
<div className="mt-4 space-y-2 border-muted border-l-2 pl-4">
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
</CollapsibleContent>
|
|
74
|
+
);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { differenceInMinutes, parse } from "date-fns";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
import { Icons } from "../atoms/icons";
|
|
7
|
+
|
|
8
|
+
export function TimeRangeInput({
|
|
9
|
+
value,
|
|
10
|
+
onChange,
|
|
11
|
+
}: {
|
|
12
|
+
value: { start: string | undefined; stop: string | undefined };
|
|
13
|
+
onChange: (value: { start: string; stop: string }) => void;
|
|
14
|
+
}) {
|
|
15
|
+
const [startTime, setStartTime] = useState(value.start || "");
|
|
16
|
+
const [stopTime, setStopTime] = useState(value.stop || "");
|
|
17
|
+
const [duration, setDuration] = useState("");
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setStartTime(value.start || "");
|
|
21
|
+
setStopTime(value.stop || "");
|
|
22
|
+
}, [value]);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!startTime || !stopTime) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const start = parse(startTime, "HH:mm", new Date());
|
|
30
|
+
let stop = parse(stopTime, "HH:mm", new Date());
|
|
31
|
+
|
|
32
|
+
if (stop < start) {
|
|
33
|
+
stop = new Date(stop.getTime() + 24 * 60 * 60 * 1000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const diff = differenceInMinutes(stop, start);
|
|
37
|
+
const hours = Math.floor(diff / 60);
|
|
38
|
+
const minutes = diff % 60;
|
|
39
|
+
setDuration(`${hours}h ${minutes}min`);
|
|
40
|
+
}, [startTime, stopTime]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex items-center w-full border border-border px-4 py-2">
|
|
44
|
+
<div className="flex items-center space-x-2 flex-1">
|
|
45
|
+
<Icons.Time size={20} color="#878787" />
|
|
46
|
+
<input
|
|
47
|
+
type="time"
|
|
48
|
+
value={startTime}
|
|
49
|
+
onChange={(e) => {
|
|
50
|
+
setStartTime(e.target.value);
|
|
51
|
+
onChange({ start: e.target.value, stop: stopTime });
|
|
52
|
+
}}
|
|
53
|
+
className="bg-transparent focus:outline-none text-sm"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex items-center justify-center flex-shrink-0 mx-4">
|
|
57
|
+
<Icons.ArrowRightAlt size={20} color="#878787" />
|
|
58
|
+
</div>
|
|
59
|
+
<div className="flex items-center space-x-2 flex-1 justify-end">
|
|
60
|
+
<input
|
|
61
|
+
type="time"
|
|
62
|
+
value={stopTime}
|
|
63
|
+
onChange={(e) => {
|
|
64
|
+
setStopTime(e.target.value);
|
|
65
|
+
onChange({ start: startTime, stop: e.target.value });
|
|
66
|
+
}}
|
|
67
|
+
className="bg-transparent focus:outline-none text-sm"
|
|
68
|
+
/>
|
|
69
|
+
<span className="text-[#878787] text-sm">{duration}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|