ajaxter-chat 3.0.16 → 3.0.17

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.
@@ -8,6 +8,8 @@ interface CallScreenProps {
8
8
  onToggleMute: () => void;
9
9
  onToggleCamera: () => void;
10
10
  primaryColor: string;
11
+ /** Collapse the drawer while keeping the call active (mic/cam stay on). */
12
+ onMinimize?: () => void;
11
13
  }
12
14
  export declare const CallScreen: React.FC<CallScreenProps>;
13
15
  export {};
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { avatarColor, initials } from '../../utils/chat';
4
- export const CallScreen = ({ session, localVideoRef, remoteVideoRef, onEnd, onToggleMute, onToggleCamera, primaryColor, }) => {
4
+ export const CallScreen = ({ session, localVideoRef, remoteVideoRef, onEnd, onToggleMute, onToggleCamera, primaryColor, onMinimize, }) => {
5
5
  const [duration, setDuration] = useState(0);
6
6
  const peer = session.peer;
7
7
  useEffect(() => {
@@ -25,7 +25,17 @@ export const CallScreen = ({ session, localVideoRef, remoteVideoRef, onEnd, onTo
25
25
  objectFit: 'cover', border: '2px solid rgba(255,255,255,0.3)',
26
26
  display: session.isCameraOn ? 'block' : 'none',
27
27
  zIndex: 10,
28
- } }), _jsxs("div", { style: { position: 'relative', zIndex: 5, display: 'flex', flexDirection: 'column', height: '100%', background: 'rgba(0,0,0,0.35)' }, children: [_jsx("div", { style: { padding: '16px 18px', display: 'flex', alignItems: 'center', gap: 10 }, children: _jsx("div", { style: { flex: 1 }, children: _jsxs("div", { style: { fontWeight: 700, fontSize: 15, color: '#fff' }, children: [session.state === 'calling' && 'Calling...', session.state === 'connected' && 'Connected', session.state === 'ended' && 'Call Ended'] }) }) }), _jsx("div", { style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16 }, children: peer && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
28
+ } }), _jsxs("div", { style: { position: 'relative', zIndex: 5, display: 'flex', flexDirection: 'column', height: '100%', background: 'rgba(0,0,0,0.35)' }, children: [_jsxs("div", { style: { padding: '16px 18px', display: 'flex', alignItems: 'center', gap: 10 }, children: [_jsx("div", { style: { flex: 1 }, children: _jsxs("div", { style: { fontWeight: 700, fontSize: 15, color: '#fff' }, children: [session.state === 'calling' && 'Calling...', session.state === 'connected' && 'Connected', session.state === 'ended' && 'Call Ended'] }) }), (session.state === 'calling' || session.state === 'connected') && onMinimize && (_jsx("button", { type: "button", onClick: onMinimize, title: "Minimize \u2014 keep call while you use the page", style: {
29
+ padding: '8px 12px',
30
+ borderRadius: 10,
31
+ border: '1px solid rgba(255,255,255,0.35)',
32
+ background: 'rgba(0,0,0,0.25)',
33
+ color: '#fff',
34
+ fontSize: 13,
35
+ fontWeight: 600,
36
+ cursor: 'pointer',
37
+ flexShrink: 0,
38
+ }, children: "Minimize" }))] }), _jsx("div", { style: { flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 16 }, children: peer && (_jsxs(_Fragment, { children: [_jsx("div", { style: {
29
39
  width: 90, height: 90, borderRadius: '50%',
30
40
  backgroundColor: avatarColor(peer.name),
31
41
  display: 'flex', alignItems: 'center', justifyContent: 'center',
@@ -256,32 +256,32 @@ export const ChatScreen = ({ activeUser, messages, config, isPaused, isReported,
256
256
  return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', animation: 'cw-slideIn 0.22s ease', position: 'relative', overflow: 'hidden' }, children: [_jsxs("div", { style: {
257
257
  background: `linear-gradient(135deg,${config.primaryColor},${config.primaryColor}cc)`,
258
258
  padding: '10px 12px', display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0,
259
- }, children: [_jsx("button", { type: "button", onClick: onBack, style: hdrBtn, "aria-label": "Back", children: _jsx("svg", { width: "16", height: "16", 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", { style: { width: 36, height: 36, borderRadius: '50%', backgroundColor: peerAvatar, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 13, flexShrink: 0, position: 'relative' }, children: [peerInit, _jsx("span", { style: { position: 'absolute', bottom: 0, right: 0, width: 9, height: 9, borderRadius: '50%', border: '2px solid', borderColor: 'transparent', backgroundColor: activeUser.status === 'online' ? '#22c55e' : activeUser.status === 'away' ? '#f59e0b' : '#9ca3af' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), onToggleMessageSound && (_jsxs("label", { style: {
259
+ }, children: [_jsx("button", { type: "button", onClick: onBack, style: hdrBtn, "aria-label": "Back", children: _jsx("svg", { width: "16", height: "16", 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", { style: { width: 36, height: 36, borderRadius: '50%', backgroundColor: peerAvatar, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 700, fontSize: 13, flexShrink: 0, position: 'relative' }, children: [peerInit, _jsx("span", { style: { position: 'absolute', bottom: 0, right: 0, width: 9, height: 9, borderRadius: '50%', border: '2px solid', borderColor: 'transparent', backgroundColor: activeUser.status === 'online' ? '#22c55e' : activeUser.status === 'away' ? '#f59e0b' : '#9ca3af' } })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, color: '#fff', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: activeUser.name }), _jsx("div", { style: { fontSize: 11, color: 'rgba(255,255,255,0.8)' }, children: activeUser.designation })] }), _jsx("span", { style: { fontSize: 13, fontWeight: 700, color: '#fff', opacity: 0.95, flexShrink: 0 }, children: headerRole }), onToggleMessageSound && (_jsx("label", { style: {
260
260
  display: 'flex',
261
261
  alignItems: 'center',
262
262
  gap: 6,
263
263
  cursor: 'pointer',
264
264
  flexShrink: 0,
265
265
  marginLeft: 4,
266
- }, children: [_jsx("span", { style: { fontSize: 10, color: 'rgba(255,255,255,0.85)', fontWeight: 600 }, children: "Sound" }), _jsx("button", { type: "button", role: "switch", "aria-checked": messageSoundEnabled, onClick: () => onToggleMessageSound(!messageSoundEnabled), style: {
267
- width: 36,
268
- height: 20,
269
- borderRadius: 10,
270
- border: 'none',
271
- background: messageSoundEnabled ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.2)',
272
- position: 'relative',
273
- cursor: 'pointer',
274
- padding: 0,
275
- }, children: _jsx("span", { style: {
276
- position: 'absolute',
277
- top: 2,
278
- left: messageSoundEnabled ? 18 : 2,
279
- width: 16,
280
- height: 16,
281
- borderRadius: '50%',
282
- background: '#fff',
283
- transition: 'left 0.15s ease',
284
- } }) })] })), config.allowWebCall && (_jsx("button", { type: "button", onClick: () => onStartCall(false), style: hdrBtn, title: "Voice Call", children: _jsx("svg", { width: "17", height: "17", 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" }) }) })), _jsx("button", { type: "button", onClick: () => setShowMenu(v => !v), style: Object.assign(Object.assign({}, hdrBtn), { background: 'rgba(255,255,255,0.2)' }), title: "More options", "aria-expanded": showMenu, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "19", r: "1.5", fill: "#fff" })] }) })] }), showMenu && (_jsxs("div", { style: { position: 'absolute', top: 52, right: 12, zIndex: 120, background: '#fff', borderRadius: 12, boxShadow: '0 8px 30px rgba(0,0,0,0.16)', padding: '6px', minWidth: 200, animation: 'cw-fadeUp 0.18s ease' }, children: [navEntriesForChat(config.chatType, viewerIsDev).map(item => (_jsx(MenuItem, { icon: item.icon, label: item.label, onClick: () => { setShowMenu(false); onNavAction(item.key); } }, item.key))), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), config.allowTranscriptDownload && (_jsx(MenuItem, { icon: "\uD83D\uDCE5", label: "Download Transcript", onClick: handleTranscript })), viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx(MenuItem, { icon: "\uD83D\uDD00", label: "Transfer to developer", onClick: () => { setShowMenu(false); setTransferOpen(true); } })), _jsx(MenuItem, { icon: isPaused ? '▶️' : '⏸', label: isPaused ? 'Resume Chat' : 'Pause Chat', onClick: () => { setShowMenu(false); setShowConfirm('pause'); } }), config.allowReport && !isReported && (_jsx(MenuItem, { icon: "\u26A0\uFE0F", label: "Report Chat", onClick: () => { setShowMenu(false); setShowConfirm('report'); } })), config.allowBlock && activeUser.type === 'user' && !isBlocked && (_jsx(MenuItem, { icon: "\uD83D\uDEAB", label: "Block User", onClick: () => { setShowMenu(false); setShowConfirm('block'); }, danger: true })), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), _jsx(MenuItem, { icon: "\u2715", label: "Close Chat", onClick: onClose })] })), isPaused && (_jsxs("div", { style: { background: '#fef3c7', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#92400e', textAlign: 'center', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }, children: ["\u23F8 Chat is paused \u2014 users cannot send messages", _jsx("button", { type: "button", onClick: onTogglePause, style: { background: '#92400e', color: '#fff', border: 'none', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', marginLeft: 4 }, children: "Resume" })] })), isBlocked && (_jsx("div", { style: { background: '#fee2e2', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#991b1b', textAlign: 'center', flexShrink: 0 }, children: "\uD83D\uDEAB This user is blocked" })), isReported && (_jsx("div", { style: { background: '#fef3c7', padding: '6px 16px', fontSize: 11, color: '#92400e', textAlign: 'center', flexShrink: 0 }, children: "\u26A0\uFE0F This chat has been reported" })), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '14px', display: 'flex', flexDirection: 'column', gap: 10, background: '#f8f9fc' }, className: "cw-scroll", children: [grouped.map(({ date, msgs }) => (_jsxs(React.Fragment, { children: [_jsx(DateDivider, { label: date }), msgs.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: config.primaryColor }, msg.id)))] }, date))), messages.length === 0 && (_jsxs("div", { style: { margin: 'auto', textAlign: 'center', color: '#c4cad4', fontSize: 13 }, children: [_jsx("div", { style: { fontSize: 28, marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hello to ", activeUser.name, "!"] })), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: { borderTop: '1px solid #eef0f5', padding: '10px 12px 8px', background: '#fff', flexShrink: 0, position: 'relative' }, children: [privacyEnabled && showPrivacy && (_jsxs("div", { style: {
266
+ }, children: _jsx("button", { type: "button", role: "switch", "aria-checked": messageSoundEnabled, onClick: () => onToggleMessageSound(!messageSoundEnabled), "aria-label": "Toggle message sound", title: "Toggle message sound", style: {
267
+ width: 36,
268
+ height: 20,
269
+ borderRadius: 10,
270
+ border: 'none',
271
+ background: messageSoundEnabled ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.2)',
272
+ position: 'relative',
273
+ cursor: 'pointer',
274
+ padding: 0,
275
+ }, children: _jsx("span", { style: {
276
+ position: 'absolute',
277
+ top: 2,
278
+ left: messageSoundEnabled ? 18 : 2,
279
+ width: 16,
280
+ height: 16,
281
+ borderRadius: '50%',
282
+ background: '#fff',
283
+ transition: 'left 0.15s ease',
284
+ } }) }) })), config.allowWebCall && (_jsx("button", { type: "button", onClick: () => onStartCall(false), style: hdrBtn, title: "Voice Call", children: _jsx("svg", { width: "17", height: "17", 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" }) }) })), _jsx("button", { type: "button", onClick: () => setShowMenu(v => !v), style: Object.assign(Object.assign({}, hdrBtn), { background: 'rgba(255,255,255,0.2)' }), title: "More options", "aria-expanded": showMenu, children: _jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: [_jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "#fff" }), _jsx("circle", { cx: "12", cy: "19", r: "1.5", fill: "#fff" })] }) })] }), showMenu && (_jsxs("div", { style: { position: 'absolute', top: 52, right: 12, zIndex: 120, background: '#fff', borderRadius: 12, boxShadow: '0 8px 30px rgba(0,0,0,0.16)', padding: '6px', minWidth: 200, animation: 'cw-fadeUp 0.18s ease' }, children: [navEntriesForChat(config.chatType, viewerIsDev).map(item => (_jsx(MenuItem, { icon: item.icon, label: item.label, onClick: () => { setShowMenu(false); onNavAction(item.key); } }, item.key))), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), config.allowTranscriptDownload && (_jsx(MenuItem, { icon: "\uD83D\uDCE5", label: "Download Transcript", onClick: handleTranscript })), viewerIsDev && activeUser.type === 'user' && otherDevelopers.length > 0 && onTransferToDeveloper && (_jsx(MenuItem, { icon: "\uD83D\uDD00", label: "Transfer to developer", onClick: () => { setShowMenu(false); setTransferOpen(true); } })), _jsx(MenuItem, { icon: isPaused ? '▶️' : '⏸', label: isPaused ? 'Resume Chat' : 'Pause Chat', onClick: () => { setShowMenu(false); setShowConfirm('pause'); } }), config.allowReport && !isReported && (_jsx(MenuItem, { icon: "\u26A0\uFE0F", label: "Report Chat", onClick: () => { setShowMenu(false); setShowConfirm('report'); } })), config.allowBlock && activeUser.type === 'user' && !isBlocked && (_jsx(MenuItem, { icon: "\uD83D\uDEAB", label: "Block User", onClick: () => { setShowMenu(false); setShowConfirm('block'); }, danger: true })), _jsx("div", { style: { borderTop: '1px solid #f0f2f5', margin: '4px 0' } }), _jsx(MenuItem, { icon: "\u2715", label: "Close Chat", onClick: onClose })] })), isPaused && (_jsxs("div", { style: { background: '#fef3c7', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#92400e', textAlign: 'center', flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }, children: ["\u23F8 Chat is paused \u2014 users cannot send messages", _jsx("button", { type: "button", onClick: onTogglePause, style: { background: '#92400e', color: '#fff', border: 'none', borderRadius: 6, padding: '2px 8px', fontSize: 11, cursor: 'pointer', marginLeft: 4 }, children: "Resume" })] })), isBlocked && (_jsx("div", { style: { background: '#fee2e2', padding: '8px 16px', fontSize: 12, fontWeight: 600, color: '#991b1b', textAlign: 'center', flexShrink: 0 }, children: "\uD83D\uDEAB This user is blocked" })), isReported && (_jsx("div", { style: { background: '#fef3c7', padding: '6px 16px', fontSize: 11, color: '#92400e', textAlign: 'center', flexShrink: 0 }, children: "\u26A0\uFE0F This chat has been reported" })), _jsxs("div", { style: { flex: 1, overflowY: 'auto', padding: '14px', display: 'flex', flexDirection: 'column', gap: 10, background: '#f8f9fc' }, className: "cw-scroll", children: [grouped.map(({ date, msgs }) => (_jsxs(React.Fragment, { children: [_jsx(DateDivider, { label: date }), msgs.map(msg => (_jsx(Bubble, { msg: msg, primaryColor: config.primaryColor }, msg.id)))] }, date))), messages.length === 0 && (_jsxs("div", { style: { margin: 'auto', textAlign: 'center', color: '#c4cad4', fontSize: 13 }, children: [_jsx("div", { style: { fontSize: 28, marginBottom: 8 }, children: "\uD83D\uDCAC" }), "Say hello to ", activeUser.name, "!"] })), _jsx("div", { ref: endRef })] }), _jsxs("div", { style: { borderTop: '1px solid #eef0f5', padding: '10px 12px 8px', background: '#fff', flexShrink: 0, position: 'relative' }, children: [privacyEnabled && showPrivacy && (_jsxs("div", { style: {
285
285
  position: 'relative',
286
286
  marginBottom: 10,
287
287
  padding: '12px 36px 12px 12px',
@@ -17,6 +17,7 @@ import { TicketDetailScreen } from './TicketDetailScreen';
17
17
  import { TicketFormScreen } from './TicketFormScreen';
18
18
  import { BlockListScreen } from './BlockList';
19
19
  import { CallScreen } from './CallScreen';
20
+ import { MiniCallBar } from './MiniCallBar';
20
21
  import { MaintenanceView } from './MaintenanceView';
21
22
  import { BottomTabs } from './Tabs/BottomTabs';
22
23
  import { ViewerBlockedScreen } from './ViewerBlockedScreen';
@@ -36,6 +37,8 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
36
37
  /* Drawer open state */
37
38
  const [isOpen, setIsOpen] = useState(false);
38
39
  const [closing, setClosing] = useState(false); // for slide-out animation
40
+ /** True when user hid the drawer during ringing/connected call; WebRTC session stays active. */
41
+ const [callMinimized, setCallMinimized] = useState(false);
39
42
  /* Navigation */
40
43
  const [activeTab, setActiveTab] = useState('home');
41
44
  const [screen, setScreen] = useState('home');
@@ -82,10 +85,16 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
82
85
  const { messages, activeUser, isPaused, isReported, selectUser, sendMessage, togglePause, reportChat, clearChat, setMessages, } = useChat();
83
86
  /* WebRTC hook */
84
87
  const { session: callSession, localVideoRef, remoteVideoRef, startCall, endCall, toggleMute, toggleCamera } = useWebRTC();
88
+ const callInProgress = callSession.state === 'calling' || callSession.state === 'connected';
89
+ useEffect(() => {
90
+ if (!callInProgress)
91
+ setCallMinimized(false);
92
+ }, [callInProgress]);
85
93
  /* ── Drawer open/close with slide animation ───────────────────────────── */
86
94
  const openDrawer = () => {
87
95
  setClosing(false);
88
96
  setIsOpen(true);
97
+ setCallMinimized(false);
89
98
  };
90
99
  const persistWidgetState = useCallback(() => {
91
100
  var _a;
@@ -315,8 +324,13 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
315
324
  }, [activeUser, startCall]);
316
325
  const handleEndCall = useCallback(() => {
317
326
  endCall();
327
+ setCallMinimized(false);
318
328
  setScreen('chat');
319
329
  }, [endCall]);
330
+ const minimizeCall = useCallback(() => {
331
+ setCallMinimized(true);
332
+ closeDrawer();
333
+ }, [closeDrawer]);
320
334
  /* ── Derived ─────────────────────────────────────────────────────────── */
321
335
  const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
322
336
  const widgetConfig = useMemo(() => {
@@ -442,7 +456,7 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
442
456
  @media (max-width: 1024px) {
443
457
  .cw-drawer-panel { width: 100%; }
444
458
  }
445
- ` }), !isOpen && (_jsxs("button", { className: "cw-root", type: "button", onClick: openDrawer, "aria-label": totalUnread > 0 ? `${theme.buttonLabel}, ${totalUnread} unread` : theme.buttonLabel, title: totalUnread > 0 ? `${totalUnread} unread message${totalUnread === 1 ? '' : 's'}` : theme.buttonLabel, style: Object.assign(Object.assign({ position: 'fixed', bottom: 24, zIndex: 9999 }, posStyle), { display: 'flex', alignItems: 'center', gap: 10, padding: '13px 22px', backgroundColor: theme.buttonColor, color: theme.buttonTextColor, border: 'none', borderRadius: 50, cursor: 'pointer', fontSize: 15, fontWeight: 700, boxShadow: `0 8px 28px ${theme.buttonColor}55`, animation: 'cw-btnPop 0.4s cubic-bezier(0.34,1.56,0.64,1)', transition: 'transform 0.2s, box-shadow 0.2s' }), onMouseEnter: e => {
459
+ ` }), !isOpen && callMinimized && callInProgress && callSession.peer && (_jsx(MiniCallBar, { session: callSession, primaryColor: primaryColor, buttonPosition: theme.buttonPosition, onExpand: openDrawer, onEnd: handleEndCall })), !isOpen && (_jsxs("button", { className: "cw-root", type: "button", onClick: openDrawer, "aria-label": totalUnread > 0 ? `${theme.buttonLabel}, ${totalUnread} unread` : theme.buttonLabel, title: totalUnread > 0 ? `${totalUnread} unread message${totalUnread === 1 ? '' : 's'}` : theme.buttonLabel, style: Object.assign(Object.assign({ position: 'fixed', bottom: 24, zIndex: 9999 }, posStyle), { display: 'flex', alignItems: 'center', gap: 10, padding: '13px 22px', backgroundColor: theme.buttonColor, color: theme.buttonTextColor, border: 'none', borderRadius: 50, cursor: 'pointer', fontSize: 15, fontWeight: 700, boxShadow: `0 8px 28px ${theme.buttonColor}55`, animation: 'cw-btnPop 0.4s cubic-bezier(0.34,1.56,0.64,1)', transition: 'transform 0.2s, box-shadow 0.2s' }), onMouseEnter: e => {
446
460
  e.currentTarget.style.transform = 'scale(1.06) translateY(-2px)';
447
461
  e.currentTarget.style.boxShadow = `0 14px 36px ${theme.buttonColor}66`;
448
462
  }, onMouseLeave: e => {
@@ -481,7 +495,7 @@ export const ChatWidget = ({ theme: localTheme, viewer }) => {
481
495
  right: theme.buttonPosition === 'bottom-left' ? 'auto' : 12,
482
496
  left: theme.buttonPosition === 'bottom-left' ? 12 : 'auto',
483
497
  zIndex: 20, display: 'flex', gap: 6,
484
- }, 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' && effectiveViewerBlocked && (_jsx(ViewerBlockedScreen, { config: widgetConfig, apiKey: apiKey, onClose: closeDrawer })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && !permissionsOk && (_jsx(PermissionsGateScreen, { primaryColor: primaryColor, widgetId: widgetConfig.id, onGranted: () => setPermissionsOk(true) })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && permissionsOk && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, apiKey: apiKey, 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 => {
498
+ }, 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' && effectiveViewerBlocked && (_jsx(ViewerBlockedScreen, { config: widgetConfig, apiKey: apiKey, onClose: closeDrawer })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && !permissionsOk && (_jsx(PermissionsGateScreen, { primaryColor: primaryColor, widgetId: widgetConfig.id, onGranted: () => setPermissionsOk(true) })), widgetConfig.status === 'ACTIVE' && !effectiveViewerBlocked && permissionsOk && (_jsxs("div", { className: "cw-scroll", style: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }, children: [screen === 'home' && (_jsx(HomeScreen, { config: widgetConfig, apiKey: apiKey, 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, onMinimize: minimizeCall })), 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 => {
485
499
  setListEntranceAnimation(false);
486
500
  setViewingTicketId(id);
487
501
  setScreen('ticket-detail');
@@ -72,37 +72,37 @@ export const HomeScreen = ({ config, apiKey, onNavigate, onOpenTicket, tickets }
72
72
  justifyContent: 'center',
73
73
  gap: 5,
74
74
  flexShrink: 0,
75
- }, children: [_jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } }), _jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } }), _jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } })] }), _jsx("div", { style: { flex: 1, minWidth: 0 } }), _jsxs("div", { style: {
75
+ }, children: [_jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } }), _jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } }), _jsx("span", { style: { width: 18, height: 2, background: '#334155', borderRadius: 1 } })] }), _jsx("div", { style: { flex: 1, minWidth: 0 } }), _jsx("div", { style: {
76
76
  display: 'flex',
77
77
  alignItems: 'center',
78
78
  gap: 6,
79
79
  flexShrink: 0,
80
80
  flexWrap: 'wrap',
81
81
  justifyContent: 'flex-end',
82
- }, children: [_jsx("span", { style: { fontSize: 11, fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.06em' }, children: "Status" }), _jsx("div", { role: "group", "aria-label": "Your status", style: {
83
- display: 'flex',
84
- borderRadius: 10,
85
- padding: 3,
86
- background: '#f1f5f9',
87
- gap: 2,
88
- }, children: STATUS_OPTIONS.map(({ value, label }) => {
89
- const isOn = presence === value;
90
- return (_jsx("button", { type: "button", onClick: () => setPresenceAndSave(value), style: {
91
- border: 'none',
92
- borderRadius: 8,
93
- padding: '7px 10px',
94
- fontSize: 11,
95
- fontWeight: 700,
96
- letterSpacing: '0.04em',
97
- cursor: 'pointer',
98
- fontFamily: 'inherit',
99
- textTransform: 'uppercase',
100
- background: isOn ? config.primaryColor : 'transparent',
101
- color: isOn ? '#fff' : '#64748b',
102
- boxShadow: isOn ? `0 2px 8px ${config.primaryColor}55` : 'none',
103
- transition: 'background 0.15s, color 0.15s',
104
- }, children: label }, value));
105
- }) })] })] }), _jsxs("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', padding: '20px 18px 28px' }, children: [_jsx("h1", { style: {
82
+ }, children: _jsx("div", { role: "group", "aria-label": "Your status", style: {
83
+ display: 'flex',
84
+ borderRadius: 10,
85
+ padding: 3,
86
+ background: '#f1f5f9',
87
+ gap: 2,
88
+ }, children: STATUS_OPTIONS.map(({ value, label }) => {
89
+ const isOn = presence === value;
90
+ return (_jsx("button", { type: "button", onClick: () => setPresenceAndSave(value), style: {
91
+ border: 'none',
92
+ borderRadius: 8,
93
+ padding: '7px 10px',
94
+ fontSize: 11,
95
+ fontWeight: 700,
96
+ letterSpacing: '0.04em',
97
+ cursor: 'pointer',
98
+ fontFamily: 'inherit',
99
+ textTransform: 'uppercase',
100
+ background: isOn ? config.primaryColor : 'transparent',
101
+ color: isOn ? '#fff' : '#64748b',
102
+ boxShadow: isOn ? `0 2px 8px ${config.primaryColor}55` : 'none',
103
+ transition: 'background 0.15s, color 0.15s',
104
+ }, children: label }, value));
105
+ }) }) })] }), _jsxs("div", { className: "cw-scroll", style: { flex: 1, overflowY: 'auto', padding: '20px 18px 28px' }, children: [_jsx("h1", { style: {
106
106
  margin: '0 0 8px',
107
107
  fontSize: 24,
108
108
  fontWeight: 800,
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { CallSession } from '../../types';
3
+ export interface MiniCallBarProps {
4
+ session: CallSession;
5
+ primaryColor: string;
6
+ buttonPosition: 'bottom-left' | 'bottom-right';
7
+ onExpand: () => void;
8
+ onEnd: () => void;
9
+ }
10
+ /**
11
+ * Shown when the user minimizes the widget during an active call (ringing or connected).
12
+ * Sits above the main launcher button so the user can work on the page and return to the call.
13
+ */
14
+ export declare const MiniCallBar: React.FC<MiniCallBarProps>;
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from 'react';
4
+ import { avatarColor, initials } from '../../utils/chat';
5
+ /**
6
+ * Shown when the user minimizes the widget during an active call (ringing or connected).
7
+ * Sits above the main launcher button so the user can work on the page and return to the call.
8
+ */
9
+ export const MiniCallBar = ({ session, primaryColor, buttonPosition, onExpand, onEnd, }) => {
10
+ var _a;
11
+ const peer = session.peer;
12
+ const [duration, setDuration] = useState(0);
13
+ useEffect(() => {
14
+ if (session.state !== 'connected' || !session.startedAt)
15
+ return;
16
+ const t = setInterval(() => {
17
+ setDuration(Math.floor((Date.now() - session.startedAt.getTime()) / 1000));
18
+ }, 1000);
19
+ return () => clearInterval(t);
20
+ }, [session.state, session.startedAt]);
21
+ const mins = String(Math.floor(duration / 60)).padStart(2, '0');
22
+ const secs = String(duration % 60).padStart(2, '0');
23
+ const pos = buttonPosition === 'bottom-left'
24
+ ? { left: 24, right: 'auto' }
25
+ : { right: 24, left: 'auto' };
26
+ return (_jsxs("div", { role: "toolbar", "aria-label": "Call in progress", style: Object.assign(Object.assign({ position: 'fixed', bottom: 88, zIndex: 10000 }, pos), { display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', maxWidth: 'min(360px, calc(100vw - 48px))', borderRadius: 14, background: `linear-gradient(135deg, ${primaryColor}ee, #0f172a)`, color: '#fff', boxShadow: '0 10px 32px rgba(0,0,0,0.28)', animation: 'cw-miniBarIn 0.28s cubic-bezier(0.22,1,0.36,1)', cursor: 'default' }), children: [_jsx("style", { children: `
27
+ @keyframes cw-miniBarIn {
28
+ from { opacity: 0; transform: translateY(12px); }
29
+ to { opacity: 1; transform: translateY(0); }
30
+ }
31
+ ` }), _jsxs("button", { type: "button", onClick: onExpand, title: "Open call", style: {
32
+ display: 'flex',
33
+ alignItems: 'center',
34
+ gap: 10,
35
+ flex: 1,
36
+ minWidth: 0,
37
+ padding: 0,
38
+ border: 'none',
39
+ background: 'transparent',
40
+ color: 'inherit',
41
+ cursor: 'pointer',
42
+ textAlign: 'left',
43
+ }, children: [peer && (_jsx("div", { style: {
44
+ width: 40,
45
+ height: 40,
46
+ borderRadius: '50%',
47
+ backgroundColor: avatarColor(peer.name),
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ justifyContent: 'center',
51
+ fontSize: 14,
52
+ fontWeight: 700,
53
+ flexShrink: 0,
54
+ animation: session.state === 'calling' ? 'cw-pulse 1.5s ease infinite' : 'none',
55
+ }, children: initials(peer.name) })), _jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [_jsx("div", { style: { fontWeight: 700, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, children: (_a = peer === null || peer === void 0 ? void 0 : peer.name) !== null && _a !== void 0 ? _a : 'Call' }), _jsxs("div", { style: { fontSize: 12, opacity: 0.9, marginTop: 2 }, children: [session.state === 'calling' && 'Calling…', session.state === 'connected' && `${mins}:${secs}`] })] })] }), _jsx("button", { type: "button", onClick: onEnd, title: "End call", style: {
56
+ width: 40,
57
+ height: 40,
58
+ borderRadius: '50%',
59
+ border: 'none',
60
+ background: '#ef4444',
61
+ cursor: 'pointer',
62
+ display: 'flex',
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
65
+ flexShrink: 0,
66
+ boxShadow: '0 2px 10px rgba(239,68,68,0.45)',
67
+ }, children: _jsx("svg", { width: "18", height: "18", 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", transform: "rotate(135 12 12)" }) }) })] }));
68
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ajaxter-chat",
3
- "version": "3.0.16",
3
+ "version": "3.0.17",
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",
@@ -10,11 +10,13 @@ interface CallScreenProps {
10
10
  onToggleMute: () => void;
11
11
  onToggleCamera: () => void;
12
12
  primaryColor: string;
13
+ /** Collapse the drawer while keeping the call active (mic/cam stay on). */
14
+ onMinimize?: () => void;
13
15
  }
14
16
 
15
17
  export const CallScreen: React.FC<CallScreenProps> = ({
16
18
  session, localVideoRef, remoteVideoRef,
17
- onEnd, onToggleMute, onToggleCamera, primaryColor,
19
+ onEnd, onToggleMute, onToggleCamera, primaryColor, onMinimize,
18
20
  }) => {
19
21
  const [duration, setDuration] = useState(0);
20
22
  const peer = session.peer as ChatUser;
@@ -64,6 +66,26 @@ export const CallScreen: React.FC<CallScreenProps> = ({
64
66
  {session.state === 'ended' && 'Call Ended'}
65
67
  </div>
66
68
  </div>
69
+ {(session.state === 'calling' || session.state === 'connected') && onMinimize && (
70
+ <button
71
+ type="button"
72
+ onClick={onMinimize}
73
+ title="Minimize — keep call while you use the page"
74
+ style={{
75
+ padding: '8px 12px',
76
+ borderRadius: 10,
77
+ border: '1px solid rgba(255,255,255,0.35)',
78
+ background: 'rgba(0,0,0,0.25)',
79
+ color: '#fff',
80
+ fontSize: 13,
81
+ fontWeight: 600,
82
+ cursor: 'pointer',
83
+ flexShrink: 0,
84
+ }}
85
+ >
86
+ Minimize
87
+ </button>
88
+ )}
67
89
  </div>
68
90
 
69
91
  {/* Center: avatar + name */}
@@ -314,12 +314,13 @@ export const ChatScreen: React.FC<ChatScreenProps> = ({
314
314
  marginLeft: 4,
315
315
  }}
316
316
  >
317
- <span style={{ fontSize: 10, color: 'rgba(255,255,255,0.85)', fontWeight: 600 }}>Sound</span>
318
317
  <button
319
318
  type="button"
320
319
  role="switch"
321
320
  aria-checked={messageSoundEnabled}
322
321
  onClick={() => onToggleMessageSound(!messageSoundEnabled)}
322
+ aria-label="Toggle message sound"
323
+ title="Toggle message sound"
323
324
  style={{
324
325
  width: 36,
325
326
  height: 20,
@@ -19,6 +19,7 @@ import { TicketDetailScreen } from './TicketDetailScreen';
19
19
  import { TicketFormScreen } from './TicketFormScreen';
20
20
  import { BlockListScreen } from './BlockList';
21
21
  import { CallScreen } from './CallScreen';
22
+ import { MiniCallBar } from './MiniCallBar';
22
23
  import { MaintenanceView } from './MaintenanceView';
23
24
  import { BottomTabs } from './Tabs/BottomTabs';
24
25
  import { ViewerBlockedScreen } from './ViewerBlockedScreen';
@@ -45,6 +46,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
45
46
  /* Drawer open state */
46
47
  const [isOpen, setIsOpen] = useState(false);
47
48
  const [closing, setClosing] = useState(false); // for slide-out animation
49
+ /** True when user hid the drawer during ringing/connected call; WebRTC session stays active. */
50
+ const [callMinimized, setCallMinimized] = useState(false);
48
51
 
49
52
  /* Navigation */
50
53
  const [activeTab, setActiveTab] = useState<BottomTab>('home');
@@ -98,10 +101,18 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
98
101
  /* WebRTC hook */
99
102
  const { session: callSession, localVideoRef, remoteVideoRef, startCall, endCall, toggleMute, toggleCamera } = useWebRTC();
100
103
 
104
+ const callInProgress =
105
+ callSession.state === 'calling' || callSession.state === 'connected';
106
+
107
+ useEffect(() => {
108
+ if (!callInProgress) setCallMinimized(false);
109
+ }, [callInProgress]);
110
+
101
111
  /* ── Drawer open/close with slide animation ───────────────────────────── */
102
112
  const openDrawer = () => {
103
113
  setClosing(false);
104
114
  setIsOpen(true);
115
+ setCallMinimized(false);
105
116
  };
106
117
 
107
118
  const persistWidgetState = useCallback(() => {
@@ -333,9 +344,15 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
333
344
 
334
345
  const handleEndCall = useCallback(() => {
335
346
  endCall();
347
+ setCallMinimized(false);
336
348
  setScreen('chat');
337
349
  }, [endCall]);
338
350
 
351
+ const minimizeCall = useCallback(() => {
352
+ setCallMinimized(true);
353
+ closeDrawer();
354
+ }, [closeDrawer]);
355
+
339
356
  /* ── Derived ─────────────────────────────────────────────────────────── */
340
357
  const isBlocked = activeUser ? blockedUids.includes(activeUser.uid) : false;
341
358
 
@@ -472,6 +489,17 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
472
489
  }
473
490
  `}</style>
474
491
 
492
+ {/* ── Minimized call bar (drawer closed, call still active) ── */}
493
+ {!isOpen && callMinimized && callInProgress && callSession.peer && (
494
+ <MiniCallBar
495
+ session={callSession}
496
+ primaryColor={primaryColor}
497
+ buttonPosition={theme.buttonPosition}
498
+ onExpand={openDrawer}
499
+ onEnd={handleEndCall}
500
+ />
501
+ )}
502
+
475
503
  {/* ── Floating Button (unread badge + tooltip when closed) ── */}
476
504
  {!isOpen && (
477
505
  <button
@@ -698,6 +726,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({ theme: localTheme, viewe
698
726
  onToggleMute={toggleMute}
699
727
  onToggleCamera={toggleCamera}
700
728
  primaryColor={primaryColor}
729
+ onMinimize={minimizeCall}
701
730
  />
702
731
  )}
703
732
 
@@ -140,9 +140,6 @@ export const HomeScreen: React.FC<HomeScreenProps> = ({ config, apiKey, onNaviga
140
140
  justifyContent: 'flex-end',
141
141
  }}
142
142
  >
143
- <span style={{ fontSize: 11, fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
144
- Status
145
- </span>
146
143
  <div
147
144
  role="group"
148
145
  aria-label="Your status"
@@ -0,0 +1,150 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { CallSession, ChatUser } from '../../types';
5
+ import { avatarColor, initials } from '../../utils/chat';
6
+
7
+ export interface MiniCallBarProps {
8
+ session: CallSession;
9
+ primaryColor: string;
10
+ buttonPosition: 'bottom-left' | 'bottom-right';
11
+ onExpand: () => void;
12
+ onEnd: () => void;
13
+ }
14
+
15
+ /**
16
+ * Shown when the user minimizes the widget during an active call (ringing or connected).
17
+ * Sits above the main launcher button so the user can work on the page and return to the call.
18
+ */
19
+ export const MiniCallBar: React.FC<MiniCallBarProps> = ({
20
+ session,
21
+ primaryColor,
22
+ buttonPosition,
23
+ onExpand,
24
+ onEnd,
25
+ }) => {
26
+ const peer = session.peer as ChatUser | null;
27
+ const [duration, setDuration] = useState(0);
28
+
29
+ useEffect(() => {
30
+ if (session.state !== 'connected' || !session.startedAt) return;
31
+ const t = setInterval(() => {
32
+ setDuration(Math.floor((Date.now() - session.startedAt!.getTime()) / 1000));
33
+ }, 1000);
34
+ return () => clearInterval(t);
35
+ }, [session.state, session.startedAt]);
36
+
37
+ const mins = String(Math.floor(duration / 60)).padStart(2, '0');
38
+ const secs = String(duration % 60).padStart(2, '0');
39
+
40
+ const pos: React.CSSProperties =
41
+ buttonPosition === 'bottom-left'
42
+ ? { left: 24, right: 'auto' }
43
+ : { right: 24, left: 'auto' };
44
+
45
+ return (
46
+ <div
47
+ role="toolbar"
48
+ aria-label="Call in progress"
49
+ style={{
50
+ position: 'fixed',
51
+ bottom: 88,
52
+ zIndex: 10000,
53
+ ...pos,
54
+ display: 'flex',
55
+ alignItems: 'center',
56
+ gap: 10,
57
+ padding: '10px 14px',
58
+ maxWidth: 'min(360px, calc(100vw - 48px))',
59
+ borderRadius: 14,
60
+ background: `linear-gradient(135deg, ${primaryColor}ee, #0f172a)`,
61
+ color: '#fff',
62
+ boxShadow: '0 10px 32px rgba(0,0,0,0.28)',
63
+ animation: 'cw-miniBarIn 0.28s cubic-bezier(0.22,1,0.36,1)',
64
+ cursor: 'default',
65
+ }}
66
+ >
67
+ <style>{`
68
+ @keyframes cw-miniBarIn {
69
+ from { opacity: 0; transform: translateY(12px); }
70
+ to { opacity: 1; transform: translateY(0); }
71
+ }
72
+ `}</style>
73
+
74
+ <button
75
+ type="button"
76
+ onClick={onExpand}
77
+ title="Open call"
78
+ style={{
79
+ display: 'flex',
80
+ alignItems: 'center',
81
+ gap: 10,
82
+ flex: 1,
83
+ minWidth: 0,
84
+ padding: 0,
85
+ border: 'none',
86
+ background: 'transparent',
87
+ color: 'inherit',
88
+ cursor: 'pointer',
89
+ textAlign: 'left',
90
+ }}
91
+ >
92
+ {peer && (
93
+ <div
94
+ style={{
95
+ width: 40,
96
+ height: 40,
97
+ borderRadius: '50%',
98
+ backgroundColor: avatarColor(peer.name),
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ fontSize: 14,
103
+ fontWeight: 700,
104
+ flexShrink: 0,
105
+ animation: session.state === 'calling' ? 'cw-pulse 1.5s ease infinite' : 'none',
106
+ }}
107
+ >
108
+ {initials(peer.name)}
109
+ </div>
110
+ )}
111
+ <div style={{ minWidth: 0, flex: 1 }}>
112
+ <div style={{ fontWeight: 700, fontSize: 14, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
113
+ {peer?.name ?? 'Call'}
114
+ </div>
115
+ <div style={{ fontSize: 12, opacity: 0.9, marginTop: 2 }}>
116
+ {session.state === 'calling' && 'Calling…'}
117
+ {session.state === 'connected' && `${mins}:${secs}`}
118
+ </div>
119
+ </div>
120
+ </button>
121
+
122
+ <button
123
+ type="button"
124
+ onClick={onEnd}
125
+ title="End call"
126
+ style={{
127
+ width: 40,
128
+ height: 40,
129
+ borderRadius: '50%',
130
+ border: 'none',
131
+ background: '#ef4444',
132
+ cursor: 'pointer',
133
+ display: 'flex',
134
+ alignItems: 'center',
135
+ justifyContent: 'center',
136
+ flexShrink: 0,
137
+ boxShadow: '0 2px 10px rgba(239,68,68,0.45)',
138
+ }}
139
+ >
140
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none">
141
+ <path
142
+ 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"
143
+ fill="#fff"
144
+ transform="rotate(135 12 12)"
145
+ />
146
+ </svg>
147
+ </button>
148
+ </div>
149
+ );
150
+ };