@vvedantb/native-ui 0.3.16 → 0.4.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.
@@ -0,0 +1,104 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { ScrollView, Text, View } from "react-native";
3
+ import { IconCheck, IconCopy } from "@tabler/icons-react-native";
4
+ import { cn } from "../cn";
5
+ import { sizeStyle, textStyle } from "../controlStyles";
6
+ import { useLargerText } from "../theme";
7
+ import { tokens } from "../tokens";
8
+ import { useScheme } from "../useScheme";
9
+ import { MONO_FONT, chatTextSize } from "./chat-text";
10
+ import { ChatIconButton } from "./icon-button";
11
+
12
+ export type CodeBlockCopyButtonProps = {
13
+ code: string;
14
+ /**
15
+ * Puts `code` on the clipboard. React Native's own `Clipboard` is
16
+ * deprecated, and native-ui adds no clipboard package, so the app passes its
17
+ * own (for example `expo-clipboard`'s `setStringAsync`). A rejected promise
18
+ * leaves the copy icon.
19
+ */
20
+ onCopy: (code: string) => void | Promise<void>;
21
+ /** Milliseconds the check shows after a copy. Default 2000, as on the web. */
22
+ timeout?: number;
23
+ /** Default "Copy code"; "Copied" while the check shows. */
24
+ label?: string;
25
+ };
26
+
27
+ /** Ghost copy button: the copy icon, then a check for `timeout` after a copy. */
28
+ export function CodeBlockCopyButton({ code, onCopy, timeout = 2000, label = "Copy code" }: CodeBlockCopyButtonProps) {
29
+ const [copied, setCopied] = useState(false);
30
+ const timer = useRef<number | undefined>(undefined);
31
+
32
+ useEffect(() => () => clearTimeout(timer.current), []);
33
+
34
+ const copy = async () => {
35
+ // As the web: presses while the check shows do nothing.
36
+ if (copied) return;
37
+ try {
38
+ await onCopy(code);
39
+ } catch {
40
+ return;
41
+ }
42
+ setCopied(true);
43
+ timer.current = setTimeout(() => setCopied(false), timeout);
44
+ };
45
+
46
+ return (
47
+ <ChatIconButton
48
+ icon={copied ? IconCheck : IconCopy}
49
+ label={copied ? "Copied" : label}
50
+ onPress={() => void copy()}
51
+ />
52
+ );
53
+ }
54
+
55
+ export type CodeBlockProps = {
56
+ code: string;
57
+ /** Shown in the header. No highlighting: native-ui has no shiki. */
58
+ language?: string;
59
+ /** With it, the header has a copy button. See `CodeBlockCopyButton`. */
60
+ onCopy?: (code: string) => void | Promise<void>;
61
+ className?: string;
62
+ };
63
+
64
+ /**
65
+ * Plain code on the flat panel (web `flatPanelSubtle`: `bg-surface-secondary`,
66
+ * no border or shadow) at radius 16. A header with the language and a copy
67
+ * button when either is given. Long lines scroll sideways; the code is
68
+ * selectable.
69
+ */
70
+ export function CodeBlock({ code, language, onCopy, className }: CodeBlockProps) {
71
+ const scheme = useScheme();
72
+ const largerText = useLargerText();
73
+ const size = chatTextSize("small", largerText);
74
+ const caption = chatTextSize("caption", largerText);
75
+ const header = Boolean(language || onCopy);
76
+ return (
77
+ <View
78
+ className={cn("w-full overflow-hidden", className)}
79
+ style={{ borderRadius: tokens.radius.lg, backgroundColor: tokens.color.surfaceSecondary[scheme] }}
80
+ >
81
+ {header ? (
82
+ <View className="min-h-11 flex-row items-center justify-between pl-3 pr-1.5">
83
+ <Text className={caption} style={[sizeStyle(caption), textStyle("muted", scheme), { fontFamily: MONO_FONT }]}>
84
+ {language ?? ""}
85
+ </Text>
86
+ {onCopy ? <CodeBlockCopyButton code={code} onCopy={onCopy} /> : null}
87
+ </View>
88
+ ) : null}
89
+ <ScrollView
90
+ horizontal
91
+ showsHorizontalScrollIndicator={false}
92
+ contentContainerClassName={header ? "px-4 pb-4" : "p-4"}
93
+ >
94
+ <Text
95
+ selectable
96
+ className={size}
97
+ style={[sizeStyle(size), textStyle("foreground", scheme), { fontFamily: MONO_FONT }]}
98
+ >
99
+ {code}
100
+ </Text>
101
+ </ScrollView>
102
+ </View>
103
+ );
104
+ }
@@ -0,0 +1,111 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+ import { Pressable, View, type PressableProps, type ViewProps } from "react-native";
3
+ import { IconChevronDown, IconChevronRight } from "@tabler/icons-react-native";
4
+ import { FadeIn } from "react-native-reanimated";
5
+ import { cn } from "../cn";
6
+ import { MIN_TOUCH_TARGET, touchSlop } from "../controlStyles";
7
+ import { NativeOnlyAnimatedView } from "../primitives/native-only-animated-view";
8
+ import { useControllableState } from "../primitives/use-controllable-state";
9
+
10
+ // A small fold for the AI parts: native-ui has no `Collapsible`. A `Pressable`
11
+ // trigger and content that mounts only while open. The content fades in on
12
+ // iOS and Android, and appears in place with Reduce Motion.
13
+
14
+ type CollapsibleContextValue = { open: boolean; setOpen: (open: boolean) => void };
15
+
16
+ const CollapsibleContext = createContext<CollapsibleContextValue | null>(null);
17
+
18
+ /** The open state of the enclosing `Collapsible`, for a chevron or a custom trigger. */
19
+ export function useCollapsible() {
20
+ const context = useContext(CollapsibleContext);
21
+ if (!context) throw new Error("Collapsible parts must be used within Collapsible");
22
+ return context;
23
+ }
24
+
25
+ export type CollapsibleProps = ViewProps & {
26
+ open?: boolean;
27
+ defaultOpen?: boolean;
28
+ onOpenChange?: (open: boolean) => void;
29
+ className?: string;
30
+ };
31
+
32
+ /** Holds its own open state, or takes `open` and `onOpenChange`, as the web Radix `Collapsible`. */
33
+ export function Collapsible({ open, defaultOpen = false, onOpenChange, ...props }: CollapsibleProps) {
34
+ const [isOpen, setOpen] = useControllableState({
35
+ value: open,
36
+ defaultValue: defaultOpen,
37
+ onChange: onOpenChange,
38
+ });
39
+ return (
40
+ <CollapsibleContext.Provider value={{ open: isOpen, setOpen }}>
41
+ <View {...props} />
42
+ </CollapsibleContext.Provider>
43
+ );
44
+ }
45
+
46
+ /** A `text-sm` line with the web `py-1`: 28 high. The slop brings it to 44. */
47
+ const TRIGGER_ROW_HEIGHT = 28;
48
+
49
+ export type CollapsibleTriggerProps = Omit<PressableProps, "children"> & {
50
+ className?: string;
51
+ children?: ReactNode;
52
+ };
53
+
54
+ /** Toggles the fold. A button with `expanded` state for VoiceOver and TalkBack. */
55
+ export function CollapsibleTrigger({
56
+ className,
57
+ onPress,
58
+ accessibilityState,
59
+ ...props
60
+ }: CollapsibleTriggerProps) {
61
+ const { open, setOpen } = useCollapsible();
62
+ return (
63
+ <Pressable
64
+ accessibilityRole="button"
65
+ hitSlop={touchSlop(MIN_TOUCH_TARGET, TRIGGER_ROW_HEIGHT)}
66
+ className={cn("active:opacity-70", className)}
67
+ {...props}
68
+ accessibilityState={{ ...accessibilityState, expanded: open }}
69
+ onPress={(event) => {
70
+ onPress?.(event);
71
+ setOpen(!open);
72
+ }}
73
+ />
74
+ );
75
+ }
76
+
77
+ /** Mounted only while the fold is open. */
78
+ export function CollapsibleContent({ children, ...props }: ViewProps & { className?: string }) {
79
+ const { open } = useCollapsible();
80
+ if (!open) return null;
81
+ return (
82
+ <NativeOnlyAnimatedView entering={FadeIn.duration(150)} {...props}>
83
+ {children}
84
+ </NativeOnlyAnimatedView>
85
+ );
86
+ }
87
+
88
+ /**
89
+ * The fold chevron: `down` turns up when open (reasoning, sources, chain of
90
+ * thought), `right` turns down (trace lines), as the web `rotate-180` and
91
+ * `rotate-90`. It turns at once; the web eases it over 150 ms.
92
+ */
93
+ export function CollapsibleChevron({
94
+ open,
95
+ variant = "down",
96
+ size,
97
+ color,
98
+ }: {
99
+ open: boolean;
100
+ variant?: "down" | "right";
101
+ size: number;
102
+ color: string;
103
+ }) {
104
+ const Icon = variant === "down" ? IconChevronDown : IconChevronRight;
105
+ const turn = variant === "down" ? "180deg" : "90deg";
106
+ return (
107
+ <View style={{ transform: [{ rotate: open ? turn : "0deg" }] }}>
108
+ <Icon size={size} color={color} />
109
+ </View>
110
+ );
111
+ }
@@ -0,0 +1,245 @@
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useRef,
5
+ useState,
6
+ type ComponentType,
7
+ type ReactNode,
8
+ type RefObject,
9
+ } from "react";
10
+ import {
11
+ Platform,
12
+ ScrollView,
13
+ Text,
14
+ View,
15
+ type NativeScrollEvent,
16
+ type ScrollViewProps,
17
+ type ViewProps,
18
+ } from "react-native";
19
+ import { IconArrowDown, type IconProps } from "@tabler/icons-react-native";
20
+ import { FadeIn, FadeOut } from "react-native-reanimated";
21
+ import { Button } from "../Button";
22
+ import { cn } from "../cn";
23
+ import { sizeStyle, textStyle, typeStyle } from "../controlStyles";
24
+ import { NativeOnlyAnimatedView } from "../primitives/native-only-animated-view";
25
+ import { useLargerText } from "../theme";
26
+ import { tokens } from "../tokens";
27
+ import { useReduceMotion } from "../useReduceMotion";
28
+ import { useScheme } from "../useScheme";
29
+ import { chatTextSize } from "./chat-text";
30
+
31
+ /** Within this many points of the end, the thread counts as at the bottom and keeps following new content. */
32
+ export const AT_BOTTOM_THRESHOLD = 70;
33
+
34
+ /** Whether a scroll position is within `AT_BOTTOM_THRESHOLD` of the end. Content shorter than the view is at the bottom. */
35
+ export function isScrolledToBottom({ contentOffset, contentSize, layoutMeasurement }: NativeScrollEvent) {
36
+ return contentSize.height - layoutMeasurement.height - contentOffset.y <= AT_BOTTOM_THRESHOLD;
37
+ }
38
+
39
+ type ConversationContextValue = {
40
+ isAtBottom: boolean;
41
+ scrollToBottom: () => void;
42
+ scrollRef: RefObject<ScrollView | null>;
43
+ onScroll: (event: NativeScrollEvent) => void;
44
+ onUserScroll: () => void;
45
+ followContent: () => void;
46
+ };
47
+
48
+ const ConversationContext = createContext<ConversationContextValue | null>(null);
49
+
50
+ /** `isAtBottom` and `scrollToBottom` of the enclosing `Conversation`, for a custom scroll button. */
51
+ export function useConversation() {
52
+ const context = useContext(ConversationContext);
53
+ if (!context) throw new Error("Conversation parts must be used within Conversation");
54
+ return context;
55
+ }
56
+
57
+ export type ConversationProps = ViewProps & { className?: string };
58
+
59
+ /**
60
+ * The thread frame (web `relative flex-1`). It sticks to the bottom: while the
61
+ * reader is at the end, new and growing messages stay in view; once they
62
+ * scroll up, it stops following and `ConversationScrollButton` shows.
63
+ */
64
+ export function Conversation({ className, ...props }: ConversationProps) {
65
+ const reduceMotion = useReduceMotion();
66
+ const scrollRef = useRef<ScrollView | null>(null);
67
+ const atBottom = useRef(true);
68
+ // Set while our own animated scroll heads down, so its first frames (not
69
+ // yet at the bottom) do not count as the reader leaving.
70
+ const autoScrolling = useRef(false);
71
+ const [isAtBottom, setIsAtBottom] = useState(true);
72
+
73
+ const update = (next: boolean) => {
74
+ atBottom.current = next;
75
+ setIsAtBottom(next);
76
+ };
77
+
78
+ const value: ConversationContextValue = {
79
+ isAtBottom,
80
+ scrollRef,
81
+ scrollToBottom: () => {
82
+ autoScrolling.current = true;
83
+ update(true);
84
+ scrollRef.current?.scrollToEnd({ animated: !reduceMotion });
85
+ },
86
+ onScroll: (event) => {
87
+ const bottom = isScrolledToBottom(event);
88
+ if (bottom) autoScrolling.current = false;
89
+ else if (autoScrolling.current) return;
90
+ update(bottom);
91
+ },
92
+ onUserScroll: () => {
93
+ autoScrolling.current = false;
94
+ },
95
+ // A jump, not an animation: streamed text grows the content many times a second.
96
+ followContent: () => {
97
+ if (atBottom.current) scrollRef.current?.scrollToEnd({ animated: false });
98
+ },
99
+ };
100
+
101
+ return (
102
+ <ConversationContext.Provider value={value}>
103
+ <View className={cn("relative flex-1", className)} {...props} />
104
+ </ConversationContext.Provider>
105
+ );
106
+ }
107
+
108
+ export type ConversationContentProps = ScrollViewProps & { className?: string };
109
+
110
+ /**
111
+ * The scrolling message column: the web `flex flex-col gap-4 px-3 py-3`. A
112
+ * `ScrollView`; for very long threads an app can drive its own `FlatList`
113
+ * with `useConversation`.
114
+ */
115
+ export function ConversationContent({
116
+ className,
117
+ contentContainerClassName,
118
+ onScroll,
119
+ onScrollBeginDrag,
120
+ onContentSizeChange,
121
+ onLayout,
122
+ ...props
123
+ }: ConversationContentProps) {
124
+ const conversation = useConversation();
125
+ return (
126
+ <ScrollView
127
+ ref={conversation.scrollRef}
128
+ className={cn("flex-1", className)}
129
+ contentContainerClassName={cn("gap-4 px-3 py-3", contentContainerClassName)}
130
+ keyboardShouldPersistTaps="handled"
131
+ keyboardDismissMode={Platform.OS === "ios" ? "interactive" : "on-drag"}
132
+ scrollEventThrottle={16}
133
+ {...props}
134
+ onScroll={(event) => {
135
+ conversation.onScroll(event.nativeEvent);
136
+ onScroll?.(event);
137
+ }}
138
+ onScrollBeginDrag={(event) => {
139
+ conversation.onUserScroll();
140
+ onScrollBeginDrag?.(event);
141
+ }}
142
+ onContentSizeChange={(width, height) => {
143
+ conversation.followContent();
144
+ onContentSizeChange?.(width, height);
145
+ }}
146
+ // The keyboard or composer resizing the view keeps the end in view too.
147
+ onLayout={(event) => {
148
+ conversation.followContent();
149
+ onLayout?.(event);
150
+ }}
151
+ />
152
+ );
153
+ }
154
+
155
+ export type ConversationEmptyStateProps = ViewProps & {
156
+ title?: string;
157
+ description?: string;
158
+ icon?: ComponentType<IconProps>;
159
+ className?: string;
160
+ children?: ReactNode;
161
+ };
162
+
163
+ /** Centred placeholder for a thread with no messages: the web title (medium) and muted description. */
164
+ export function ConversationEmptyState({
165
+ title = "No messages yet",
166
+ description = "Start a conversation to see messages here",
167
+ icon: Icon,
168
+ className,
169
+ children,
170
+ ...props
171
+ }: ConversationEmptyStateProps) {
172
+ const scheme = useScheme();
173
+ const largerText = useLargerText();
174
+ const size = chatTextSize("small", largerText);
175
+ return (
176
+ <View className={cn("flex-1 items-center justify-center gap-3 p-8", className)} {...props}>
177
+ {children ?? (
178
+ <>
179
+ {Icon ? <Icon size={largerText ? 28 : 24} color={tokens.color.muted[scheme]} /> : null}
180
+ <View className="items-center gap-1">
181
+ <Text
182
+ accessibilityRole="header"
183
+ className={cn(size, "text-center font-instrument-sans-medium")}
184
+ style={[sizeStyle(size), textStyle("foreground", scheme), typeStyle("medium")]}
185
+ >
186
+ {title}
187
+ </Text>
188
+ {description ? (
189
+ <Text
190
+ className={cn(size, "text-center font-instrument-sans")}
191
+ style={[sizeStyle(size), textStyle("muted", scheme)]}
192
+ >
193
+ {description}
194
+ </Text>
195
+ ) : null}
196
+ </View>
197
+ </>
198
+ )}
199
+ </View>
200
+ );
201
+ }
202
+
203
+ export type ConversationScrollButtonProps = {
204
+ /** The accessibility label. Default "Scroll to bottom". */
205
+ label?: string;
206
+ /** Replaces the down arrow. */
207
+ icon?: ComponentType<IconProps>;
208
+ testID?: string;
209
+ };
210
+
211
+ /**
212
+ * Round outline arrow, centred 16 above the bottom of the `Conversation`,
213
+ * shown while the reader is away from the end (the web `absolute bottom-4`).
214
+ * The web `Button` `icon` size (40, slop to 48). It has an opaque surface
215
+ * fill and no shadow, so it reads over the messages under it.
216
+ */
217
+ export function ConversationScrollButton({
218
+ label = "Scroll to bottom",
219
+ icon: Icon = IconArrowDown,
220
+ testID,
221
+ }: ConversationScrollButtonProps) {
222
+ const { isAtBottom, scrollToBottom } = useConversation();
223
+ const scheme = useScheme();
224
+ const largerText = useLargerText();
225
+ if (isAtBottom) return null;
226
+ return (
227
+ <NativeOnlyAnimatedView
228
+ entering={FadeIn.duration(150)}
229
+ exiting={FadeOut.duration(150)}
230
+ pointerEvents="box-none"
231
+ style={{ position: "absolute", left: 0, right: 0, bottom: 16, alignItems: "center" }}
232
+ >
233
+ <Button
234
+ variant="outline"
235
+ size="icon"
236
+ accessibilityLabel={label}
237
+ onPress={scrollToBottom}
238
+ testID={testID}
239
+ style={{ borderRadius: tokens.radius.pill, backgroundColor: tokens.color.surface[scheme] }}
240
+ >
241
+ <Icon size={largerText ? 20 : 16} color={tokens.color.foreground[scheme]} />
242
+ </Button>
243
+ </NativeOnlyAnimatedView>
244
+ );
245
+ }
@@ -0,0 +1,66 @@
1
+ import { Pressable, Text, View } from "react-native";
2
+ import { IconFile } from "@tabler/icons-react-native";
3
+ import { cn } from "../cn";
4
+ import { MIN_TOUCH_TARGET, sizeStyle, textStyle, touchSlop } from "../controlStyles";
5
+ import { palette } from "../palette";
6
+ import { useLargerText } from "../theme";
7
+ import { tokens } from "../tokens";
8
+ import { useScheme } from "../useScheme";
9
+ import { MONO_FONT, chatTextSize } from "./chat-text";
10
+
11
+ /** A `text-xs` line with the web `py-0.5` and border: 22 high. The slop brings it to 44. */
12
+ const CHIP_HEIGHT = 22;
13
+
14
+ function FileChipLabel({ name }: { name: string }) {
15
+ const scheme = useScheme();
16
+ const largerText = useLargerText();
17
+ const size = chatTextSize("caption", largerText);
18
+ return (
19
+ <View
20
+ className="max-w-full flex-row items-center gap-1.5 self-start border px-1.5 py-0.5"
21
+ style={{
22
+ borderRadius: tokens.radius.md,
23
+ borderColor: tokens.color.border[scheme],
24
+ backgroundColor: palette.default[scheme],
25
+ }}
26
+ >
27
+ <IconFile size={largerText ? 16 : 12} color={tokens.color.foreground[scheme]} />
28
+ <Text
29
+ numberOfLines={1}
30
+ ellipsizeMode="middle"
31
+ className={cn(size, "shrink")}
32
+ style={[sizeStyle(size), textStyle("foreground", scheme), { fontFamily: MONO_FONT }]}
33
+ >
34
+ {name}
35
+ </Text>
36
+ </View>
37
+ );
38
+ }
39
+
40
+ export type FileChipProps = {
41
+ name: string;
42
+ path?: string;
43
+ onOpenFile?: (path: string) => void;
44
+ };
45
+
46
+ /**
47
+ * A file name in a trace line or step body: the web `rounded-md border
48
+ * bg-secondary px-1.5 py-0.5 font-mono text-xs` chip. It is a button that
49
+ * opens the file when both `path` and `onOpenFile` are given, and plain text
50
+ * otherwise. Long names are cut in the middle, so the extension shows.
51
+ */
52
+ export function FileChip({ name, path, onOpenFile }: FileChipProps) {
53
+ if (!path || !onOpenFile) return <FileChipLabel name={name} />;
54
+ return (
55
+ <Pressable
56
+ accessibilityRole="button"
57
+ accessibilityLabel={name}
58
+ accessibilityHint={path}
59
+ hitSlop={touchSlop(MIN_TOUCH_TARGET, CHIP_HEIGHT)}
60
+ onPress={() => onOpenFile(path)}
61
+ className="min-w-0 max-w-full active:opacity-70"
62
+ >
63
+ <FileChipLabel name={name} />
64
+ </Pressable>
65
+ );
66
+ }
@@ -0,0 +1,51 @@
1
+ import type { ComponentType } from "react";
2
+ import type { IconProps } from "@tabler/icons-react-native";
3
+ import { Button, type ButtonProps } from "../Button";
4
+ import { cn } from "../cn";
5
+ import { MIN_TOUCH_TARGET, buttonColors, touchSlop } from "../controlStyles";
6
+ import { useLargerText } from "../theme";
7
+ import { tokens } from "../tokens";
8
+ import { useScheme } from "../useScheme";
9
+
10
+ /** The web `icon-sm` button: 32 square with a 16 icon. */
11
+ export const CHAT_ICON_BUTTON_SIZE = 32;
12
+
13
+ export type ChatIconButtonProps = Omit<ButtonProps, "children" | "size" | "icon"> & {
14
+ icon: ComponentType<IconProps>;
15
+ /** Names the button for VoiceOver and TalkBack; it has no text. */
16
+ label: string;
17
+ /** Default: the variant's label colour. */
18
+ iconColor?: string;
19
+ };
20
+
21
+ /**
22
+ * A round `Button` at the web `icon-sm` size (32), with the slop to 44. With
23
+ * larger text it is painted 44 with a 20 icon. The message
24
+ * actions, the composer tools and its send button, and the code copy button.
25
+ */
26
+ export function ChatIconButton({
27
+ icon: Icon,
28
+ label,
29
+ iconColor,
30
+ variant = "ghost",
31
+ className,
32
+ ...props
33
+ }: ChatIconButtonProps) {
34
+ const scheme = useScheme();
35
+ const largerText = useLargerText();
36
+ const size = largerText ? MIN_TOUCH_TARGET : CHAT_ICON_BUTTON_SIZE;
37
+ const color = iconColor ?? buttonColors(variant, scheme)?.color ?? tokens.color.foreground[scheme];
38
+ return (
39
+ <Button
40
+ variant={variant}
41
+ size="icon"
42
+ accessibilityLabel={label}
43
+ hitSlop={touchSlop(size)}
44
+ className={cn(largerText ? "h-11 w-11" : "h-8 w-8", className)}
45
+ style={{ borderRadius: size / 2 }}
46
+ {...props}
47
+ >
48
+ <Icon size={largerText ? 20 : 16} color={color} />
49
+ </Button>
50
+ );
51
+ }
@@ -0,0 +1,123 @@
1
+ // AI chat parts, ported from the `@vvedantb/ui` AI elements for touch. On the
2
+ // `@vvedantb/native-ui/ai` entry only, so apps without a chat do not bundle
3
+ // them. Generic: no model names and no app logic.
4
+ export { useStreamingDisclosure } from "./use-streaming-disclosure";
5
+ export { chatTextSize, formatElapsed, MONO_FONT } from "./chat-text";
6
+ export type { ChatTextKind } from "./chat-text";
7
+ export { Shimmer, SHIMMER_LOW_OPACITY } from "./shimmer";
8
+ export type { ShimmerProps } from "./shimmer";
9
+ export {
10
+ Collapsible,
11
+ CollapsibleTrigger,
12
+ CollapsibleContent,
13
+ CollapsibleChevron,
14
+ useCollapsible,
15
+ } from "./collapsible";
16
+ export type { CollapsibleProps, CollapsibleTriggerProps } from "./collapsible";
17
+ export {
18
+ Conversation,
19
+ ConversationContent,
20
+ ConversationEmptyState,
21
+ ConversationScrollButton,
22
+ useConversation,
23
+ isScrolledToBottom,
24
+ AT_BOTTOM_THRESHOLD,
25
+ } from "./conversation";
26
+ export type {
27
+ ConversationProps,
28
+ ConversationContentProps,
29
+ ConversationEmptyStateProps,
30
+ ConversationScrollButtonProps,
31
+ } from "./conversation";
32
+ export {
33
+ Message,
34
+ MessageContent,
35
+ MessageActionsBar,
36
+ MessageAction,
37
+ MESSAGE_ACTIONS,
38
+ userBubbleStyle,
39
+ } from "./message";
40
+ export type {
41
+ MessageRole,
42
+ MessageProps,
43
+ MessageContentProps,
44
+ MessageActionsBarProps,
45
+ MessageActionKind,
46
+ MessageActionProps,
47
+ } from "./message";
48
+ export {
49
+ Reasoning,
50
+ ReasoningTrigger,
51
+ ReasoningContent,
52
+ useReasoning,
53
+ reasoningMessage,
54
+ } from "./reasoning";
55
+ export type { ReasoningProps, ReasoningTriggerProps, ReasoningContentProps } from "./reasoning";
56
+ export {
57
+ AgentTrace,
58
+ AgentTraceLine,
59
+ AgentTraceIcon,
60
+ AgentTraceTitle,
61
+ AgentTraceDescription,
62
+ AgentTraceCollapsible,
63
+ AgentTraceCollapsibleTrigger,
64
+ AgentTraceCollapsibleContent,
65
+ traceLineColor,
66
+ } from "./trace";
67
+ export type {
68
+ AgentTraceStatus,
69
+ AgentTraceLineProps,
70
+ AgentTraceIconProps,
71
+ AgentTraceTitleProps,
72
+ AgentTraceCollapsibleTriggerProps,
73
+ } from "./trace";
74
+ export {
75
+ ChainOfThought,
76
+ ChainOfThoughtHeader,
77
+ ChainOfThoughtContent,
78
+ ChainOfThoughtStep,
79
+ ChainOfThoughtSearchResults,
80
+ ChainOfThoughtSearchResult,
81
+ } from "./chain-of-thought";
82
+ export type {
83
+ ChainOfThoughtHeaderProps,
84
+ ChainOfThoughtStepProps,
85
+ ChainOfThoughtStepStatus,
86
+ } from "./chain-of-thought";
87
+ export { ActivityStep } from "./activity-step";
88
+ export type { ActivityStepProps, ActivityStepStatus } from "./activity-step";
89
+ export {
90
+ PromptInput,
91
+ PromptInputButton,
92
+ PromptInputSubmit,
93
+ isGenerating,
94
+ } from "./prompt-input";
95
+ export type {
96
+ ChatStatus,
97
+ PromptInputProps,
98
+ PromptInputButtonProps,
99
+ PromptInputSubmitProps,
100
+ } from "./prompt-input";
101
+ export { Suggestions, Suggestion } from "./suggestion";
102
+ export type { SuggestionsProps, SuggestionProps } from "./suggestion";
103
+ export { Sources, SourcesTrigger, SourcesContent, Source, sourcesLabel } from "./sources";
104
+ export type { SourcesTriggerProps, SourceProps } from "./sources";
105
+ export { FileChip } from "./file-chip";
106
+ export type { FileChipProps } from "./file-chip";
107
+ export {
108
+ Attachments,
109
+ Attachment,
110
+ getMediaCategory,
111
+ getAttachmentLabel,
112
+ } from "./attachments";
113
+ export type {
114
+ AttachmentData,
115
+ AttachmentMediaCategory,
116
+ AttachmentVariant,
117
+ AttachmentsProps,
118
+ AttachmentProps,
119
+ } from "./attachments";
120
+ export { CodeBlock, CodeBlockCopyButton } from "./code-block";
121
+ export type { CodeBlockProps, CodeBlockCopyButtonProps } from "./code-block";
122
+ export { ResponseText, ChatTextColorContext } from "./response-text";
123
+ export type { ResponseTextProps } from "./response-text";