ajaxter-chat 3.0.10 β†’ 3.0.12

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 CHANGED
@@ -3,6 +3,8 @@
3
3
  A production-ready **drawer/slider-based** chat widget for React.js and Next.js.
4
4
  All configuration is loaded remotely from your hosted `chatData.json`.
5
5
 
6
+ **Backend developers:** see **[BACKEND.md](./BACKEND.md)** for WebSocket/REST events, auth, payloads (`ChatMessage`, `Ticket`, users, history), and WebRTC signalling.
7
+
6
8
  ---
7
9
 
8
10
  ## Setup (2 env vars only)
@@ -109,7 +111,7 @@ export default function MyApp({ Component, pageProps }) {
109
111
 
110
112
  | Feature | Details |
111
113
  |---|---|
112
- | **Drawer/Slider UI** | Slides in from right (or left) with smooth animation. Backdrop closes it. |
114
+ | **Drawer/Slider UI** | Slides in from right (or left) with smooth animation. |
113
115
  | **Home Screen** | "Hi there πŸ‘‹" hero, cards for Support / New Conversation / Raise Ticket |
114
116
  | **User List** | Slide-in panel with online status dot, designation, project |
115
117
  | **Chat Screen** | Matches the UI image β€” hamburger back, title "Support", phone + fullscreen icons |
@@ -127,7 +129,7 @@ export default function MyApp({ Component, pageProps }) {
127
129
  | **Ticket Screen** | Raise tickets with title, description, priority selector |
128
130
  | **Ticket History** | Shows all tickets with status badge and priority color |
129
131
  | **Recent Chats Tab** | Shows past conversations with unread badges |
130
- | **Maximize/Minimize** | Toggle drawer width 380px ↔ 480px |
132
+ | **Responsive width** | ~30% width on desktop; full width on small screens |
131
133
  | **Slide Close** | Smooth slide-out animation on close |
132
134
  | **SSR Safe** | isMounted guard, works in Next.js App Router and Pages Router |
133
135
  | **CHAT_STATUS** | ACTIVE / DISABLE / MAINTENANCE handled |
@@ -40,6 +40,8 @@ export const ChatWidget = ({ theme: localTheme }) => {
40
40
  const [chatReturnCtx, setChatReturnCtx] = useState('conversation');
41
41
  const [viewingTicketId, setViewingTicketId] = useState(null);
42
42
  const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
43
+ /** Stagger list animation only when opening from home burger menu */
44
+ const [listEntranceAnimation, setListEntranceAnimation] = useState(false);
43
45
  /* App state */
44
46
  const [tickets, setTickets] = useState((_a = data === null || data === void 0 ? void 0 : data.sampleTickets) !== null && _a !== void 0 ? _a : []);
45
47
  const [recentChats, setRecentChats] = useState([]);
@@ -156,7 +158,8 @@ export const ChatWidget = ({ theme: localTheme }) => {
156
158
  setMessageSoundEnabledState(enabled);
157
159
  }, [data === null || data === void 0 ? void 0 : data.widget]);
158
160
  /* ── Navigation ──────────────────────────────────────────────────────── */
159
- const handleCardClick = useCallback((ctx) => {
161
+ const handleCardClick = useCallback((ctx, options) => {
162
+ setListEntranceAnimation(!!(options === null || options === void 0 ? void 0 : options.fromMenu));
160
163
  if (ctx === 'ticket') {
161
164
  setActiveTab('tickets');
162
165
  setScreen('tickets');
@@ -167,6 +170,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
167
170
  }
168
171
  }, []);
169
172
  const handleNavFromMenu = useCallback((ctx) => {
173
+ setListEntranceAnimation(false);
170
174
  clearChat();
171
175
  if (ctx === 'ticket') {
172
176
  setActiveTab('tickets');
@@ -184,6 +188,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
184
188
  }, []);
185
189
  const handleSelectUser = useCallback((user, returnCtxOverride) => {
186
190
  var _a;
191
+ setListEntranceAnimation(false);
187
192
  setChatReturnCtx(returnCtxOverride !== null && returnCtxOverride !== void 0 ? returnCtxOverride : userListCtx);
188
193
  const history = (_a = data === null || data === void 0 ? void 0 : data.sampleChats[user.uid]) !== null && _a !== void 0 ? _a : [];
189
194
  selectUser(user, history);
@@ -196,19 +201,28 @@ export const ChatWidget = ({ theme: localTheme }) => {
196
201
  });
197
202
  }, [data, selectUser, userListCtx]);
198
203
  const handleBackFromChat = useCallback(() => {
204
+ setListEntranceAnimation(false);
199
205
  clearChat();
200
206
  setUserListCtx(chatReturnCtx);
201
207
  setScreen('user-list');
202
208
  }, [clearChat, chatReturnCtx]);
203
209
  const handleOpenTicket = useCallback((id) => {
210
+ setListEntranceAnimation(false);
204
211
  setViewingTicketId(id);
205
212
  setScreen('ticket-detail');
206
213
  setActiveTab('tickets');
207
214
  }, []);
208
215
  const handleTabChange = useCallback((tab) => {
216
+ setListEntranceAnimation(false);
209
217
  setActiveTab(tab);
210
218
  setScreen(tab === 'home' ? 'home' : tab === 'chats' ? 'recent-chats' : 'tickets');
211
219
  }, []);
220
+ useEffect(() => {
221
+ if (!listEntranceAnimation)
222
+ return;
223
+ const t = window.setTimeout(() => setListEntranceAnimation(false), 520);
224
+ return () => window.clearTimeout(t);
225
+ }, [listEntranceAnimation]);
212
226
  /* ── Block/Unblock ───────────────────────────────────────────────────── */
213
227
  const handleBlock = useCallback(() => {
214
228
  if (!activeUser)
@@ -347,7 +361,7 @@ export const ChatWidget = ({ theme: localTheme }) => {
347
361
  }, onMouseLeave: e => {
348
362
  e.currentTarget.style.transform = 'scale(1)';
349
363
  e.currentTarget.style.boxShadow = `0 8px 28px ${theme.buttonColor}55`;
350
- }, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z", stroke: theme.buttonTextColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }), _jsx("span", { children: theme.buttonLabel })] })), isOpen && (_jsx("div", { onClick: closeDrawer, style: {
364
+ }, children: [_jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z", stroke: theme.buttonTextColor, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }), _jsx("span", { children: theme.buttonLabel })] })), isOpen && (_jsx("div", { "aria-hidden": true, style: {
351
365
  position: 'fixed', inset: 0, zIndex: 9997,
352
366
  backgroundColor: 'rgba(0,0,0,0.35)',
353
367
  opacity: closing ? 0 : 1,
@@ -364,7 +378,11 @@ export const ChatWidget = ({ theme: localTheme }) => {
364
378
  right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
365
379
  left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
366
380
  zIndex: 20, display: 'flex', gap: 6,
367
- }, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, onOpenTicket: handleOpenTicket, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_e = widgetConfig.viewerType) !== null && _e !== void 0 ? _e : 'user', onBack: () => setScreen('home'), onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer' })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: handleBackFromChat, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper, messageSoundEnabled: messageSoundEnabled, onToggleMessageSound: toggleMessageSound })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: u => handleSelectUser(u, listCtxForUser(u, viewerIsDev)) })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () => setScreen('ticket-new'), onSelectTicket: id => { setViewingTicketId(id); setScreen('ticket-detail'); } })), screen === 'ticket-new' && (_jsx(TicketFormScreen, { config: widgetConfig, onSubmit: handleRaiseTicket, onCancel: () => setScreen('tickets') })), screen === 'ticket-detail' && viewingTicketId && ((() => {
381
+ }, children: _jsx(CornerBtn, { onClick: closeDrawer, title: "Close", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M18 6L6 18M6 6l12 12", stroke: "#fff", strokeWidth: "2.5", strokeLinecap: "round" }) }) }) })), widgetConfig.status === 'MAINTENANCE' && (_jsx(MaintenanceView, { primaryColor: primaryColor })), widgetConfig.status === 'DISABLE' && (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12 }, children: [_jsx("div", { style: { fontSize: 40 }, children: "\uD83D\uDD12" }), _jsx("p", { style: { fontWeight: 700, color: '#1a2332' }, children: "Chat is disabled" }), _jsx("button", { onClick: closeDrawer, style: { padding: '9px 20px', borderRadius: 10, border: 'none', background: primaryColor, color: '#fff', cursor: 'pointer', fontWeight: 700 }, children: "Close" })] })), widgetConfig.status === 'ACTIVE' && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, onNavigate: handleCardClick, onOpenTicket: handleOpenTicket, tickets: tickets })), screen === 'user-list' && (_jsx(UserListScreen, { context: userListCtx, users: filteredUsers, primaryColor: primaryColor, viewerType: (_e = widgetConfig.viewerType) !== null && _e !== void 0 ? _e : 'user', onBack: () => { setListEntranceAnimation(false); setScreen('home'); }, onSelectUser: handleSelectUser, onBlockList: userListCtx === 'conversation' ? () => setScreen('block-list') : undefined, useHomeHeader: userListCtx === 'support' && widgetConfig.viewerType !== 'developer', animateEntrance: listEntranceAnimation })), screen === 'chat' && activeUser && (_jsx(ChatScreen, { activeUser: activeUser, messages: messages, config: widgetConfig, isPaused: isPaused, isReported: isReported, isBlocked: isBlocked, onSend: sendMessage, onBack: handleBackFromChat, onClose: closeDrawer, onTogglePause: handleTogglePause, onReport: reportChat, onBlock: handleBlock, onStartCall: handleStartCall, onNavAction: handleNavFromMenu, otherDevelopers: otherDevelopers, onTransferToDeveloper: handleTransferToDeveloper, messageSoundEnabled: messageSoundEnabled, onToggleMessageSound: toggleMessageSound })), screen === 'call' && callSession.peer && (_jsx(CallScreen, { session: callSession, localVideoRef: localVideoRef, remoteVideoRef: remoteVideoRef, onEnd: handleEndCall, onToggleMute: toggleMute, onToggleCamera: toggleCamera, primaryColor: primaryColor })), screen === 'recent-chats' && (_jsx(RecentChatsScreen, { chats: recentChats, config: widgetConfig, onSelectChat: u => handleSelectUser(u, listCtxForUser(u, viewerIsDev)), animateEntrance: listEntranceAnimation })), screen === 'tickets' && (_jsx(TicketScreen, { tickets: tickets, config: widgetConfig, onNewTicket: () => { setListEntranceAnimation(false); setScreen('ticket-new'); }, onSelectTicket: id => {
382
+ setListEntranceAnimation(false);
383
+ setViewingTicketId(id);
384
+ setScreen('ticket-detail');
385
+ }, animateEntrance: listEntranceAnimation })), screen === 'ticket-new' && (_jsx(TicketFormScreen, { config: widgetConfig, onSubmit: handleRaiseTicket, onCancel: () => setScreen('tickets') })), screen === 'ticket-detail' && viewingTicketId && ((() => {
368
386
  const t = tickets.find(x => x.id === viewingTicketId);
369
387
  return t ? (_jsx(TicketDetailScreen, { ticket: t, config: widgetConfig, onBack: () => { setViewingTicketId(null); setScreen('tickets'); } })) : null;
370
388
  })()), screen === 'block-list' && (_jsx(BlockListScreen, { blockedUsers: blockedUsers, config: widgetConfig, onUnblock: handleUnblock, onBack: () => { setScreen('home'); setActiveTab('home'); } }))] })), widgetConfig.status === 'ACTIVE' &&
@@ -1,8 +1,12 @@
1
1
  import React from 'react';
2
2
  import { WidgetConfig, UserListContext, Ticket } from '../../types';
3
+ export interface HomeNavigateOptions {
4
+ /** When true, list screens play stagger animation (home burger menu only) */
5
+ fromMenu?: boolean;
6
+ }
3
7
  interface HomeScreenProps {
4
8
  config: WidgetConfig;
5
- onNavigate: (ctx: UserListContext | 'ticket') => void;
9
+ onNavigate: (ctx: UserListContext | 'ticket', options?: HomeNavigateOptions) => void;
6
10
  /** Open a specific pending ticket (full detail) */
7
11
  onOpenTicket: (ticketId: string) => void;
8
12
  tickets: Ticket[];
@@ -23,7 +23,9 @@ export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
23
23
  return;
24
24
  window.location.href = `tel:${raw.replace(/\s/g, '')}`;
25
25
  };
26
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }, children: [_jsx(SlideNavMenu, { open: menuOpen, onClose: () => setMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_d = config.viewerType) !== null && _d !== void 0 ? _d : 'user', onSelect: onNavigate }), _jsx("div", { style: {
26
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', position: 'relative', overflow: 'hidden', background: '#fafbfc' }, children: [_jsx(SlideNavMenu, { open: menuOpen, onClose: () => setMenuOpen(false), primaryColor: config.primaryColor, chatType: config.chatType, viewerType: (_d = config.viewerType) !== null && _d !== void 0 ? _d : 'user', onSelect: ctx => {
27
+ onNavigate(ctx, { fromMenu: true });
28
+ } }), _jsx("div", { style: {
27
29
  flexShrink: 0,
28
30
  padding: '14px 16px 10px',
29
31
  display: 'flex',
@@ -51,7 +53,7 @@ export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
51
53
  color: '#0f172a',
52
54
  letterSpacing: '-0.03em',
53
55
  lineHeight: 1.2,
54
- }, children: config.welcomeTitle }), _jsx("p", { style: { margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }, children: config.welcomeSubtitle }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: "Continue Conversations" }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }, children: pendingTickets.length > 0 ? (pendingTickets.map(t => (_jsxs("button", { type: "button", onClick: () => onOpenTicket(t.id), style: {
56
+ }, children: config.welcomeTitle }), _jsx("p", { style: { margin: '0 0 28px', fontSize: 14, color: '#64748b', lineHeight: 1.55 }, children: config.welcomeSubtitle }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: "Continue with tickets" }), _jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }, children: pendingTickets.length > 0 ? (pendingTickets.map(t => (_jsxs("button", { type: "button", onClick: () => onOpenTicket(t.id), style: {
55
57
  width: '100%',
56
58
  textAlign: 'left',
57
59
  padding: '14px 16px',
@@ -75,7 +77,7 @@ export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
75
77
  color: '#64748b',
76
78
  fontSize: 14,
77
79
  fontWeight: 500,
78
- }, children: "Start via Raise ticket below" })] })) }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: viewerIsDev ? 'Support tools' : 'Talk to our experts' }), showSupport && (_jsxs("button", { type: "button", onClick: () => onNavigate('support'), style: {
80
+ }, children: "Start via Raise ticket below" })] })) }), _jsx("h2", { style: { margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }, children: viewerIsDev ? 'Support tools' : 'Talk to support experts' }), showSupport && (_jsxs("button", { type: "button", onClick: () => onNavigate('support'), style: {
79
81
  width: '100%',
80
82
  display: 'flex',
81
83
  alignItems: 'center',
@@ -149,5 +151,5 @@ export const HomeScreen = ({ config, onNavigate, onOpenTicket, tickets }) => {
149
151
  fontSize: 14,
150
152
  fontWeight: 700,
151
153
  cursor: config.supportPhone ? 'pointer' : 'not-allowed',
152
- }, children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { 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", fill: "#fff" }) }), "Call Us"] })] })] })] })] }));
154
+ }, children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { 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", fill: "#fff" }) }), "Get Free Widget"] })] })] })] })] }));
153
155
  };
@@ -12,6 +12,7 @@ interface RecentChatsScreenProps {
12
12
  chats: RecentChat[];
13
13
  config: WidgetConfig;
14
14
  onSelectChat: (user: ChatUser) => void;
15
+ animateEntrance?: boolean;
15
16
  }
16
17
  export declare const RecentChatsScreen: React.FC<RecentChatsScreenProps>;
17
18
  export {};
@@ -1,8 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useMemo, useRef, useEffect } from 'react';
2
3
  import { avatarColor, initials, formatTime } from '../../utils/chat';
3
- export const RecentChatsScreen = ({ chats, config, onSelectChat }) => (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%' }, children: [_jsxs("div", { style: { background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`, padding: '18px 18px 22px', flexShrink: 0 }, children: [_jsx("h2", { style: { margin: 0, fontSize: 20, fontWeight: 800, color: '#fff', letterSpacing: '-0.02em' }, children: "Recent Chats" }), _jsx("p", { style: { margin: '3px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: "Your conversation history" })] }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: chats.length === 0 ? (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: "\uD83D\uDCAC" }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: "No chats yet" }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: "Start a conversation from home" })] })) : chats.map((chat, i) => (_jsxs("button", { onClick: () => onSelectChat(chat.user), style: {
4
- width: '100%', padding: '13px 16px', display: 'flex', alignItems: 'center', gap: 13,
5
- background: 'transparent', border: 'none', borderBottom: '1px solid #f0f2f5',
6
- cursor: 'pointer', textAlign: 'left', animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s`,
7
- transition: 'background 0.14s',
8
- }, onMouseEnter: e => e.currentTarget.style.background = '#f8faff', onMouseLeave: e => e.currentTarget.style.background = 'transparent', children: [_jsxs("div", { style: { position: 'relative', flexShrink: 0 }, children: [_jsx("div", { style: { width: 46, height: 46, borderRadius: '50%', backgroundColor: avatarColor(chat.user.name), display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 15 }, children: initials(chat.user.name) }), chat.unread > 0 && (_jsx("span", { style: { position: 'absolute', top: -2, right: -2, width: 18, height: 18, borderRadius: '50%', background: '#ef4444', color: '#fff', fontSize: 10, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '2px solid #fff' }, children: chat.unread }))] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 3 }, children: [_jsx("span", { style: { fontWeight: 700, fontSize: 14, color: '#1a2332' }, children: chat.user.name }), _jsx("span", { style: { fontSize: 11, color: '#b0bec5' }, children: formatTime(chat.lastTime) })] }), _jsxs("div", { style: { fontSize: 13, color: '#7b8fa1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 5 }, children: [chat.isPaused && _jsx("span", { style: { fontSize: 10, background: '#fef3c7', color: '#92400e', padding: '1px 5px', borderRadius: 4, fontWeight: 700 }, children: "PAUSED" }), chat.lastMessage] })] })] }, chat.id))) })] }));
4
+ function matchesChat(chat, q) {
5
+ if (!q.trim())
6
+ return true;
7
+ const s = q.trim().toLowerCase();
8
+ return (chat.user.name.toLowerCase().includes(s) ||
9
+ chat.lastMessage.toLowerCase().includes(s));
10
+ }
11
+ export const RecentChatsScreen = ({ chats, config, onSelectChat, animateEntrance = false, }) => {
12
+ const [query, setQuery] = useState('');
13
+ const searchRef = useRef(null);
14
+ useEffect(() => {
15
+ var _a;
16
+ (_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus();
17
+ }, []);
18
+ const filtered = useMemo(() => chats.filter(c => matchesChat(c, query)), [chats, query]);
19
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%' }, children: [_jsxs("div", { style: { background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`, padding: '18px 18px 14px', flexShrink: 0 }, children: [_jsx("h2", { style: { margin: 0, fontSize: 20, fontWeight: 800, color: '#fff', letterSpacing: '-0.02em' }, children: "Recent Chats" }), _jsx("p", { style: { margin: '3px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: "Your conversation history" })] }), _jsx("div", { style: { padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }, children: _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }, children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0, opacity: 0.55 }, children: [_jsx("circle", { cx: "11", cy: "11", r: "7", stroke: "#64748b", strokeWidth: "2" }), _jsx("path", { d: "M20 20l-4-4", stroke: "#64748b", strokeWidth: "2", strokeLinecap: "round" })] }), _jsx("input", { ref: searchRef, type: "search", value: query, onChange: e => setQuery(e.target.value), placeholder: "Search chats\u2026", autoComplete: "off", "aria-label": "Search chats", style: {
20
+ flex: 1,
21
+ minWidth: 0,
22
+ border: 'none',
23
+ outline: 'none',
24
+ background: 'transparent',
25
+ fontSize: 14,
26
+ padding: '10px 0',
27
+ fontFamily: 'inherit',
28
+ color: '#1a2332',
29
+ } })] }) }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: filtered.length === 0 ? (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: query.trim() ? 'πŸ”' : 'πŸ’¬' }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: query.trim() ? 'No matches' : 'No chats yet' }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: query.trim() ? 'Try a different search' : 'Start a conversation from home' })] })) : filtered.map((chat, i) => (_jsxs("button", { onClick: () => onSelectChat(chat.user), style: Object.assign(Object.assign({ width: '100%', padding: '13px 16px', display: 'flex', alignItems: 'center', gap: 13, background: 'transparent', border: 'none', borderBottom: '1px solid #f0f2f5', cursor: 'pointer', textAlign: 'left' }, (animateEntrance ? { animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s` } : {})), { transition: 'background 0.14s' }), onMouseEnter: e => e.currentTarget.style.background = '#f8faff', onMouseLeave: e => e.currentTarget.style.background = 'transparent', children: [_jsxs("div", { style: { position: 'relative', flexShrink: 0 }, children: [_jsx("div", { style: { width: 46, height: 46, borderRadius: '50%', backgroundColor: avatarColor(chat.user.name), display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 15 }, children: initials(chat.user.name) }), chat.unread > 0 && (_jsx("span", { style: { position: 'absolute', top: -2, right: -2, width: 18, height: 18, borderRadius: '50%', background: '#ef4444', color: '#fff', fontSize: 10, fontWeight: 700, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '2px solid #fff' }, children: chat.unread }))] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', marginBottom: 3 }, children: [_jsx("span", { style: { fontWeight: 700, fontSize: 14, color: '#1a2332' }, children: chat.user.name }), _jsx("span", { style: { fontSize: 11, color: '#b0bec5' }, children: formatTime(chat.lastTime) })] }), _jsxs("div", { style: { fontSize: 13, color: '#7b8fa1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'flex', alignItems: 'center', gap: 5 }, children: [chat.isPaused && _jsx("span", { style: { fontSize: 10, background: '#fef3c7', color: '#92400e', padding: '1px 5px', borderRadius: 4, fontWeight: 700 }, children: "PAUSED" }), chat.lastMessage] })] })] }, chat.id))) })] }));
30
+ };
@@ -45,7 +45,7 @@ export const TicketFormScreen = ({ config, onSubmit, onCancel }) => {
45
45
  alignItems: 'center',
46
46
  justifyContent: 'center',
47
47
  cursor: 'pointer',
48
- }, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M19 12H5M5 12L12 19M5 12L12 5", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsxs("div", { children: [_jsx("h2", { style: { margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }, children: "New ticket" }), _jsx("p", { style: { margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }, children: "Describe your issue below" })] })] }), _jsxs("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', padding: '20px 18px', minHeight: 0 }, children: [_jsx("input", { placeholder: "Title *", value: title, onChange: e => setTitle(e.target.value), style: inputStyle(config.primaryColor), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("textarea", { placeholder: "Describe the issue\u2026", value: desc, onChange: e => setDesc(e.target.value), rows: 5, style: Object.assign(Object.assign({}, inputStyle(config.primaryColor)), { resize: 'none', marginTop: 12 }), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("div", { style: { display: 'flex', gap: 8, marginTop: 12, paddingBottom: 8 }, children: ['low', 'medium', 'high'].map(p => (_jsx("button", { type: "button", onClick: () => setPriority(p), style: {
48
+ }, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M19 12H5M5 12L12 19M5 12L12 5", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsxs("div", { children: [_jsx("h2", { style: { margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }, children: "Raise a ticket" }), _jsx("p", { style: { margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }, children: "Please fill out the ticket form below and we will get back to you as soon as possible." })] })] }), _jsxs("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', padding: '20px 18px', minHeight: 0 }, children: [_jsx("input", { placeholder: "Title *", value: title, onChange: e => setTitle(e.target.value), style: inputStyle(config.primaryColor), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("textarea", { placeholder: "Describe the issue\u2026", value: desc, onChange: e => setDesc(e.target.value), rows: 5, style: Object.assign(Object.assign({}, inputStyle(config.primaryColor)), { resize: 'none', marginTop: 12 }), onFocus: e => (e.target.style.borderColor = config.primaryColor), onBlur: e => (e.target.style.borderColor = '#e5e7eb') }), _jsx("div", { style: { display: 'flex', gap: 8, marginTop: 12, paddingBottom: 8 }, children: ['low', 'medium', 'high'].map(p => (_jsx("button", { type: "button", onClick: () => setPriority(p), style: {
49
49
  flex: 1,
50
50
  padding: '8px',
51
51
  border: `1.5px solid ${priority === p ? pm[p].color : '#e5e7eb'}`,
@@ -5,6 +5,7 @@ interface TicketScreenProps {
5
5
  config: WidgetConfig;
6
6
  onNewTicket: () => void;
7
7
  onSelectTicket: (id: string) => void;
8
+ animateEntrance?: boolean;
8
9
  }
9
10
  export declare const TicketScreen: React.FC<TicketScreenProps>;
10
11
  export {};
@@ -1,5 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- export const TicketScreen = ({ tickets, config, onNewTicket, onSelectTicket }) => {
2
+ import { useState, useMemo, useRef, useEffect } from 'react';
3
+ function matchesTicket(t, q) {
4
+ if (!q.trim())
5
+ return true;
6
+ const s = q.trim().toLowerCase();
7
+ return (t.title.toLowerCase().includes(s) ||
8
+ t.description.toLowerCase().includes(s) ||
9
+ t.id.toLowerCase().includes(s));
10
+ }
11
+ export const TicketScreen = ({ tickets, config, onNewTicket, onSelectTicket, animateEntrance = false, }) => {
12
+ const [query, setQuery] = useState('');
13
+ const searchRef = useRef(null);
14
+ useEffect(() => {
15
+ var _a;
16
+ (_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus();
17
+ }, []);
18
+ const filtered = useMemo(() => tickets.filter(t => matchesTicket(t, query)), [tickets, query]);
3
19
  const sm = {
4
20
  open: { label: 'Open', bg: `${config.primaryColor}14`, color: config.primaryColor },
5
21
  'in-progress': { label: 'In Progress', bg: '#fef3c7', color: '#d97706' },
@@ -13,15 +29,21 @@ export const TicketScreen = ({ tickets, config, onNewTicket, onSelectTicket }) =
13
29
  };
14
30
  return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%' }, children: [_jsx("div", { style: {
15
31
  background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
16
- padding: '18px 18px 22px', flexShrink: 0, position: 'relative',
17
- }, children: _jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }, children: [_jsxs("div", { children: [_jsx("h2", { style: { margin: 0, fontSize: 20, fontWeight: 800, color: '#fff', letterSpacing: '-0.02em' }, children: "Tickets" }), _jsxs("p", { style: { margin: '3px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: [tickets.length, " ticket", tickets.length !== 1 ? 's' : '', " raised"] })] }), _jsx("button", { type: "button", onClick: onNewTicket, style: {
32
+ padding: '18px 18px 14px', flexShrink: 0, position: 'relative',
33
+ }, children: _jsxs("div", { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }, children: [_jsxs("div", { style: { minWidth: 0 }, children: [_jsx("h2", { style: { margin: 0, fontSize: 20, fontWeight: 800, color: '#fff', letterSpacing: '-0.02em' }, children: "Tickets" }), _jsxs("p", { style: { margin: '3px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: [tickets.length, " ticket", tickets.length !== 1 ? 's' : '', " raised"] })] }), _jsx("button", { type: "button", onClick: onNewTicket, style: {
18
34
  background: 'rgba(255,255,255,0.22)', border: 'none', borderRadius: 20,
19
35
  padding: '7px 14px', color: '#fff', fontWeight: 700, fontSize: 13,
20
36
  cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 5,
21
- }, children: "+ New" })] }) }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: tickets.length === 0 ? (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: "\uD83C\uDFAB" }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: "No tickets yet" }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: "Raise a ticket for major issues" })] })) : tickets.map((t, i) => (_jsxs("button", { type: "button", onClick: () => onSelectTicket(t.id), style: {
22
- width: '100%', padding: '14px 16px', borderBottom: '1px solid #f0f2f5',
23
- animation: `cw-fadeUp 0.3s ease both`, animationDelay: `${i * 0.05}s`,
24
- background: 'transparent', borderLeft: 'none', borderRight: 'none', borderTop: 'none',
25
- cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit',
26
- }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 5 }, children: [_jsx("span", { style: { fontWeight: 700, fontSize: 14, color: '#1a2332', flex: 1, paddingRight: 10 }, children: t.title }), _jsx("span", { style: { fontSize: 10, fontWeight: 700, padding: '3px 9px', borderRadius: 20, backgroundColor: sm[t.status].bg, color: sm[t.status].color, whiteSpace: 'nowrap', textTransform: 'uppercase', letterSpacing: '0.04em', flexShrink: 0 }, children: sm[t.status].label })] }), t.description && _jsx("p", { style: { margin: '0 0 7px', fontSize: 13, color: '#7b8fa1', lineHeight: 1.5 }, children: t.description }), _jsxs("div", { style: { display: 'flex', gap: 10, fontSize: 11, color: '#b0bec5' }, children: [_jsxs("span", { style: { color: pm[t.priority].color, fontWeight: 700 }, children: ["\u25CF ", pm[t.priority].label] }), _jsxs("span", { children: ["#", t.id] }), _jsx("span", { children: new Date(t.createdAt).toLocaleDateString([], { month: 'short', day: 'numeric' }) })] })] }, t.id))) })] }));
37
+ flexShrink: 0,
38
+ }, children: "+ New" })] }) }), _jsx("div", { style: { padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }, children: _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }, children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0, opacity: 0.55 }, children: [_jsx("circle", { cx: "11", cy: "11", r: "7", stroke: "#64748b", strokeWidth: "2" }), _jsx("path", { d: "M20 20l-4-4", stroke: "#64748b", strokeWidth: "2", strokeLinecap: "round" })] }), _jsx("input", { ref: searchRef, type: "search", value: query, onChange: e => setQuery(e.target.value), placeholder: "Search tickets\u2026", autoComplete: "off", "aria-label": "Search tickets", style: {
39
+ flex: 1,
40
+ minWidth: 0,
41
+ border: 'none',
42
+ outline: 'none',
43
+ background: 'transparent',
44
+ fontSize: 14,
45
+ padding: '10px 0',
46
+ fontFamily: 'inherit',
47
+ color: '#1a2332',
48
+ } })] }) }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: filtered.length === 0 ? (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: query.trim() ? 'πŸ”' : '🎫' }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: query.trim() ? 'No matches' : 'No tickets yet' }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: query.trim() ? 'Try a different search' : 'Raise a ticket for major issues' })] })) : filtered.map((t, i) => (_jsxs("button", { type: "button", onClick: () => onSelectTicket(t.id), style: Object.assign(Object.assign({ width: '100%', padding: '14px 16px', borderBottom: '1px solid #f0f2f5' }, (animateEntrance ? { animation: `cw-fadeUp 0.3s ease both`, animationDelay: `${i * 0.05}s` } : {})), { background: 'transparent', borderLeft: 'none', borderRight: 'none', borderTop: 'none', cursor: 'pointer', textAlign: 'left', fontFamily: 'inherit' }), children: [_jsxs("div", { style: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 5 }, children: [_jsx("span", { style: { fontWeight: 700, fontSize: 14, color: '#1a2332', flex: 1, paddingRight: 10 }, children: t.title }), _jsx("span", { style: { fontSize: 10, fontWeight: 700, padding: '3px 9px', borderRadius: 20, backgroundColor: sm[t.status].bg, color: sm[t.status].color, whiteSpace: 'nowrap', textTransform: 'uppercase', letterSpacing: '0.04em', flexShrink: 0 }, children: sm[t.status].label })] }), t.description && _jsx("p", { style: { margin: '0 0 7px', fontSize: 13, color: '#7b8fa1', lineHeight: 1.5 }, children: t.description }), _jsxs("div", { style: { display: 'flex', gap: 10, fontSize: 11, color: '#b0bec5' }, children: [_jsxs("span", { style: { color: pm[t.priority].color, fontWeight: 700 }, children: ["\u25CF ", pm[t.priority].label] }), _jsxs("span", { children: ["#", t.id] }), _jsx("span", { children: new Date(t.createdAt).toLocaleDateString([], { month: 'short', day: 'numeric' }) })] })] }, t.id))) })] }));
27
49
  };
@@ -12,6 +12,8 @@ interface UserListScreenProps {
12
12
  onBlockList?: () => void;
13
13
  /** β€œNeed Support” (user β†’ agents): show home icon instead of back arrow */
14
14
  useHomeHeader?: boolean;
15
+ /** Stagger animation β€” only when opening from home burger menu */
16
+ animateEntrance?: boolean;
15
17
  }
16
18
  export declare const UserListScreen: React.FC<UserListScreenProps>;
17
19
  export {};
@@ -1,6 +1,23 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useMemo, useRef, useEffect } from 'react';
2
3
  import { avatarColor, initials } from '../../utils/chat';
3
- export const UserListScreen = ({ context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, onBlockList, useHomeHeader = false, }) => {
4
+ function matchesUser(u, q) {
5
+ if (!q.trim())
6
+ return true;
7
+ const s = q.trim().toLowerCase();
8
+ return (u.name.toLowerCase().includes(s) ||
9
+ u.email.toLowerCase().includes(s) ||
10
+ u.designation.toLowerCase().includes(s) ||
11
+ u.project.toLowerCase().includes(s));
12
+ }
13
+ export const UserListScreen = ({ context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, onBlockList, useHomeHeader = false, animateEntrance = false, }) => {
14
+ const [query, setQuery] = useState('');
15
+ const searchRef = useRef(null);
16
+ useEffect(() => {
17
+ var _a;
18
+ (_a = searchRef.current) === null || _a === void 0 ? void 0 : _a.focus();
19
+ }, []);
20
+ const filtered = useMemo(() => users.filter(u => matchesUser(u, query)), [users, query]);
4
21
  const isStaff = viewerType === 'developer';
5
22
  const title = context === 'support'
6
23
  ? (isStaff ? 'Provide Support' : 'Need Support')
@@ -8,7 +25,8 @@ export const UserListScreen = ({ context, users, primaryColor, viewerType = 'use
8
25
  const subtitle = context === 'support'
9
26
  ? (isStaff ? 'All chat users β€” choose who to help' : 'Choose a support agent')
10
27
  : (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
11
- return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease' }, children: [_jsxs("div", { style: { background: `linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0, position: 'relative' }, children: [useHomeHeader ? _jsx(HomeBtn, { onClick: onBack }) : _jsx(BackBtn, { onClick: onBack }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 16, color: '#fff' }, children: title }), _jsx("div", { style: { fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: subtitle })] }), context === 'conversation' && onBlockList && (_jsxs("button", { type: "button", onClick: onBlockList, style: {
28
+ const rootAnim = animateEntrance ? 'cw-slideIn 0.22s ease' : undefined;
29
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: rootAnim }, children: [_jsxs("div", { style: { background: `linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding: '14px 18px', display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0, position: 'relative' }, children: [useHomeHeader ? _jsx(HomeBtn, { onClick: onBack }) : _jsx(BackBtn, { onClick: onBack }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 16, color: '#fff' }, children: title }), _jsx("div", { style: { fontSize: 12, color: 'rgba(255,255,255,0.8)' }, children: subtitle })] }), context === 'conversation' && onBlockList && (_jsxs("button", { type: "button", onClick: onBlockList, style: {
12
30
  flexShrink: 0,
13
31
  background: 'rgba(255,255,255,0.2)',
14
32
  border: 'none',
@@ -21,14 +39,19 @@ export const UserListScreen = ({ context, users, primaryColor, viewerType = 'use
21
39
  display: 'flex',
22
40
  alignItems: 'center',
23
41
  gap: 6,
24
- }, title: "Blocked users", children: [_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "1.8" }), _jsx("line", { x1: "4.93", y1: "4.93", x2: "19.07", y2: "19.07", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" })] }), "Blocked"] }))] }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: users.length === 0 ? (_jsx(Empty, {})) : users.map((u, i) => (_jsxs("button", { onClick: () => onSelectUser(u), style: {
25
- width: '100%', padding: '13px 18px', display: 'flex',
26
- alignItems: 'center', gap: 13, background: 'transparent',
27
- border: 'none', borderBottom: '1px solid #f0f2f5',
28
- cursor: 'pointer', textAlign: 'left',
29
- animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s`,
30
- transition: 'background 0.14s',
31
- }, onMouseEnter: e => e.currentTarget.style.background = '#f8faff', onMouseLeave: e => e.currentTarget.style.background = 'transparent', children: [_jsxs("div", { style: { position: 'relative', flexShrink: 0 }, children: [_jsx("div", { style: {
42
+ }, title: "Blocked users", children: [_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "1.8" }), _jsx("line", { x1: "4.93", y1: "4.93", x2: "19.07", y2: "19.07", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" })] }), "Blocked"] }))] }), _jsx("div", { style: { padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }, children: _jsxs("label", { style: { display: 'block', margin: 0 }, children: [_jsx("span", { style: { position: 'absolute', width: 1, height: 1, padding: 0, overflow: 'hidden', clip: 'rect(0,0,0,0)' }, children: "Search" }), _jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }, children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0, opacity: 0.55 }, children: [_jsx("circle", { cx: "11", cy: "11", r: "7", stroke: "#64748b", strokeWidth: "2" }), _jsx("path", { d: "M20 20l-4-4", stroke: "#64748b", strokeWidth: "2", strokeLinecap: "round" })] }), _jsx("input", { ref: searchRef, type: "search", value: query, onChange: e => setQuery(e.target.value), placeholder: "Search by name\u2026", autoComplete: "off", style: {
43
+ flex: 1,
44
+ minWidth: 0,
45
+ border: 'none',
46
+ outline: 'none',
47
+ background: 'transparent',
48
+ fontSize: 14,
49
+ padding: '10px 0',
50
+ fontFamily: 'inherit',
51
+ color: '#1a2332',
52
+ } })] })] }) }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: filtered.length === 0 ? (_jsx(Empty, { hasQuery: !!query.trim() })) : filtered.map((u, i) => (_jsxs("button", { onClick: () => onSelectUser(u), style: Object.assign(Object.assign({ width: '100%', padding: '13px 18px', display: 'flex', alignItems: 'center', gap: 13, background: 'transparent', border: 'none', borderBottom: '1px solid #f0f2f5', cursor: 'pointer', textAlign: 'left' }, (animateEntrance
53
+ ? { animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s` }
54
+ : {})), { transition: 'background 0.14s' }), onMouseEnter: e => e.currentTarget.style.background = '#f8faff', onMouseLeave: e => e.currentTarget.style.background = 'transparent', children: [_jsxs("div", { style: { position: 'relative', flexShrink: 0 }, children: [_jsx("div", { style: {
32
55
  width: 44, height: 44, borderRadius: '50%',
33
56
  backgroundColor: avatarColor(u.name),
34
57
  display: 'flex', alignItems: 'center', justifyContent: 'center',
@@ -55,4 +78,4 @@ const HomeBtn = ({ onClick }) => (_jsx("button", { type: "button", onClick: onCl
55
78
  width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center',
56
79
  cursor: 'pointer', flexShrink: 0,
57
80
  }, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("path", { d: "M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H4a1 1 0 01-1-1V9.5z", stroke: "#fff", strokeWidth: "2.2", fill: "none", strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M9 21V12h6v9", stroke: "#fff", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" })] }) }));
58
- const Empty = () => (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: "\uD83D\uDC65" }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: "No users available" }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: "Check back later" })] }));
81
+ const Empty = ({ hasQuery }) => (_jsxs("div", { style: { padding: '50px 24px', textAlign: 'center' }, children: [_jsx("div", { style: { fontSize: 36, marginBottom: 10 }, children: hasQuery ? 'πŸ”' : 'πŸ‘₯' }), _jsx("div", { style: { fontWeight: 700, color: '#1a2332', marginBottom: 6 }, children: hasQuery ? 'No matches' : 'No users available' }), _jsx("div", { style: { fontSize: 13, color: '#7b8fa1' }, children: hasQuery ? 'Try a different search' : 'Check back later' })] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ajaxter-chat",
3
- "version": "3.0.10",
3
+ "version": "3.0.12",
4
4
  "description": "Drawer-based chat widget with support chat, tickets, WebRTC calling, voice messages, block list, and transcript download.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,6 +50,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
50
50
  const [chatReturnCtx, setChatReturnCtx] = useState<UserListContext>('conversation');
51
51
  const [viewingTicketId, setViewingTicketId] = useState<string | null>(null);
52
52
  const [messageSoundEnabled, setMessageSoundEnabledState] = useState(true);
53
+ /** Stagger list animation only when opening from home burger menu */
54
+ const [listEntranceAnimation, setListEntranceAnimation] = useState(false);
53
55
 
54
56
  /* App state */
55
57
  const [tickets, setTickets] = useState<Ticket[]>(data?.sampleTickets ?? []);
@@ -172,7 +174,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
172
174
  }, [data?.widget]);
173
175
 
174
176
  /* ── Navigation ──────────────────────────────────────────────────────── */
175
- const handleCardClick = useCallback((ctx: UserListContext | 'ticket') => {
177
+ const handleCardClick = useCallback((ctx: UserListContext | 'ticket', options?: { fromMenu?: boolean }) => {
178
+ setListEntranceAnimation(!!options?.fromMenu);
176
179
  if (ctx === 'ticket') {
177
180
  setActiveTab('tickets');
178
181
  setScreen('tickets');
@@ -183,6 +186,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
183
186
  }, []);
184
187
 
185
188
  const handleNavFromMenu = useCallback((ctx: UserListContext | 'ticket') => {
189
+ setListEntranceAnimation(false);
186
190
  clearChat();
187
191
  if (ctx === 'ticket') {
188
192
  setActiveTab('tickets');
@@ -199,6 +203,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
199
203
  }, []);
200
204
 
201
205
  const handleSelectUser = useCallback((user: ChatUser, returnCtxOverride?: UserListContext) => {
206
+ setListEntranceAnimation(false);
202
207
  setChatReturnCtx(returnCtxOverride ?? userListCtx);
203
208
  const history = data?.sampleChats[user.uid] ?? [];
204
209
  selectUser(user, history);
@@ -211,22 +216,31 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
211
216
  }, [data, selectUser, userListCtx]);
212
217
 
213
218
  const handleBackFromChat = useCallback(() => {
219
+ setListEntranceAnimation(false);
214
220
  clearChat();
215
221
  setUserListCtx(chatReturnCtx);
216
222
  setScreen('user-list');
217
223
  }, [clearChat, chatReturnCtx]);
218
224
 
219
225
  const handleOpenTicket = useCallback((id: string) => {
226
+ setListEntranceAnimation(false);
220
227
  setViewingTicketId(id);
221
228
  setScreen('ticket-detail');
222
229
  setActiveTab('tickets');
223
230
  }, []);
224
231
 
225
232
  const handleTabChange = useCallback((tab: BottomTab) => {
233
+ setListEntranceAnimation(false);
226
234
  setActiveTab(tab);
227
235
  setScreen(tab === 'home' ? 'home' : tab === 'chats' ? 'recent-chats' : 'tickets');
228
236
  }, []);
229
237
 
238
+ useEffect(() => {
239
+ if (!listEntranceAnimation) return;
240
+ const t = window.setTimeout(() => setListEntranceAnimation(false), 520);
241
+ return () => window.clearTimeout(t);
242
+ }, [listEntranceAnimation]);
243
+
230
244
  /* ── Block/Unblock ───────────────────────────────────────────────────── */
231
245
  const handleBlock = useCallback(() => {
232
246
  if (!activeUser) return;
@@ -409,10 +423,10 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
409
423
  </button>
410
424
  )}
411
425
 
412
- {/* ── Backdrop (mobile) ── */}
426
+ {/* ── Backdrop (visual only β€” does not close widget on click) ── */}
413
427
  {isOpen && (
414
428
  <div
415
- onClick={closeDrawer}
429
+ aria-hidden
416
430
  style={{
417
431
  position: 'fixed', inset: 0, zIndex: 9997,
418
432
  backgroundColor: 'rgba(0,0,0,0.35)',
@@ -517,10 +531,11 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
517
531
  users={filteredUsers}
518
532
  primaryColor={primaryColor}
519
533
  viewerType={widgetConfig.viewerType ?? 'user'}
520
- onBack={() => setScreen('home')}
534
+ onBack={() => { setListEntranceAnimation(false); setScreen('home'); }}
521
535
  onSelectUser={handleSelectUser}
522
536
  onBlockList={userListCtx === 'conversation' ? () => setScreen('block-list') : undefined}
523
537
  useHomeHeader={userListCtx === 'support' && widgetConfig.viewerType !== 'developer'}
538
+ animateEntrance={listEntranceAnimation}
524
539
  />
525
540
  )}
526
541
 
@@ -564,6 +579,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
564
579
  chats={recentChats}
565
580
  config={widgetConfig}
566
581
  onSelectChat={u => handleSelectUser(u, listCtxForUser(u, viewerIsDev))}
582
+ animateEntrance={listEntranceAnimation}
567
583
  />
568
584
  )}
569
585
 
@@ -571,8 +587,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
571
587
  <TicketScreen
572
588
  tickets={tickets}
573
589
  config={widgetConfig}
574
- onNewTicket={() => setScreen('ticket-new')}
575
- onSelectTicket={id => { setViewingTicketId(id); setScreen('ticket-detail'); }}
590
+ onNewTicket={() => { setListEntranceAnimation(false); setScreen('ticket-new'); }}
591
+ onSelectTicket={id => {
592
+ setListEntranceAnimation(false);
593
+ setViewingTicketId(id);
594
+ setScreen('ticket-detail');
595
+ }}
596
+ animateEntrance={listEntranceAnimation}
576
597
  />
577
598
  )}
578
599
 
@@ -3,9 +3,14 @@ import { WidgetConfig, UserListContext, Ticket } from '../../types';
3
3
  import { SlideNavMenu } from '../SlideNavMenu';
4
4
  import { truncateWords } from '../../utils/chat';
5
5
 
6
+ export interface HomeNavigateOptions {
7
+ /** When true, list screens play stagger animation (home burger menu only) */
8
+ fromMenu?: boolean;
9
+ }
10
+
6
11
  interface HomeScreenProps {
7
12
  config: WidgetConfig;
8
- onNavigate: (ctx: UserListContext | 'ticket') => void;
13
+ onNavigate: (ctx: UserListContext | 'ticket', options?: HomeNavigateOptions) => void;
9
14
  /** Open a specific pending ticket (full detail) */
10
15
  onOpenTicket: (ticketId: string) => void;
11
16
  tickets: Ticket[];
@@ -46,7 +51,9 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOp
46
51
  primaryColor={config.primaryColor}
47
52
  chatType={config.chatType}
48
53
  viewerType={config.viewerType ?? 'user'}
49
- onSelect={onNavigate}
54
+ onSelect={ctx => {
55
+ onNavigate(ctx, { fromMenu: true });
56
+ }}
50
57
  />
51
58
 
52
59
  {/* Top bar β€” burger left */}
@@ -105,7 +112,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOp
105
112
  </p>
106
113
 
107
114
  {/* Continue Conversations */}
108
- <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Continue Conversations</h2>
115
+ <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>Continue with tickets</h2>
109
116
  <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 28 }}>
110
117
  {pendingTickets.length > 0 ? (
111
118
  pendingTickets.map(t => (
@@ -163,7 +170,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOp
163
170
 
164
171
  {/* Talk to our experts / staff tools */}
165
172
  <h2 style={{ margin: '0 0 12px', fontSize: 15, fontWeight: 800, color: '#0f172a' }}>
166
- {viewerIsDev ? 'Support tools' : 'Talk to our experts'}
173
+ {viewerIsDev ? 'Support tools' : 'Talk to support experts'}
167
174
  </h2>
168
175
 
169
176
  {showSupport && (
@@ -306,7 +313,7 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, onNavigate, onOp
306
313
  fill="#fff"
307
314
  />
308
315
  </svg>
309
- Call Us
316
+ Get Free Widget
310
317
  </button>
311
318
  </div>
312
319
  </div>
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useState, useMemo, useRef, useEffect } from 'react';
2
2
  import { ChatUser, WidgetConfig } from '../../types';
3
3
  import { avatarColor, initials, formatTime } from '../../utils/chat';
4
4
 
@@ -10,54 +10,107 @@ interface RecentChatsScreenProps {
10
10
  chats: RecentChat[];
11
11
  config: WidgetConfig;
12
12
  onSelectChat: (user: ChatUser) => void;
13
+ animateEntrance?: boolean;
13
14
  }
14
15
 
15
- export const RecentChatsScreen: React.FC<RecentChatsScreenProps> = ({ chats, config, onSelectChat }) => (
16
- <div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
17
- <div style={{ background:`linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`, padding:'18px 18px 22px', flexShrink:0 }}>
18
- <h2 style={{ margin:0, fontSize:20, fontWeight:800, color:'#fff', letterSpacing:'-0.02em' }}>Recent Chats</h2>
19
- <p style={{ margin:'3px 0 0', fontSize:12, color:'rgba(255,255,255,0.8)' }}>Your conversation history</p>
20
- </div>
16
+ function matchesChat(chat: RecentChat, q: string): boolean {
17
+ if (!q.trim()) return true;
18
+ const s = q.trim().toLowerCase();
19
+ return (
20
+ chat.user.name.toLowerCase().includes(s) ||
21
+ chat.lastMessage.toLowerCase().includes(s)
22
+ );
23
+ }
24
+
25
+ export const RecentChatsScreen: React.FC<RecentChatsScreenProps> = ({
26
+ chats, config, onSelectChat, animateEntrance = false,
27
+ }) => {
28
+ const [query, setQuery] = useState('');
29
+ const searchRef = useRef<HTMLInputElement>(null);
30
+
31
+ useEffect(() => {
32
+ searchRef.current?.focus();
33
+ }, []);
34
+
35
+ const filtered = useMemo(() => chats.filter(c => matchesChat(c, query)), [chats, query]);
21
36
 
22
- <div style={{ flex:1, overflowY:'auto' }}>
23
- {chats.length === 0 ? (
24
- <div style={{ padding:'50px 24px', textAlign:'center' }}>
25
- <div style={{ fontSize:36, marginBottom:10 }}>πŸ’¬</div>
26
- <div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>No chats yet</div>
27
- <div style={{ fontSize:13, color:'#7b8fa1' }}>Start a conversation from home</div>
37
+ return (
38
+ <div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
39
+ <div style={{ background:`linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`, padding:'18px 18px 14px', flexShrink:0 }}>
40
+ <h2 style={{ margin:0, fontSize:20, fontWeight:800, color:'#fff', letterSpacing:'-0.02em' }}>Recent Chats</h2>
41
+ <p style={{ margin:'3px 0 0', fontSize:12, color:'rgba(255,255,255,0.8)' }}>Your conversation history</p>
42
+ </div>
43
+
44
+ <div style={{ padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }}>
45
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }}>
46
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, opacity: 0.55 }}>
47
+ <circle cx="11" cy="11" r="7" stroke="#64748b" strokeWidth="2" />
48
+ <path d="M20 20l-4-4" stroke="#64748b" strokeWidth="2" strokeLinecap="round" />
49
+ </svg>
50
+ <input
51
+ ref={searchRef}
52
+ type="search"
53
+ value={query}
54
+ onChange={e => setQuery(e.target.value)}
55
+ placeholder="Search chats…"
56
+ autoComplete="off"
57
+ aria-label="Search chats"
58
+ style={{
59
+ flex: 1,
60
+ minWidth: 0,
61
+ border: 'none',
62
+ outline: 'none',
63
+ background: 'transparent',
64
+ fontSize: 14,
65
+ padding: '10px 0',
66
+ fontFamily: 'inherit',
67
+ color: '#1a2332',
68
+ }}
69
+ />
28
70
  </div>
29
- ) : chats.map((chat, i) => (
30
- <button key={chat.id} onClick={() => onSelectChat(chat.user)} style={{
31
- width:'100%', padding:'13px 16px', display:'flex', alignItems:'center', gap:13,
32
- background:'transparent', border:'none', borderBottom:'1px solid #f0f2f5',
33
- cursor:'pointer', textAlign:'left', animation:`cw-fadeUp 0.28s ease both`, animationDelay:`${i*0.05}s`,
34
- transition:'background 0.14s',
35
- }}
36
- onMouseEnter={e=>(e.currentTarget as HTMLElement).style.background='#f8faff'}
37
- onMouseLeave={e=>(e.currentTarget as HTMLElement).style.background='transparent'}
38
- >
39
- <div style={{ position:'relative', flexShrink:0 }}>
40
- <div style={{ width:46, height:46, borderRadius:'50%', backgroundColor:avatarColor(chat.user.name), display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontWeight:700, fontSize:15 }}>
41
- {initials(chat.user.name)}
42
- </div>
43
- {chat.unread > 0 && (
44
- <span style={{ position:'absolute', top:-2, right:-2, width:18, height:18, borderRadius:'50%', background:'#ef4444', color:'#fff', fontSize:10, fontWeight:700, display:'flex', alignItems:'center', justifyContent:'center', border:'2px solid #fff' }}>
45
- {chat.unread}
46
- </span>
47
- )}
71
+ </div>
72
+
73
+ <div style={{ flex:1, overflowY:'auto' }}>
74
+ {filtered.length === 0 ? (
75
+ <div style={{ padding:'50px 24px', textAlign:'center' }}>
76
+ <div style={{ fontSize:36, marginBottom:10 }}>{query.trim() ? 'πŸ”' : 'πŸ’¬'}</div>
77
+ <div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>{query.trim() ? 'No matches' : 'No chats yet'}</div>
78
+ <div style={{ fontSize:13, color:'#7b8fa1' }}>{query.trim() ? 'Try a different search' : 'Start a conversation from home'}</div>
48
79
  </div>
49
- <div style={{ flex:1, minWidth:0 }}>
50
- <div style={{ display:'flex', justifyContent:'space-between', marginBottom:3 }}>
51
- <span style={{ fontWeight:700, fontSize:14, color:'#1a2332' }}>{chat.user.name}</span>
52
- <span style={{ fontSize:11, color:'#b0bec5' }}>{formatTime(chat.lastTime)}</span>
80
+ ) : filtered.map((chat, i) => (
81
+ <button key={chat.id} onClick={() => onSelectChat(chat.user)} style={{
82
+ width:'100%', padding:'13px 16px', display:'flex', alignItems:'center', gap:13,
83
+ background:'transparent', border:'none', borderBottom:'1px solid #f0f2f5',
84
+ cursor:'pointer', textAlign:'left',
85
+ ...(animateEntrance ? { animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s` } : {}),
86
+ transition:'background 0.14s',
87
+ }}
88
+ onMouseEnter={e=>(e.currentTarget as HTMLElement).style.background='#f8faff'}
89
+ onMouseLeave={e=>(e.currentTarget as HTMLElement).style.background='transparent'}
90
+ >
91
+ <div style={{ position:'relative', flexShrink:0 }}>
92
+ <div style={{ width:46, height:46, borderRadius:'50%', backgroundColor:avatarColor(chat.user.name), display:'flex', alignItems:'center', justifyContent:'center', color:'#fff', fontWeight:700, fontSize:15 }}>
93
+ {initials(chat.user.name)}
94
+ </div>
95
+ {chat.unread > 0 && (
96
+ <span style={{ position:'absolute', top:-2, right:-2, width:18, height:18, borderRadius:'50%', background:'#ef4444', color:'#fff', fontSize:10, fontWeight:700, display:'flex', alignItems:'center', justifyContent:'center', border:'2px solid #fff' }}>
97
+ {chat.unread}
98
+ </span>
99
+ )}
53
100
  </div>
54
- <div style={{ fontSize:13, color:'#7b8fa1', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', display:'flex', alignItems:'center', gap:5 }}>
55
- {chat.isPaused && <span style={{ fontSize:10, background:'#fef3c7', color:'#92400e', padding:'1px 5px', borderRadius:4, fontWeight:700 }}>PAUSED</span>}
56
- {chat.lastMessage}
101
+ <div style={{ flex:1, minWidth:0 }}>
102
+ <div style={{ display:'flex', justifyContent:'space-between', marginBottom:3 }}>
103
+ <span style={{ fontWeight:700, fontSize:14, color:'#1a2332' }}>{chat.user.name}</span>
104
+ <span style={{ fontSize:11, color:'#b0bec5' }}>{formatTime(chat.lastTime)}</span>
105
+ </div>
106
+ <div style={{ fontSize:13, color:'#7b8fa1', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap', display:'flex', alignItems:'center', gap:5 }}>
107
+ {chat.isPaused && <span style={{ fontSize:10, background:'#fef3c7', color:'#92400e', padding:'1px 5px', borderRadius:4, fontWeight:700 }}>PAUSED</span>}
108
+ {chat.lastMessage}
109
+ </div>
57
110
  </div>
58
- </div>
59
- </button>
60
- ))}
111
+ </button>
112
+ ))}
113
+ </div>
61
114
  </div>
62
- </div>
63
- );
115
+ );
116
+ };
@@ -70,8 +70,8 @@ export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSu
70
70
  </svg>
71
71
  </button>
72
72
  <div>
73
- <h2 style={{ margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }}>New ticket</h2>
74
- <p style={{ margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }}>Describe your issue below</p>
73
+ <h2 style={{ margin: 0, fontSize: 18, fontWeight: 800, color: '#fff' }}>Raise a ticket</h2>
74
+ <p style={{ margin: '4px 0 0', fontSize: 12, color: 'rgba(255,255,255,0.85)' }}>Please fill out the ticket form below and we will get back to you as soon as possible.</p>
75
75
  </div>
76
76
  </div>
77
77
 
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useState, useMemo, useRef, useEffect } from 'react';
2
2
  import { Ticket, WidgetConfig } from '../../types';
3
3
 
4
4
  interface TicketScreenProps {
@@ -6,9 +6,31 @@ interface TicketScreenProps {
6
6
  config: WidgetConfig;
7
7
  onNewTicket: () => void;
8
8
  onSelectTicket:(id: string) => void;
9
+ animateEntrance?: boolean;
9
10
  }
10
11
 
11
- export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onNewTicket, onSelectTicket }) => {
12
+ function matchesTicket(t: Ticket, q: string): boolean {
13
+ if (!q.trim()) return true;
14
+ const s = q.trim().toLowerCase();
15
+ return (
16
+ t.title.toLowerCase().includes(s) ||
17
+ t.description.toLowerCase().includes(s) ||
18
+ t.id.toLowerCase().includes(s)
19
+ );
20
+ }
21
+
22
+ export const TicketScreen: React.FC<TicketScreenProps> = ({
23
+ tickets, config, onNewTicket, onSelectTicket, animateEntrance = false,
24
+ }) => {
25
+ const [query, setQuery] = useState('');
26
+ const searchRef = useRef<HTMLInputElement>(null);
27
+
28
+ useEffect(() => {
29
+ searchRef.current?.focus();
30
+ }, []);
31
+
32
+ const filtered = useMemo(() => tickets.filter(t => matchesTicket(t, query)), [tickets, query]);
33
+
12
34
  const sm: Record<Ticket['status'], { label: string; bg: string; color: string }> = {
13
35
  open: { label:'Open', bg:`${config.primaryColor}14`, color: config.primaryColor },
14
36
  'in-progress': { label:'In Progress', bg:'#fef3c7', color:'#d97706' },
@@ -26,10 +48,10 @@ export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onN
26
48
  <div style={{ display:'flex', flexDirection:'column', height:'100%' }}>
27
49
  <div style={{
28
50
  background:`linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
29
- padding:'18px 18px 22px', flexShrink:0, position:'relative',
51
+ padding:'18px 18px 14px', flexShrink:0, position:'relative',
30
52
  }}>
31
- <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start' }}>
32
- <div>
53
+ <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', gap: 12 }}>
54
+ <div style={{ minWidth: 0 }}>
33
55
  <h2 style={{ margin:0, fontSize:20, fontWeight:800, color:'#fff', letterSpacing:'-0.02em' }}>Tickets</h2>
34
56
  <p style={{ margin:'3px 0 0', fontSize:12, color:'rgba(255,255,255,0.8)' }}>
35
57
  {tickets.length} ticket{tickets.length!==1?'s':''} raised
@@ -39,27 +61,57 @@ export const TicketScreen: React.FC<TicketScreenProps> = ({ tickets, config, onN
39
61
  background:'rgba(255,255,255,0.22)', border:'none', borderRadius:20,
40
62
  padding:'7px 14px', color:'#fff', fontWeight:700, fontSize:13,
41
63
  cursor:'pointer', display:'flex', alignItems:'center', gap:5,
64
+ flexShrink: 0,
42
65
  }}>
43
66
  + New
44
67
  </button>
45
68
  </div>
46
69
  </div>
47
70
 
71
+ <div style={{ padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }}>
72
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }}>
73
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, opacity: 0.55 }}>
74
+ <circle cx="11" cy="11" r="7" stroke="#64748b" strokeWidth="2" />
75
+ <path d="M20 20l-4-4" stroke="#64748b" strokeWidth="2" strokeLinecap="round" />
76
+ </svg>
77
+ <input
78
+ ref={searchRef}
79
+ type="search"
80
+ value={query}
81
+ onChange={e => setQuery(e.target.value)}
82
+ placeholder="Search tickets…"
83
+ autoComplete="off"
84
+ aria-label="Search tickets"
85
+ style={{
86
+ flex: 1,
87
+ minWidth: 0,
88
+ border: 'none',
89
+ outline: 'none',
90
+ background: 'transparent',
91
+ fontSize: 14,
92
+ padding: '10px 0',
93
+ fontFamily: 'inherit',
94
+ color: '#1a2332',
95
+ }}
96
+ />
97
+ </div>
98
+ </div>
99
+
48
100
  <div style={{ flex:1, overflowY:'auto' }}>
49
- {tickets.length === 0 ? (
101
+ {filtered.length === 0 ? (
50
102
  <div style={{ padding:'50px 24px', textAlign:'center' }}>
51
- <div style={{ fontSize:36, marginBottom:10 }}>🎫</div>
52
- <div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>No tickets yet</div>
53
- <div style={{ fontSize:13, color:'#7b8fa1' }}>Raise a ticket for major issues</div>
103
+ <div style={{ fontSize:36, marginBottom:10 }}>{query.trim() ? 'πŸ”' : '🎫'}</div>
104
+ <div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>{query.trim() ? 'No matches' : 'No tickets yet'}</div>
105
+ <div style={{ fontSize:13, color:'#7b8fa1' }}>{query.trim() ? 'Try a different search' : 'Raise a ticket for major issues'}</div>
54
106
  </div>
55
- ) : tickets.map((t, i) => (
107
+ ) : filtered.map((t, i) => (
56
108
  <button
57
109
  key={t.id}
58
110
  type="button"
59
111
  onClick={() => onSelectTicket(t.id)}
60
112
  style={{
61
113
  width:'100%', padding:'14px 16px', borderBottom:'1px solid #f0f2f5',
62
- animation:`cw-fadeUp 0.3s ease both`, animationDelay:`${i*0.05}s`,
114
+ ...(animateEntrance ? { animation: `cw-fadeUp 0.3s ease both`, animationDelay: `${i * 0.05}s` } : {}),
63
115
  background:'transparent', borderLeft:'none', borderRight:'none', borderTop:'none',
64
116
  cursor:'pointer', textAlign:'left', fontFamily:'inherit',
65
117
  }}
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useState, useMemo, useRef, useEffect } from 'react';
2
2
  import { ChatUser, UserListContext } from '../../types';
3
3
  import { avatarColor, initials } from '../../utils/chat';
4
4
 
@@ -14,12 +14,35 @@ interface UserListScreenProps {
14
14
  onBlockList?: () => void;
15
15
  /** β€œNeed Support” (user β†’ agents): show home icon instead of back arrow */
16
16
  useHomeHeader?: boolean;
17
+ /** Stagger animation β€” only when opening from home burger menu */
18
+ animateEntrance?: boolean;
19
+ }
20
+
21
+ function matchesUser(u: ChatUser, q: string): boolean {
22
+ if (!q.trim()) return true;
23
+ const s = q.trim().toLowerCase();
24
+ return (
25
+ u.name.toLowerCase().includes(s) ||
26
+ u.email.toLowerCase().includes(s) ||
27
+ u.designation.toLowerCase().includes(s) ||
28
+ u.project.toLowerCase().includes(s)
29
+ );
17
30
  }
18
31
 
19
32
  export const UserListScreen: React.FC<UserListScreenProps> = ({
20
33
  context, users, primaryColor, viewerType = 'user', onBack, onSelectUser, onBlockList,
21
34
  useHomeHeader = false,
35
+ animateEntrance = false,
22
36
  }) => {
37
+ const [query, setQuery] = useState('');
38
+ const searchRef = useRef<HTMLInputElement>(null);
39
+
40
+ useEffect(() => {
41
+ searchRef.current?.focus();
42
+ }, []);
43
+
44
+ const filtered = useMemo(() => users.filter(u => matchesUser(u, query)), [users, query]);
45
+
23
46
  const isStaff = viewerType === 'developer';
24
47
  const title = context === 'support'
25
48
  ? (isStaff ? 'Provide Support' : 'Need Support')
@@ -28,8 +51,10 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
28
51
  ? (isStaff ? 'All chat users β€” choose who to help' : 'Choose a support agent')
29
52
  : (isStaff ? 'Chat with another developer or coordinate handoff' : 'Choose a colleague');
30
53
 
54
+ const rootAnim = animateEntrance ? 'cw-slideIn 0.22s ease' : undefined;
55
+
31
56
  return (
32
- <div style={{ display:'flex', flexDirection:'column', height:'100%', animation:'cw-slideIn 0.22s ease' }}>
57
+ <div style={{ display:'flex', flexDirection:'column', height:'100%', animation: rootAnim }}>
33
58
  {/* Header */}
34
59
  <div style={{ background:`linear-gradient(135deg,${primaryColor},${primaryColor}cc)`, padding:'14px 18px', display:'flex', alignItems:'center', gap:12, flexShrink:0, position:'relative' }}>
35
60
  {useHomeHeader ? <HomeBtn onClick={onBack} /> : <BackBtn onClick={onBack} />}
@@ -66,11 +91,42 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
66
91
  )}
67
92
  </div>
68
93
 
94
+ <div style={{ padding: '10px 14px', background: '#fff', borderBottom: '1px solid #eef0f5', flexShrink: 0 }}>
95
+ <label style={{ display: 'block', margin: 0 }}>
96
+ <span style={{ position: 'absolute', width: 1, height: 1, padding: 0, overflow: 'hidden', clip: 'rect(0,0,0,0)' }}>Search</span>
97
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '0 12px', borderRadius: 10, border: '1.5px solid #e5e7eb', background: '#f8fafc' }}>
98
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0, opacity: 0.55 }}>
99
+ <circle cx="11" cy="11" r="7" stroke="#64748b" strokeWidth="2" />
100
+ <path d="M20 20l-4-4" stroke="#64748b" strokeWidth="2" strokeLinecap="round" />
101
+ </svg>
102
+ <input
103
+ ref={searchRef}
104
+ type="search"
105
+ value={query}
106
+ onChange={e => setQuery(e.target.value)}
107
+ placeholder="Search by name…"
108
+ autoComplete="off"
109
+ style={{
110
+ flex: 1,
111
+ minWidth: 0,
112
+ border: 'none',
113
+ outline: 'none',
114
+ background: 'transparent',
115
+ fontSize: 14,
116
+ padding: '10px 0',
117
+ fontFamily: 'inherit',
118
+ color: '#1a2332',
119
+ }}
120
+ />
121
+ </div>
122
+ </label>
123
+ </div>
124
+
69
125
  {/* User list */}
70
126
  <div style={{ flex:1, overflowY:'auto' }}>
71
- {users.length === 0 ? (
72
- <Empty />
73
- ) : users.map((u, i) => (
127
+ {filtered.length === 0 ? (
128
+ <Empty hasQuery={!!query.trim()} />
129
+ ) : filtered.map((u, i) => (
74
130
  <button
75
131
  key={u.uid}
76
132
  onClick={() => onSelectUser(u)}
@@ -79,13 +135,14 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
79
135
  alignItems:'center', gap:13, background:'transparent',
80
136
  border:'none', borderBottom:'1px solid #f0f2f5',
81
137
  cursor:'pointer', textAlign:'left',
82
- animation:`cw-fadeUp 0.28s ease both`, animationDelay:`${i*0.05}s`,
138
+ ...(animateEntrance
139
+ ? { animation: `cw-fadeUp 0.28s ease both`, animationDelay: `${i * 0.05}s` }
140
+ : {}),
83
141
  transition:'background 0.14s',
84
142
  }}
85
143
  onMouseEnter={e => (e.currentTarget as HTMLElement).style.background = '#f8faff'}
86
144
  onMouseLeave={e => (e.currentTarget as HTMLElement).style.background = 'transparent'}
87
145
  >
88
- {/* Avatar with online dot */}
89
146
  <div style={{ position:'relative', flexShrink:0 }}>
90
147
  <div style={{
91
148
  width:44, height:44, borderRadius:'50%',
@@ -99,14 +156,12 @@ export const UserListScreen: React.FC<UserListScreenProps> = ({
99
156
  backgroundColor: u.status==='online' ? '#22c55e' : u.status==='away' ? '#f59e0b' : '#d1d5db',
100
157
  }} />
101
158
  </div>
102
- {/* Info */}
103
159
  <div style={{ flex:1, minWidth:0 }}>
104
160
  <div style={{ fontWeight:700, fontSize:14, color:'#1a2332', marginBottom:2 }}>{u.name}</div>
105
161
  <div style={{ fontSize:12, color:'#7b8fa1', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
106
162
  {u.designation} Β· {u.project}
107
163
  </div>
108
164
  </div>
109
- {/* Type badge */}
110
165
  <span style={{
111
166
  fontSize:10, fontWeight:700, padding:'3px 9px', borderRadius:20,
112
167
  textTransform:'uppercase', letterSpacing:'0.05em', flexShrink:0,
@@ -147,10 +202,10 @@ const HomeBtn: React.FC<{ onClick: () => void }> = ({ onClick }) => (
147
202
  </button>
148
203
  );
149
204
 
150
- const Empty: React.FC = () => (
205
+ const Empty: React.FC<{ hasQuery: boolean }> = ({ hasQuery }) => (
151
206
  <div style={{ padding:'50px 24px', textAlign:'center' }}>
152
- <div style={{ fontSize:36, marginBottom:10 }}>πŸ‘₯</div>
153
- <div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>No users available</div>
154
- <div style={{ fontSize:13, color:'#7b8fa1' }}>Check back later</div>
207
+ <div style={{ fontSize:36, marginBottom:10 }}>{hasQuery ? 'πŸ”' : 'πŸ‘₯'}</div>
208
+ <div style={{ fontWeight:700, color:'#1a2332', marginBottom:6 }}>{hasQuery ? 'No matches' : 'No users available'}</div>
209
+ <div style={{ fontSize:13, color:'#7b8fa1' }}>{hasQuery ? 'Try a different search' : 'Check back later'}</div>
155
210
  </div>
156
211
  );