@vvedantb/ui 0.1.1 → 0.1.2

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 (74) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +3 -2
  3. package/src/ai-elements/artifact.tsx +99 -99
  4. package/src/ai-elements/attachments.tsx +366 -366
  5. package/src/ai-elements/chain-of-thought.tsx +212 -212
  6. package/src/ai-elements/code-block.tsx +478 -478
  7. package/src/ai-elements/conversation.tsx +157 -157
  8. package/src/ai-elements/image.tsx +25 -25
  9. package/src/ai-elements/inline-citation.tsx +252 -252
  10. package/src/ai-elements/message.tsx +269 -269
  11. package/src/ai-elements/model-selector.tsx +211 -211
  12. package/src/ai-elements/prompt-input.tsx +1018 -1018
  13. package/src/ai-elements/queue.tsx +251 -251
  14. package/src/ai-elements/reasoning.tsx +178 -178
  15. package/src/ai-elements/shimmer.tsx +56 -56
  16. package/src/ai-elements/sources.tsx +77 -77
  17. package/src/ai-elements/speech-input.tsx +290 -290
  18. package/src/ai-elements/suggestion.tsx +191 -191
  19. package/src/css.d.ts +2 -2
  20. package/src/index.ts +96 -96
  21. package/src/motion/presets.ts +35 -35
  22. package/src/motion/presets.vibot.ts +105 -105
  23. package/src/ui/_menu-classes.ts +30 -30
  24. package/src/ui/_menu-classes.vibot.ts +33 -33
  25. package/src/ui/accordion.tsx +81 -81
  26. package/src/ui/accordion.verve.tsx +56 -56
  27. package/src/ui/badge.tsx +34 -34
  28. package/src/ui/breadcrumb.tsx +83 -83
  29. package/src/ui/button-group.tsx +84 -84
  30. package/src/ui/button.tsx +57 -57
  31. package/src/ui/card.tsx +27 -27
  32. package/src/ui/carousel.tsx +211 -211
  33. package/src/ui/checkbox.tsx +27 -27
  34. package/src/ui/clear-input.tsx +170 -170
  35. package/src/ui/clear-input.vibot.tsx +76 -76
  36. package/src/ui/clearInputDissolve.ts +214 -214
  37. package/src/ui/collapsible.tsx +7 -7
  38. package/src/ui/command.tsx +148 -148
  39. package/src/ui/context-menu.tsx +173 -173
  40. package/src/ui/dialog.tsx +149 -149
  41. package/src/ui/dropdown-menu.tsx +178 -178
  42. package/src/ui/hover-card.tsx +31 -31
  43. package/src/ui/input-group.tsx +162 -162
  44. package/src/ui/input-group.verve.tsx +150 -150
  45. package/src/ui/input.tsx +21 -21
  46. package/src/ui/kbd.tsx +18 -18
  47. package/src/ui/label.tsx +20 -20
  48. package/src/ui/labeled-switch-row.tsx +53 -53
  49. package/src/ui/modalTransition.ts +85 -85
  50. package/src/ui/pagination.tsx +47 -47
  51. package/src/ui/popover.tsx +33 -33
  52. package/src/ui/progress.tsx +25 -25
  53. package/src/ui/radix-select.tsx +202 -202
  54. package/src/ui/scroll-area.tsx +55 -55
  55. package/src/ui/scroll-area.verve.tsx +43 -43
  56. package/src/ui/search-input.tsx +29 -29
  57. package/src/ui/select.tsx +163 -163
  58. package/src/ui/separator.tsx +28 -28
  59. package/src/ui/sheet.tsx +87 -87
  60. package/src/ui/skeleton.tsx +18 -18
  61. package/src/ui/sonner.css +16 -16
  62. package/src/ui/sonner.tsx +47 -47
  63. package/src/ui/spinner.tsx +27 -27
  64. package/src/ui/surface-classes.ts +55 -55
  65. package/src/ui/switch.tsx +26 -26
  66. package/src/ui/table.tsx +123 -123
  67. package/src/ui/tabs.tsx +140 -140
  68. package/src/ui/tabsSliding.ts +56 -56
  69. package/src/ui/textarea.tsx +21 -21
  70. package/src/ui/time-picker.tsx +170 -170
  71. package/src/ui/tooltip.tsx +25 -25
  72. package/src/ui/visually-hidden.tsx +3 -3
  73. package/src/utils/cn.ts +16 -16
  74. package/src/utils/ref.ts +10 -10
@@ -1,178 +1,178 @@
1
- "use client";
2
- import type { ComponentProps, ReactNode } from "react";
3
- import { useControllableState } from "@radix-ui/react-use-controllable-state";
4
- import {
5
- Collapsible,
6
- CollapsibleContent,
7
- CollapsibleTrigger,
8
- } from "../ui/collapsible";
9
- import { cn } from "../utils/cn";
10
- import { cjk } from "@streamdown/cjk";
11
- import { code } from "@streamdown/code";
12
- import { math } from "@streamdown/math";
13
- import { mermaid } from "@streamdown/mermaid";
14
- import { IconBrain, IconChevronDown } from "@tabler/icons-react";
15
- import { createContext, useContext, useEffect, useRef, useState } from "react";
16
- import { Streamdown } from "streamdown";
17
- import { Shimmer } from "./shimmer";
18
- interface ReasoningContextValue {
19
- isStreaming: boolean;
20
- isOpen: boolean;
21
- setIsOpen: (open: boolean) => void;
22
- duration: number | undefined;
23
- }
24
- const ReasoningContext = createContext<ReasoningContextValue | null>(null);
25
- export const useReasoning = () => {
26
- const context = useContext(ReasoningContext);
27
- if (!context) {
28
- throw new Error("Reasoning components must be used within Reasoning");
29
- }
30
- return context;
31
- };
32
- export type ReasoningProps = ComponentProps<typeof Collapsible> & {
33
- isStreaming?: boolean;
34
- open?: boolean;
35
- defaultOpen?: boolean;
36
- onOpenChange?: (open: boolean) => void;
37
- duration?: number;
38
- };
39
- const AUTO_CLOSE_DELAY = 1000;
40
- const MS_IN_S = 1000;
41
- export function Reasoning({
42
- className,
43
- isStreaming = false,
44
- open,
45
- defaultOpen,
46
- onOpenChange,
47
- duration: durationProp,
48
- children,
49
- ...props
50
- }: ReasoningProps) {
51
- const resolvedDefaultOpen = defaultOpen ?? isStreaming;
52
- // Track if defaultOpen was explicitly set to false (to prevent auto-open)
53
- const isExplicitlyClosed = defaultOpen === false;
54
- const [isOpen, setIsOpen] = useControllableState<boolean>({
55
- defaultProp: resolvedDefaultOpen,
56
- onChange: onOpenChange,
57
- prop: open,
58
- });
59
- const [duration, setDuration] = useControllableState<number | undefined>({
60
- defaultProp: undefined,
61
- prop: durationProp,
62
- });
63
- const hasEverStreamedRef = useRef(isStreaming);
64
- const [hasAutoClosed, setHasAutoClosed] = useState(false);
65
- const startTimeRef = useRef<number | null>(null);
66
- // Track when streaming starts and compute duration
67
- useEffect(() => {
68
- if (isStreaming) {
69
- hasEverStreamedRef.current = true;
70
- if (startTimeRef.current === null) {
71
- startTimeRef.current = Date.now();
72
- }
73
- } else if (startTimeRef.current !== null) {
74
- setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S));
75
- startTimeRef.current = null;
76
- }
77
- }, [isStreaming, setDuration]);
78
- // Auto-open when streaming starts (unless explicitly closed)
79
- useEffect(() => {
80
- if (isStreaming && !isOpen && !isExplicitlyClosed) {
81
- setIsOpen(true);
82
- }
83
- }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
84
- // Auto-close when streaming ends (once only, and only if it ever streamed)
85
- useEffect(() => {
86
- if (
87
- hasEverStreamedRef.current &&
88
- !isStreaming &&
89
- isOpen &&
90
- !hasAutoClosed
91
- ) {
92
- const timer = setTimeout(() => {
93
- setIsOpen(false);
94
- setHasAutoClosed(true);
95
- }, AUTO_CLOSE_DELAY);
96
- return () => clearTimeout(timer);
97
- }
98
- }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
99
- const handleOpenChange = (newOpen: boolean) => {
100
- setIsOpen(newOpen);
101
- };
102
- const contextValue = { duration, isOpen, isStreaming, setIsOpen };
103
- return (
104
- <ReasoningContext.Provider value={contextValue}>
105
- <Collapsible
106
- className={cn("not-prose mb-4", className)}
107
- onOpenChange={handleOpenChange}
108
- open={isOpen}
109
- {...props}
110
- >
111
- {children}
112
- </Collapsible>
113
- </ReasoningContext.Provider>
114
- );
115
- }
116
- export type ReasoningTriggerProps = ComponentProps<
117
- typeof CollapsibleTrigger
118
- > & {
119
- getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
120
- };
121
- const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
122
- if (isStreaming || duration === 0) {
123
- return <Shimmer duration={1}>Thinking...</Shimmer>;
124
- }
125
- if (duration === undefined) {
126
- return <p>Thought for a few seconds</p>;
127
- }
128
- return <p>Thought for {duration} seconds</p>;
129
- };
130
- export function ReasoningTrigger({
131
- className,
132
- children,
133
- getThinkingMessage = defaultGetThinkingMessage,
134
- ...props
135
- }: ReasoningTriggerProps) {
136
- const { isStreaming, isOpen, duration } = useReasoning();
137
- return (
138
- <CollapsibleTrigger
139
- className={cn(
140
- "flex w-full items-center gap-2 py-1 text-sm text-muted transition-[color,transform] duration-150 ease-out active:scale-[0.99] hover:text-foreground",
141
- className,
142
- )}
143
- {...props}
144
- >
145
- {children ?? (
146
- <>
147
- <IconBrain className="size-4" />
148
- {getThinkingMessage(isStreaming, duration)}
149
- <IconChevronDown
150
- className={cn(
151
- "size-4 transition-transform duration-150 ease-out",
152
- isOpen ? "rotate-180" : "rotate-0",
153
- )}
154
- />
155
- </>
156
- )}
157
- </CollapsibleTrigger>
158
- );
159
- }
160
- export type ReasoningContentProps = ComponentProps<
161
- typeof CollapsibleContent
162
- > & {
163
- children: string;
164
- };
165
- const streamdownPlugins = { cjk, code, math, mermaid };
166
- export function ReasoningContent({
167
- className,
168
- children,
169
- ...props
170
- }: ReasoningContentProps) {
171
- return (
172
- <CollapsibleContent className={cn("pt-2 pb-1", className)} {...props}>
173
- <div className="rounded-lg bg-surface-secondary/40 p-3">
174
- <Streamdown plugins={streamdownPlugins}>{children}</Streamdown>
175
- </div>
176
- </CollapsibleContent>
177
- );
178
- }
1
+ "use client";
2
+ import type { ComponentProps, ReactNode } from "react";
3
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from "../ui/collapsible";
9
+ import { cn } from "../utils/cn";
10
+ import { cjk } from "@streamdown/cjk";
11
+ import { code } from "@streamdown/code";
12
+ import { math } from "@streamdown/math";
13
+ import { mermaid } from "@streamdown/mermaid";
14
+ import { IconBrain, IconChevronDown } from "@tabler/icons-react";
15
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
16
+ import { Streamdown } from "streamdown";
17
+ import { Shimmer } from "./shimmer";
18
+ interface ReasoningContextValue {
19
+ isStreaming: boolean;
20
+ isOpen: boolean;
21
+ setIsOpen: (open: boolean) => void;
22
+ duration: number | undefined;
23
+ }
24
+ const ReasoningContext = createContext<ReasoningContextValue | null>(null);
25
+ export const useReasoning = () => {
26
+ const context = useContext(ReasoningContext);
27
+ if (!context) {
28
+ throw new Error("Reasoning components must be used within Reasoning");
29
+ }
30
+ return context;
31
+ };
32
+ export type ReasoningProps = ComponentProps<typeof Collapsible> & {
33
+ isStreaming?: boolean;
34
+ open?: boolean;
35
+ defaultOpen?: boolean;
36
+ onOpenChange?: (open: boolean) => void;
37
+ duration?: number;
38
+ };
39
+ const AUTO_CLOSE_DELAY = 1000;
40
+ const MS_IN_S = 1000;
41
+ export function Reasoning({
42
+ className,
43
+ isStreaming = false,
44
+ open,
45
+ defaultOpen,
46
+ onOpenChange,
47
+ duration: durationProp,
48
+ children,
49
+ ...props
50
+ }: ReasoningProps) {
51
+ const resolvedDefaultOpen = defaultOpen ?? isStreaming;
52
+ // Track if defaultOpen was explicitly set to false (to prevent auto-open)
53
+ const isExplicitlyClosed = defaultOpen === false;
54
+ const [isOpen, setIsOpen] = useControllableState<boolean>({
55
+ defaultProp: resolvedDefaultOpen,
56
+ onChange: onOpenChange,
57
+ prop: open,
58
+ });
59
+ const [duration, setDuration] = useControllableState<number | undefined>({
60
+ defaultProp: undefined,
61
+ prop: durationProp,
62
+ });
63
+ const hasEverStreamedRef = useRef(isStreaming);
64
+ const [hasAutoClosed, setHasAutoClosed] = useState(false);
65
+ const startTimeRef = useRef<number | null>(null);
66
+ // Track when streaming starts and compute duration
67
+ useEffect(() => {
68
+ if (isStreaming) {
69
+ hasEverStreamedRef.current = true;
70
+ if (startTimeRef.current === null) {
71
+ startTimeRef.current = Date.now();
72
+ }
73
+ } else if (startTimeRef.current !== null) {
74
+ setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S));
75
+ startTimeRef.current = null;
76
+ }
77
+ }, [isStreaming, setDuration]);
78
+ // Auto-open when streaming starts (unless explicitly closed)
79
+ useEffect(() => {
80
+ if (isStreaming && !isOpen && !isExplicitlyClosed) {
81
+ setIsOpen(true);
82
+ }
83
+ }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
84
+ // Auto-close when streaming ends (once only, and only if it ever streamed)
85
+ useEffect(() => {
86
+ if (
87
+ hasEverStreamedRef.current &&
88
+ !isStreaming &&
89
+ isOpen &&
90
+ !hasAutoClosed
91
+ ) {
92
+ const timer = setTimeout(() => {
93
+ setIsOpen(false);
94
+ setHasAutoClosed(true);
95
+ }, AUTO_CLOSE_DELAY);
96
+ return () => clearTimeout(timer);
97
+ }
98
+ }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
99
+ const handleOpenChange = (newOpen: boolean) => {
100
+ setIsOpen(newOpen);
101
+ };
102
+ const contextValue = { duration, isOpen, isStreaming, setIsOpen };
103
+ return (
104
+ <ReasoningContext.Provider value={contextValue}>
105
+ <Collapsible
106
+ className={cn("not-prose mb-4", className)}
107
+ onOpenChange={handleOpenChange}
108
+ open={isOpen}
109
+ {...props}
110
+ >
111
+ {children}
112
+ </Collapsible>
113
+ </ReasoningContext.Provider>
114
+ );
115
+ }
116
+ export type ReasoningTriggerProps = ComponentProps<
117
+ typeof CollapsibleTrigger
118
+ > & {
119
+ getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
120
+ };
121
+ const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
122
+ if (isStreaming || duration === 0) {
123
+ return <Shimmer duration={1}>Thinking...</Shimmer>;
124
+ }
125
+ if (duration === undefined) {
126
+ return <p>Thought for a few seconds</p>;
127
+ }
128
+ return <p>Thought for {duration} seconds</p>;
129
+ };
130
+ export function ReasoningTrigger({
131
+ className,
132
+ children,
133
+ getThinkingMessage = defaultGetThinkingMessage,
134
+ ...props
135
+ }: ReasoningTriggerProps) {
136
+ const { isStreaming, isOpen, duration } = useReasoning();
137
+ return (
138
+ <CollapsibleTrigger
139
+ className={cn(
140
+ "flex w-full items-center gap-2 py-1 text-sm text-muted transition-[color,transform] duration-150 ease-out active:scale-[0.99] hover:text-foreground",
141
+ className,
142
+ )}
143
+ {...props}
144
+ >
145
+ {children ?? (
146
+ <>
147
+ <IconBrain className="size-4" />
148
+ {getThinkingMessage(isStreaming, duration)}
149
+ <IconChevronDown
150
+ className={cn(
151
+ "size-4 transition-transform duration-150 ease-out",
152
+ isOpen ? "rotate-180" : "rotate-0",
153
+ )}
154
+ />
155
+ </>
156
+ )}
157
+ </CollapsibleTrigger>
158
+ );
159
+ }
160
+ export type ReasoningContentProps = ComponentProps<
161
+ typeof CollapsibleContent
162
+ > & {
163
+ children: string;
164
+ };
165
+ const streamdownPlugins = { cjk, code, math, mermaid };
166
+ export function ReasoningContent({
167
+ className,
168
+ children,
169
+ ...props
170
+ }: ReasoningContentProps) {
171
+ return (
172
+ <CollapsibleContent className={cn("pt-2 pb-1", className)} {...props}>
173
+ <div className="rounded-lg bg-surface-secondary/40 p-3">
174
+ <Streamdown plugins={streamdownPlugins}>{children}</Streamdown>
175
+ </div>
176
+ </CollapsibleContent>
177
+ );
178
+ }
@@ -1,56 +1,56 @@
1
- "use client";
2
- import { type CSSProperties, type ElementType } from "react";
3
- import { motion } from "motion/react";
4
- import { cn } from "../utils/cn";
5
- interface ShimmerCSSProperties extends CSSProperties {
6
- "--spread"?: string;
7
- "--bg"?: string;
8
- }
9
- interface ShimmerProps {
10
- children: string;
11
- as?: ElementType;
12
- className?: string;
13
- duration?: number;
14
- spread?: number;
15
- }
16
- function Shimmer({
17
- children,
18
- as: Component = "span",
19
- className,
20
- duration = 2,
21
- spread = 2,
22
- }: ShimmerProps) {
23
- const MotionComponent = motion.create(Component);
24
- const totalSpread = spread * children.length;
25
- const style = {
26
- "--spread": `${totalSpread}px`,
27
- "--bg": `linear-gradient(
28
- 90deg,
29
- var(--muted) 0%,
30
- var(--foreground) calc(50% - var(--spread)),
31
- var(--muted) 50%,
32
- var(--foreground) calc(50% + var(--spread)),
33
- var(--muted) 100%
34
- )`,
35
- backgroundImage: "var(--bg)",
36
- backgroundSize: "200% 100%",
37
- } satisfies ShimmerCSSProperties;
38
- return (
39
- <MotionComponent
40
- className={cn(
41
- "inline-block bg-clip-text text-transparent [-webkit-text-fill-color:transparent]",
42
- className,
43
- )}
44
- style={style}
45
- animate={{ backgroundPosition: ["100% center", "0% center"] }}
46
- transition={{
47
- duration,
48
- ease: "linear",
49
- repeat: Number.POSITIVE_INFINITY,
50
- }}
51
- >
52
- {children}
53
- </MotionComponent>
54
- );
55
- }
56
- export { Shimmer, type ShimmerProps };
1
+ "use client";
2
+ import { type CSSProperties, type ElementType } from "react";
3
+ import { motion } from "motion/react";
4
+ import { cn } from "../utils/cn";
5
+ interface ShimmerCSSProperties extends CSSProperties {
6
+ "--spread"?: string;
7
+ "--bg"?: string;
8
+ }
9
+ interface ShimmerProps {
10
+ children: string;
11
+ as?: ElementType;
12
+ className?: string;
13
+ duration?: number;
14
+ spread?: number;
15
+ }
16
+ function Shimmer({
17
+ children,
18
+ as: Component = "span",
19
+ className,
20
+ duration = 2,
21
+ spread = 2,
22
+ }: ShimmerProps) {
23
+ const MotionComponent = motion.create(Component);
24
+ const totalSpread = spread * children.length;
25
+ const style = {
26
+ "--spread": `${totalSpread}px`,
27
+ "--bg": `linear-gradient(
28
+ 90deg,
29
+ var(--muted) 0%,
30
+ var(--foreground) calc(50% - var(--spread)),
31
+ var(--muted) 50%,
32
+ var(--foreground) calc(50% + var(--spread)),
33
+ var(--muted) 100%
34
+ )`,
35
+ backgroundImage: "var(--bg)",
36
+ backgroundSize: "200% 100%",
37
+ } satisfies ShimmerCSSProperties;
38
+ return (
39
+ <MotionComponent
40
+ className={cn(
41
+ "inline-block bg-clip-text text-transparent [-webkit-text-fill-color:transparent]",
42
+ className,
43
+ )}
44
+ style={style}
45
+ animate={{ backgroundPosition: ["100% center", "0% center"] }}
46
+ transition={{
47
+ duration,
48
+ ease: "linear",
49
+ repeat: Number.POSITIVE_INFINITY,
50
+ }}
51
+ >
52
+ {children}
53
+ </MotionComponent>
54
+ );
55
+ }
56
+ export { Shimmer, type ShimmerProps };
@@ -1,77 +1,77 @@
1
- "use client";
2
-
3
- import type { ComponentProps } from "react";
4
-
5
- import {
6
- Collapsible,
7
- CollapsibleContent,
8
- CollapsibleTrigger,
9
- } from "../ui/collapsible";
10
- import { cn } from "../utils/cn";
11
- import { IconBook, IconChevronDown } from "@tabler/icons-react";
12
-
13
- export type SourcesProps = ComponentProps<typeof Collapsible>;
14
-
15
- export const Sources = ({ className, ...props }: SourcesProps) => (
16
- <Collapsible className={cn("w-full", className)} {...props} />
17
- );
18
-
19
- export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
20
- count: number;
21
- };
22
-
23
- export const SourcesTrigger = ({
24
- className,
25
- count,
26
- children,
27
- ...props
28
- }: SourcesTriggerProps) => (
29
- <CollapsibleTrigger
30
- className={cn(
31
- "group inline-flex items-center gap-1.5 text-xs text-muted transition-[color,transform] duration-150 ease-out active:scale-[0.97] hover:text-foreground",
32
- className,
33
- )}
34
- {...props}
35
- >
36
- {children ?? (
37
- <>
38
- <IconBook className="size-3.5" stroke={1.5} />
39
- <span>Used {count} sources</span>
40
- <IconChevronDown className="size-3.5 transition-transform duration-150 ease-out group-data-[state=open]:rotate-180" />
41
- </>
42
- )}
43
- </CollapsibleTrigger>
44
- );
45
-
46
- export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
47
-
48
- export const SourcesContent = ({
49
- className,
50
- ...props
51
- }: SourcesContentProps) => (
52
- <CollapsibleContent className={cn("mt-2 space-y-2", className)} {...props} />
53
- );
54
-
55
- export type SourceProps = ComponentProps<"a">;
56
-
57
- export const Source = ({
58
- href,
59
- title,
60
- children,
61
- className,
62
- ...props
63
- }: SourceProps) => (
64
- <a
65
- className={cn(
66
- "flex items-center justify-between gap-2 rounded-md px-3 py-2 text-xs text-muted transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary hover:text-foreground",
67
- className,
68
- )}
69
- href={href}
70
- rel="noreferrer"
71
- target="_blank"
72
- {...props}
73
- >
74
- <span className="line-clamp-1">{children ?? title}</span>
75
- <IconBook className="size-3.5 shrink-0" stroke={1.5} />
76
- </a>
77
- );
1
+ "use client";
2
+
3
+ import type { ComponentProps } from "react";
4
+
5
+ import {
6
+ Collapsible,
7
+ CollapsibleContent,
8
+ CollapsibleTrigger,
9
+ } from "../ui/collapsible";
10
+ import { cn } from "../utils/cn";
11
+ import { IconBook, IconChevronDown } from "@tabler/icons-react";
12
+
13
+ export type SourcesProps = ComponentProps<typeof Collapsible>;
14
+
15
+ export const Sources = ({ className, ...props }: SourcesProps) => (
16
+ <Collapsible className={cn("w-full", className)} {...props} />
17
+ );
18
+
19
+ export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
20
+ count: number;
21
+ };
22
+
23
+ export const SourcesTrigger = ({
24
+ className,
25
+ count,
26
+ children,
27
+ ...props
28
+ }: SourcesTriggerProps) => (
29
+ <CollapsibleTrigger
30
+ className={cn(
31
+ "group inline-flex items-center gap-1.5 text-xs text-muted transition-[color,transform] duration-150 ease-out active:scale-[0.97] hover:text-foreground",
32
+ className,
33
+ )}
34
+ {...props}
35
+ >
36
+ {children ?? (
37
+ <>
38
+ <IconBook className="size-3.5" stroke={1.5} />
39
+ <span>Used {count} sources</span>
40
+ <IconChevronDown className="size-3.5 transition-transform duration-150 ease-out group-data-[state=open]:rotate-180" />
41
+ </>
42
+ )}
43
+ </CollapsibleTrigger>
44
+ );
45
+
46
+ export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
47
+
48
+ export const SourcesContent = ({
49
+ className,
50
+ ...props
51
+ }: SourcesContentProps) => (
52
+ <CollapsibleContent className={cn("mt-2 space-y-2", className)} {...props} />
53
+ );
54
+
55
+ export type SourceProps = ComponentProps<"a">;
56
+
57
+ export const Source = ({
58
+ href,
59
+ title,
60
+ children,
61
+ className,
62
+ ...props
63
+ }: SourceProps) => (
64
+ <a
65
+ className={cn(
66
+ "flex items-center justify-between gap-2 rounded-md px-3 py-2 text-xs text-muted transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary hover:text-foreground",
67
+ className,
68
+ )}
69
+ href={href}
70
+ rel="noreferrer"
71
+ target="_blank"
72
+ {...props}
73
+ >
74
+ <span className="line-clamp-1">{children ?? title}</span>
75
+ <IconBook className="size-3.5 shrink-0" stroke={1.5} />
76
+ </a>
77
+ );