@cryterion/expo-chat-ui 1.0.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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # @tovia/chat-ui
2
+
3
+ A reusable chat UI component for Expo + React Native applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @tovia/chat-ui
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Basic Usage
14
+
15
+ ```tsx
16
+ import { Chat, ChatMessage } from '@tovia/chat-ui';
17
+
18
+ function ChatScreen() {
19
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
20
+ const [isLoading, setIsLoading] = useState(false);
21
+
22
+ const handleSend = async (text: string) => {
23
+ // Add user message
24
+ const userMessage: ChatMessage = {
25
+ id: `user-${Date.now()}`,
26
+ role: 'user',
27
+ content: text,
28
+ timestamp: Date.now(),
29
+ };
30
+ setMessages(prev => [...prev, userMessage]);
31
+
32
+ // Your AI/backend logic here...
33
+ };
34
+
35
+ return (
36
+ <Chat
37
+ messages={messages}
38
+ onSend={handleSend}
39
+ isLoading={isLoading}
40
+ />
41
+ );
42
+ }
43
+ ```
44
+
45
+ ### With Custom Theme
46
+
47
+ ```tsx
48
+ import { Chat, ChatTheme } from '@tovia/chat-ui';
49
+
50
+ const customTheme: Partial<ChatTheme> = {
51
+ colors: {
52
+ primary: '#007AFF',
53
+ userBubble: '#007AFF',
54
+ assistantBubble: '#E5E5EA',
55
+ userText: '#FFFFFF',
56
+ assistantText: '#000000',
57
+ },
58
+ };
59
+
60
+ function ChatScreen() {
61
+ return (
62
+ <Chat
63
+ messages={messages}
64
+ onSend={handleSend}
65
+ theme={customTheme}
66
+ />
67
+ );
68
+ }
69
+ ```
70
+
71
+ ### Using Individual Components
72
+
73
+ You can also use the individual building blocks:
74
+
75
+ ```tsx
76
+ import { MessageList, ChatInput, MessageBubble } from '@tovia/chat-ui';
77
+
78
+ function CustomChat() {
79
+ return (
80
+ <View style={{ flex: 1 }}>
81
+ <MessageList messages={messages} isLoading={isLoading} />
82
+ <ChatInput onSend={handleSend} />
83
+ </View>
84
+ );
85
+ }
86
+ ```
87
+
88
+ ## API
89
+
90
+ ### `<Chat />` Props
91
+
92
+ | Prop | Type | Required | Description |
93
+ |------|------|----------|-------------|
94
+ | `messages` | `ChatMessage[]` | Yes | Array of messages to display |
95
+ | `onSend` | `(text: string) => void \| Promise<void>` | Yes | Callback when user sends a message |
96
+ | `isLoading` | `boolean` | No | Shows typing indicator when true |
97
+ | `disabled` | `boolean` | No | Disables input when true |
98
+ | `theme` | `Partial<ChatTheme>` | No | Custom theme colors |
99
+ | `renderEmptyState` | `ReactNode \| () => ReactNode` | No | Custom empty state |
100
+ | `onCopyMessage` | `(message: ChatMessage) => void` | No | Custom copy handler |
101
+
102
+ ### `ChatMessage` Type
103
+
104
+ ```ts
105
+ interface ChatMessage {
106
+ id: string;
107
+ role: 'user' | 'assistant' | 'system';
108
+ content: string;
109
+ timestamp: number | Date;
110
+ meta?: Record<string, unknown>;
111
+ }
112
+ ```
113
+
114
+ ### `ChatTheme` Type
115
+
116
+ ```ts
117
+ interface ChatTheme {
118
+ mode: 'light' | 'dark';
119
+ colors: {
120
+ primary: string;
121
+ background: string;
122
+ inputBackground: string;
123
+ userBubble: string;
124
+ assistantBubble: string;
125
+ userText: string;
126
+ assistantText: string;
127
+ text: string;
128
+ textMuted: string;
129
+ placeholder: string;
130
+ border: string;
131
+ sendButtonDisabled: string;
132
+ };
133
+ }
134
+ ```
135
+
136
+ ## License
137
+
138
+ MIT
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@cryterion/expo-chat-ui",
3
+ "version": "1.0.0",
4
+ "description": "A reusable chat UI component for Expo + React Native applications",
5
+ "author": "cryterion",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mphassani/expo-chat-ui.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/mphassani/expo-chat-ui/issues"
13
+ },
14
+ "homepage": "https://github.com/mphassani/expo-chat-ui#readme",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "main": "src/index.ts",
19
+ "module": "src/index.ts",
20
+ "types": "src/index.ts",
21
+ "react-native": "src/index.ts",
22
+ "source": "src/index.ts",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./src/index.ts",
26
+ "require": "./src/index.ts",
27
+ "types": "./src/index.ts"
28
+ }
29
+ },
30
+ "files": [
31
+ "src"
32
+ ],
33
+ "sideEffects": false,
34
+ "scripts": {
35
+ "typecheck": "tsc --noEmit",
36
+ "prepublishOnly": "npm run typecheck"
37
+ },
38
+ "keywords": [
39
+ "react-native",
40
+ "expo",
41
+ "chat",
42
+ "ui",
43
+ "component",
44
+ "messaging",
45
+ "conversation",
46
+ "chatbot"
47
+ ],
48
+ "peerDependencies": {
49
+ "react": ">=18.0.0",
50
+ "react-native": ">=0.72.0",
51
+ "expo": ">=50.0.0",
52
+ "@expo/vector-icons": ">=14.0.0",
53
+ "expo-clipboard": ">=7.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@expo/vector-icons": "^14.1.0",
57
+ "@types/react": "^18.2.79",
58
+ "expo": "^50.0.0",
59
+ "expo-clipboard": "^7.0.0",
60
+ "react": "18.2.0",
61
+ "react-native": "0.73.6",
62
+ "typescript": "~5.9.3"
63
+ }
64
+ }
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { mergeTheme } from '../theme/defaultTheme';
4
+ import { ChatProps } from '../types';
5
+ import { ChatInput } from './ChatInput';
6
+ import { MessageList } from './MessageList';
7
+
8
+ /**
9
+ * Main Chat component that combines MessageList and ChatInput
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <Chat
14
+ * messages={messages}
15
+ * onSend={handleSend}
16
+ * isLoading={isLoading}
17
+ * theme={{ mode: 'dark' }}
18
+ * />
19
+ * ```
20
+ */
21
+ export function Chat({
22
+ messages,
23
+ onSend,
24
+ isLoading,
25
+ disabled,
26
+ theme: themeProp,
27
+ renderEmptyState,
28
+ onCopyMessage,
29
+ placeholder,
30
+ emptyStateTitle,
31
+ emptyStateSubtitle,
32
+ }: ChatProps) {
33
+ const theme = mergeTheme(themeProp);
34
+
35
+ return (
36
+ <View style={[styles.container, { backgroundColor: theme.colors.background }]}>
37
+ <MessageList
38
+ messages={messages}
39
+ isLoading={isLoading}
40
+ theme={theme}
41
+ renderEmptyState={renderEmptyState}
42
+ onCopyMessage={onCopyMessage}
43
+ emptyStateTitle={emptyStateTitle}
44
+ emptyStateSubtitle={emptyStateSubtitle}
45
+ />
46
+ <ChatInput
47
+ onSend={onSend}
48
+ disabled={disabled || isLoading}
49
+ theme={theme}
50
+ placeholder={placeholder}
51
+ />
52
+ </View>
53
+ );
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ container: {
58
+ flex: 1,
59
+ },
60
+ });
@@ -0,0 +1,118 @@
1
+ import { Ionicons } from '@expo/vector-icons';
2
+ import React, { useState } from 'react';
3
+ import {
4
+ KeyboardAvoidingView,
5
+ Platform,
6
+ StyleSheet,
7
+ TextInput,
8
+ TouchableOpacity,
9
+ View,
10
+ } from 'react-native';
11
+ import { ChatInputProps } from '../types';
12
+
13
+ export function ChatInput({
14
+ onSend,
15
+ disabled,
16
+ theme,
17
+ placeholder = 'Type a message...',
18
+ }: ChatInputProps) {
19
+ const [text, setText] = useState('');
20
+ const { colors } = theme;
21
+
22
+ const handleSend = () => {
23
+ const trimmedText = text.trim();
24
+ if (trimmedText && !disabled) {
25
+ onSend(trimmedText);
26
+ setText('');
27
+ }
28
+ };
29
+
30
+ const canSend = text.trim().length > 0 && !disabled;
31
+
32
+ return (
33
+ <KeyboardAvoidingView
34
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
35
+ keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
36
+ >
37
+ <View
38
+ style={[
39
+ styles.container,
40
+ {
41
+ backgroundColor: colors.background,
42
+ borderTopColor: colors.border,
43
+ },
44
+ ]}
45
+ >
46
+ <View
47
+ style={[
48
+ styles.inputContainer,
49
+ { backgroundColor: colors.inputBackground },
50
+ ]}
51
+ >
52
+ <TextInput
53
+ style={[styles.input, { color: colors.text }]}
54
+ placeholder={placeholder}
55
+ placeholderTextColor={colors.placeholder}
56
+ value={text}
57
+ onChangeText={setText}
58
+ multiline
59
+ maxLength={2000}
60
+ editable={!disabled}
61
+ onSubmitEditing={handleSend}
62
+ blurOnSubmit={false}
63
+ />
64
+ <TouchableOpacity
65
+ style={[
66
+ styles.sendButton,
67
+ {
68
+ backgroundColor: canSend
69
+ ? colors.primary
70
+ : colors.sendButtonDisabled,
71
+ },
72
+ ]}
73
+ onPress={handleSend}
74
+ disabled={!canSend}
75
+ activeOpacity={0.7}
76
+ >
77
+ <Ionicons name="send" size={18} color="#FFFFFF" />
78
+ </TouchableOpacity>
79
+ </View>
80
+ </View>
81
+ </KeyboardAvoidingView>
82
+ );
83
+ }
84
+
85
+ const styles = StyleSheet.create({
86
+ container: {
87
+ paddingHorizontal: 16,
88
+ paddingVertical: 12,
89
+ borderTopWidth: StyleSheet.hairlineWidth,
90
+ },
91
+ inputContainer: {
92
+ flexDirection: 'row',
93
+ alignItems: 'flex-end',
94
+ borderRadius: 24,
95
+ paddingLeft: 16,
96
+ paddingRight: 6,
97
+ paddingVertical: 6,
98
+ shadowColor: '#000',
99
+ shadowOffset: { width: 0, height: 1 },
100
+ shadowOpacity: 0.1,
101
+ shadowRadius: 2,
102
+ elevation: 2,
103
+ },
104
+ input: {
105
+ flex: 1,
106
+ fontSize: 16,
107
+ maxHeight: 100,
108
+ paddingVertical: 8,
109
+ paddingRight: 8,
110
+ },
111
+ sendButton: {
112
+ width: 36,
113
+ height: 36,
114
+ borderRadius: 18,
115
+ alignItems: 'center',
116
+ justifyContent: 'center',
117
+ },
118
+ });
@@ -0,0 +1,113 @@
1
+ import { Ionicons } from '@expo/vector-icons';
2
+ import * as Clipboard from 'expo-clipboard';
3
+ import React from 'react';
4
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
5
+ import { ChatMessage, MessageBubbleProps } from '../types';
6
+
7
+ /**
8
+ * Format timestamp to time string (e.g., "2:30 PM")
9
+ */
10
+ function formatTime(timestamp: number | Date): string {
11
+ const date = typeof timestamp === 'number' ? new Date(timestamp) : timestamp;
12
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
13
+ }
14
+
15
+ /**
16
+ * Default copy to clipboard implementation
17
+ */
18
+ async function defaultCopyToClipboard(message: ChatMessage): Promise<void> {
19
+ await Clipboard.setStringAsync(message.content);
20
+ }
21
+
22
+ export function MessageBubble({ message, theme, onCopy }: MessageBubbleProps) {
23
+ const isUser = message.role === 'user';
24
+ const { colors } = theme;
25
+
26
+ const backgroundColor = isUser ? colors.userBubble : colors.assistantBubble;
27
+ const textColor = isUser ? colors.userText : colors.assistantText;
28
+ const mutedColor = isUser ? colors.userBubbleMuted : colors.assistantBubbleMuted;
29
+
30
+ const handleCopy = () => {
31
+ if (onCopy) {
32
+ onCopy(message);
33
+ } else {
34
+ defaultCopyToClipboard(message);
35
+ }
36
+ };
37
+
38
+ return (
39
+ <View
40
+ style={[
41
+ styles.container,
42
+ isUser ? styles.userContainer : styles.assistantContainer,
43
+ ]}
44
+ >
45
+ <View
46
+ style={[
47
+ styles.bubble,
48
+ { backgroundColor },
49
+ isUser ? styles.userBubble : styles.assistantBubble,
50
+ ]}
51
+ >
52
+ <Text style={[styles.messageText, { color: textColor }]}>
53
+ {message.content}
54
+ </Text>
55
+ <View style={styles.footer}>
56
+ <Text style={[styles.timestamp, { color: mutedColor }]}>
57
+ {formatTime(message.timestamp)}
58
+ </Text>
59
+ <TouchableOpacity
60
+ style={styles.copyButton}
61
+ onPress={handleCopy}
62
+ activeOpacity={0.7}
63
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
64
+ >
65
+ <Ionicons name="copy-outline" size={14} color={mutedColor} />
66
+ </TouchableOpacity>
67
+ </View>
68
+ </View>
69
+ </View>
70
+ );
71
+ }
72
+
73
+ const styles = StyleSheet.create({
74
+ container: {
75
+ paddingHorizontal: 16,
76
+ paddingVertical: 4,
77
+ },
78
+ userContainer: {
79
+ alignItems: 'flex-end',
80
+ },
81
+ assistantContainer: {
82
+ alignItems: 'flex-start',
83
+ },
84
+ bubble: {
85
+ maxWidth: '80%',
86
+ paddingHorizontal: 16,
87
+ paddingVertical: 10,
88
+ borderRadius: 20,
89
+ },
90
+ userBubble: {
91
+ borderBottomRightRadius: 4,
92
+ },
93
+ assistantBubble: {
94
+ borderBottomLeftRadius: 4,
95
+ },
96
+ messageText: {
97
+ fontSize: 16,
98
+ lineHeight: 22,
99
+ },
100
+ footer: {
101
+ flexDirection: 'row',
102
+ alignItems: 'center',
103
+ justifyContent: 'flex-end',
104
+ marginTop: 4,
105
+ gap: 8,
106
+ },
107
+ timestamp: {
108
+ fontSize: 11,
109
+ },
110
+ copyButton: {
111
+ padding: 2,
112
+ },
113
+ });
@@ -0,0 +1,180 @@
1
+ import { Ionicons } from '@expo/vector-icons';
2
+ import React, { useEffect, useRef } from 'react';
3
+ import { FlatList, StyleSheet, Text, View } from 'react-native';
4
+ import { ChatMessage, MessageListProps } from '../types';
5
+ import { MessageBubble } from './MessageBubble';
6
+
7
+ /**
8
+ * Default empty state component
9
+ */
10
+ function DefaultEmptyState({
11
+ title,
12
+ subtitle,
13
+ iconColor,
14
+ textColor,
15
+ textMutedColor,
16
+ }: {
17
+ title: string;
18
+ subtitle: string;
19
+ iconColor: string;
20
+ textColor: string;
21
+ textMutedColor: string;
22
+ }) {
23
+ return (
24
+ <View style={styles.emptyContainer}>
25
+ <Ionicons name="chatbubbles-outline" size={64} color={iconColor} />
26
+ <Text style={[styles.emptyTitle, { color: textColor }]}>{title}</Text>
27
+ <Text style={[styles.emptySubtitle, { color: textMutedColor }]}>
28
+ {subtitle}
29
+ </Text>
30
+ </View>
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Typing indicator component
36
+ */
37
+ function TypingIndicator({ bubbleColor, dotColor }: { bubbleColor: string; dotColor: string }) {
38
+ return (
39
+ <View style={styles.loadingContainer}>
40
+ <View style={[styles.typingIndicator, { backgroundColor: bubbleColor }]}>
41
+ <View style={[styles.dot, styles.dot1, { backgroundColor: dotColor }]} />
42
+ <View style={[styles.dot, styles.dot2, { backgroundColor: dotColor }]} />
43
+ <View style={[styles.dot, styles.dot3, { backgroundColor: dotColor }]} />
44
+ </View>
45
+ </View>
46
+ );
47
+ }
48
+
49
+ export function MessageList({
50
+ messages,
51
+ isLoading,
52
+ theme,
53
+ renderEmptyState,
54
+ onCopyMessage,
55
+ emptyStateTitle = 'Start a conversation',
56
+ emptyStateSubtitle = 'Type a message below to get started',
57
+ }: MessageListProps) {
58
+ const flatListRef = useRef<FlatList<ChatMessage>>(null);
59
+ const { colors } = theme;
60
+
61
+ useEffect(() => {
62
+ if (messages.length > 0 && flatListRef.current) {
63
+ setTimeout(() => {
64
+ flatListRef.current?.scrollToEnd({ animated: true });
65
+ }, 100);
66
+ }
67
+ }, [messages.length]);
68
+
69
+ const renderItem = ({ item }: { item: ChatMessage }) => (
70
+ <MessageBubble message={item} theme={theme} onCopy={onCopyMessage} />
71
+ );
72
+
73
+ const renderEmptyStateComponent = () => {
74
+ if (renderEmptyState) {
75
+ if (typeof renderEmptyState === 'function') {
76
+ return renderEmptyState();
77
+ }
78
+ return renderEmptyState;
79
+ }
80
+
81
+ return (
82
+ <DefaultEmptyState
83
+ title={emptyStateTitle}
84
+ subtitle={emptyStateSubtitle}
85
+ iconColor={colors.text}
86
+ textColor={colors.text}
87
+ textMutedColor={colors.textMuted}
88
+ />
89
+ );
90
+ };
91
+
92
+ const renderLoadingIndicator = () => {
93
+ if (!isLoading) return null;
94
+ return (
95
+ <TypingIndicator
96
+ bubbleColor={colors.loadingBubble}
97
+ dotColor={colors.loadingDots}
98
+ />
99
+ );
100
+ };
101
+
102
+ // Using key to force remount when transitioning between empty/non-empty states
103
+ // This fixes the scroll size not resetting when clearing messages
104
+ const listKey = messages.length === 0 ? 'empty' : 'populated';
105
+
106
+ return (
107
+ <View style={[styles.container, { backgroundColor: colors.background }]}>
108
+ <FlatList
109
+ key={listKey}
110
+ ref={flatListRef}
111
+ data={messages}
112
+ renderItem={renderItem}
113
+ keyExtractor={(item: ChatMessage) => item.id}
114
+ contentContainerStyle={[
115
+ styles.listContent,
116
+ messages.length === 0 && styles.emptyListContent,
117
+ ]}
118
+ ListEmptyComponent={renderEmptyStateComponent}
119
+ ListFooterComponent={renderLoadingIndicator}
120
+ showsVerticalScrollIndicator={false}
121
+ />
122
+ </View>
123
+ );
124
+ }
125
+
126
+ const styles = StyleSheet.create({
127
+ container: {
128
+ flex: 1,
129
+ },
130
+ listContent: {
131
+ paddingVertical: 16,
132
+ },
133
+ emptyListContent: {
134
+ flex: 1,
135
+ justifyContent: 'center',
136
+ },
137
+ emptyContainer: {
138
+ alignItems: 'center',
139
+ paddingHorizontal: 32,
140
+ },
141
+ emptyTitle: {
142
+ fontSize: 20,
143
+ fontWeight: '600',
144
+ marginTop: 16,
145
+ marginBottom: 8,
146
+ textAlign: 'center',
147
+ },
148
+ emptySubtitle: {
149
+ fontSize: 16,
150
+ textAlign: 'center',
151
+ },
152
+ loadingContainer: {
153
+ paddingHorizontal: 16,
154
+ paddingVertical: 8,
155
+ alignItems: 'flex-start',
156
+ },
157
+ typingIndicator: {
158
+ flexDirection: 'row',
159
+ alignItems: 'center',
160
+ paddingHorizontal: 16,
161
+ paddingVertical: 12,
162
+ borderRadius: 20,
163
+ borderBottomLeftRadius: 4,
164
+ },
165
+ dot: {
166
+ width: 8,
167
+ height: 8,
168
+ borderRadius: 4,
169
+ marginHorizontal: 2,
170
+ },
171
+ dot1: {
172
+ opacity: 0.4,
173
+ },
174
+ dot2: {
175
+ opacity: 0.6,
176
+ },
177
+ dot3: {
178
+ opacity: 0.8,
179
+ },
180
+ });
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ // Types
2
+ export type {
3
+ ChatMessage,
4
+ ChatTheme,
5
+ ChatThemeColors,
6
+ ChatProps,
7
+ MessageBubbleProps,
8
+ MessageListProps,
9
+ ChatInputProps,
10
+ } from './types';
11
+
12
+ // Components
13
+ export { Chat } from './components/Chat';
14
+ export { ChatInput } from './components/ChatInput';
15
+ export { MessageBubble } from './components/MessageBubble';
16
+ export { MessageList } from './components/MessageList';
17
+
18
+ // Theme utilities
19
+ export {
20
+ lightTheme,
21
+ darkTheme,
22
+ lightColors,
23
+ darkColors,
24
+ getDefaultTheme,
25
+ mergeTheme,
26
+ } from './theme/defaultTheme';
@@ -0,0 +1,87 @@
1
+ import { ChatTheme, ChatThemeColors } from '../types';
2
+
3
+ /**
4
+ * Default light theme colors
5
+ * Based on the original Tovia app theme
6
+ */
7
+ export const lightColors: ChatThemeColors = {
8
+ primary: '#FF8360',
9
+ background: '#EFF1ED',
10
+ inputBackground: '#FFFFFF',
11
+ userBubble: '#FF8360',
12
+ assistantBubble: '#E8E4D3',
13
+ userText: '#FFFFFF',
14
+ assistantText: '#2A2B2A',
15
+ text: '#2A2B2A',
16
+ textMuted: 'rgba(0, 0, 0, 0.5)',
17
+ placeholder: '#999999',
18
+ border: 'rgba(0, 0, 0, 0.1)',
19
+ sendButtonDisabled: '#CCCCCC',
20
+ userBubbleMuted: 'rgba(255, 255, 255, 0.7)',
21
+ assistantBubbleMuted: 'rgba(0, 0, 0, 0.4)',
22
+ loadingBubble: '#E8E4D3',
23
+ loadingDots: '#888888',
24
+ };
25
+
26
+ /**
27
+ * Default dark theme colors
28
+ */
29
+ export const darkColors: ChatThemeColors = {
30
+ primary: '#FF8360',
31
+ background: '#2A2B2A',
32
+ inputBackground: '#3A3B3A',
33
+ userBubble: '#FF8360',
34
+ assistantBubble: '#3A3B3A',
35
+ userText: '#FFFFFF',
36
+ assistantText: '#F8F4E3',
37
+ text: '#F8F4E3',
38
+ textMuted: 'rgba(255, 255, 255, 0.5)',
39
+ placeholder: '#666666',
40
+ border: 'rgba(255, 255, 255, 0.1)',
41
+ sendButtonDisabled: '#666666',
42
+ userBubbleMuted: 'rgba(255, 255, 255, 0.7)',
43
+ assistantBubbleMuted: 'rgba(255, 255, 255, 0.5)',
44
+ loadingBubble: '#3A3B3A',
45
+ loadingDots: '#888888',
46
+ };
47
+
48
+ /**
49
+ * Default light theme
50
+ */
51
+ export const lightTheme: ChatTheme = {
52
+ mode: 'light',
53
+ colors: lightColors,
54
+ };
55
+
56
+ /**
57
+ * Default dark theme
58
+ */
59
+ export const darkTheme: ChatTheme = {
60
+ mode: 'dark',
61
+ colors: darkColors,
62
+ };
63
+
64
+ /**
65
+ * Get the default theme based on mode
66
+ */
67
+ export function getDefaultTheme(mode: 'light' | 'dark' = 'light'): ChatTheme {
68
+ return mode === 'dark' ? darkTheme : lightTheme;
69
+ }
70
+
71
+ /**
72
+ * Merge a partial theme with the default theme
73
+ */
74
+ export function mergeTheme(
75
+ partial?: Partial<ChatTheme> & { colors?: Partial<ChatThemeColors> }
76
+ ): ChatTheme {
77
+ const mode = partial?.mode ?? 'light';
78
+ const defaultTheme = getDefaultTheme(mode);
79
+
80
+ return {
81
+ mode,
82
+ colors: {
83
+ ...defaultTheme.colors,
84
+ ...(partial?.colors ?? {}),
85
+ },
86
+ };
87
+ }
package/src/types.ts ADDED
@@ -0,0 +1,137 @@
1
+ import { ReactNode } from 'react';
2
+
3
+ /**
4
+ * Represents a single chat message
5
+ */
6
+ export interface ChatMessage {
7
+ /** Unique identifier for the message */
8
+ id: string;
9
+ /** Role of the message sender */
10
+ role: 'user' | 'assistant' | 'system';
11
+ /** Text content of the message */
12
+ content: string;
13
+ /** Timestamp of when the message was sent (ms since epoch or Date) */
14
+ timestamp: number | Date;
15
+ /** Optional metadata for extending message data */
16
+ meta?: Record<string, unknown>;
17
+ }
18
+
19
+ /**
20
+ * Theme colors for the chat UI
21
+ */
22
+ export interface ChatThemeColors {
23
+ /** Primary/accent color (used for send button, etc.) */
24
+ primary: string;
25
+ /** Main background color */
26
+ background: string;
27
+ /** Input field background */
28
+ inputBackground: string;
29
+ /** User message bubble background */
30
+ userBubble: string;
31
+ /** Assistant message bubble background */
32
+ assistantBubble: string;
33
+ /** Text color inside user bubbles */
34
+ userText: string;
35
+ /** Text color inside assistant bubbles */
36
+ assistantText: string;
37
+ /** Primary text color */
38
+ text: string;
39
+ /** Muted/secondary text color */
40
+ textMuted: string;
41
+ /** Placeholder text color */
42
+ placeholder: string;
43
+ /** Border color */
44
+ border: string;
45
+ /** Disabled send button color */
46
+ sendButtonDisabled: string;
47
+ /** Muted color for icons/timestamps in user bubbles */
48
+ userBubbleMuted: string;
49
+ /** Muted color for icons/timestamps in assistant bubbles */
50
+ assistantBubbleMuted: string;
51
+ /** Loading indicator bubble background */
52
+ loadingBubble: string;
53
+ /** Loading indicator dots color */
54
+ loadingDots: string;
55
+ }
56
+
57
+ /**
58
+ * Complete theme configuration
59
+ */
60
+ export interface ChatTheme {
61
+ /** Color mode */
62
+ mode: 'light' | 'dark';
63
+ /** Theme colors */
64
+ colors: ChatThemeColors;
65
+ }
66
+
67
+ /**
68
+ * Props for the main Chat component
69
+ */
70
+ export interface ChatProps {
71
+ /** Array of messages to display */
72
+ messages: ChatMessage[];
73
+ /** Callback when user sends a message */
74
+ onSend: (text: string) => void | Promise<void>;
75
+ /** Shows typing indicator when true */
76
+ isLoading?: boolean;
77
+ /** Disables input when true */
78
+ disabled?: boolean;
79
+ /** Custom theme (merged with defaults based on mode) */
80
+ theme?: Partial<ChatTheme> & { colors?: Partial<ChatThemeColors> };
81
+ /** Custom empty state renderer */
82
+ renderEmptyState?: ReactNode | (() => ReactNode);
83
+ /** Custom copy message handler (defaults to expo-clipboard) */
84
+ onCopyMessage?: (message: ChatMessage) => void;
85
+ /** Input placeholder text */
86
+ placeholder?: string;
87
+ /** Empty state title */
88
+ emptyStateTitle?: string;
89
+ /** Empty state subtitle */
90
+ emptyStateSubtitle?: string;
91
+ }
92
+
93
+ /**
94
+ * Props for MessageBubble component
95
+ */
96
+ export interface MessageBubbleProps {
97
+ /** The message to display */
98
+ message: ChatMessage;
99
+ /** Theme colors */
100
+ theme: ChatTheme;
101
+ /** Custom copy handler */
102
+ onCopy?: (message: ChatMessage) => void;
103
+ }
104
+
105
+ /**
106
+ * Props for MessageList component
107
+ */
108
+ export interface MessageListProps {
109
+ /** Array of messages to display */
110
+ messages: ChatMessage[];
111
+ /** Shows typing indicator when true */
112
+ isLoading?: boolean;
113
+ /** Theme configuration */
114
+ theme: ChatTheme;
115
+ /** Custom empty state renderer */
116
+ renderEmptyState?: ReactNode | (() => ReactNode);
117
+ /** Custom copy handler */
118
+ onCopyMessage?: (message: ChatMessage) => void;
119
+ /** Empty state title */
120
+ emptyStateTitle?: string;
121
+ /** Empty state subtitle */
122
+ emptyStateSubtitle?: string;
123
+ }
124
+
125
+ /**
126
+ * Props for ChatInput component
127
+ */
128
+ export interface ChatInputProps {
129
+ /** Callback when user sends a message */
130
+ onSend: (text: string) => void | Promise<void>;
131
+ /** Disables input when true */
132
+ disabled?: boolean;
133
+ /** Theme configuration */
134
+ theme: ChatTheme;
135
+ /** Placeholder text */
136
+ placeholder?: string;
137
+ }