ajaxter-chat 3.0.9 → 3.0.11

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.
@@ -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,9 +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}
537
+ useHomeHeader={userListCtx === 'support' && widgetConfig.viewerType !== 'developer'}
538
+ animateEntrance={listEntranceAnimation}
523
539
  />
524
540
  )}
525
541
 
@@ -563,6 +579,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
563
579
  chats={recentChats}
564
580
  config={widgetConfig}
565
581
  onSelectChat={u => handleSelectUser(u, listCtxForUser(u, viewerIsDev))}
582
+ animateEntrance={listEntranceAnimation}
566
583
  />
567
584
  )}
568
585
 
@@ -570,8 +587,13 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme }) =>
570
587
  <TicketScreen
571
588
  tickets={tickets}
572
589
  config={widgetConfig}
573
- onNewTicket={() => setScreen('ticket-new')}
574
- 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}
575
597
  />
576
598
  )}
577
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
+ };
@@ -39,7 +39,7 @@ export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSu
39
39
  };
40
40
 
41
41
  return (
42
- <div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#fff' }}>
42
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: '#fff', minHeight: 0 }}>
43
43
  <div
44
44
  style={{
45
45
  background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
@@ -70,12 +70,12 @@ 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
 
78
- <div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px' }}>
78
+ <div className="cw-scroll" style={{ flex: 1, overflowY: 'auto', padding: '20px 18px', minHeight: 0 }}>
79
79
  <input
80
80
  placeholder="Title *"
81
81
  value={title}
@@ -93,7 +93,7 @@ export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSu
93
93
  onFocus={e => (e.target.style.borderColor = config.primaryColor)}
94
94
  onBlur={e => (e.target.style.borderColor = '#e5e7eb')}
95
95
  />
96
- <div style={{ display: 'flex', gap: 8, marginTop: 12, marginBottom: 20 }}>
96
+ <div style={{ display: 'flex', gap: 8, marginTop: 12, paddingBottom: 8 }}>
97
97
  {(['low', 'medium', 'high'] as Ticket['priority'][]).map(p => (
98
98
  <button
99
99
  key={p}
@@ -116,13 +116,24 @@ export const TicketFormScreen: React.FC<TicketFormScreenProps> = ({ config, onSu
116
116
  </button>
117
117
  ))}
118
118
  </div>
119
+ </div>
120
+
121
+ <div
122
+ style={{
123
+ flexShrink: 0,
124
+ padding: '12px 18px 18px',
125
+ borderTop: '1px solid #eef0f5',
126
+ background: '#fff',
127
+ boxShadow: '0 -4px 20px rgba(15,23,42,0.06)',
128
+ }}
129
+ >
119
130
  <button
120
131
  type="button"
121
132
  onClick={handleSubmit}
122
133
  disabled={!title.trim()}
123
134
  style={{
124
135
  width: '100%',
125
- padding: '12px',
136
+ padding: '14px',
126
137
  borderRadius: 10,
127
138
  border: 'none',
128
139
  background: title.trim() ? config.primaryColor : '#e5e7eb',
@@ -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
  }}