ajaxter-chat 3.0.4 → 3.0.6

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.
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState, useEffect, useCallback, useRef } from 'react';
4
- import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat } from '../types';
4
+ import { ChatWidgetProps, BottomTab, Screen, UserListContext, ChatUser, Ticket, RecentChat, ChatMessage } from '../types';
5
5
  import { loadLocalConfig } from '../config';
6
6
  import { mergeTheme } from '../utils/theme';
7
7
  import { useRemoteConfig } from '../hooks/useRemoteConfig';
@@ -115,6 +115,17 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
115
115
  }
116
116
  }, []);
117
117
 
118
+ const handleNavFromMenu = useCallback((ctx: UserListContext | 'ticket') => {
119
+ clearChat();
120
+ if (ctx === 'ticket') {
121
+ setActiveTab('tickets');
122
+ setScreen('tickets');
123
+ } else {
124
+ setUserListCtx(ctx);
125
+ setScreen('user-list');
126
+ }
127
+ }, [clearChat]);
128
+
118
129
  const handleSelectUser = useCallback((user: ChatUser) => {
119
130
  // Load history from sample chats if available
120
131
  const history = data?.sampleChats[user.uid] ?? [];
@@ -185,11 +196,40 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
185
196
  const primaryColor = theme.primaryColor;
186
197
 
187
198
  const allUsers = data ? [...data.developers, ...data.users] : [];
199
+ const viewerIsDev = widgetConfig?.viewerType === 'developer';
200
+ const viewerUid = widgetConfig?.viewerUid;
201
+
188
202
  const filteredUsers = screen === 'user-list'
189
- ? allUsers.filter(u => userListCtx === 'support' ? u.type === 'developer' : u.type === 'user')
203
+ ? allUsers.filter(u => {
204
+ if (userListCtx === 'support') {
205
+ if (viewerIsDev) return u.type === 'user';
206
+ return u.type === 'developer';
207
+ }
208
+ if (viewerIsDev) {
209
+ return u.type === 'developer' && u.uid !== viewerUid;
210
+ }
211
+ return u.type === 'user';
212
+ })
190
213
  : [];
214
+
215
+ const otherDevelopers = data?.developers.filter(d => d.uid !== viewerUid) ?? [];
191
216
  const blockedUsers = allUsers.filter(u => blockedUids.includes(u.uid));
192
217
 
218
+ const handleTransferToDeveloper = useCallback((dev: ChatUser) => {
219
+ if (!activeUser || !widgetConfig) return;
220
+ const agent = widgetConfig.viewerName?.trim() || 'Agent';
221
+ const transferNote: ChatMessage = {
222
+ id: `tr_${Date.now()}_${Math.random().toString(36).slice(2)}`,
223
+ senderId: 'me',
224
+ receiverId: dev.uid,
225
+ text: `— ${agent} transferred this conversation from ${activeUser.name} to ${dev.name} —`,
226
+ timestamp: new Date().toISOString(),
227
+ type: 'text',
228
+ status: 'sent',
229
+ };
230
+ selectUser(dev, [...messages, transferNote]);
231
+ }, [activeUser, messages, selectUser, widgetConfig]);
232
+
193
233
  /* Position */
194
234
  const posStyle: React.CSSProperties = theme.buttonPosition === 'bottom-left'
195
235
  ? { left: 24, right: 'auto' }
@@ -366,7 +406,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
366
406
  <div className="cw-scroll" style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
367
407
 
368
408
  {screen === 'home' && (
369
- <HomeScreen config={widgetConfig} onNavigate={handleCardClick} />
409
+ <HomeScreen config={widgetConfig} onNavigate={handleCardClick} tickets={tickets} />
370
410
  )}
371
411
 
372
412
  {screen === 'user-list' && (
@@ -374,6 +414,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
374
414
  context={userListCtx}
375
415
  users={filteredUsers}
376
416
  primaryColor={primaryColor}
417
+ viewerType={widgetConfig.viewerType ?? 'user'}
377
418
  onBack={() => setScreen('home')}
378
419
  onSelectUser={handleSelectUser}
379
420
  />
@@ -394,6 +435,9 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
394
435
  onReport={reportChat}
395
436
  onBlock={handleBlock}
396
437
  onStartCall={handleStartCall}
438
+ onNavAction={handleNavFromMenu}
439
+ otherDevelopers={otherDevelopers}
440
+ onTransferToDeveloper={handleTransferToDeveloper}
397
441
  />
398
442
  )}
399
443
 
@@ -1,106 +1,270 @@
1
- import React from 'react';
2
- import { WidgetConfig, UserListContext } from '../../types';
1
+ import React, { useState } from 'react';
2
+ import { WidgetConfig, UserListContext, Ticket } from '../../types';
3
+ import { SlideNavMenu } from '../SlideNavMenu';
3
4
 
4
5
  interface HomeScreenProps {
5
6
  config: WidgetConfig;
6
7
  onNavigate: (ctx: UserListContext | 'ticket') => void;
8
+ tickets: Ticket[];
7
9
  }
8
10
 
9
- export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate }) => {
11
+ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, tickets }) => {
12
+ const [menuOpen, setMenuOpen] = useState(false);
10
13
  const showSupport = config.chatType === 'SUPPORT' || config.chatType === 'BOTH';
11
- const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
14
+ const showChat = config.chatType === 'CHAT' || config.chatType === 'BOTH';
15
+ const viewerIsDev = config.viewerType === 'developer';
12
16
 
13
- const cards = [
14
- showSupport && {
15
- key: 'support' as UserListContext,
16
- icon: '🛠',
17
- title: 'Need Support',
18
- subtitle: 'We typically reply in a few minutes',
19
- onClick: () => onNavigate('support'),
20
- },
21
- showChat && {
22
- key: 'conversation' as UserListContext,
23
- icon: '💬',
24
- title: 'New Conversation',
25
- subtitle: 'With your colleague',
26
- onClick: () => onNavigate('conversation'),
27
- },
28
- {
29
- key: 'ticket',
30
- icon: '🎫',
31
- title: 'Raise Ticket',
32
- subtitle: 'For major changes',
33
- onClick: () => onNavigate('ticket'),
34
- },
35
- ].filter(Boolean) as Array<{ key: string; icon: string; title: string; subtitle: string; onClick: () => void }>;
17
+ const continueItems = tickets.slice(0, 2);
18
+
19
+ const handleCallUs = () => {
20
+ const raw = config.supportPhone?.trim();
21
+ if (!raw) return;
22
+ window.location.href = `tel:${raw.replace(/\s/g, '')}`;
23
+ };
36
24
 
37
25
  return (
38
- <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
39
- {/* Hero */}
40
- <div style={{
41
- background: `linear-gradient(145deg, ${config.primaryColor}, ${config.primaryColor}dd)`,
42
- padding: '36px 24px 52px',
43
- flexShrink: 0,
44
- position: 'relative',
45
- overflow: 'hidden',
46
- }}>
47
- {/* Decorative circles */}
48
- <div style={{ position:'absolute', top:-40, right:-40, width:140, height:140, borderRadius:'50%', background:'rgba(255,255,255,0.07)' }} />
49
- <div style={{ position:'absolute', bottom:-20, left:-20, width:90, height:90, borderRadius:'50%', background:'rgba(255,255,255,0.05)' }} />
50
- <h1 style={{ margin:'0 0 8px', fontSize:26, fontWeight:800, color:'#fff', letterSpacing:'-0.03em' }}>
26
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }}>
27
+ <SlideNavMenu
28
+ open={menuOpen}
29
+ onClose={() => setMenuOpen(false)}
30
+ primaryColor={config.primaryColor}
31
+ chatType={config.chatType}
32
+ viewerType={config.viewerType ?? 'user'}
33
+ onSelect={onNavigate}
34
+ />
35
+
36
+ {/* Top bar burger left */}
37
+ <div
38
+ style={{
39
+ flexShrink: 0,
40
+ padding: '14px 16px 10px',
41
+ display: 'flex',
42
+ alignItems: 'center',
43
+ gap: 12,
44
+ background: '#fff',
45
+ borderBottom: '1px solid #eef0f5',
46
+ }}
47
+ >
48
+ <button
49
+ type="button"
50
+ aria-label="Open menu"
51
+ onClick={() => setMenuOpen(true)}
52
+ style={{
53
+ width: 40,
54
+ height: 40,
55
+ borderRadius: 10,
56
+ border: 'none',
57
+ background: '#f1f5f9',
58
+ cursor: 'pointer',
59
+ display: 'flex',
60
+ flexDirection: 'column',
61
+ alignItems: 'center',
62
+ justifyContent: 'center',
63
+ gap: 5,
64
+ flexShrink: 0,
65
+ }}
66
+ >
67
+ <span style={{ width: 18, height: 2, background: '#334155', borderRadius: 1 }} />
68
+ <span style={{ width: 18, height: 2, background: '#334155', borderRadius: 1 }} />
69
+ <span style={{ width: 18, height: 2, background: '#334155', borderRadius: 1 }} />
70
+ </button>
71
+ </div>
72
+
73
+ <div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px 28px' }}>
74
+ {/* Title + description */}
75
+ <h1
76
+ style={{
77
+ margin: '0 0 8px',
78
+ fontSize: 24,
79
+ fontWeight: 800,
80
+ color: '#0f172a',
81
+ letterSpacing: '-0.03em',
82
+ lineHeight: 1.2,
83
+ }}
84
+ >
51
85
  {config.welcomeTitle}
52
86
  </h1>
53
- <p style={{ margin:0, fontSize:14, color:'rgba(255,255,255,0.85)', lineHeight:1.6 }}>
87
+ <p style={{ margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }}>
54
88
  {config.welcomeSubtitle}
55
89
  </p>
56
- </div>
57
90
 
58
- {/* Cards float over hero */}
59
- <div style={{ flex:1, overflowY:'auto', padding:'0 16px 20px', marginTop:-28, display:'flex', flexDirection:'column', gap:10 }}>
60
- {cards.map((card, i) => (
91
+ {/* Continue Conversations */}
92
+ <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Continue Conversations</h2>
93
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }}>
94
+ {continueItems.length > 0 ? (
95
+ continueItems.map(t => (
96
+ <button
97
+ key={t.id}
98
+ type="button"
99
+ onClick={() => onNavigate('ticket')}
100
+ style={{
101
+ width: '100%',
102
+ textAlign: 'left',
103
+ padding: '14px 16px',
104
+ borderRadius: 14,
105
+ border: 'none',
106
+ background: '#e0f2fe',
107
+ color: '#0369a1',
108
+ fontSize: 14,
109
+ fontWeight: 600,
110
+ cursor: 'pointer',
111
+ boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
112
+ }}
113
+ >
114
+ {t.title}
115
+ </button>
116
+ ))
117
+ ) : (
118
+ <>
119
+ <div
120
+ style={{
121
+ padding: '14px 16px',
122
+ borderRadius: 14,
123
+ background: '#e0f2fe',
124
+ color: '#64748b',
125
+ fontSize: 14,
126
+ fontWeight: 500,
127
+ }}
128
+ >
129
+ No open tickets yet
130
+ </div>
131
+ <div
132
+ style={{
133
+ padding: '14px 16px',
134
+ borderRadius: 14,
135
+ background: '#e0f2fe',
136
+ color: '#64748b',
137
+ fontSize: 14,
138
+ fontWeight: 500,
139
+ }}
140
+ >
141
+ Start via Raise ticket below
142
+ </div>
143
+ </>
144
+ )}
145
+ </div>
146
+
147
+ {/* Talk to our experts / staff tools */}
148
+ <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>
149
+ {viewerIsDev ? 'Support tools' : 'Talk to our experts'}
150
+ </h2>
151
+
152
+ {showSupport && (
61
153
  <button
62
- key={card.key}
63
- onClick={card.onClick}
154
+ type="button"
155
+ onClick={() => onNavigate('support')}
64
156
  style={{
65
- width:'100%', background:'#fff', border:'none', borderRadius:14,
66
- padding:'18px 20px', display:'flex', alignItems:'center',
67
- justifyContent:'space-between', cursor:'pointer', textAlign:'left',
68
- boxShadow:'0 2px 14px rgba(0,0,0,0.10)',
69
- animation:`cw-fadeUp 0.35s ease both`,
70
- animationDelay:`${i * 0.08}s`,
71
- transition:'transform 0.15s, box-shadow 0.15s',
157
+ width: '100%',
158
+ display: 'flex',
159
+ alignItems: 'center',
160
+ justifyContent: 'center',
161
+ gap: 10,
162
+ padding: '14px 18px',
163
+ marginBottom: showChat ? 10 : 14,
164
+ borderRadius: 14,
165
+ border: 'none',
166
+ background: '#ede9fe',
167
+ color: '#5b21b6',
168
+ fontSize: 15,
169
+ fontWeight: 700,
170
+ cursor: 'pointer',
171
+ boxShadow: '0 2px 8px rgba(91,33,182,0.12)',
72
172
  }}
73
- onMouseEnter={e => {
74
- (e.currentTarget as HTMLElement).style.transform = 'translateY(-2px)';
75
- (e.currentTarget as HTMLElement).style.boxShadow = '0 8px 24px rgba(0,0,0,0.14)';
173
+ >
174
+ <span style={{ fontSize: 18 }}>👤</span>
175
+ {viewerIsDev ? 'Provide Support' : 'Support'}
176
+ </button>
177
+ )}
178
+
179
+ {showChat && showSupport && (
180
+ <button
181
+ type="button"
182
+ onClick={() => onNavigate('conversation')}
183
+ style={{
184
+ width: '100%',
185
+ padding: '12px 16px',
186
+ marginBottom: 14,
187
+ borderRadius: 12,
188
+ border: '1.5px solid #e9d5ff',
189
+ background: '#fff',
190
+ color: '#6d28d9',
191
+ fontSize: 14,
192
+ fontWeight: 600,
193
+ cursor: 'pointer',
76
194
  }}
77
- onMouseLeave={e => {
78
- (e.currentTarget as HTMLElement).style.transform = 'translateY(0)';
79
- (e.currentTarget as HTMLElement).style.boxShadow = '0 2px 14px rgba(0,0,0,0.10)';
195
+ >
196
+ {viewerIsDev ? 'Chat with a developer' : 'New Conversation'}
197
+ </button>
198
+ )}
199
+
200
+ {showChat && !showSupport && (
201
+ <button
202
+ type="button"
203
+ onClick={() => onNavigate('conversation')}
204
+ style={{
205
+ width: '100%',
206
+ display: 'flex',
207
+ alignItems: 'center',
208
+ justifyContent: 'center',
209
+ gap: 10,
210
+ padding: '14px 18px',
211
+ marginBottom: 14,
212
+ borderRadius: 14,
213
+ border: 'none',
214
+ background: '#ede9fe',
215
+ color: '#5b21b6',
216
+ fontSize: 15,
217
+ fontWeight: 700,
218
+ cursor: 'pointer',
80
219
  }}
81
220
  >
82
- <div style={{ display:'flex', alignItems:'center', gap:14 }}>
83
- <div style={{
84
- width:44, height:44, borderRadius:12,
85
- backgroundColor:`${config.primaryColor}14`,
86
- display:'flex', alignItems:'center', justifyContent:'center',
87
- fontSize:20, flexShrink:0,
88
- }}>{card.icon}</div>
89
- <div>
90
- <div style={{ fontWeight:700, fontSize:15, color:'#1a2332', marginBottom:2 }}>{card.title}</div>
91
- <div style={{ fontSize:12, color:'#7b8fa1' }}>{card.subtitle}</div>
92
- </div>
93
- </div>
94
- <SendArrow color={config.primaryColor} />
221
+ <span style={{ fontSize: 18 }}>💬</span>
222
+ New Conversation
95
223
  </button>
96
- ))}
224
+ )}
225
+
226
+ <div
227
+ style={{
228
+ borderRadius: 18,
229
+ padding: '22px 20px 20px',
230
+ background: 'linear-gradient(145deg, #fce7f3 0%, #e9d5ff 45%, #ddd6fe 100%)',
231
+ position: 'relative',
232
+ overflow: 'hidden',
233
+ }}
234
+ >
235
+ <div style={{ position: 'absolute', top: -20, right: -20, width: 100, height: 100, borderRadius: '50%', background: 'rgba(255,255,255,0.35)' }} />
236
+ <p style={{ margin: '0 0 16px', fontSize: 15, fontWeight: 700, color: '#4c1d95', lineHeight: 1.45, position: 'relative' }}>
237
+ Need specialized help? Our teams are ready to assist you with any questions.
238
+ </p>
239
+ <button
240
+ type="button"
241
+ onClick={handleCallUs}
242
+ disabled={!config.supportPhone}
243
+ style={{
244
+ display: 'inline-flex',
245
+ alignItems: 'center',
246
+ gap: 8,
247
+ padding: '10px 18px',
248
+ borderRadius: 12,
249
+ border: 'none',
250
+ background: config.supportPhone ? config.primaryColor : '#94a3b8',
251
+ color: '#fff',
252
+ fontSize: 14,
253
+ fontWeight: 700,
254
+ cursor: config.supportPhone ? 'pointer' : 'not-allowed',
255
+ position: 'relative',
256
+ }}
257
+ >
258
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
259
+ <path
260
+ d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.68A2 2 0 012 0h3a2 2 0 012 1.72c.127.96.361 1.903.7 2.81a2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0122 14.92v2z"
261
+ fill="#fff"
262
+ />
263
+ </svg>
264
+ Call Us
265
+ </button>
266
+ </div>
97
267
  </div>
98
268
  </div>
99
269
  );
100
270
  };
101
-
102
- const SendArrow: React.FC<{ color: string }> = ({ color }) => (
103
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" style={{ flexShrink:0 }}>
104
- <path d="M5 12h14M12 5l7 7-7 7" stroke={color} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"/>
105
- </svg>
106
- );
@@ -0,0 +1,142 @@
1
+ import React from 'react';
2
+ import { ChatType, UserListContext } from '../types';
3
+
4
+ export interface SlideNavMenuProps {
5
+ open: boolean;
6
+ onClose: () => void;
7
+ primaryColor: string;
8
+ chatType: ChatType;
9
+ /** When `developer`, relabels the first two entries for staff */
10
+ viewerType?: 'user' | 'developer';
11
+ onSelect: (ctx: UserListContext | 'ticket') => void;
12
+ /** When set, shows “Back to home” at the bottom (e.g. chat screen) */
13
+ onBackHome?: () => void;
14
+ }
15
+
16
+ export const SlideNavMenu: React.FC<SlideNavMenuProps> = ({
17
+ open,
18
+ onClose,
19
+ primaryColor,
20
+ chatType,
21
+ viewerType = 'user',
22
+ onSelect,
23
+ onBackHome,
24
+ }) => {
25
+ const showSupport = chatType === 'SUPPORT' || chatType === 'BOTH';
26
+ const showChat = chatType === 'CHAT' || chatType === 'BOTH';
27
+ const isStaff = viewerType === 'developer';
28
+
29
+ const items: Array<{ key: UserListContext | 'ticket'; icon: string; title: string } | null> = [
30
+ showSupport ? { key: 'support', icon: '🛠', title: isStaff ? 'Provide Support' : 'Need Support' } : null,
31
+ showChat ? { key: 'conversation', icon: '💬', title: isStaff ? 'Chat with developer' : 'New Conversation' } : null,
32
+ { key: 'ticket', icon: '🎫', title: 'Raise ticket' },
33
+ ];
34
+
35
+ if (!open) return null;
36
+
37
+ return (
38
+ <>
39
+ <button
40
+ type="button"
41
+ aria-label="Close menu"
42
+ onClick={onClose}
43
+ style={{
44
+ position: 'absolute',
45
+ inset: 0,
46
+ zIndex: 200,
47
+ background: 'rgba(15,23,42,0.45)',
48
+ border: 'none',
49
+ cursor: 'pointer',
50
+ animation: 'cw-fadeIn 0.2s ease',
51
+ }}
52
+ />
53
+ <style>{`@keyframes cw-fadeIn { from { opacity: 0; } to { opacity: 1; } }`}</style>
54
+ <nav
55
+ style={{
56
+ position: 'absolute',
57
+ top: 0,
58
+ left: 0,
59
+ bottom: 0,
60
+ width: 'min(300px, 88%)',
61
+ zIndex: 210,
62
+ background: '#fff',
63
+ boxShadow: '8px 0 32px rgba(0,0,0,0.12)',
64
+ display: 'flex',
65
+ flexDirection: 'column',
66
+ padding: '20px 0 16px',
67
+ animation: 'cw-slideNavIn 0.28s cubic-bezier(0.22, 1, 0.36, 1)',
68
+ }}
69
+ >
70
+ <style>{`@keyframes cw-slideNavIn { from { transform: translateX(-100%); } to { transform: translateX(0); } }`}</style>
71
+ <div style={{ padding: '0 20px 16px', borderBottom: '1px solid #eef0f5' }}>
72
+ <p style={{ margin: 0, fontSize: 13, fontWeight: 700, color: '#64748b', letterSpacing: '0.04em' }}>Menu</p>
73
+ </div>
74
+ <div style={{ flex: 1, overflowY: 'auto', padding: '12px 12px' }}>
75
+ {items.filter(Boolean).map(item => {
76
+ const it = item!;
77
+ return (
78
+ <button
79
+ key={it.key}
80
+ type="button"
81
+ onClick={() => {
82
+ onSelect(it.key);
83
+ onClose();
84
+ }}
85
+ style={{
86
+ width: '100%',
87
+ display: 'flex',
88
+ alignItems: 'center',
89
+ gap: 12,
90
+ padding: '14px 14px',
91
+ marginBottom: 6,
92
+ border: 'none',
93
+ borderRadius: 12,
94
+ background: '#f8fafc',
95
+ cursor: 'pointer',
96
+ textAlign: 'left',
97
+ fontSize: 15,
98
+ fontWeight: 600,
99
+ color: '#1e293b',
100
+ transition: 'background 0.15s',
101
+ }}
102
+ onMouseEnter={e => {
103
+ (e.currentTarget as HTMLButtonElement).style.background = `${primaryColor}12`;
104
+ }}
105
+ onMouseLeave={e => {
106
+ (e.currentTarget as HTMLButtonElement).style.background = '#f8fafc';
107
+ }}
108
+ >
109
+ <span style={{ fontSize: 20 }}>{it.icon}</span>
110
+ {it.title}
111
+ </button>
112
+ );
113
+ })}
114
+ </div>
115
+ {onBackHome && (
116
+ <div style={{ padding: '0 12px', borderTop: '1px solid #eef0f5', paddingTop: 12 }}>
117
+ <button
118
+ type="button"
119
+ onClick={() => {
120
+ onBackHome();
121
+ onClose();
122
+ }}
123
+ style={{
124
+ width: '100%',
125
+ padding: '12px 14px',
126
+ border: '1.5px solid #e2e8f0',
127
+ borderRadius: 12,
128
+ background: '#fff',
129
+ fontSize: 14,
130
+ fontWeight: 600,
131
+ color: '#475569',
132
+ cursor: 'pointer',
133
+ }}
134
+ >
135
+ ← Back to home
136
+ </button>
137
+ </div>
138
+ )}
139
+ </nav>
140
+ </>
141
+ );
142
+ };
@@ -6,15 +6,22 @@ interface UserListScreenProps {
6
6
  context: UserListContext;
7
7
  users: ChatUser[];
8
8
  primaryColor: string;
9
+ /** `developer` = staff using the widget (lists customers vs teammates) */
10
+ viewerType?: 'user' | 'developer';
9
11
  onBack: () => void;
10
12
  onSelectUser: (user: ChatUser) => void;
11
13
  }
12
14
 
13
15
  export const UserListScreen: React.FC<UserListScreenProps> = ({
14
- context, users, primaryColor, onBack, onSelectUser,
16
+ context, users, primaryColor, viewerType = 'user', onBack, onSelectUser,
15
17
  }) => {
16
- const title = context === 'support' ? 'Need Support' : 'New Conversation';
17
- const subtitle = context === 'support' ? 'Choose a support agent' : 'Choose a colleague';
18
+ const isStaff = viewerType === 'developer';
19
+ const title = context === 'support'
20
+ ? (isStaff ? 'Provide Support' : 'Need Support')
21
+ : (isStaff ? 'Developers' : 'New Conversation');
22
+ const subtitle = context === 'support'
23
+ ? (isStaff ? 'All chat users — choose who to help' : 'Choose a support agent')
24
+ : (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
18
25
 
19
26
  return (
20
27
  <div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease' }}>
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export { CallScreen } from './components/CallScreen';
9
9
  export { MaintenanceView } from './components/MaintenanceView';
10
10
  export { BottomTabs } from './components/Tabs/BottomTabs';
11
11
  export { EmojiPicker } from './components/EmojiPicker';
12
+ export { SlideNavMenu } from './components/SlideNavMenu';
12
13
 
13
14
  export { useChat } from './hooks/useChat';
14
15
  export { useWebRTC } from './hooks/useWebRTC';
@@ -9,6 +9,21 @@ export interface WidgetConfig {
9
9
  buttonPosition: 'bottom-right' | 'bottom-left';
10
10
  welcomeTitle: string;
11
11
  welcomeSubtitle: string;
12
+ /** Shown in footer (e.g. branch / location name) */
13
+ branch?: string;
14
+ /** Optional label above branch (e.g. "Answers by") */
15
+ footerPoweredBy?: string;
16
+ /** Shown on home “Call Us” (tel: link) */
17
+ supportPhone?: string;
18
+ /**
19
+ * Who is using the widget. `developer` = support staff: “Need Support” becomes “Provide Support”
20
+ * and lists customers; “New Conversation” lists other developers (excl. viewerUid).
21
+ */
22
+ viewerType?: 'user' | 'developer';
23
+ /** Current user id when viewerType is developer — excluded from developer pick lists */
24
+ viewerUid?: string;
25
+ /** Display name for transfer notes (optional) */
26
+ viewerName?: string;
12
27
  allowVoiceMessage: boolean;
13
28
  allowAttachment: boolean;
14
29
  allowEmoji: boolean;
@@ -69,7 +84,11 @@ export interface ChatMessage {
69
84
  status: 'sent' | 'delivered' | 'read';
70
85
  attachmentName?: string;
71
86
  attachmentSize?: string;
87
+ /** Blob URL for attachment download (local send) */
88
+ attachmentUrl?: string;
72
89
  voiceDuration?: number; // seconds
90
+ /** Blob URL for in-bubble audio playback (local recording) */
91
+ voiceUrl?: string;
73
92
  }
74
93
 
75
94
  // ─── Ticket ─────────────────────────────────────────────────────────────────