@vvedantb/ui 0.0.0-stage → 0.1.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +124 -4
  3. package/src/ai-elements/artifact.tsx +99 -0
  4. package/src/ai-elements/attachments.tsx +366 -0
  5. package/src/ai-elements/chain-of-thought.tsx +212 -0
  6. package/src/ai-elements/code-block.tsx +478 -0
  7. package/src/ai-elements/conversation.tsx +157 -0
  8. package/src/ai-elements/image.tsx +25 -0
  9. package/src/ai-elements/inline-citation.tsx +252 -0
  10. package/src/ai-elements/message.tsx +269 -0
  11. package/src/ai-elements/model-selector.tsx +211 -0
  12. package/src/ai-elements/prompt-input.tsx +1018 -0
  13. package/src/ai-elements/queue.tsx +251 -0
  14. package/src/ai-elements/reasoning.tsx +178 -0
  15. package/src/ai-elements/shimmer.tsx +56 -0
  16. package/src/ai-elements/sources.tsx +77 -0
  17. package/src/ai-elements/speech-input.tsx +290 -0
  18. package/src/ai-elements/suggestion.tsx +191 -0
  19. package/src/css.d.ts +2 -0
  20. package/src/index.ts +96 -0
  21. package/src/motion/presets.ts +35 -0
  22. package/src/motion/presets.vibot.ts +105 -0
  23. package/src/ui/_menu-classes.ts +30 -0
  24. package/src/ui/_menu-classes.vibot.ts +33 -0
  25. package/src/ui/accordion.tsx +81 -0
  26. package/src/ui/accordion.verve.tsx +56 -0
  27. package/src/ui/badge.tsx +34 -0
  28. package/src/ui/breadcrumb.tsx +83 -0
  29. package/src/ui/button-group.tsx +84 -0
  30. package/src/ui/button.tsx +57 -0
  31. package/src/ui/card.tsx +27 -0
  32. package/src/ui/carousel.tsx +211 -0
  33. package/src/ui/checkbox.tsx +27 -0
  34. package/src/ui/clear-input.tsx +170 -0
  35. package/src/ui/clear-input.vibot.tsx +76 -0
  36. package/src/ui/clearInputDissolve.ts +214 -0
  37. package/src/ui/collapsible.tsx +7 -0
  38. package/src/ui/command.tsx +148 -0
  39. package/src/ui/context-menu.tsx +173 -0
  40. package/src/ui/dialog.tsx +149 -0
  41. package/src/ui/dropdown-menu.tsx +178 -0
  42. package/src/ui/hover-card.tsx +31 -0
  43. package/src/ui/input-group.tsx +162 -0
  44. package/src/ui/input-group.verve.tsx +150 -0
  45. package/src/ui/input.tsx +21 -0
  46. package/src/ui/kbd.tsx +18 -0
  47. package/src/ui/label.tsx +20 -0
  48. package/src/ui/labeled-switch-row.tsx +53 -0
  49. package/src/ui/modalTransition.ts +85 -0
  50. package/src/ui/pagination.tsx +47 -0
  51. package/src/ui/popover.tsx +33 -0
  52. package/src/ui/progress.tsx +25 -0
  53. package/src/ui/radix-select.tsx +202 -0
  54. package/src/ui/scroll-area.tsx +55 -0
  55. package/src/ui/scroll-area.verve.tsx +43 -0
  56. package/src/ui/search-input.tsx +29 -0
  57. package/src/ui/select.tsx +163 -0
  58. package/src/ui/separator.tsx +28 -0
  59. package/src/ui/sheet.tsx +87 -0
  60. package/src/ui/skeleton.tsx +18 -0
  61. package/src/ui/sonner.css +16 -0
  62. package/src/ui/sonner.tsx +47 -0
  63. package/src/ui/spinner.tsx +27 -0
  64. package/src/ui/surface-classes.ts +55 -0
  65. package/src/ui/switch.tsx +26 -0
  66. package/src/ui/table.tsx +123 -0
  67. package/src/ui/tabs.tsx +140 -0
  68. package/src/ui/tabsSliding.ts +56 -0
  69. package/src/ui/textarea.tsx +21 -0
  70. package/src/ui/time-picker.tsx +170 -0
  71. package/src/ui/tooltip.tsx +25 -0
  72. package/src/ui/visually-hidden.tsx +3 -0
  73. package/src/utils/cn.ts +16 -0
  74. package/src/utils/ref.ts +10 -0
  75. package/README.md +0 -3
@@ -0,0 +1,251 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import { motion, AnimatePresence } from "motion/react";
5
+ import { cn } from "../utils/cn";
6
+ import {
7
+ flatPanelSubtleClass,
8
+ flatPanelSubtleHoverClass,
9
+ } from "../ui/surface-classes";
10
+ import { Button } from "../ui/button";
11
+ import { Badge } from "../ui/badge";
12
+ import { ScrollArea } from "../ui/scroll-area";
13
+ import { Textarea } from "../ui/textarea";
14
+ import {
15
+ IconChevronDown,
16
+ IconEdit,
17
+ IconTrash,
18
+ IconCheck,
19
+ IconX,
20
+ IconStack2,
21
+ } from "@tabler/icons-react";
22
+
23
+ export interface QueueProps {
24
+ className?: string;
25
+ children?: ReactNode;
26
+ count: number;
27
+ }
28
+
29
+ export function Queue({ className, children, count }: QueueProps) {
30
+ return (
31
+ <AnimatePresence initial={false}>
32
+ {count > 0 ? (
33
+ <motion.div
34
+ key="queue"
35
+ initial={{ height: 0, opacity: 0 }}
36
+ animate={{ height: "auto", opacity: 1 }}
37
+ exit={{ height: 0, opacity: 0 }}
38
+ transition={{ type: "spring", bounce: 0, duration: 0.35 }}
39
+ className={cn("w-full overflow-hidden", className)}
40
+ >
41
+ {children}
42
+ </motion.div>
43
+ ) : null}
44
+ </AnimatePresence>
45
+ );
46
+ }
47
+
48
+ export interface QueueHeaderProps {
49
+ count: number;
50
+ isOpen: boolean;
51
+ onToggle: () => void;
52
+ className?: string;
53
+ }
54
+
55
+ export function QueueHeader({
56
+ count,
57
+ isOpen,
58
+ onToggle,
59
+ className,
60
+ }: QueueHeaderProps) {
61
+ return (
62
+ <button
63
+ type="button"
64
+ onClick={onToggle}
65
+ className={cn(
66
+ "flex items-center justify-between w-full",
67
+ "px-4 py-2.5",
68
+ flatPanelSubtleClass,
69
+ flatPanelSubtleHoverClass,
70
+ "rounded-t-2xl",
71
+ "group",
72
+ className,
73
+ )}
74
+ >
75
+ <div className="flex items-center gap-2">
76
+ <IconStack2 size={16} className="text-muted" />
77
+ <span className="text-sm font-medium text-foreground">
78
+ Queued Messages
79
+ </span>
80
+ <Badge variant="secondary" className="text-xs">
81
+ {count}
82
+ </Badge>
83
+ </div>
84
+ <IconChevronDown
85
+ size={16}
86
+ className={cn(
87
+ "text-muted transition-transform duration-200 ease-out",
88
+ isOpen && "rotate-180",
89
+ )}
90
+ />
91
+ </button>
92
+ );
93
+ }
94
+
95
+ export interface QueueListProps {
96
+ className?: string;
97
+ children?: ReactNode;
98
+ open?: boolean;
99
+ }
100
+
101
+ export function QueueList({
102
+ className,
103
+ children,
104
+ open = true,
105
+ }: QueueListProps) {
106
+ return (
107
+ <AnimatePresence initial={false}>
108
+ {open ? (
109
+ <motion.div
110
+ key="queue-list"
111
+ initial={{ height: 0, opacity: 0 }}
112
+ animate={{ height: "auto", opacity: 1 }}
113
+ exit={{ height: 0, opacity: 0 }}
114
+ transition={{ type: "spring", bounce: 0, duration: 0.35 }}
115
+ className="overflow-hidden"
116
+ >
117
+ <div className={cn(flatPanelSubtleClass, "rounded-b-2xl", className)}>
118
+ <ScrollArea className="max-h-[240px]">
119
+ <div className="p-2 space-y-1.5">{children}</div>
120
+ </ScrollArea>
121
+ </div>
122
+ </motion.div>
123
+ ) : null}
124
+ </AnimatePresence>
125
+ );
126
+ }
127
+
128
+ export interface QueueItemProps {
129
+ text: string;
130
+ model?: string;
131
+ personalityName?: string;
132
+ personaName?: string;
133
+ personaImage?: string;
134
+ onEdit: (newText: string) => void;
135
+ onRemove: () => void;
136
+ className?: string;
137
+ }
138
+
139
+ export function QueueItem({
140
+ text,
141
+ model,
142
+ personalityName,
143
+ personaName,
144
+ personaImage,
145
+ onEdit,
146
+ onRemove,
147
+ className,
148
+ }: QueueItemProps) {
149
+ const [isEditing, setIsEditing] = useState(false);
150
+ const [editText, setEditText] = useState(text);
151
+
152
+ const handleSaveEdit = () => {
153
+ if (editText.trim()) {
154
+ onEdit(editText.trim());
155
+ setIsEditing(false);
156
+ }
157
+ };
158
+
159
+ const handleCancelEdit = () => {
160
+ setEditText(text);
161
+ setIsEditing(false);
162
+ };
163
+
164
+ return (
165
+ <div
166
+ className={cn(
167
+ "group relative",
168
+ "p-2.5 rounded-lg",
169
+ "transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary",
170
+ className,
171
+ )}
172
+ >
173
+ {isEditing ? (
174
+ <div className="flex flex-col gap-2">
175
+ <Textarea
176
+ value={editText}
177
+ onChange={(e) => setEditText(e.currentTarget.value)}
178
+ className="min-h-[60px] text-sm"
179
+ autoFocus
180
+ />
181
+ <div className="flex justify-end gap-2">
182
+ <Button
183
+ size="sm"
184
+ variant="ghost"
185
+ onClick={handleCancelEdit}
186
+ className="h-7 px-2"
187
+ >
188
+ <IconX size={14} />
189
+ <span className="ml-1 text-xs">Cancel</span>
190
+ </Button>
191
+ <Button
192
+ size="sm"
193
+ onClick={handleSaveEdit}
194
+ disabled={!editText.trim()}
195
+ className="h-7 px-2"
196
+ >
197
+ <IconCheck size={14} />
198
+ <span className="ml-1 text-xs">Save</span>
199
+ </Button>
200
+ </div>
201
+ </div>
202
+ ) : (
203
+ <div className="flex items-center gap-2">
204
+ {personaImage && (
205
+ <img
206
+ src={personaImage}
207
+ alt={personaName || "Persona"}
208
+ className="size-5 rounded-full shrink-0"
209
+ />
210
+ )}
211
+ <p className="text-sm text-foreground/90 flex-1 truncate">{text}</p>
212
+ <div className="flex items-center gap-1.5 shrink-0">
213
+ {model && (
214
+ <Badge variant="outline" className="text-xs h-5 px-1.5">
215
+ {model.split("/").pop()}
216
+ </Badge>
217
+ )}
218
+ {personalityName && (
219
+ <Badge variant="outline" className="text-xs h-5 px-1.5">
220
+ {personalityName}
221
+ </Badge>
222
+ )}
223
+ {personaName && !personaImage && (
224
+ <Badge variant="outline" className="text-xs h-5 px-1.5">
225
+ {personaName}
226
+ </Badge>
227
+ )}
228
+ </div>
229
+ <div className="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
230
+ <Button
231
+ size="sm"
232
+ variant="ghost"
233
+ onClick={() => setIsEditing(true)}
234
+ className="h-6 w-6 p-0"
235
+ >
236
+ <IconEdit size={13} />
237
+ </Button>
238
+ <Button
239
+ size="sm"
240
+ variant="ghost"
241
+ onClick={onRemove}
242
+ className="h-6 w-6 p-0 text-danger hover:text-danger"
243
+ >
244
+ <IconTrash size={13} />
245
+ </Button>
246
+ </div>
247
+ </div>
248
+ )}
249
+ </div>
250
+ );
251
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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 };
@@ -0,0 +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
+ );