@oppulence/design-system 1.0.4 → 1.0.5

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.
Files changed (46) hide show
  1. package/hooks/use-resize-observer.ts +24 -0
  2. package/lib/ai.ts +31 -0
  3. package/package.json +19 -1
  4. package/src/components/atoms/animated-size-container.tsx +59 -0
  5. package/src/components/atoms/currency-input.tsx +16 -0
  6. package/src/components/atoms/icons.tsx +840 -0
  7. package/src/components/atoms/image.tsx +23 -0
  8. package/src/components/atoms/index.ts +10 -0
  9. package/src/components/atoms/loader.tsx +92 -0
  10. package/src/components/atoms/quantity-input.tsx +103 -0
  11. package/src/components/atoms/record-button.tsx +178 -0
  12. package/src/components/atoms/submit-button.tsx +26 -0
  13. package/src/components/atoms/text-effect.tsx +251 -0
  14. package/src/components/atoms/text-shimmer.tsx +74 -0
  15. package/src/components/molecules/actions.tsx +53 -0
  16. package/src/components/molecules/branch.tsx +192 -0
  17. package/src/components/molecules/code-block.tsx +151 -0
  18. package/src/components/molecules/form.tsx +177 -0
  19. package/src/components/molecules/index.ts +12 -0
  20. package/src/components/molecules/inline-citation.tsx +295 -0
  21. package/src/components/molecules/message.tsx +64 -0
  22. package/src/components/molecules/sources.tsx +116 -0
  23. package/src/components/molecules/suggestion.tsx +53 -0
  24. package/src/components/molecules/task.tsx +74 -0
  25. package/src/components/molecules/time-range-input.tsx +73 -0
  26. package/src/components/molecules/tool-call-indicator.tsx +42 -0
  27. package/src/components/molecules/tool.tsx +130 -0
  28. package/src/components/organisms/combobox-dropdown.tsx +171 -0
  29. package/src/components/organisms/conversation.tsx +98 -0
  30. package/src/components/organisms/date-range-picker.tsx +53 -0
  31. package/src/components/organisms/editor/extentions/bubble-menu/bubble-item.tsx +30 -0
  32. package/src/components/organisms/editor/extentions/bubble-menu/bubble-menu-button.tsx +27 -0
  33. package/src/components/organisms/editor/extentions/bubble-menu/index.tsx +63 -0
  34. package/src/components/organisms/editor/extentions/bubble-menu/link-item.tsx +104 -0
  35. package/src/components/organisms/editor/extentions/register.ts +22 -0
  36. package/src/components/organisms/editor/index.tsx +50 -0
  37. package/src/components/organisms/editor/styles.css +31 -0
  38. package/src/components/organisms/editor/utils.ts +19 -0
  39. package/src/components/organisms/index.ts +11 -0
  40. package/src/components/organisms/multiple-selector.tsx +632 -0
  41. package/src/components/organisms/prompt-input.tsx +747 -0
  42. package/src/components/organisms/reasoning.tsx +170 -0
  43. package/src/components/organisms/response.tsx +121 -0
  44. package/src/components/organisms/toast-toaster.tsx +84 -0
  45. package/src/components/organisms/toast.tsx +124 -0
  46. 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
+ }