@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.
- package/LICENSE +21 -0
- package/package.json +124 -4
- package/src/ai-elements/artifact.tsx +99 -0
- package/src/ai-elements/attachments.tsx +366 -0
- package/src/ai-elements/chain-of-thought.tsx +212 -0
- package/src/ai-elements/code-block.tsx +478 -0
- package/src/ai-elements/conversation.tsx +157 -0
- package/src/ai-elements/image.tsx +25 -0
- package/src/ai-elements/inline-citation.tsx +252 -0
- package/src/ai-elements/message.tsx +269 -0
- package/src/ai-elements/model-selector.tsx +211 -0
- package/src/ai-elements/prompt-input.tsx +1018 -0
- package/src/ai-elements/queue.tsx +251 -0
- package/src/ai-elements/reasoning.tsx +178 -0
- package/src/ai-elements/shimmer.tsx +56 -0
- package/src/ai-elements/sources.tsx +77 -0
- package/src/ai-elements/speech-input.tsx +290 -0
- package/src/ai-elements/suggestion.tsx +191 -0
- package/src/css.d.ts +2 -0
- package/src/index.ts +96 -0
- package/src/motion/presets.ts +35 -0
- package/src/motion/presets.vibot.ts +105 -0
- package/src/ui/_menu-classes.ts +30 -0
- package/src/ui/_menu-classes.vibot.ts +33 -0
- package/src/ui/accordion.tsx +81 -0
- package/src/ui/accordion.verve.tsx +56 -0
- package/src/ui/badge.tsx +34 -0
- package/src/ui/breadcrumb.tsx +83 -0
- package/src/ui/button-group.tsx +84 -0
- package/src/ui/button.tsx +57 -0
- package/src/ui/card.tsx +27 -0
- package/src/ui/carousel.tsx +211 -0
- package/src/ui/checkbox.tsx +27 -0
- package/src/ui/clear-input.tsx +170 -0
- package/src/ui/clear-input.vibot.tsx +76 -0
- package/src/ui/clearInputDissolve.ts +214 -0
- package/src/ui/collapsible.tsx +7 -0
- package/src/ui/command.tsx +148 -0
- package/src/ui/context-menu.tsx +173 -0
- package/src/ui/dialog.tsx +149 -0
- package/src/ui/dropdown-menu.tsx +178 -0
- package/src/ui/hover-card.tsx +31 -0
- package/src/ui/input-group.tsx +162 -0
- package/src/ui/input-group.verve.tsx +150 -0
- package/src/ui/input.tsx +21 -0
- package/src/ui/kbd.tsx +18 -0
- package/src/ui/label.tsx +20 -0
- package/src/ui/labeled-switch-row.tsx +53 -0
- package/src/ui/modalTransition.ts +85 -0
- package/src/ui/pagination.tsx +47 -0
- package/src/ui/popover.tsx +33 -0
- package/src/ui/progress.tsx +25 -0
- package/src/ui/radix-select.tsx +202 -0
- package/src/ui/scroll-area.tsx +55 -0
- package/src/ui/scroll-area.verve.tsx +43 -0
- package/src/ui/search-input.tsx +29 -0
- package/src/ui/select.tsx +163 -0
- package/src/ui/separator.tsx +28 -0
- package/src/ui/sheet.tsx +87 -0
- package/src/ui/skeleton.tsx +18 -0
- package/src/ui/sonner.css +16 -0
- package/src/ui/sonner.tsx +47 -0
- package/src/ui/spinner.tsx +27 -0
- package/src/ui/surface-classes.ts +55 -0
- package/src/ui/switch.tsx +26 -0
- package/src/ui/table.tsx +123 -0
- package/src/ui/tabs.tsx +140 -0
- package/src/ui/tabsSliding.ts +56 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/time-picker.tsx +170 -0
- package/src/ui/tooltip.tsx +25 -0
- package/src/ui/visually-hidden.tsx +3 -0
- package/src/utils/cn.ts +16 -0
- package/src/utils/ref.ts +10 -0
- package/README.md +0 -3
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
import { IconMicrophone, IconSquare } from "@tabler/icons-react";
|
|
4
|
+
import { Button } from "../ui/button";
|
|
5
|
+
import { Spinner } from "../ui/spinner";
|
|
6
|
+
import { cn } from "../utils/cn";
|
|
7
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
|
+
interface SpeechRecognition extends EventTarget {
|
|
9
|
+
continuous: boolean;
|
|
10
|
+
interimResults: boolean;
|
|
11
|
+
lang: string;
|
|
12
|
+
start(): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
onstart: ((this: SpeechRecognition, ev: Event) => void) | null;
|
|
15
|
+
onend: ((this: SpeechRecognition, ev: Event) => void) | null;
|
|
16
|
+
onresult:
|
|
17
|
+
| ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void)
|
|
18
|
+
| null;
|
|
19
|
+
onerror:
|
|
20
|
+
| ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void)
|
|
21
|
+
| null;
|
|
22
|
+
}
|
|
23
|
+
interface SpeechRecognitionEvent extends Event {
|
|
24
|
+
results: SpeechRecognitionResultList;
|
|
25
|
+
resultIndex: number;
|
|
26
|
+
}
|
|
27
|
+
interface SpeechRecognitionResultList {
|
|
28
|
+
readonly length: number;
|
|
29
|
+
item(index: number): SpeechRecognitionResult;
|
|
30
|
+
[index: number]: SpeechRecognitionResult;
|
|
31
|
+
}
|
|
32
|
+
interface SpeechRecognitionResult {
|
|
33
|
+
readonly length: number;
|
|
34
|
+
item(index: number): SpeechRecognitionAlternative;
|
|
35
|
+
[index: number]: SpeechRecognitionAlternative;
|
|
36
|
+
isFinal: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface SpeechRecognitionAlternative {
|
|
39
|
+
transcript: string;
|
|
40
|
+
confidence: number;
|
|
41
|
+
}
|
|
42
|
+
interface SpeechRecognitionErrorEvent extends Event {
|
|
43
|
+
error: string;
|
|
44
|
+
}
|
|
45
|
+
declare global {
|
|
46
|
+
interface Window {
|
|
47
|
+
SpeechRecognition: new () => SpeechRecognition;
|
|
48
|
+
webkitSpeechRecognition: new () => SpeechRecognition;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
type SpeechInputMode = "speech-recognition" | "media-recorder" | "none";
|
|
52
|
+
export type SpeechInputProps = ComponentProps<typeof Button> & {
|
|
53
|
+
onTranscriptionChange?: (text: string) => void;
|
|
54
|
+
onAudioRecorded?: (audioBlob: Blob) => Promise<string>;
|
|
55
|
+
lang?: string;
|
|
56
|
+
};
|
|
57
|
+
const detectSpeechInputMode = (): SpeechInputMode => {
|
|
58
|
+
if (typeof window === "undefined") {
|
|
59
|
+
return "none";
|
|
60
|
+
}
|
|
61
|
+
if ("SpeechRecognition" in window || "webkitSpeechRecognition" in window) {
|
|
62
|
+
return "speech-recognition";
|
|
63
|
+
}
|
|
64
|
+
if ("MediaRecorder" in window && "mediaDevices" in navigator) {
|
|
65
|
+
return "media-recorder";
|
|
66
|
+
}
|
|
67
|
+
return "none";
|
|
68
|
+
};
|
|
69
|
+
export const SpeechInput = ({
|
|
70
|
+
className,
|
|
71
|
+
onTranscriptionChange,
|
|
72
|
+
onAudioRecorded,
|
|
73
|
+
lang = "en-US",
|
|
74
|
+
...props
|
|
75
|
+
}: SpeechInputProps) => {
|
|
76
|
+
const [isListening, setIsListening] = useState(false);
|
|
77
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
78
|
+
const [mode] = useState<SpeechInputMode>(detectSpeechInputMode);
|
|
79
|
+
const [isRecognitionReady, setIsRecognitionReady] = useState(false);
|
|
80
|
+
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
|
81
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
82
|
+
const streamRef = useRef<MediaStream | null>(null);
|
|
83
|
+
const audioChunksRef = useRef<Blob[]>([]);
|
|
84
|
+
const onTranscriptionChangeRef = useRef<
|
|
85
|
+
SpeechInputProps["onTranscriptionChange"]
|
|
86
|
+
>(onTranscriptionChange);
|
|
87
|
+
const onAudioRecordedRef =
|
|
88
|
+
useRef<SpeechInputProps["onAudioRecorded"]>(onAudioRecorded);
|
|
89
|
+
onTranscriptionChangeRef.current = onTranscriptionChange;
|
|
90
|
+
onAudioRecordedRef.current = onAudioRecorded;
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (mode !== "speech-recognition") {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const SpeechRecognitionCtor =
|
|
96
|
+
window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
97
|
+
const speechRecognition = new SpeechRecognitionCtor();
|
|
98
|
+
speechRecognition.continuous = true;
|
|
99
|
+
speechRecognition.interimResults = true;
|
|
100
|
+
speechRecognition.lang = lang;
|
|
101
|
+
const handleStart = () => {
|
|
102
|
+
setIsListening(true);
|
|
103
|
+
};
|
|
104
|
+
const handleEnd = () => {
|
|
105
|
+
setIsListening(false);
|
|
106
|
+
};
|
|
107
|
+
const handleResult = (event: Event) => {
|
|
108
|
+
const speechEvent = event as SpeechRecognitionEvent;
|
|
109
|
+
let finalTranscript = "";
|
|
110
|
+
for (
|
|
111
|
+
let i = speechEvent.resultIndex;
|
|
112
|
+
i < speechEvent.results.length;
|
|
113
|
+
i += 1
|
|
114
|
+
) {
|
|
115
|
+
const result = speechEvent.results[i];
|
|
116
|
+
if (result?.isFinal) {
|
|
117
|
+
finalTranscript += result[0]?.transcript ?? "";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (finalTranscript) {
|
|
121
|
+
onTranscriptionChangeRef.current?.(finalTranscript);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const handleError = () => {
|
|
125
|
+
setIsListening(false);
|
|
126
|
+
};
|
|
127
|
+
speechRecognition.addEventListener("start", handleStart);
|
|
128
|
+
speechRecognition.addEventListener("end", handleEnd);
|
|
129
|
+
speechRecognition.addEventListener("result", handleResult);
|
|
130
|
+
speechRecognition.addEventListener("error", handleError);
|
|
131
|
+
recognitionRef.current = speechRecognition;
|
|
132
|
+
setIsRecognitionReady(true);
|
|
133
|
+
return () => {
|
|
134
|
+
speechRecognition.removeEventListener("start", handleStart);
|
|
135
|
+
speechRecognition.removeEventListener("end", handleEnd);
|
|
136
|
+
speechRecognition.removeEventListener("result", handleResult);
|
|
137
|
+
speechRecognition.removeEventListener("error", handleError);
|
|
138
|
+
speechRecognition.stop();
|
|
139
|
+
recognitionRef.current = null;
|
|
140
|
+
setIsRecognitionReady(false);
|
|
141
|
+
};
|
|
142
|
+
}, [mode, lang]);
|
|
143
|
+
useEffect(
|
|
144
|
+
() => () => {
|
|
145
|
+
if (mediaRecorderRef.current?.state === "recording") {
|
|
146
|
+
mediaRecorderRef.current.stop();
|
|
147
|
+
}
|
|
148
|
+
if (streamRef.current) {
|
|
149
|
+
for (const track of streamRef.current.getTracks()) {
|
|
150
|
+
track.stop();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
[],
|
|
155
|
+
);
|
|
156
|
+
const startMediaRecorder = async () => {
|
|
157
|
+
if (!onAudioRecordedRef.current) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
162
|
+
streamRef.current = stream;
|
|
163
|
+
const mediaRecorder = new MediaRecorder(stream);
|
|
164
|
+
audioChunksRef.current = [];
|
|
165
|
+
const handleDataAvailable = (event: BlobEvent) => {
|
|
166
|
+
if (event.data.size > 0) {
|
|
167
|
+
audioChunksRef.current.push(event.data);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const handleStop = async () => {
|
|
171
|
+
for (const track of stream.getTracks()) {
|
|
172
|
+
track.stop();
|
|
173
|
+
}
|
|
174
|
+
streamRef.current = null;
|
|
175
|
+
const audioBlob = new Blob(audioChunksRef.current, {
|
|
176
|
+
type: "audio/webm",
|
|
177
|
+
});
|
|
178
|
+
if (audioBlob.size > 0 && onAudioRecordedRef.current) {
|
|
179
|
+
setIsProcessing(true);
|
|
180
|
+
try {
|
|
181
|
+
const transcript = await onAudioRecordedRef.current(audioBlob);
|
|
182
|
+
if (transcript) {
|
|
183
|
+
onTranscriptionChangeRef.current?.(transcript);
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
} finally {
|
|
187
|
+
setIsProcessing(false);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const handleError = () => {
|
|
192
|
+
setIsListening(false);
|
|
193
|
+
for (const track of stream.getTracks()) {
|
|
194
|
+
track.stop();
|
|
195
|
+
}
|
|
196
|
+
streamRef.current = null;
|
|
197
|
+
};
|
|
198
|
+
mediaRecorder.addEventListener("dataavailable", handleDataAvailable);
|
|
199
|
+
mediaRecorder.addEventListener("stop", handleStop);
|
|
200
|
+
mediaRecorder.addEventListener("error", handleError);
|
|
201
|
+
mediaRecorderRef.current = mediaRecorder;
|
|
202
|
+
mediaRecorder.start();
|
|
203
|
+
setIsListening(true);
|
|
204
|
+
} catch {
|
|
205
|
+
setIsListening(false);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const stopMediaRecorder = () => {
|
|
209
|
+
if (mediaRecorderRef.current?.state === "recording") {
|
|
210
|
+
mediaRecorderRef.current.stop();
|
|
211
|
+
}
|
|
212
|
+
setIsListening(false);
|
|
213
|
+
};
|
|
214
|
+
const toggleListening = () => {
|
|
215
|
+
if (mode === "speech-recognition" && recognitionRef.current) {
|
|
216
|
+
if (isListening) {
|
|
217
|
+
recognitionRef.current.stop();
|
|
218
|
+
} else {
|
|
219
|
+
recognitionRef.current.start();
|
|
220
|
+
}
|
|
221
|
+
} else if (mode === "media-recorder") {
|
|
222
|
+
if (isListening) {
|
|
223
|
+
stopMediaRecorder();
|
|
224
|
+
} else {
|
|
225
|
+
startMediaRecorder();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const isDisabled =
|
|
230
|
+
mode === "none" ||
|
|
231
|
+
(mode === "speech-recognition" && !isRecognitionReady) ||
|
|
232
|
+
(mode === "media-recorder" && !onAudioRecorded) ||
|
|
233
|
+
isProcessing;
|
|
234
|
+
return (
|
|
235
|
+
<div className="relative inline-flex items-center justify-center">
|
|
236
|
+
{isListening
|
|
237
|
+
? [0, 1, 2].map((index) => (
|
|
238
|
+
<div
|
|
239
|
+
className="absolute inset-0 animate-ping rounded-full border-2 border-danger/30"
|
|
240
|
+
key={index}
|
|
241
|
+
style={{
|
|
242
|
+
animationDelay: `${index * 0.3}s`,
|
|
243
|
+
animationDuration: "2s",
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
))
|
|
247
|
+
: null}
|
|
248
|
+
|
|
249
|
+
<Button
|
|
250
|
+
className={cn(
|
|
251
|
+
"relative z-10 rounded-full transition-[background-color,transform] duration-200 ease-smooth active:duration-100 active:ease-out",
|
|
252
|
+
isListening
|
|
253
|
+
? "bg-destructive text-danger-foreground hover:bg-destructive/80 hover:text-danger-foreground"
|
|
254
|
+
: "bg-primary text-primary-foreground hover:bg-primary/80 hover:text-primary-foreground",
|
|
255
|
+
className,
|
|
256
|
+
)}
|
|
257
|
+
disabled={isDisabled}
|
|
258
|
+
onClick={toggleListening}
|
|
259
|
+
{...props}
|
|
260
|
+
>
|
|
261
|
+
<span className="relative inline-flex size-4 items-center justify-center">
|
|
262
|
+
<span
|
|
263
|
+
className={cn(
|
|
264
|
+
"absolute inset-0 flex items-center justify-center transition-opacity duration-150 ease-out",
|
|
265
|
+
isProcessing ? "opacity-100" : "opacity-0",
|
|
266
|
+
)}
|
|
267
|
+
>
|
|
268
|
+
<Spinner />
|
|
269
|
+
</span>
|
|
270
|
+
<span
|
|
271
|
+
className={cn(
|
|
272
|
+
"absolute inset-0 flex items-center justify-center transition-opacity duration-150 ease-out",
|
|
273
|
+
!isProcessing && isListening ? "opacity-100" : "opacity-0",
|
|
274
|
+
)}
|
|
275
|
+
>
|
|
276
|
+
<IconSquare className="size-4" />
|
|
277
|
+
</span>
|
|
278
|
+
<span
|
|
279
|
+
className={cn(
|
|
280
|
+
"flex items-center justify-center transition-opacity duration-150 ease-out",
|
|
281
|
+
isProcessing || isListening ? "opacity-0" : "opacity-100",
|
|
282
|
+
)}
|
|
283
|
+
>
|
|
284
|
+
<IconMicrophone className="size-4" />
|
|
285
|
+
</span>
|
|
286
|
+
</span>
|
|
287
|
+
</Button>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
import { useRef, useState, useEffect } from "react";
|
|
4
|
+
import { Button } from "../ui/button";
|
|
5
|
+
import { cn } from "../utils/cn";
|
|
6
|
+
import { flatInteractiveClass } from "../ui/surface-classes";
|
|
7
|
+
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
|
8
|
+
import { AnimatePresence, motion } from "motion/react";
|
|
9
|
+
import { fastTransition } from "../motion/presets.vibot";
|
|
10
|
+
export type SuggestionsProps = {
|
|
11
|
+
className?: string;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
orientation?: "horizontal" | "vertical";
|
|
14
|
+
};
|
|
15
|
+
function HorizontalSuggestions({
|
|
16
|
+
className,
|
|
17
|
+
children,
|
|
18
|
+
}: Pick<SuggestionsProps, "className" | "children">) {
|
|
19
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
21
|
+
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
22
|
+
const isDragging = useRef(false);
|
|
23
|
+
const dragStartX = useRef(0);
|
|
24
|
+
const dragScrollLeft = useRef(0);
|
|
25
|
+
const checkScrollability = () => {
|
|
26
|
+
const el = scrollRef.current;
|
|
27
|
+
if (!el) return;
|
|
28
|
+
setCanScrollLeft(el.scrollLeft > 0);
|
|
29
|
+
setCanScrollRight(el.scrollLeft < el.scrollWidth - el.clientWidth - 1);
|
|
30
|
+
};
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const el = scrollRef.current;
|
|
33
|
+
if (!el) return;
|
|
34
|
+
checkScrollability();
|
|
35
|
+
el.addEventListener("scroll", checkScrollability, { passive: true });
|
|
36
|
+
const ro = new ResizeObserver(checkScrollability);
|
|
37
|
+
ro.observe(el);
|
|
38
|
+
return () => {
|
|
39
|
+
el.removeEventListener("scroll", checkScrollability);
|
|
40
|
+
ro.disconnect();
|
|
41
|
+
};
|
|
42
|
+
}, [checkScrollability]);
|
|
43
|
+
const scrollBy = (amount: number) => {
|
|
44
|
+
scrollRef.current?.scrollBy({ left: amount, behavior: "smooth" });
|
|
45
|
+
};
|
|
46
|
+
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
47
|
+
const el = scrollRef.current;
|
|
48
|
+
if (!el) return;
|
|
49
|
+
isDragging.current = true;
|
|
50
|
+
dragStartX.current = e.pageX - el.offsetLeft;
|
|
51
|
+
dragScrollLeft.current = el.scrollLeft;
|
|
52
|
+
el.style.cursor = "grabbing";
|
|
53
|
+
};
|
|
54
|
+
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
55
|
+
if (!isDragging.current) return;
|
|
56
|
+
const el = scrollRef.current;
|
|
57
|
+
if (!el) return;
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
const x = e.pageX - el.offsetLeft;
|
|
60
|
+
el.scrollLeft = dragScrollLeft.current - (x - dragStartX.current);
|
|
61
|
+
};
|
|
62
|
+
const handleMouseUp = () => {
|
|
63
|
+
const el = scrollRef.current;
|
|
64
|
+
if (!el) return;
|
|
65
|
+
isDragging.current = false;
|
|
66
|
+
el.style.cursor = "grab";
|
|
67
|
+
};
|
|
68
|
+
return (
|
|
69
|
+
<div className={cn("flex w-full items-center gap-1", className)}>
|
|
70
|
+
<AnimatePresence initial={false}>
|
|
71
|
+
{canScrollLeft ? (
|
|
72
|
+
<motion.div
|
|
73
|
+
key="scroll-left"
|
|
74
|
+
initial={{ opacity: 0 }}
|
|
75
|
+
animate={{ opacity: 1 }}
|
|
76
|
+
exit={{ opacity: 0 }}
|
|
77
|
+
transition={fastTransition}
|
|
78
|
+
>
|
|
79
|
+
<Button
|
|
80
|
+
size="icon"
|
|
81
|
+
variant="ghost"
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => scrollBy(-200)}
|
|
84
|
+
className={cn(
|
|
85
|
+
flatInteractiveClass,
|
|
86
|
+
"h-7 w-7 shrink-0 rounded-full",
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
<IconChevronLeft size={14} />
|
|
90
|
+
</Button>
|
|
91
|
+
</motion.div>
|
|
92
|
+
) : null}
|
|
93
|
+
</AnimatePresence>
|
|
94
|
+
|
|
95
|
+
<div
|
|
96
|
+
ref={scrollRef}
|
|
97
|
+
className="flex-1 overflow-x-auto select-none cursor-grab [&::-webkit-scrollbar]:hidden"
|
|
98
|
+
style={{ scrollbarWidth: "none" }}
|
|
99
|
+
onMouseDown={handleMouseDown}
|
|
100
|
+
onMouseMove={handleMouseMove}
|
|
101
|
+
onMouseUp={handleMouseUp}
|
|
102
|
+
onMouseLeave={handleMouseUp}
|
|
103
|
+
>
|
|
104
|
+
<div className="flex w-max flex-nowrap items-center gap-2 px-1">
|
|
105
|
+
{children}
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<AnimatePresence initial={false}>
|
|
110
|
+
{canScrollRight ? (
|
|
111
|
+
<motion.div
|
|
112
|
+
key="scroll-right"
|
|
113
|
+
initial={{ opacity: 0 }}
|
|
114
|
+
animate={{ opacity: 1 }}
|
|
115
|
+
exit={{ opacity: 0 }}
|
|
116
|
+
transition={fastTransition}
|
|
117
|
+
>
|
|
118
|
+
<Button
|
|
119
|
+
size="icon"
|
|
120
|
+
variant="ghost"
|
|
121
|
+
type="button"
|
|
122
|
+
onClick={() => scrollBy(200)}
|
|
123
|
+
className={cn(
|
|
124
|
+
flatInteractiveClass,
|
|
125
|
+
"h-7 w-7 shrink-0 rounded-full",
|
|
126
|
+
)}
|
|
127
|
+
>
|
|
128
|
+
<IconChevronRight size={14} />
|
|
129
|
+
</Button>
|
|
130
|
+
</motion.div>
|
|
131
|
+
) : null}
|
|
132
|
+
</AnimatePresence>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
export const Suggestions = ({
|
|
137
|
+
className,
|
|
138
|
+
children,
|
|
139
|
+
orientation = "horizontal",
|
|
140
|
+
}: SuggestionsProps) => {
|
|
141
|
+
if (orientation === "vertical") {
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
className={cn(
|
|
145
|
+
"flex w-full flex-col items-stretch gap-2",
|
|
146
|
+
"[&_button]:h-auto [&_button]:w-full [&_button]:justify-start [&_button]:rounded-lg [&_button]:whitespace-normal [&_button]:text-left [&_button]:py-2",
|
|
147
|
+
className,
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
{children}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return (
|
|
155
|
+
<HorizontalSuggestions className={className}>
|
|
156
|
+
{children}
|
|
157
|
+
</HorizontalSuggestions>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
|
|
161
|
+
suggestion: string;
|
|
162
|
+
onClick?: (suggestion: string) => void;
|
|
163
|
+
};
|
|
164
|
+
export const Suggestion = ({
|
|
165
|
+
suggestion,
|
|
166
|
+
onClick,
|
|
167
|
+
className,
|
|
168
|
+
variant = "outline",
|
|
169
|
+
size = "sm",
|
|
170
|
+
children,
|
|
171
|
+
...props
|
|
172
|
+
}: SuggestionProps) => {
|
|
173
|
+
const handleClick = () => {
|
|
174
|
+
onClick?.(suggestion);
|
|
175
|
+
};
|
|
176
|
+
return (
|
|
177
|
+
<Button
|
|
178
|
+
className={cn(
|
|
179
|
+
"h-auto cursor-pointer rounded-full bg-surface-secondary px-3 py-1.5 text-sm text-muted hover:bg-surface-tertiary hover:text-foreground",
|
|
180
|
+
className,
|
|
181
|
+
)}
|
|
182
|
+
onClick={handleClick}
|
|
183
|
+
size={size}
|
|
184
|
+
type="button"
|
|
185
|
+
variant={variant}
|
|
186
|
+
{...props}
|
|
187
|
+
>
|
|
188
|
+
{children || suggestion}
|
|
189
|
+
</Button>
|
|
190
|
+
);
|
|
191
|
+
};
|
package/src/css.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Colour, radius, shadow and font tokens live in @vvedantb/tokens. Import that
|
|
2
|
+
// stylesheet in the app. This package does not ship a second token set.
|
|
3
|
+
export { cn } from "./utils/cn";
|
|
4
|
+
export { floatingSurfaceClass } from "./ui/_menu-classes";
|
|
5
|
+
export * from "./ui/surface-classes";
|
|
6
|
+
export * from "./motion/presets";
|
|
7
|
+
|
|
8
|
+
export * from "./ui/badge";
|
|
9
|
+
export * from "./ui/breadcrumb";
|
|
10
|
+
export * from "./ui/button";
|
|
11
|
+
export * from "./ui/button-group";
|
|
12
|
+
export * from "./ui/card";
|
|
13
|
+
export * from "./ui/carousel";
|
|
14
|
+
export * from "./ui/checkbox";
|
|
15
|
+
export * from "./ui/collapsible";
|
|
16
|
+
export * from "./ui/command";
|
|
17
|
+
export * from "./ui/context-menu";
|
|
18
|
+
export * from "./ui/dialog";
|
|
19
|
+
export * from "./ui/dropdown-menu";
|
|
20
|
+
export * from "./ui/hover-card";
|
|
21
|
+
export * from "./ui/input";
|
|
22
|
+
export * from "./ui/label";
|
|
23
|
+
export * from "./ui/labeled-switch-row";
|
|
24
|
+
export * from "./ui/pagination";
|
|
25
|
+
export * from "./ui/popover";
|
|
26
|
+
export * from "./ui/progress";
|
|
27
|
+
export {
|
|
28
|
+
Select as RadixSelect,
|
|
29
|
+
SelectContent as RadixSelectContent,
|
|
30
|
+
SelectGroup as RadixSelectGroup,
|
|
31
|
+
SelectItem as RadixSelectItem,
|
|
32
|
+
SelectLabel as RadixSelectLabel,
|
|
33
|
+
SelectScrollDownButton as RadixSelectScrollDownButton,
|
|
34
|
+
SelectScrollUpButton as RadixSelectScrollUpButton,
|
|
35
|
+
SelectSeparator as RadixSelectSeparator,
|
|
36
|
+
SelectTrigger as RadixSelectTrigger,
|
|
37
|
+
SelectValue as RadixSelectValue,
|
|
38
|
+
} from "./ui/radix-select";
|
|
39
|
+
export * from "./ui/search-input";
|
|
40
|
+
export * from "./ui/select";
|
|
41
|
+
export * from "./ui/separator";
|
|
42
|
+
export * from "./ui/skeleton";
|
|
43
|
+
export * from "./ui/sonner";
|
|
44
|
+
export * from "./ui/spinner";
|
|
45
|
+
export * from "./ui/switch";
|
|
46
|
+
export * from "./ui/table";
|
|
47
|
+
export * from "./ui/tabs";
|
|
48
|
+
export * from "./ui/textarea";
|
|
49
|
+
export * from "./ui/time-picker";
|
|
50
|
+
export * from "./ui/tooltip";
|
|
51
|
+
export * from "./ui/kbd";
|
|
52
|
+
export * from "./ui/sheet";
|
|
53
|
+
export * from "./ui/visually-hidden";
|
|
54
|
+
export * from "./ui/clearInputDissolve";
|
|
55
|
+
|
|
56
|
+
// Dual copies: richer file uses the plain name; the other keeps a suffix.
|
|
57
|
+
export * from "./ui/accordion";
|
|
58
|
+
export {
|
|
59
|
+
Accordion as AccordionVerve,
|
|
60
|
+
AccordionItem as AccordionItemVerve,
|
|
61
|
+
AccordionTrigger as AccordionTriggerVerve,
|
|
62
|
+
AccordionContent as AccordionContentVerve,
|
|
63
|
+
} from "./ui/accordion.verve";
|
|
64
|
+
export * from "./ui/clear-input";
|
|
65
|
+
export { ClearInput as ClearInputVibot } from "./ui/clear-input.vibot";
|
|
66
|
+
export * from "./ui/input-group";
|
|
67
|
+
export {
|
|
68
|
+
InputGroup as InputGroupVerve,
|
|
69
|
+
InputGroupAddon as InputGroupAddonVerve,
|
|
70
|
+
InputGroupButton as InputGroupButtonVerve,
|
|
71
|
+
InputGroupText as InputGroupTextVerve,
|
|
72
|
+
InputGroupInput as InputGroupInputVerve,
|
|
73
|
+
InputGroupTextarea as InputGroupTextareaVerve,
|
|
74
|
+
} from "./ui/input-group.verve";
|
|
75
|
+
export * from "./ui/scroll-area";
|
|
76
|
+
export {
|
|
77
|
+
ScrollArea as ScrollAreaVerve,
|
|
78
|
+
ScrollBar as ScrollBarVerve,
|
|
79
|
+
} from "./ui/scroll-area.verve";
|
|
80
|
+
|
|
81
|
+
export * from "./ai-elements/artifact";
|
|
82
|
+
export * from "./ai-elements/attachments";
|
|
83
|
+
export * from "./ai-elements/chain-of-thought";
|
|
84
|
+
export * from "./ai-elements/code-block";
|
|
85
|
+
export * from "./ai-elements/conversation";
|
|
86
|
+
export * from "./ai-elements/image";
|
|
87
|
+
export * from "./ai-elements/inline-citation";
|
|
88
|
+
export * from "./ai-elements/message";
|
|
89
|
+
export * from "./ai-elements/model-selector";
|
|
90
|
+
export * from "./ai-elements/prompt-input";
|
|
91
|
+
export * from "./ai-elements/queue";
|
|
92
|
+
export * from "./ai-elements/reasoning";
|
|
93
|
+
export * from "./ai-elements/shimmer";
|
|
94
|
+
export * from "./ai-elements/sources";
|
|
95
|
+
export * from "./ai-elements/speech-input";
|
|
96
|
+
export * from "./ai-elements/suggestion";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Transition, Variants } from "motion/react";
|
|
2
|
+
|
|
3
|
+
export const motionEase: [number, number, number, number] = [0.22, 1, 0.36, 1];
|
|
4
|
+
|
|
5
|
+
export const motionDuration = {
|
|
6
|
+
fast: 0.22,
|
|
7
|
+
base: 0.3,
|
|
8
|
+
slow: 0.36,
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export const motionTiming = {
|
|
12
|
+
route: 0.3,
|
|
13
|
+
sidebar: 0.28,
|
|
14
|
+
stagger: 0.03,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const motionDistance = {
|
|
18
|
+
routeX: 12,
|
|
19
|
+
pageY: 8,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export const defaultTransition: Transition = {
|
|
23
|
+
duration: motionDuration.base,
|
|
24
|
+
ease: motionEase,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const fadeUp: Variants = {
|
|
28
|
+
hidden: { opacity: 0, y: motionDistance.pageY },
|
|
29
|
+
show: { opacity: 1, y: 0, transition: defaultTransition },
|
|
30
|
+
exit: {
|
|
31
|
+
opacity: 0,
|
|
32
|
+
y: 6,
|
|
33
|
+
transition: { duration: motionDuration.fast, ease: motionEase },
|
|
34
|
+
},
|
|
35
|
+
};
|