ajaxter-chat 1.0.0 → 1.0.3

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
@@ -1,4 +1,4 @@
1
- # react-chat-widget-extension
1
+ # Chat Widget
2
2
 
3
3
  A reusable, fully configurable floating chat widget for **React.js** and **Next.js** applications.
4
4
 
@@ -14,7 +14,7 @@ A reusable, fully configurable floating chat widget for **React.js** and **Next.
14
14
  ## Folder Structure
15
15
 
16
16
  ```
17
- react-chat-widget-extension/
17
+ my_first_project/
18
18
  ├── src/
19
19
  │ ├── index.ts # Public API exports
20
20
  │ ├── types/
@@ -63,9 +63,9 @@ react-chat-widget-extension/
63
63
  ## Installation
64
64
 
65
65
  ```bash
66
- npm install react-chat-widget-extension
66
+ npm install ajaxter-chat
67
67
  # or
68
- yarn add react-chat-widget-extension
68
+ yarn add ajaxter-chat
69
69
  ```
70
70
 
71
71
  ---
@@ -98,7 +98,7 @@ NEXT_PUBLIC_CHAT_TYPE=BOTH
98
98
  |--------------------|---------------------------------------|------------------------------------------|
99
99
  | `CHAT_HOST_URL` | string | Base URL of your chat/user API |
100
100
  | `CHAT_HOST_PORT` | number | Port for your API server |
101
- | `CHAT_USER_LIST` | string | Endpoint path to fetch users |
101
+ | `CHAT_USER_LIST` | string | User list URL see **User List API** below |
102
102
  | `CHAT_STATUS` | `ACTIVE` \| `DISABLE` \| `MAINTENANCE` | Controls widget visibility & state |
103
103
  | `CHAT_TYPE` | `SUPPORT` \| `CHAT` \| `BOTH` | Controls which users are shown |
104
104
 
@@ -126,7 +126,20 @@ NEXT_PUBLIC_CHAT_TYPE=BOTH
126
126
 
127
127
  ## User List API
128
128
 
129
- The widget fetches users from:
129
+ The widget calls `CHAT_USER_LIST` in three ways:
130
+
131
+ 1. **Same-origin / BFF (recommended)** — value starts with `/`, e.g. `/api/v1/chat/users`. The browser only shows a request to **your** app; your route handler proxies to the real API server-side, so the upstream URL (e.g. `http://your-api.com:4000/...`) does not appear as the client request URL in DevTools.
132
+ 2. **Full URL** — value starts with `http://` or `https://`; that exact URL is fetched (visible in Network).
133
+ 3. **Legacy** — otherwise it is built as
134
+ `CHAT_HOST_URL` + optional `:CHAT_HOST_PORT` + path.
135
+
136
+ Example (Next.js route at `app/api/v1/chat/users/route.ts` that forwards to your backend):
137
+
138
+ ```env
139
+ NEXT_PUBLIC_CHAT_USER_LIST=/api/v1/chat/users
140
+ ```
141
+
142
+ Legacy example:
130
143
 
131
144
  ```
132
145
  GET ${CHAT_HOST_URL}:${CHAT_HOST_PORT}/${CHAT_USER_LIST}
@@ -163,7 +176,7 @@ Expected response:
163
176
 
164
177
  ```tsx
165
178
  // App.tsx
166
- import { ChatWidget } from 'react-chat-widget-extension';
179
+ import { ChatWidget } from 'ajaxter-chat';
167
180
 
168
181
  function App() {
169
182
  return (
@@ -191,7 +204,7 @@ function App() {
191
204
  ```tsx
192
205
  // app/ChatWidgetWrapper.tsx
193
206
  'use client';
194
- import { ChatWidget } from 'react-chat-widget-extension';
207
+ import { ChatWidget } from 'ajaxter-chat';
195
208
 
196
209
  export function ChatWidgetWrapper() {
197
210
  return <ChatWidget theme={{ primaryColor: '#0ea5e9' }} />;
@@ -218,7 +231,7 @@ export default function RootLayout({ children }) {
218
231
 
219
232
  ```tsx
220
233
  // pages/_app.tsx
221
- import { ChatWidget } from 'react-chat-widget-extension';
234
+ import { ChatWidget } from 'ajaxter-chat';
222
235
 
223
236
  export default function MyApp({ Component, pageProps }) {
224
237
  return (
@@ -276,7 +289,7 @@ socket.on('chat:message', (msg: ChatMessage) => {
276
289
  ## Build
277
290
 
278
291
  ```bash
279
- cd react-chat-widget-extension
292
+ cd my_first_project
280
293
  npm install
281
294
  npm run build # Compile TypeScript → dist/
282
295
  npm run type-check # Verify types without emitting
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { BottomNavTab } from '../../types';
3
+ interface BottomNavProps {
4
+ active: BottomNavTab;
5
+ onChange: (tab: BottomNavTab) => void;
6
+ primaryColor: string;
7
+ fontFamily: string;
8
+ }
9
+ export declare const BottomNav: React.FC<BottomNavProps>;
10
+ export {};
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const BottomNav = ({ active, onChange, primaryColor, fontFamily, }) => {
3
+ const item = (tab, label, icon) => {
4
+ const isOn = active === tab;
5
+ return (_jsxs("button", { type: "button", onClick: () => onChange(tab), style: {
6
+ flex: 1,
7
+ display: 'flex',
8
+ flexDirection: 'column',
9
+ alignItems: 'center',
10
+ justifyContent: 'center',
11
+ gap: '4px',
12
+ padding: '10px 8px',
13
+ border: 'none',
14
+ background: 'transparent',
15
+ cursor: 'pointer',
16
+ fontFamily,
17
+ color: isOn ? primaryColor : '#b0b0b0',
18
+ transition: 'color 0.2s',
19
+ }, children: [_jsx("span", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center' }, children: icon }), _jsx("span", { style: { fontSize: '11px', fontWeight: isOn ? 600 : 500 }, children: label })] }));
20
+ };
21
+ return (_jsxs("nav", { style: {
22
+ flexShrink: 0,
23
+ display: 'flex',
24
+ alignItems: 'stretch',
25
+ backgroundColor: '#fff',
26
+ borderTop: '1px solid #eee',
27
+ paddingBottom: 'env(safe-area-inset-bottom, 0)',
28
+ }, "aria-label": "Widget sections", children: [item('home', 'Home', _jsx(HomeIcon, { active: active === 'home', color: primaryColor, muted: "#b0b0b0" })), item('chats', 'Chat', _jsx(ChatBubbleIcon, { active: active === 'chats', color: primaryColor, muted: "#b0b0b0" })), item('tickets', 'Tickets', _jsx(TicketIcon, { active: active === 'tickets', color: primaryColor, muted: "#b0b0b0" }))] }));
29
+ };
30
+ const HomeIcon = ({ active, color, muted, }) => (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1V9.5z", stroke: active ? color : muted, strokeWidth: "2", strokeLinejoin: "round" }) }));
31
+ const ChatBubbleIcon = ({ active, color, muted, }) => (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z", stroke: active ? color : muted, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
32
+ const TicketIcon = ({ active, color, muted, }) => (_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M4 4h16v4H4V4zm0 6h10v10H4V10zm12 0h4v10h-4V10z", stroke: active ? color : muted, strokeWidth: "2", strokeLinejoin: "round" }) }));
@@ -1,11 +1,15 @@
1
1
  import React from 'react';
2
2
  import { ChatMessage, ChatUser } from '../../types';
3
- interface ChatBoxProps {
3
+ export interface ChatBoxProps {
4
4
  activeUser: ChatUser | null;
5
5
  messages: ChatMessage[];
6
6
  onSendMessage: (text: string) => void;
7
7
  primaryColor: string;
8
8
  fontFamily: string;
9
+ /** Minimal: teal bar with back/close, footer with extra actions (reference UI). */
10
+ variant?: 'default' | 'minimal';
11
+ onBack?: () => void;
12
+ onClose?: () => void;
13
+ headerTitle?: string;
9
14
  }
10
15
  export declare const ChatBox: React.FC<ChatBoxProps>;
11
- export {};
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useRef, useEffect } from 'react';
3
- export const ChatBox = ({ activeUser, messages, onSendMessage, primaryColor, fontFamily, }) => {
3
+ export const ChatBox = ({ activeUser, messages, onSendMessage, primaryColor, fontFamily, variant = 'default', onBack, onClose, headerTitle, }) => {
4
4
  const [inputText, setInputText] = useState('');
5
5
  const messagesEndRef = useRef(null);
6
6
  const inputRef = useRef(null);
@@ -22,6 +22,61 @@ export const ChatBox = ({ activeUser, messages, onSendMessage, primaryColor, fon
22
22
  handleSend();
23
23
  }
24
24
  };
25
+ if (variant === 'minimal') {
26
+ return (_jsxs("div", { style: {
27
+ flex: 1,
28
+ display: 'flex',
29
+ flexDirection: 'column',
30
+ fontFamily,
31
+ overflow: 'hidden',
32
+ backgroundColor: '#fff',
33
+ }, children: [_jsxs("div", { style: {
34
+ flexShrink: 0,
35
+ padding: '14px 16px',
36
+ backgroundColor: primaryColor,
37
+ color: '#fff',
38
+ display: 'flex',
39
+ alignItems: 'center',
40
+ justifyContent: 'space-between',
41
+ }, children: [_jsx("button", { type: "button", onClick: onBack, "aria-label": "Back", style: {
42
+ background: 'transparent',
43
+ border: 'none',
44
+ color: '#fff',
45
+ cursor: 'pointer',
46
+ padding: '4px',
47
+ display: 'flex',
48
+ alignItems: 'center',
49
+ }, children: _jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M15 18L9 12L15 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("div", { style: { fontWeight: 600, fontSize: '15px', flex: 1, textAlign: 'center' }, children: headerTitle !== null && headerTitle !== void 0 ? headerTitle : (activeUser ? activeUser.name : 'Chat') }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
50
+ background: 'transparent',
51
+ border: 'none',
52
+ color: '#fff',
53
+ cursor: 'pointer',
54
+ padding: '4px',
55
+ display: 'flex',
56
+ }, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) })] }), _jsxs("div", { style: {
57
+ flex: 1,
58
+ overflowY: 'auto',
59
+ padding: '16px',
60
+ backgroundColor: '#fff',
61
+ }, children: [!activeUser ? (_jsx("div", { style: { textAlign: 'center', color: '#bbb', fontSize: '14px', marginTop: '32px' }, children: "Select someone to chat" })) : messages.length === 0 ? (_jsxs("div", { style: { textAlign: 'center', color: '#ccc', fontSize: '13px', marginTop: '32px' }, children: ["Say hello to ", activeUser.name, "! \uD83D\uDC4B"] })) : (messages.map((msg) => (_jsx(MessageBubble, { message: msg, primaryColor: primaryColor }, msg.id)))), _jsx("div", { ref: messagesEndRef })] }), _jsxs("div", { style: {
62
+ borderTop: '1px solid #eee',
63
+ backgroundColor: '#fff',
64
+ padding: '10px 12px',
65
+ display: 'flex',
66
+ alignItems: 'center',
67
+ gap: '8px',
68
+ }, children: [_jsx("textarea", { ref: inputRef, value: inputText, onChange: (e) => setInputText(e.target.value), onKeyDown: handleKeyDown, placeholder: "Type and press [enter]..", rows: 1, disabled: !activeUser, style: {
69
+ flex: 1,
70
+ resize: 'none',
71
+ border: 'none',
72
+ outline: 'none',
73
+ fontFamily,
74
+ fontSize: '14px',
75
+ color: '#1a1a2e',
76
+ maxHeight: '80px',
77
+ lineHeight: 1.45,
78
+ } }), _jsx(IconButton, { label: "Like", disabled: !activeUser, children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "#888", strokeWidth: "1.8", children: _jsx("path", { d: "M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" }) }) }), _jsx(IconButton, { label: "Attach", disabled: !activeUser, children: _jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "#888", strokeWidth: "1.8", children: _jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) }) }), _jsx(IconButton, { label: "Emoji", disabled: !activeUser, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "#888", strokeWidth: "1.8", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01" })] }) })] })] }));
79
+ }
25
80
  if (!activeUser) {
26
81
  return (_jsxs("div", { style: {
27
82
  flex: 1,
@@ -138,6 +193,16 @@ export const ChatBox = ({ activeUser, messages, onSendMessage, primaryColor, fon
138
193
  transition: 'all 0.2s ease',
139
194
  }, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: inputText.trim() ? '#fff' : '#bbb', strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] })] }));
140
195
  };
196
+ const IconButton = ({ label, disabled, children }) => (_jsx("button", { type: "button", "aria-label": label, disabled: disabled, style: {
197
+ background: 'transparent',
198
+ border: 'none',
199
+ padding: '6px',
200
+ cursor: disabled ? 'not-allowed' : 'pointer',
201
+ opacity: disabled ? 0.45 : 1,
202
+ display: 'flex',
203
+ alignItems: 'center',
204
+ justifyContent: 'center',
205
+ }, children: children }));
141
206
  const MessageBubble = ({ message, primaryColor }) => {
142
207
  const isMe = message.senderId === 'me';
143
208
  const time = new Date(message.timestamp).toLocaleTimeString([], {
@@ -45,6 +45,6 @@ export const ChatWidget = ({ theme }) => {
45
45
  // DISABLE: render nothing at all
46
46
  if (config.status === 'DISABLE')
47
47
  return null;
48
- return (_jsxs(_Fragment, { children: [isOpen && (_jsx(ChatWindow, { config: config, theme: theme, buttonPosition: t.buttonPosition })), _jsx(ChatButton, { isOpen: isOpen, onClick: () => setIsOpen((prev) => !prev), theme: theme })] }));
48
+ return (_jsxs(_Fragment, { children: [isOpen && (_jsx(ChatWindow, { config: config, theme: theme, buttonPosition: t.buttonPosition, onClose: () => setIsOpen(false) })), _jsx(ChatButton, { isOpen: isOpen, onClick: () => setIsOpen((prev) => !prev), theme: theme })] }));
49
49
  };
50
50
  export default ChatWidget;
@@ -4,6 +4,7 @@ interface ChatWindowProps {
4
4
  config: ChatConfig;
5
5
  theme?: ChatWidgetTheme;
6
6
  buttonPosition: 'bottom-right' | 'bottom-left';
7
+ onClose: () => void;
7
8
  }
8
9
  export declare const ChatWindow: React.FC<ChatWindowProps>;
9
10
  export {};
@@ -1,39 +1,112 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useMemo, useCallback } from 'react';
3
3
  import { UserList } from '../UserList';
4
4
  import { ChatBox } from '../ChatBox';
5
5
  import { MaintenanceView } from '../MaintenanceView';
6
+ import { HomeView } from '../HomeView';
7
+ import { BottomNav } from '../BottomNav';
6
8
  import { useUsers } from '../../hooks/useUsers';
7
9
  import { useChat } from '../../hooks/useChat';
8
10
  import { buildUserListUrl } from '../../config';
9
11
  import { mergeTheme } from '../../utils/theme';
10
- export const ChatWindow = ({ config, theme, buttonPosition, }) => {
11
- var _a;
12
- const t = mergeTheme(theme);
12
+ export const ChatWindow = ({ config, theme, buttonPosition, onClose, }) => {
13
+ var _a, _b, _c, _d, _e, _f;
14
+ const t = mergeTheme(Object.assign(Object.assign({}, theme), { widgetMinWidth: (_a = theme === null || theme === void 0 ? void 0 : theme.widgetMinWidth) !== null && _a !== void 0 ? _a : config.widgetMinWidth, widgetMaxWidth: (_b = theme === null || theme === void 0 ? void 0 : theme.widgetMaxWidth) !== null && _b !== void 0 ? _b : config.widgetMaxWidth, widgetMinHeight: (_c = theme === null || theme === void 0 ? void 0 : theme.widgetMinHeight) !== null && _c !== void 0 ? _c : config.widgetMinHeight, widgetMaxHeight: (_d = theme === null || theme === void 0 ? void 0 : theme.widgetMaxHeight) !== null && _d !== void 0 ? _d : config.widgetMaxHeight, widgetDefaultSize: (_e = theme === null || theme === void 0 ? void 0 : theme.widgetDefaultSize) !== null && _e !== void 0 ? _e : config.widgetDefaultSize }));
15
+ const minW = Math.min(t.widgetMinWidth, t.widgetMaxWidth);
16
+ const maxW = Math.max(t.widgetMinWidth, t.widgetMaxWidth);
17
+ const minH = Math.min(t.widgetMinHeight, t.widgetMaxHeight);
18
+ const maxH = Math.max(t.widgetMinHeight, t.widgetMaxHeight);
19
+ const [sizeRatio, setSizeRatio] = useState(t.widgetDefaultSize);
20
+ const widthPx = minW + (maxW - minW) * sizeRatio;
21
+ const heightPx = minH + (maxH - minH) * sizeRatio;
22
+ const [bottomTab, setBottomTab] = useState('home');
23
+ const [homeFlow, setHomeFlow] = useState('home');
24
+ const [pickUserMode, setPickUserMode] = useState(null);
25
+ const [tickets, setTickets] = useState([]);
26
+ const [ticketSubject, setTicketSubject] = useState('');
27
+ const [ticketBody, setTicketBody] = useState('');
13
28
  const userListUrl = buildUserListUrl(config);
14
- const [activeTab, setActiveTab] = useState('developers');
15
- const filterType = config.chatType === 'SUPPORT'
29
+ const filterType = pickUserMode === 'support'
16
30
  ? 'developer'
17
- : config.chatType === 'CHAT'
31
+ : pickUserMode === 'conversation'
18
32
  ? 'user'
19
- : activeTab === 'developers'
33
+ : config.chatType === 'SUPPORT'
20
34
  ? 'developer'
21
- : 'user';
35
+ : config.chatType === 'CHAT'
36
+ ? 'user'
37
+ : 'developer';
22
38
  const { users, loading, error } = useUsers({
23
39
  url: userListUrl,
24
40
  filterType,
25
- enabled: config.status === 'ACTIVE',
41
+ enabled: config.status === 'ACTIVE' && homeFlow === 'pickUser',
26
42
  });
27
- const { messages, activeUser, selectUser, sendMessage } = useChat();
43
+ const { messages, activeUser, recentChats, selectUser, sendMessage, openRecent, } = useChat();
28
44
  const positionStyle = buttonPosition === 'bottom-left'
29
45
  ? { left: '24px', right: 'auto' }
30
46
  : { right: '24px', left: 'auto' };
31
- return (_jsxs("div", { style: Object.assign(Object.assign({ position: 'fixed', bottom: '90px' }, positionStyle), { zIndex: 9998, width: '680px', maxWidth: 'calc(100vw - 32px)', height: '520px', maxHeight: 'calc(100vh - 120px)', backgroundColor: t.backgroundColor, borderRadius: t.borderRadius, boxShadow: '0 24px 80px rgba(0,0,0,0.18), 0 8px 24px rgba(0,0,0,0.08)', display: 'flex', flexDirection: 'column', overflow: 'hidden', fontFamily: t.fontFamily, animation: 'cw-slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)' }), children: [_jsx("style", { children: `
47
+ const goHomeCards = useCallback(() => {
48
+ setHomeFlow('home');
49
+ setPickUserMode(null);
50
+ }, []);
51
+ const handleBackFromChat = useCallback(() => {
52
+ if (pickUserMode) {
53
+ setHomeFlow('pickUser');
54
+ return;
55
+ }
56
+ setBottomTab('chats');
57
+ setHomeFlow('home');
58
+ }, [pickUserMode]);
59
+ const handleBottomTab = useCallback((tab) => {
60
+ setBottomTab(tab);
61
+ goHomeCards();
62
+ }, [goHomeCards]);
63
+ const startSupport = useCallback(() => {
64
+ setPickUserMode('support');
65
+ setHomeFlow('pickUser');
66
+ }, []);
67
+ const startConversation = useCallback(() => {
68
+ setPickUserMode('conversation');
69
+ setHomeFlow('pickUser');
70
+ }, []);
71
+ const startRaiseTicket = useCallback(() => {
72
+ setHomeFlow('raiseTicket');
73
+ setTicketSubject('');
74
+ setTicketBody('');
75
+ }, []);
76
+ const submitTicket = useCallback(() => {
77
+ if (!ticketSubject.trim())
78
+ return;
79
+ const id = `tkt_${Date.now()}`;
80
+ setTickets((prev) => [
81
+ {
82
+ id,
83
+ subject: ticketSubject.trim(),
84
+ body: ticketBody.trim(),
85
+ createdAt: new Date(),
86
+ status: 'open',
87
+ },
88
+ ...prev,
89
+ ]);
90
+ setHomeFlow('home');
91
+ setBottomTab('tickets');
92
+ }, [ticketSubject, ticketBody]);
93
+ const pickUserHeader = useMemo(() => {
94
+ if (pickUserMode === 'support')
95
+ return 'Support';
96
+ if (pickUserMode === 'conversation')
97
+ return 'New conversation';
98
+ return 'People';
99
+ }, [pickUserMode]);
100
+ return (_jsxs("div", { style: Object.assign(Object.assign({ position: 'fixed', bottom: '90px' }, positionStyle), { zIndex: 9998, width: `${widthPx}px`, maxWidth: 'calc(100vw - 32px)', height: `${heightPx}px`, maxHeight: 'calc(100vh - 120px)', backgroundColor: t.backgroundColor, borderRadius: t.borderRadius, boxShadow: '0 24px 80px rgba(0,0,0,0.18), 0 8px 24px rgba(0,0,0,0.08)', display: 'flex', flexDirection: 'column', overflow: 'hidden', fontFamily: t.fontFamily, animation: 'cw-slideUp 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)' }), children: [_jsx("style", { children: `
32
101
  @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap');
33
102
  @keyframes cw-slideUp {
34
103
  from { opacity: 0; transform: translateY(20px) scale(0.97); }
35
104
  to { opacity: 1; transform: translateY(0) scale(1); }
36
105
  }
106
+ @keyframes cw-slideFromRight {
107
+ from { opacity: 0; transform: translateX(24px); }
108
+ to { opacity: 1; transform: translateX(0); }
109
+ }
37
110
  @keyframes shimmer {
38
111
  0% { background-position: 200% 0; }
39
112
  100% { background-position: -200% 0; }
@@ -41,92 +114,173 @@ export const ChatWindow = ({ config, theme, buttonPosition, }) => {
41
114
  ::-webkit-scrollbar { width: 4px; }
42
115
  ::-webkit-scrollbar-track { background: transparent; }
43
116
  ::-webkit-scrollbar-thumb { background: #e0e0e0; border-radius: 4px; }
44
- ` }), _jsxs("div", { style: {
45
- padding: '18px 20px',
46
- background: `linear-gradient(135deg, ${t.primaryColor}, ${adjustColor(t.primaryColor, -20)})`,
47
- color: '#fff',
48
- display: 'flex',
49
- alignItems: 'center',
50
- justifyContent: 'space-between',
51
- flexShrink: 0,
52
- }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '12px' }, children: [_jsx("div", { style: {
53
- width: '38px',
54
- height: '38px',
55
- borderRadius: '50%',
56
- backgroundColor: 'rgba(255,255,255,0.2)',
117
+ ` }), config.status === 'MAINTENANCE' ? (_jsx(MaintenanceView, { fontFamily: t.fontFamily, primaryColor: t.primaryColor })) : (_jsxs(_Fragment, { children: [_jsxs("div", { style: { flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }, children: [bottomTab === 'home' && homeFlow === 'home' && (_jsx(HomeView, { primaryColor: t.primaryColor, fontFamily: t.fontFamily, showNeedSupport: config.showNeedSupport, showNewConversation: config.showNewConversation, onNeedSupport: startSupport, onNewConversation: startConversation, onRaiseTicket: startRaiseTicket, onClose: onClose })), bottomTab === 'home' && homeFlow === 'pickUser' && (_jsxs("div", { style: {
118
+ flex: 1,
119
+ display: 'flex',
120
+ flexDirection: 'column',
121
+ minHeight: 0,
122
+ backgroundColor: '#fff',
123
+ animation: 'cw-slideFromRight 0.28s ease-out',
124
+ }, children: [_jsxs("div", { style: {
125
+ flexShrink: 0,
126
+ padding: '14px 16px',
127
+ backgroundColor: t.primaryColor,
128
+ color: '#fff',
129
+ display: 'flex',
130
+ alignItems: 'center',
131
+ gap: '12px',
132
+ }, children: [_jsx("button", { type: "button", onClick: goHomeCards, "aria-label": "Back", style: {
133
+ background: 'transparent',
134
+ border: 'none',
135
+ color: '#fff',
136
+ cursor: 'pointer',
137
+ padding: '4px',
138
+ }, children: _jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M15 18L9 12L15 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("div", { style: { fontWeight: 700, fontSize: '16px' }, children: pickUserHeader })] }), _jsxs("div", { style: { flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }, children: [_jsx("div", { style: {
139
+ padding: '10px 16px',
140
+ fontSize: '11px',
141
+ fontWeight: 700,
142
+ color: '#bbb',
143
+ textTransform: 'uppercase',
144
+ letterSpacing: '0.08em',
145
+ borderBottom: '1px solid #f0f0f0',
146
+ }, children: "Select a contact" }), _jsx(UserList, { users: users, loading: loading, error: error, activeUserId: (_f = activeUser === null || activeUser === void 0 ? void 0 : activeUser.uid) !== null && _f !== void 0 ? _f : null, onSelectUser: (u) => {
147
+ selectUser(u);
148
+ setHomeFlow('chat');
149
+ }, primaryColor: t.primaryColor, fontFamily: t.fontFamily })] })] })), bottomTab === 'home' && homeFlow === 'chat' && (_jsx("div", { style: {
150
+ flex: 1,
151
+ display: 'flex',
152
+ flexDirection: 'column',
153
+ minHeight: 0,
154
+ animation: 'cw-slideFromRight 0.28s ease-out',
155
+ }, children: _jsx(ChatBox, { activeUser: activeUser, messages: messages, onSendMessage: sendMessage, primaryColor: t.primaryColor, fontFamily: t.fontFamily, variant: "minimal", onBack: handleBackFromChat, onClose: onClose, headerTitle: activeUser === null || activeUser === void 0 ? void 0 : activeUser.name }) })), bottomTab === 'home' && homeFlow === 'raiseTicket' && (_jsxs("div", { style: {
156
+ flex: 1,
57
157
  display: 'flex',
58
- alignItems: 'center',
59
- justifyContent: 'center',
60
- }, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z", stroke: "#fff", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: '16px', letterSpacing: '-0.02em' }, children: config.chatType === 'SUPPORT'
61
- ? 'Support Chat'
62
- : config.chatType === 'CHAT'
63
- ? 'Team Chat'
64
- : 'Chat Center' }), _jsx("div", { style: { fontSize: '12px', opacity: 0.8 }, children: config.status === 'ACTIVE' ? 'We\'re online' : 'Unavailable' })] })] }), _jsx(StatusBadge, { status: config.status })] }), config.chatType === 'BOTH' && config.status === 'ACTIVE' && (_jsx("div", { style: {
65
- display: 'flex',
66
- borderBottom: '1px solid #f0f0f5',
67
- backgroundColor: '#fafafa',
68
- flexShrink: 0,
69
- }, children: ['developers', 'users'].map((tab) => (_jsx("button", { onClick: () => setActiveTab(tab), style: {
70
- flex: 1,
71
- padding: '11px',
72
- border: 'none',
73
- background: 'transparent',
74
- cursor: 'pointer',
75
- fontSize: '13px',
76
- fontWeight: 600,
77
- fontFamily: t.fontFamily,
78
- color: activeTab === tab ? t.primaryColor : '#999',
79
- borderBottom: activeTab === tab ? `2px solid ${t.primaryColor}` : '2px solid transparent',
80
- transition: 'all 0.2s',
81
- textTransform: 'capitalize',
82
- }, children: tab === 'developers' ? '🛠 Support (Devs)' : '👥 Users' }, tab))) })), config.status === 'MAINTENANCE' ? (_jsx(MaintenanceView, { fontFamily: t.fontFamily, primaryColor: t.primaryColor })) : (_jsxs("div", { style: { display: 'flex', flex: 1, overflow: 'hidden' }, children: [_jsxs("div", { style: {
83
- width: '240px',
84
- borderRight: '1px solid #f0f0f5',
85
- display: 'flex',
86
- flexDirection: 'column',
158
+ flexDirection: 'column',
159
+ minHeight: 0,
160
+ backgroundColor: '#fff',
161
+ animation: 'cw-slideFromRight 0.28s ease-out',
162
+ }, children: [_jsxs("div", { style: {
163
+ flexShrink: 0,
164
+ padding: '14px 16px',
165
+ backgroundColor: t.primaryColor,
166
+ color: '#fff',
167
+ display: 'flex',
168
+ alignItems: 'center',
169
+ justifyContent: 'space-between',
170
+ }, children: [_jsx("button", { type: "button", onClick: goHomeCards, "aria-label": "Back", style: {
171
+ background: 'transparent',
172
+ border: 'none',
173
+ color: '#fff',
174
+ cursor: 'pointer',
175
+ padding: '4px',
176
+ }, children: _jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: _jsx("path", { d: "M15 18L9 12L15 6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), _jsx("span", { style: { fontWeight: 700, fontSize: '16px' }, children: "Raise ticket" }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
177
+ background: 'transparent',
178
+ border: 'none',
179
+ color: '#fff',
180
+ cursor: 'pointer',
181
+ padding: '4px',
182
+ }, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) })] }), _jsxs("div", { style: { padding: '20px', flex: 1, overflowY: 'auto' }, children: [_jsx("label", { style: { display: 'block', fontSize: '12px', fontWeight: 600, color: '#666', marginBottom: '6px' }, children: "Subject" }), _jsx("input", { value: ticketSubject, onChange: (e) => setTicketSubject(e.target.value), placeholder: "Brief summary", style: {
183
+ width: '100%',
184
+ boxSizing: 'border-box',
185
+ padding: '10px 12px',
186
+ borderRadius: '10px',
187
+ border: '1px solid #e5e5e5',
188
+ fontFamily: t.fontFamily,
189
+ fontSize: '14px',
190
+ marginBottom: '16px',
191
+ } }), _jsx("label", { style: { display: 'block', fontSize: '12px', fontWeight: 600, color: '#666', marginBottom: '6px' }, children: "Description" }), _jsx("textarea", { value: ticketBody, onChange: (e) => setTicketBody(e.target.value), placeholder: "Describe the issue or change\u2026", rows: 5, style: {
192
+ width: '100%',
193
+ boxSizing: 'border-box',
194
+ padding: '10px 12px',
195
+ borderRadius: '10px',
196
+ border: '1px solid #e5e5e5',
197
+ fontFamily: t.fontFamily,
198
+ fontSize: '14px',
199
+ resize: 'vertical',
200
+ } }), _jsx("button", { type: "button", onClick: submitTicket, style: {
201
+ marginTop: '18px',
202
+ width: '100%',
203
+ padding: '12px',
204
+ borderRadius: '10px',
205
+ border: 'none',
206
+ backgroundColor: t.primaryColor,
207
+ color: '#fff',
208
+ fontWeight: 700,
209
+ fontFamily: t.fontFamily,
210
+ cursor: 'pointer',
211
+ fontSize: '15px',
212
+ }, children: "Submit ticket" })] })] })), bottomTab === 'chats' && (_jsxs("div", { style: {
213
+ flex: 1,
214
+ display: 'flex',
215
+ flexDirection: 'column',
216
+ minHeight: 0,
217
+ backgroundColor: '#fff',
218
+ }, children: [_jsxs("div", { style: {
219
+ padding: '16px 20px',
220
+ backgroundColor: t.primaryColor,
221
+ color: '#fff',
222
+ fontWeight: 700,
223
+ fontSize: '17px',
224
+ display: 'flex',
225
+ alignItems: 'center',
226
+ justifyContent: 'space-between',
227
+ }, children: ["Recent chats", _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
228
+ background: 'transparent',
229
+ border: 'none',
230
+ color: '#fff',
231
+ cursor: 'pointer',
232
+ }, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) })] }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: recentChats.length === 0 ? (_jsx("p", { style: { padding: '24px', textAlign: 'center', color: '#aaa', fontSize: '14px', margin: 0 }, children: "No recent chats yet" })) : (recentChats.map((r) => (_jsxs("button", { type: "button", onClick: () => {
233
+ openRecent(r.user);
234
+ setBottomTab('home');
235
+ setHomeFlow('chat');
236
+ setPickUserMode(null);
237
+ }, style: {
238
+ width: '100%',
239
+ padding: '14px 18px',
240
+ border: 'none',
241
+ borderBottom: '1px solid #f0f0f0',
242
+ background: '#fff',
243
+ textAlign: 'left',
244
+ cursor: 'pointer',
245
+ fontFamily: t.fontFamily,
246
+ }, children: [_jsx("div", { style: { fontWeight: 600, color: '#1a1a2e' }, children: r.user.name }), _jsx("div", { style: { fontSize: '13px', color: '#888', marginTop: '4px' }, children: r.lastMessage }), _jsx("div", { style: { fontSize: '11px', color: '#bbb', marginTop: '6px' }, children: r.updatedAt.toLocaleString() })] }, r.user.uid)))) })] })), bottomTab === 'tickets' && (_jsxs("div", { style: {
247
+ flex: 1,
248
+ display: 'flex',
249
+ flexDirection: 'column',
250
+ minHeight: 0,
251
+ backgroundColor: '#fff',
252
+ }, children: [_jsxs("div", { style: {
253
+ padding: '16px 20px',
254
+ backgroundColor: t.primaryColor,
255
+ color: '#fff',
256
+ fontWeight: 700,
257
+ fontSize: '17px',
258
+ display: 'flex',
259
+ alignItems: 'center',
260
+ justifyContent: 'space-between',
261
+ }, children: ["Tickets", _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
262
+ background: 'transparent',
263
+ border: 'none',
264
+ color: '#fff',
265
+ cursor: 'pointer',
266
+ }, children: _jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) })] }), _jsx("div", { style: { flex: 1, overflowY: 'auto' }, children: tickets.length === 0 ? (_jsx("p", { style: { padding: '24px', textAlign: 'center', color: '#aaa', fontSize: '14px', margin: 0 }, children: "No tickets yet. Use Home \u2192 Raise Ticket." })) : (tickets.map((tk) => (_jsxs("div", { style: {
267
+ padding: '14px 18px',
268
+ borderBottom: '1px solid #f0f0f0',
269
+ fontFamily: t.fontFamily,
270
+ }, children: [_jsx("div", { style: { fontWeight: 600, color: '#1a1a2e' }, children: tk.subject }), tk.body ? (_jsx("div", { style: { fontSize: '13px', color: '#666', marginTop: '6px', lineHeight: 1.45 }, children: tk.body })) : null, _jsxs("div", { style: { fontSize: '11px', color: '#bbb', marginTop: '8px' }, children: [tk.createdAt.toLocaleString(), " \u00B7 ", tk.status] })] }, tk.id)))) })] }))] }), _jsx("div", { style: {
271
+ padding: '8px 14px 6px',
272
+ borderTop: '1px solid #eee',
273
+ backgroundColor: '#fafafa',
87
274
  flexShrink: 0,
88
- overflow: 'hidden',
89
- }, children: [_jsxs("div", { style: {
90
- padding: '12px 16px',
91
- borderBottom: '1px solid #f5f5f5',
92
- fontSize: '11px',
93
- fontWeight: 700,
94
- color: '#bbb',
95
- textTransform: 'uppercase',
96
- letterSpacing: '0.08em',
97
- }, children: [filterType === 'developer' ? 'Developers' : 'Users', ' ', !loading && !error && `(${users.length})`] }), _jsx(UserList, { users: users, loading: loading, error: error, activeUserId: (_a = activeUser === null || activeUser === void 0 ? void 0 : activeUser.uid) !== null && _a !== void 0 ? _a : null, onSelectUser: selectUser, primaryColor: t.primaryColor, fontFamily: t.fontFamily })] }), _jsx(ChatBox, { activeUser: activeUser, messages: messages, onSendMessage: sendMessage, primaryColor: t.primaryColor, fontFamily: t.fontFamily })] }))] }));
98
- };
99
- const StatusBadge = ({ status }) => {
100
- var _a;
101
- const map = {
102
- ACTIVE: { label: 'Active', bg: 'rgba(255,255,255,0.2)', dot: '#4caf50' },
103
- MAINTENANCE: { label: 'Maintenance', bg: 'rgba(255,193,7,0.3)', dot: '#ffc107' },
104
- DISABLE: { label: 'Disabled', bg: 'rgba(0,0,0,0.2)', dot: '#f44336' },
105
- };
106
- const s = (_a = map[status]) !== null && _a !== void 0 ? _a : map['DISABLE'];
107
- return (_jsxs("span", { style: {
108
- display: 'inline-flex',
109
- alignItems: 'center',
110
- gap: '5px',
111
- padding: '4px 10px',
112
- borderRadius: '20px',
113
- backgroundColor: s.bg,
114
- fontSize: '11px',
115
- fontWeight: 600,
116
- color: '#fff',
117
- }, children: [_jsx("span", { style: {
118
- width: '6px',
119
- height: '6px',
120
- borderRadius: '50%',
121
- backgroundColor: s.dot,
122
- display: 'inline-block',
123
- } }), s.label] }));
275
+ }, children: _jsxs("div", { style: {
276
+ display: 'flex',
277
+ alignItems: 'center',
278
+ gap: '10px',
279
+ fontSize: '11px',
280
+ color: '#888',
281
+ fontFamily: t.fontFamily,
282
+ }, children: [_jsx("span", { style: { flexShrink: 0 }, children: "Size" }), _jsx("input", { type: "range", min: 0, max: 1, step: 0.02, value: sizeRatio, onChange: (e) => setSizeRatio(parseFloat(e.target.value)), "aria-label": "Widget size", style: {
283
+ flex: 1,
284
+ accentColor: t.primaryColor,
285
+ } })] }) }), _jsx(BottomNav, { active: bottomTab, onChange: handleBottomTab, primaryColor: t.primaryColor, fontFamily: t.fontFamily })] }))] }));
124
286
  };
125
- // Simple color darkener for gradient
126
- function adjustColor(hex, amount) {
127
- const num = parseInt(hex.replace('#', ''), 16);
128
- const r = Math.max(0, Math.min(255, (num >> 16) + amount));
129
- const g = Math.max(0, Math.min(255, ((num >> 8) & 0xff) + amount));
130
- const b = Math.max(0, Math.min(255, (num & 0xff) + amount));
131
- return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`;
132
- }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ export interface HomeViewProps {
3
+ primaryColor: string;
4
+ fontFamily: string;
5
+ showNeedSupport: boolean;
6
+ showNewConversation: boolean;
7
+ onNeedSupport: () => void;
8
+ onNewConversation: () => void;
9
+ onRaiseTicket: () => void;
10
+ onClose: () => void;
11
+ }
12
+ export declare const HomeView: React.FC<HomeViewProps>;
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const SendIcon = ({ color }) => (_jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: _jsx("path", { d: "M22 2L11 13M22 2L15 22L11 13L2 9L22 2Z", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
3
+ export const HomeView = ({ primaryColor, fontFamily, showNeedSupport, showNewConversation, onNeedSupport, onNewConversation, onRaiseTicket, onClose, }) => {
4
+ const cardStyle = {
5
+ width: '100%',
6
+ backgroundColor: '#fff',
7
+ borderRadius: '12px',
8
+ padding: '16px 18px',
9
+ display: 'flex',
10
+ alignItems: 'center',
11
+ justifyContent: 'space-between',
12
+ gap: '12px',
13
+ border: 'none',
14
+ cursor: 'pointer',
15
+ textAlign: 'left',
16
+ fontFamily,
17
+ boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
18
+ transition: 'transform 0.15s ease, box-shadow 0.15s ease',
19
+ };
20
+ return (_jsxs("div", { style: {
21
+ flex: 1,
22
+ display: 'flex',
23
+ flexDirection: 'column',
24
+ backgroundColor: primaryColor,
25
+ fontFamily,
26
+ minHeight: 0,
27
+ }, children: [_jsx("div", { style: {
28
+ padding: '20px 20px 8px',
29
+ display: 'flex',
30
+ justifyContent: 'flex-end',
31
+ }, children: _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", style: {
32
+ background: 'transparent',
33
+ border: 'none',
34
+ color: '#fff',
35
+ cursor: 'pointer',
36
+ padding: '4px',
37
+ }, children: _jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })] }) }) }), _jsxs("div", { style: { padding: '0 24px 20px', flexShrink: 0 }, children: [_jsx("h1", { style: {
38
+ margin: 0,
39
+ color: '#fff',
40
+ fontSize: '26px',
41
+ fontWeight: 700,
42
+ letterSpacing: '-0.02em',
43
+ }, children: "Hi there \uD83D\uDC4B" }), _jsx("p", { style: { margin: '10px 0 0', color: 'rgba(255,255,255,0.92)', fontSize: '14px', lineHeight: 1.5 }, children: "Need help? start a conversation:" })] }), _jsxs("div", { style: {
44
+ flex: 1,
45
+ padding: '0 20px 16px',
46
+ display: 'flex',
47
+ flexDirection: 'column',
48
+ gap: '12px',
49
+ overflowY: 'auto',
50
+ }, children: [showNeedSupport && (_jsxs("button", { type: "button", onClick: onNeedSupport, style: cardStyle, children: [_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: '15px', color: '#1a1a2e' }, children: "Need Support" }), _jsx("div", { style: { fontSize: '13px', color: '#888', marginTop: '4px' }, children: "We typically reply in a few minutes" })] }), _jsx(SendIcon, { color: primaryColor })] })), showNewConversation && (_jsxs("button", { type: "button", onClick: onNewConversation, style: cardStyle, children: [_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: '15px', color: '#1a1a2e' }, children: "New Conversation" }), _jsx("div", { style: { fontSize: '13px', color: '#888', marginTop: '4px' }, children: "With your colleague" })] }), _jsx(SendIcon, { color: primaryColor })] })), _jsxs("button", { type: "button", onClick: onRaiseTicket, style: cardStyle, children: [_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 700, fontSize: '15px', color: '#1a1a2e' }, children: "Raise Ticket" }), _jsx("div", { style: { fontSize: '13px', color: '#888', marginTop: '4px' }, children: "For major changes" })] }), _jsx(SendIcon, { color: primaryColor })] })] })] }));
51
+ };
@@ -1,3 +1,16 @@
1
1
  import { ChatConfig } from '../types';
2
2
  export declare function loadChatConfig(): ChatConfig;
3
+ /**
4
+ * Resolves the URL used by `fetchUsers`.
5
+ *
6
+ * **Same-origin / internal proxy (recommended):** If `CHAT_USER_LIST` starts with `/`
7
+ * (e.g. `/api/v1/chat/users`), the browser only requests your app origin. The real
8
+ * backend URL stays on the server (Next.js route, BFF, etc.) and does not appear in
9
+ * the client Network tab as a cross-origin call.
10
+ *
11
+ * **Absolute URL:** If `CHAT_USER_LIST` is a full `http(s)://...` string, it is used as-is
12
+ * (visible in Network as that host).
13
+ *
14
+ * **Legacy:** Otherwise the path is joined with `CHAT_HOST_URL` and optional `CHAT_HOST_PORT`.
15
+ */
3
16
  export declare function buildUserListUrl(config: ChatConfig): string;
@@ -29,23 +29,85 @@ function validateChatType(value) {
29
29
  console.warn(`[ChatWidget] Invalid CHAT_TYPE "${value}". Defaulting to "SUPPORT".`);
30
30
  return 'SUPPORT';
31
31
  }
32
+ function parseBool(value, defaultValue) {
33
+ if (value === undefined || value === '')
34
+ return defaultValue;
35
+ const v = value.toLowerCase().trim();
36
+ if (v === 'true' || v === '1' || v === 'yes')
37
+ return true;
38
+ if (v === 'false' || v === '0' || v === 'no')
39
+ return false;
40
+ return defaultValue;
41
+ }
42
+ function parsePositiveInt(value, fallback) {
43
+ if (value === undefined || value === '')
44
+ return fallback;
45
+ const n = parseInt(value, 10);
46
+ return Number.isFinite(n) && n > 0 ? n : fallback;
47
+ }
48
+ function parseOptionalPort(value) {
49
+ if (value === undefined || value.trim() === '')
50
+ return undefined;
51
+ const n = parseInt(value, 10);
52
+ return Number.isFinite(n) && n > 0 ? n : undefined;
53
+ }
54
+ function parseSizeRatio(value, fallback) {
55
+ if (value === undefined || value === '')
56
+ return fallback;
57
+ const n = parseFloat(value);
58
+ if (!Number.isFinite(n))
59
+ return fallback;
60
+ return Math.min(1, Math.max(0, n));
61
+ }
32
62
  export function loadChatConfig() {
33
- var _a, _b, _c;
63
+ var _a, _b;
34
64
  const hostUrl = (_a = getEnvVar('CHAT_HOST_URL')) !== null && _a !== void 0 ? _a : 'http://localhost';
35
- const hostPort = parseInt((_b = getEnvVar('CHAT_HOST_PORT')) !== null && _b !== void 0 ? _b : '3001', 10);
36
- const userListEndpoint = (_c = getEnvVar('CHAT_USER_LIST')) !== null && _c !== void 0 ? _c : 'api/users';
65
+ const hostPort = parseOptionalPort(getEnvVar('CHAT_HOST_PORT'));
66
+ const userListEndpoint = (_b = getEnvVar('CHAT_USER_LIST')) !== null && _b !== void 0 ? _b : 'api/users';
37
67
  const status = validateStatus(getEnvVar('CHAT_STATUS'));
38
68
  const chatType = validateChatType(getEnvVar('CHAT_TYPE'));
69
+ const showNeedSupport = parseBool(getEnvVar('CHAT_SHOW_NEED_SUPPORT'), true);
70
+ const showNewConversation = parseBool(getEnvVar('CHAT_SHOW_NEW_CONVERSATION'), true);
39
71
  return {
40
72
  hostUrl,
41
73
  hostPort,
42
74
  userListEndpoint,
43
75
  status,
44
76
  chatType,
77
+ showNeedSupport,
78
+ showNewConversation,
79
+ widgetMinWidth: parsePositiveInt(getEnvVar('CHAT_WIDGET_MIN_WIDTH'), 320),
80
+ widgetMaxWidth: parsePositiveInt(getEnvVar('CHAT_WIDGET_MAX_WIDTH'), 720),
81
+ widgetMinHeight: parsePositiveInt(getEnvVar('CHAT_WIDGET_MIN_HEIGHT'), 420),
82
+ widgetMaxHeight: parsePositiveInt(getEnvVar('CHAT_WIDGET_MAX_HEIGHT'), 720),
83
+ widgetDefaultSize: parseSizeRatio(getEnvVar('CHAT_WIDGET_DEFAULT_SIZE'), 0.45),
45
84
  };
46
85
  }
86
+ /**
87
+ * Resolves the URL used by `fetchUsers`.
88
+ *
89
+ * **Same-origin / internal proxy (recommended):** If `CHAT_USER_LIST` starts with `/`
90
+ * (e.g. `/api/v1/chat/users`), the browser only requests your app origin. The real
91
+ * backend URL stays on the server (Next.js route, BFF, etc.) and does not appear in
92
+ * the client Network tab as a cross-origin call.
93
+ *
94
+ * **Absolute URL:** If `CHAT_USER_LIST` is a full `http(s)://...` string, it is used as-is
95
+ * (visible in Network as that host).
96
+ *
97
+ * **Legacy:** Otherwise the path is joined with `CHAT_HOST_URL` and optional `CHAT_HOST_PORT`.
98
+ */
47
99
  export function buildUserListUrl(config) {
100
+ const raw = config.userListEndpoint.trim();
101
+ if (raw.startsWith('/')) {
102
+ return raw;
103
+ }
104
+ if (/^https?:\/\//i.test(raw)) {
105
+ return raw;
106
+ }
48
107
  const base = config.hostUrl.replace(/\/$/, '');
49
- const endpoint = config.userListEndpoint.replace(/^\//, '');
50
- return `${base}:${config.hostPort}/${endpoint}`;
108
+ const endpoint = raw.replace(/^\//, '');
109
+ if (config.hostPort !== undefined) {
110
+ return `${base}:${config.hostPort}/${endpoint}`;
111
+ }
112
+ return `${base}/${endpoint}`;
51
113
  }
@@ -1,10 +1,12 @@
1
- import { ChatMessage, ChatUser } from '../types';
1
+ import { ChatMessage, ChatUser, RecentChat } from '../types';
2
2
  interface UseChatReturn {
3
3
  messages: ChatMessage[];
4
4
  activeUser: ChatUser | null;
5
+ recentChats: RecentChat[];
5
6
  selectUser: (user: ChatUser) => void;
6
7
  sendMessage: (text: string) => void;
7
8
  clearChat: () => void;
9
+ openRecent: (user: ChatUser) => void;
8
10
  }
9
11
  export declare function useChat(): UseChatReturn;
10
12
  export {};
@@ -2,10 +2,21 @@ import { useState, useCallback } from 'react';
2
2
  export function useChat() {
3
3
  const [messages, setMessages] = useState([]);
4
4
  const [activeUser, setActiveUser] = useState(null);
5
+ const [recentChats, setRecentChats] = useState([]);
6
+ const upsertRecent = useCallback((user, lastMessage) => {
7
+ const updatedAt = new Date();
8
+ setRecentChats((prev) => {
9
+ const rest = prev.filter((r) => r.user.uid !== user.uid);
10
+ return [{ user, lastMessage, updatedAt }, ...rest].slice(0, 50);
11
+ });
12
+ }, []);
5
13
  const selectUser = useCallback((user) => {
6
14
  setActiveUser(user);
7
15
  setMessages([]);
8
- // TODO: Connect WebSocket here — load message history for this user
16
+ }, []);
17
+ const openRecent = useCallback((user) => {
18
+ setActiveUser(user);
19
+ setMessages([]);
9
20
  }, []);
10
21
  const sendMessage = useCallback((text) => {
11
22
  if (!activeUser || !text.trim())
@@ -19,11 +30,19 @@ export function useChat() {
19
30
  status: 'sent',
20
31
  };
21
32
  setMessages((prev) => [...prev, newMsg]);
22
- // TODO: Emit via WebSocket — socket.emit('message', newMsg)
23
- }, [activeUser]);
33
+ upsertRecent(activeUser, text.trim());
34
+ }, [activeUser, upsertRecent]);
24
35
  const clearChat = useCallback(() => {
25
36
  setMessages([]);
26
37
  setActiveUser(null);
27
38
  }, []);
28
- return { messages, activeUser, selectUser, sendMessage, clearChat };
39
+ return {
40
+ messages,
41
+ activeUser,
42
+ recentChats,
43
+ selectUser,
44
+ sendMessage,
45
+ clearChat,
46
+ openRecent,
47
+ };
29
48
  }
package/dist/index.d.ts CHANGED
@@ -9,5 +9,5 @@ export { useUsers } from './hooks/useUsers';
9
9
  export { useChat } from './hooks/useChat';
10
10
  export { loadChatConfig, buildUserListUrl } from './config';
11
11
  export { fetchUsers } from './services/userService';
12
- export type { ChatUser, ChatMessage, ChatConfig, ChatWidgetTheme, ChatWidgetProps, ChatStatus, ChatType, UserType, TabType, } from './types';
12
+ export type { ChatUser, ChatMessage, ChatConfig, ChatWidgetTheme, ChatWidgetProps, ChatStatus, ChatType, UserType, TabType, BottomNavTab, HomeFlow, RaisedTicket, RecentChat, } from './types';
13
13
  export { defaultTheme, mergeTheme } from './utils/theme';
@@ -1,9 +1,11 @@
1
1
  export async function fetchUsers(url) {
2
+ const sameOriginPath = url.startsWith('/');
2
3
  const response = await fetch(url, {
3
4
  method: 'GET',
4
5
  headers: {
5
6
  'Content-Type': 'application/json',
6
7
  },
8
+ credentials: sameOriginPath ? 'same-origin' : 'omit',
7
9
  });
8
10
  if (!response.ok) {
9
11
  throw new Error(`[ChatWidget] Failed to fetch users: ${response.status} ${response.statusText}`);
@@ -2,6 +2,8 @@ export type ChatStatus = 'ACTIVE' | 'DISABLE' | 'MAINTENANCE';
2
2
  export type ChatType = 'SUPPORT' | 'CHAT' | 'BOTH';
3
3
  export type UserType = 'developer' | 'user';
4
4
  export type TabType = 'developers' | 'users';
5
+ export type BottomNavTab = 'home' | 'chats' | 'tickets';
6
+ export type HomeFlow = 'home' | 'pickUser' | 'chat' | 'raiseTicket';
5
7
  export interface ChatUser {
6
8
  name: string;
7
9
  uid: string;
@@ -20,10 +22,34 @@ export interface ChatMessage {
20
22
  }
21
23
  export interface ChatConfig {
22
24
  hostUrl: string;
23
- hostPort: number;
25
+ /** When omitted, URLs use `hostUrl` only (no `:port` segment). */
26
+ hostPort?: number;
24
27
  userListEndpoint: string;
25
28
  status: ChatStatus;
26
29
  chatType: ChatType;
30
+ /** Show “Need Support” card on home (env `CHAT_SHOW_NEED_SUPPORT`). */
31
+ showNeedSupport: boolean;
32
+ /** Show “New Conversation” card on home (env `CHAT_SHOW_NEW_CONVERSATION`). */
33
+ showNewConversation: boolean;
34
+ /** Pixel bounds for the resize slider (env `CHAT_WIDGET_*`). */
35
+ widgetMinWidth: number;
36
+ widgetMaxWidth: number;
37
+ widgetMinHeight: number;
38
+ widgetMaxHeight: number;
39
+ /** Default widget size as ratio 0–1 between min and max. */
40
+ widgetDefaultSize: number;
41
+ }
42
+ export interface RaisedTicket {
43
+ id: string;
44
+ subject: string;
45
+ body: string;
46
+ createdAt: Date;
47
+ status: 'open' | 'closed';
48
+ }
49
+ export interface RecentChat {
50
+ user: ChatUser;
51
+ lastMessage: string;
52
+ updatedAt: Date;
27
53
  }
28
54
  export interface ChatWidgetTheme {
29
55
  fontFamily?: string;
@@ -34,6 +60,12 @@ export interface ChatWidgetTheme {
34
60
  buttonLabel?: string;
35
61
  buttonPosition?: 'bottom-right' | 'bottom-left';
36
62
  borderRadius?: string;
63
+ /** Override config min width (px). */
64
+ widgetMinWidth?: number;
65
+ widgetMaxWidth?: number;
66
+ widgetMinHeight?: number;
67
+ widgetMaxHeight?: number;
68
+ widgetDefaultSize?: number;
37
69
  }
38
70
  export interface ChatWidgetProps {
39
71
  theme?: ChatWidgetTheme;
@@ -1,12 +1,17 @@
1
1
  export const defaultTheme = {
2
2
  fontFamily: "'DM Sans', 'Segoe UI', sans-serif",
3
- primaryColor: '#6C63FF',
3
+ primaryColor: '#13947e',
4
4
  backgroundColor: '#ffffff',
5
- buttonColor: '#6C63FF',
5
+ buttonColor: '#13947e',
6
6
  buttonTextColor: '#ffffff',
7
7
  buttonLabel: 'Chat with us',
8
8
  buttonPosition: 'bottom-right',
9
9
  borderRadius: '16px',
10
+ widgetMinWidth: 320,
11
+ widgetMaxWidth: 720,
12
+ widgetMinHeight: 420,
13
+ widgetMaxHeight: 720,
14
+ widgetDefaultSize: 0.45,
10
15
  };
11
16
  export function mergeTheme(custom) {
12
17
  return Object.assign(Object.assign({}, defaultTheme), custom);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ajaxter-chat",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "A reusable, configurable chat widget for React.js and Next.js applications.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",