@channlize/react 0.1.1 → 0.4.0

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/dist/index.js CHANGED
@@ -20,10 +20,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AuthenticationError: () => import_core4.AuthenticationError,
23
24
  ChannlizeChat: () => ChannlizeChat,
25
+ ChannlizeError: () => import_core4.ChannlizeError,
24
26
  ChannlizeProvider: () => ChannlizeProvider,
25
27
  ContactForm: () => ContactForm,
28
+ RateLimitError: () => import_core4.RateLimitError,
26
29
  SupportChat: () => SupportChat,
30
+ ValidationError: () => import_core4.ValidationError,
31
+ defaultContactFormLabels: () => defaultContactFormLabels,
32
+ defaultSupportChatLabels: () => defaultSupportChatLabels,
27
33
  useChannlize: () => useChannlize,
28
34
  useChannlizeAnalytics: () => useChannlizeAnalytics,
29
35
  useChatSession: () => useChatSession,
@@ -40,8 +46,19 @@ function ChannlizeProvider({
40
46
  config,
41
47
  children
42
48
  }) {
43
- const client = new import_core.ChannlizeClient(config);
44
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChannlizeContext.Provider, { value: { client, config }, children });
49
+ const { apiKey, baseUrl, projectId, timeout, serverApiKey } = config;
50
+ const client = (0, import_react.useMemo)(
51
+ () => new import_core.ChannlizeClient({
52
+ apiKey,
53
+ baseUrl,
54
+ projectId,
55
+ timeout,
56
+ serverApiKey
57
+ }),
58
+ [apiKey, baseUrl, projectId, timeout, serverApiKey]
59
+ );
60
+ const value = (0, import_react.useMemo)(() => ({ client, config }), [client, config]);
61
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChannlizeContext.Provider, { value, children });
45
62
  }
46
63
  function useChannlize() {
47
64
  const context = (0, import_react.useContext)(ChannlizeContext);
@@ -93,6 +110,7 @@ function useContactForm(options = {}) {
93
110
 
94
111
  // src/hooks/useChatSession.ts
95
112
  var import_react3 = require("react");
113
+ var import_core2 = require("@channlize/core");
96
114
  function useChatSession(options = {}) {
97
115
  const { client, config } = useChannlize();
98
116
  const [session, setSession] = (0, import_react3.useState)(null);
@@ -103,9 +121,27 @@ function useChatSession(options = {}) {
103
121
  const eventSourceRef = (0, import_react3.useRef)(null);
104
122
  const sessionRef = (0, import_react3.useRef)(null);
105
123
  const isInitializedRef = (0, import_react3.useRef)(false);
106
- const baseUrl = config.baseUrl || "https://api.channlize.com";
124
+ const baseUrl = config.baseUrl || import_core2.DEFAULT_BASE_URL;
107
125
  const storageKey = options.storageKey || `channlize_session_${config.apiKey}`;
108
126
  const persistSession = options.persistSession ?? true;
127
+ const latest = (0, import_react3.useRef)({
128
+ onMessage: options.onMessage,
129
+ onError: options.onError,
130
+ debug: options.debug
131
+ });
132
+ (0, import_react3.useEffect)(() => {
133
+ latest.current = {
134
+ onMessage: options.onMessage,
135
+ onError: options.onError,
136
+ debug: options.debug
137
+ };
138
+ });
139
+ const log = (0, import_react3.useCallback)(
140
+ (level, ...args) => {
141
+ if (latest.current.debug) console[level]("[channlize]", ...args);
142
+ },
143
+ []
144
+ );
109
145
  (0, import_react3.useEffect)(() => {
110
146
  sessionRef.current = session;
111
147
  }, [session]);
@@ -115,51 +151,50 @@ function useChatSession(options = {}) {
115
151
  try {
116
152
  localStorage.setItem(storageKey, sessionId);
117
153
  } catch (err) {
118
- console.warn("Failed to save session to localStorage:", err);
154
+ log("warn", "Failed to save session to localStorage:", err);
119
155
  }
120
156
  }
121
157
  },
122
- [persistSession, storageKey]
158
+ [persistSession, storageKey, log]
123
159
  );
124
160
  const getSessionFromStorage = (0, import_react3.useCallback)(() => {
125
161
  if (persistSession && typeof window !== "undefined") {
126
162
  try {
127
163
  return localStorage.getItem(storageKey);
128
164
  } catch (err) {
129
- console.warn("Failed to get session from localStorage:", err);
165
+ log("warn", "Failed to get session from localStorage:", err);
130
166
  }
131
167
  }
132
168
  return null;
133
- }, [persistSession, storageKey]);
169
+ }, [persistSession, storageKey, log]);
134
170
  const clearSessionFromStorage = (0, import_react3.useCallback)(() => {
135
171
  if (persistSession && typeof window !== "undefined") {
136
172
  try {
137
173
  localStorage.removeItem(storageKey);
138
174
  } catch (err) {
139
- console.warn("Failed to clear session from localStorage:", err);
175
+ log("warn", "Failed to clear session from localStorage:", err);
140
176
  }
141
177
  }
142
- }, [persistSession, storageKey]);
178
+ }, [persistSession, storageKey, log]);
143
179
  const connectToSSE = (0, import_react3.useCallback)(
144
180
  (sseEndpoint) => {
145
181
  if (eventSourceRef.current) {
146
182
  eventSourceRef.current.close();
147
183
  }
148
184
  const fullUrl = sseEndpoint.startsWith("http") ? sseEndpoint : `${baseUrl}${sseEndpoint}`;
149
- console.log("[Channlize SSE] Connecting to:", fullUrl);
185
+ log("log", "SSE connecting to", fullUrl);
150
186
  const eventSource = new EventSource(fullUrl);
151
187
  eventSourceRef.current = eventSource;
152
188
  eventSource.onopen = () => {
153
- console.log("[Channlize SSE] Connection opened");
189
+ log("log", "SSE connection opened");
154
190
  setIsConnected(true);
155
191
  setError(null);
156
192
  };
157
193
  eventSource.addEventListener("connected", (event) => {
158
- console.log("SSE connected:", event.data);
194
+ log("log", "SSE connected:", event.data);
159
195
  });
160
196
  eventSource.addEventListener("message", (event) => {
161
197
  try {
162
- console.log("SSE message:", event.data);
163
198
  const message = JSON.parse(event.data);
164
199
  const chatMessage = {
165
200
  ...message,
@@ -169,9 +204,9 @@ function useChatSession(options = {}) {
169
204
  const exists = prev.some((m) => m.id === chatMessage.id);
170
205
  return exists ? prev : [...prev, chatMessage];
171
206
  });
172
- options.onMessage?.(chatMessage);
207
+ latest.current.onMessage?.(chatMessage);
173
208
  } catch (err) {
174
- console.warn("Failed to parse SSE message:", err, event.data);
209
+ log("warn", "Failed to parse SSE message:", err, event.data);
175
210
  }
176
211
  });
177
212
  eventSource.onmessage = (event) => {
@@ -186,10 +221,10 @@ function useChatSession(options = {}) {
186
221
  const exists = prev.some((m) => m.id === message.id);
187
222
  return exists ? prev : [...prev, message];
188
223
  });
189
- options.onMessage?.(message);
224
+ latest.current.onMessage?.(message);
190
225
  }
191
226
  } catch (err) {
192
- console.warn("Failed to parse default SSE message:", err);
227
+ log("warn", "Failed to parse default SSE message:", err);
193
228
  }
194
229
  };
195
230
  eventSource.onerror = (event) => {
@@ -197,7 +232,7 @@ function useChatSession(options = {}) {
197
232
  setIsConnected(false);
198
233
  return;
199
234
  }
200
- console.error("[Channlize SSE] Connection error", {
235
+ log("error", "SSE connection error", {
201
236
  readyState: eventSource.readyState,
202
237
  url: fullUrl,
203
238
  event
@@ -209,10 +244,10 @@ function useChatSession(options = {}) {
209
244
  const error2 = new Error(`SSE ${stateMessage}`);
210
245
  error2.code = "SSE_ERROR";
211
246
  setError(error2);
212
- options.onError?.(error2);
247
+ latest.current.onError?.(error2);
213
248
  };
214
249
  },
215
- [baseUrl, options.onError, options.onMessage]
250
+ [baseUrl, log]
216
251
  );
217
252
  const startSession = (0, import_react3.useCallback)(async () => {
218
253
  if (isConnecting || session) return;
@@ -222,9 +257,10 @@ function useChatSession(options = {}) {
222
257
  const existingSessionId = options.sessionId || getSessionFromStorage();
223
258
  if (existingSessionId) {
224
259
  try {
225
- console.log("\u{1F4E6} Resuming session from storage:", existingSessionId);
260
+ log("log", "Resuming session", existingSessionId);
226
261
  const existingSession = await client.getChatSession(existingSessionId);
227
262
  const existingMessages = await client.getChatMessages(existingSessionId);
263
+ sessionRef.current = existingSession;
228
264
  setSession(existingSession);
229
265
  const normalizedMessages2 = existingMessages.map(
230
266
  (msg) => ({
@@ -235,15 +271,16 @@ function useChatSession(options = {}) {
235
271
  setMessages(normalizedMessages2);
236
272
  const sseEndpoint = `/v1/chat/sessions/${existingSessionId}/events`;
237
273
  connectToSSE(sseEndpoint);
238
- console.log("\u2705 Session resumed successfully");
274
+ log("log", "Session resumed");
239
275
  return;
240
276
  } catch (err) {
241
- console.warn("\u26A0\uFE0F Failed to resume session, creating new one:", err);
277
+ log("warn", "Could not resume the session, starting a new one:", err);
242
278
  clearSessionFromStorage();
243
279
  }
244
280
  }
245
- console.log("\u{1F195} Creating new chat session");
281
+ log("log", "Creating a chat session");
246
282
  const response = await client.createChatSession(options.metadata);
283
+ sessionRef.current = response.session;
247
284
  setSession(response.session);
248
285
  saveSessionToStorage(response.session.id);
249
286
  const normalizedMessages = (response.session.messages || []).map(
@@ -256,19 +293,19 @@ function useChatSession(options = {}) {
256
293
  if (response.sseEndpoint) {
257
294
  connectToSSE(response.sseEndpoint);
258
295
  }
259
- console.log("\u2705 New session created:", response.session.id);
296
+ log("log", "Session created", response.session.id);
260
297
  } catch (err) {
261
298
  const error2 = err;
262
299
  setError(error2);
263
- options.onError?.(error2);
300
+ latest.current.onError?.(error2);
264
301
  } finally {
265
302
  setIsConnecting(false);
266
303
  }
267
304
  }, [
268
305
  client,
269
306
  connectToSSE,
307
+ log,
270
308
  options.metadata,
271
- options.onError,
272
309
  options.sessionId,
273
310
  getSessionFromStorage,
274
311
  saveSessionToStorage,
@@ -299,11 +336,11 @@ function useChatSession(options = {}) {
299
336
  } catch (err) {
300
337
  const error2 = err;
301
338
  setError(error2);
302
- options.onError?.(error2);
339
+ latest.current.onError?.(error2);
303
340
  throw error2;
304
341
  }
305
342
  },
306
- [client, options.onError]
343
+ [client]
307
344
  );
308
345
  const endSession = (0, import_react3.useCallback)(async () => {
309
346
  if (!sessionRef.current) return;
@@ -314,16 +351,26 @@ function useChatSession(options = {}) {
314
351
  eventSourceRef.current = null;
315
352
  }
316
353
  clearSessionFromStorage();
354
+ sessionRef.current = null;
317
355
  setSession(null);
318
356
  setMessages([]);
319
357
  setIsConnected(false);
320
- console.log("\u2705 Session ended and cleared from storage");
358
+ log("log", "Session ended");
321
359
  } catch (err) {
322
360
  const error2 = err;
323
361
  setError(error2);
324
- options.onError?.(error2);
362
+ latest.current.onError?.(error2);
363
+ }
364
+ }, [client, log, clearSessionFromStorage]);
365
+ const reconnect = (0, import_react3.useCallback)(() => {
366
+ const current = sessionRef.current;
367
+ if (!current) return;
368
+ if (eventSourceRef.current && eventSourceRef.current.readyState !== EventSource.CLOSED) {
369
+ return;
325
370
  }
326
- }, [client, options.onError, clearSessionFromStorage]);
371
+ setError(null);
372
+ connectToSSE(`/v1/chat/sessions/${current.id}/events`);
373
+ }, [connectToSSE]);
327
374
  (0, import_react3.useEffect)(() => {
328
375
  if (options.autoConnect && !session && !isConnecting && !isInitializedRef.current) {
329
376
  isInitializedRef.current = true;
@@ -346,7 +393,8 @@ function useChatSession(options = {}) {
346
393
  error,
347
394
  sendMessage,
348
395
  startSession,
349
- endSession
396
+ endSession,
397
+ reconnect
350
398
  };
351
399
  }
352
400
 
@@ -363,189 +411,1288 @@ function useChannlizeAnalytics() {
363
411
 
364
412
  // src/components/Form.tsx
365
413
  var import_react5 = require("react");
414
+
415
+ // src/components/chat/icons.tsx
366
416
  var import_jsx_runtime2 = require("react/jsx-runtime");
417
+ function Icon({ size = 20, children, ...props }) {
418
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
419
+ "svg",
420
+ {
421
+ width: size,
422
+ height: size,
423
+ viewBox: "0 0 24 24",
424
+ fill: "none",
425
+ stroke: "currentColor",
426
+ strokeWidth: 2,
427
+ strokeLinecap: "round",
428
+ strokeLinejoin: "round",
429
+ "aria-hidden": "true",
430
+ focusable: "false",
431
+ ...props,
432
+ children
433
+ }
434
+ );
435
+ }
436
+ var ChatIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...p, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 11.5a8.4 8.4 0 0 1-9 8.4 9.3 9.3 0 0 1-3.7-.8L3 21l1.9-4.9A8 8 0 0 1 3 11.5 8.6 8.6 0 0 1 12 3a8.6 8.6 0 0 1 9 8.5Z" }) });
437
+ var CloseIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...p, children: [
438
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M18 6 6 18" }),
439
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m6 6 12 12" })
440
+ ] });
441
+ var ChevronDownIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...p, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m6 9 6 6 6-6" }) });
442
+ var MoreIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...p, children: [
443
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "5", cy: "12", r: "1", fill: "currentColor" }),
444
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "1", fill: "currentColor" }),
445
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "19", cy: "12", r: "1", fill: "currentColor" })
446
+ ] });
447
+ var SendIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...p, children: [
448
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 19V5" }),
449
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m5 12 7-7 7 7" })
450
+ ] });
451
+ var PaperclipIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...p, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m21.4 11.1-9.2 9.2a6 6 0 0 1-8.5-8.5l9.2-9.2a4 4 0 0 1 5.7 5.7l-9.2 9.2a2 2 0 0 1-2.8-2.8l8.5-8.5" }) });
452
+ var FileIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...p, children: [
453
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z" }),
454
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M14 3v5h5" })
455
+ ] });
456
+ var AlertIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...p, children: [
457
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "12", r: "9" }),
458
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 8v4" }),
459
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12 16h.01" })
460
+ ] });
461
+ var ExitIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Icon, { ...p, children: [
462
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }),
463
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "m16 17 5-5-5-5" }),
464
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M21 12H9" })
465
+ ] });
466
+ var CheckIcon = (p) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Icon, { ...p, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M20 6 9 17l-5-5" }) });
467
+
468
+ // src/components/chat/utils.ts
469
+ var GROUP_GAP_MS = 5 * 60 * 1e3;
470
+ function isValidDate(date) {
471
+ return date instanceof Date && !isNaN(date.getTime());
472
+ }
473
+ function dayKey(date) {
474
+ return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
475
+ }
476
+ function groupMessages(messages) {
477
+ const sections = [];
478
+ let section;
479
+ let group;
480
+ let lastTime = 0;
481
+ for (const msg of messages) {
482
+ const time = isValidDate(msg.timestamp) ? msg.timestamp : /* @__PURE__ */ new Date();
483
+ const key = dayKey(time);
484
+ if (!section || section.key !== key) {
485
+ section = { key, date: time, groups: [] };
486
+ sections.push(section);
487
+ group = void 0;
488
+ }
489
+ const sameAuthor = group && group.author === msg.author && (msg.author === "user" || group.authorName === msg.authorName);
490
+ const closeInTime = time.getTime() - lastTime < GROUP_GAP_MS;
491
+ if (!group || !sameAuthor || !closeInTime) {
492
+ group = {
493
+ key: msg.key,
494
+ author: msg.author,
495
+ authorName: msg.authorName,
496
+ avatarUrl: msg.avatarUrl,
497
+ messages: []
498
+ };
499
+ section.groups.push(group);
500
+ }
501
+ group.messages.push(msg);
502
+ lastTime = time.getTime();
503
+ }
504
+ return sections;
505
+ }
506
+ function formatTime(date, locale) {
507
+ if (!isValidDate(date)) return "";
508
+ try {
509
+ return new Intl.DateTimeFormat(locale, {
510
+ hour: "numeric",
511
+ minute: "2-digit"
512
+ }).format(date);
513
+ } catch {
514
+ return "";
515
+ }
516
+ }
517
+ function formatFullDate(date, locale) {
518
+ if (!isValidDate(date)) return "";
519
+ try {
520
+ return new Intl.DateTimeFormat(locale, {
521
+ dateStyle: "medium",
522
+ timeStyle: "short"
523
+ }).format(date);
524
+ } catch {
525
+ return "";
526
+ }
527
+ }
528
+ function formatDayLabel(date, labels, locale, now = /* @__PURE__ */ new Date()) {
529
+ if (!isValidDate(date)) return "";
530
+ const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
531
+ const days = Math.round((startOfDay(now) - startOfDay(date)) / 864e5);
532
+ if (days === 0) return labels.today;
533
+ if (days === 1) return labels.yesterday;
534
+ try {
535
+ if (days > 1 && days < 7) {
536
+ return new Intl.DateTimeFormat(locale, { weekday: "long" }).format(date);
537
+ }
538
+ return new Intl.DateTimeFormat(locale, {
539
+ month: "short",
540
+ day: "numeric",
541
+ ...date.getFullYear() !== now.getFullYear() && { year: "numeric" }
542
+ }).format(date);
543
+ } catch {
544
+ return date.toDateString();
545
+ }
546
+ }
547
+ function formatFileSize(bytes) {
548
+ if (typeof bytes !== "number" || !isFinite(bytes) || bytes < 0) return "";
549
+ if (bytes < 1024) return `${bytes} B`;
550
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
551
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
552
+ }
553
+ function safeUrl(url, baseUrl) {
554
+ if (!url || typeof url !== "string") return void 0;
555
+ try {
556
+ const base = baseUrl || (typeof window !== "undefined" ? window.location.href : void 0);
557
+ const parsed = new URL(url, base);
558
+ if (["http:", "https:", "blob:"].includes(parsed.protocol)) {
559
+ return parsed.href;
560
+ }
561
+ } catch {
562
+ }
563
+ return void 0;
564
+ }
565
+ function toDisplayAttachments(attachments, baseUrl) {
566
+ if (!Array.isArray(attachments)) return [];
567
+ return attachments.map((a, i) => ({
568
+ key: a.id || `${a.name}-${i}`,
569
+ name: a.name || "Attachment",
570
+ size: a.size,
571
+ type: a.type,
572
+ url: safeUrl(a.url, baseUrl)
573
+ }));
574
+ }
575
+ function isImage(attachment) {
576
+ if (attachment.type) return attachment.type.startsWith("image/");
577
+ return /\.(png|jpe?g|gif|webp|avif)$/i.test(attachment.name);
578
+ }
579
+ var URL_PATTERN = /\b(?:https?:\/\/|www\.)[^\s<>"']+/gi;
580
+ var TRAILING_PUNCTUATION = /[.,;:!?'"\]}]+$/;
581
+ function linkify(text) {
582
+ const parts = [];
583
+ if (!text) return parts;
584
+ let last = 0;
585
+ for (const match of text.matchAll(URL_PATTERN)) {
586
+ const start = match.index ?? 0;
587
+ let raw = match[0];
588
+ for (; ; ) {
589
+ const trimmed = raw.replace(TRAILING_PUNCTUATION, "");
590
+ const opens = (trimmed.match(/\(/g) || []).length;
591
+ const closes = (trimmed.match(/\)/g) || []).length;
592
+ const next = trimmed.endsWith(")") && closes > opens ? trimmed.slice(0, -1) : trimmed;
593
+ if (next === raw) break;
594
+ raw = next;
595
+ }
596
+ const href = safeUrl(
597
+ raw.toLowerCase().startsWith("www.") ? `https://${raw}` : raw
598
+ );
599
+ if (!href || !/^https?:/.test(href)) continue;
600
+ if (start > last) {
601
+ parts.push({ type: "text", value: text.slice(last, start) });
602
+ }
603
+ parts.push({ type: "link", value: raw, href });
604
+ last = start + raw.length;
605
+ }
606
+ if (last < text.length) {
607
+ parts.push({ type: "text", value: text.slice(last) });
608
+ }
609
+ return parts;
610
+ }
611
+ function initials(name) {
612
+ if (!name) return "?";
613
+ const words = name.trim().split(/\s+/).filter(Boolean);
614
+ if (words.length === 0) return "?";
615
+ const first = words[0]?.[0] ?? "";
616
+ const second = words.length > 1 ? words[words.length - 1]?.[0] ?? "" : "";
617
+ return (first + second).toUpperCase();
618
+ }
619
+ function parseHexColor(color) {
620
+ const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());
621
+ let hex = m?.[1];
622
+ if (!hex) return null;
623
+ if (hex.length === 3) {
624
+ hex = hex.split("").map((c) => c + c).join("");
625
+ }
626
+ const n = parseInt(hex, 16);
627
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
628
+ }
629
+ function relativeLuminance([r, g, b]) {
630
+ const channel = (v) => {
631
+ const s = v / 255;
632
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
633
+ };
634
+ return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
635
+ }
636
+ function readableTextColor(background) {
637
+ const rgb = parseHexColor(background);
638
+ if (!rgb) return "#ffffff";
639
+ const lum = relativeLuminance(rgb);
640
+ const onWhite = 1.05 / (lum + 0.05);
641
+ const onDark = (lum + 0.05) / (relativeLuminance([17, 24, 39]) + 0.05);
642
+ return onWhite >= 4.5 || onWhite >= onDark ? "#ffffff" : "#111827";
643
+ }
644
+
645
+ // src/components/form/styles.ts
646
+ var FORM_CSS = `
647
+ .channlize-contact{
648
+ --chz-primary:#2563eb;
649
+ --chz-on-primary:#ffffff;
650
+ --chz-bg:#ffffff;
651
+ --chz-surface:#f6f7f9;
652
+ --chz-field:#ffffff;
653
+ --chz-text:#111827;
654
+ --chz-muted:#5d6572;
655
+ --chz-border:#d5d9e0;
656
+ --chz-border-soft:#e3e6eb;
657
+ --chz-danger:#b42318;
658
+ --chz-danger-bg:#fdecec;
659
+ --chz-success:#067647;
660
+ --chz-success-bg:#e7f6ec;
661
+ --chz-radius:10px;
662
+ display:block;
663
+ box-sizing:border-box;
664
+ width:100%;
665
+ max-width:100%;
666
+ font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
667
+ font-size:14px;
668
+ line-height:1.5;
669
+ font-weight:400;
670
+ letter-spacing:normal;
671
+ text-transform:none;
672
+ text-align:left;
673
+ color:var(--chz-text);
674
+ -webkit-font-smoothing:antialiased;
675
+ -moz-osx-font-smoothing:grayscale;
676
+ }
677
+ .channlize-contact[data-theme="dark"]{
678
+ --chz-bg:#16181d;
679
+ --chz-surface:#1f2228;
680
+ --chz-field:#1b1e24;
681
+ --chz-text:#f2f4f7;
682
+ --chz-muted:#a7aebb;
683
+ --chz-border:#3a404a;
684
+ --chz-border-soft:#2f343d;
685
+ --chz-danger:#ff9b9b;
686
+ --chz-danger-bg:#3b1e22;
687
+ --chz-success:#6ce9a6;
688
+ --chz-success-bg:#12301f;
689
+ }
690
+ @media (prefers-color-scheme: dark){
691
+ .channlize-contact[data-theme="auto"]{
692
+ --chz-bg:#16181d;
693
+ --chz-surface:#1f2228;
694
+ --chz-field:#1b1e24;
695
+ --chz-text:#f2f4f7;
696
+ --chz-muted:#a7aebb;
697
+ --chz-border:#3a404a;
698
+ --chz-border-soft:#2f343d;
699
+ --chz-danger:#ff9b9b;
700
+ --chz-danger-bg:#3b1e22;
701
+ --chz-success:#6ce9a6;
702
+ --chz-success-bg:#12301f;
703
+ }
704
+ }
705
+ .channlize-contact[data-variant="card"]{background:var(--chz-bg);border:1px solid var(--chz-border-soft);border-radius:16px;padding:24px;}
706
+ @media (max-width: 480px){.channlize-contact[data-variant="card"]{padding:18px;}}
707
+ .channlize-contact *,.channlize-contact *::before,.channlize-contact *::after{box-sizing:border-box;letter-spacing:inherit;}
708
+ .channlize-contact :where(h2,p,form,fieldset,label){margin:0;padding:0;border:0;font:inherit;color:inherit;background:none;}
709
+ .channlize-contact :where(button,input,textarea,select){font:inherit;color:inherit;letter-spacing:inherit;text-transform:none;margin:0;}
710
+ .channlize-contact :where(button){appearance:none;-webkit-appearance:none;background:none;border:0;padding:0;cursor:pointer;line-height:inherit;text-decoration:none;}
711
+ .channlize-contact :where(button:disabled){cursor:default;}
712
+ .channlize-contact :where(svg){display:block;flex-shrink:0;}
713
+ .channlize-contact :where(:focus-visible){outline:2px solid var(--chz-primary);outline-offset:2px;}
714
+
715
+ .channlize-contact .channlize-form{display:flex;flex-direction:column;gap:18px;}
716
+ .channlize-contact .channlize-form-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:18px;}
717
+ .channlize-contact .channlize-field{display:flex;flex-direction:column;gap:6px;min-width:0;}
718
+ .channlize-contact .channlize-label{display:flex;align-items:baseline;gap:6px;font-size:13.5px;font-weight:600;line-height:1.3;color:var(--chz-text);}
719
+ .channlize-contact .channlize-optional{font-weight:400;color:var(--chz-muted);font-size:12.5px;}
720
+ .channlize-contact .channlize-form-input,.channlize-contact .channlize-form-select,.channlize-contact .channlize-form-textarea{display:block;width:100%;min-width:0;padding:10px 12px;border-radius:var(--chz-radius);border:1px solid var(--chz-border);background:var(--chz-field);color:var(--chz-text);font-size:15px;line-height:1.4;box-shadow:none;outline:none;transition:border-color .15s ease,box-shadow .15s ease;}
721
+ .channlize-contact .channlize-form-input,.channlize-contact .channlize-form-select{height:44px;}
722
+ .channlize-contact .channlize-form-textarea{min-height:132px;resize:vertical;}
723
+ .channlize-contact .channlize-form-input::placeholder,.channlize-contact .channlize-form-textarea::placeholder{color:var(--chz-muted);opacity:1;}
724
+ .channlize-contact .channlize-form-input:hover,.channlize-contact .channlize-form-select:hover,.channlize-contact .channlize-form-textarea:hover{border-color:color-mix(in srgb, var(--chz-text) 30%, var(--chz-border));}
725
+ .channlize-contact .channlize-form-input:focus,.channlize-contact .channlize-form-select:focus,.channlize-contact .channlize-form-textarea:focus{border-color:var(--chz-primary);box-shadow:0 0 0 3px rgba(37,99,235,.18);box-shadow:0 0 0 3px color-mix(in srgb, var(--chz-primary) 22%, transparent);outline:none;}
726
+ .channlize-contact .channlize-form-select{appearance:none;-webkit-appearance:none;padding-right:38px;background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);background-position:calc(100% - 18px) 19px,calc(100% - 13px) 19px;background-size:5px 5px,5px 5px;background-repeat:no-repeat;cursor:pointer;}
727
+ .channlize-contact .channlize-form-select[data-empty="true"]{color:var(--chz-muted);}
728
+ .channlize-contact .channlize-form-select option{color:var(--chz-text);background:var(--chz-field);}
729
+ .channlize-contact .channlize-field[data-invalid="true"] .channlize-form-input,.channlize-contact .channlize-field[data-invalid="true"] .channlize-form-select,.channlize-contact .channlize-field[data-invalid="true"] .channlize-form-textarea{border-color:var(--chz-danger);}
730
+ .channlize-contact .channlize-field[data-invalid="true"] .channlize-form-input:focus,.channlize-contact .channlize-field[data-invalid="true"] .channlize-form-select:focus,.channlize-contact .channlize-field[data-invalid="true"] .channlize-form-textarea:focus{box-shadow:0 0 0 3px color-mix(in srgb, var(--chz-danger) 22%, transparent);}
731
+ .channlize-contact .channlize-field-error{display:flex;align-items:flex-start;gap:6px;font-size:13px;line-height:1.35;color:var(--chz-danger);}
732
+ .channlize-contact .channlize-field-error svg{margin-top:1px;}
733
+ .channlize-contact .channlize-field-meta{display:flex;justify-content:space-between;gap:12px;}
734
+ .channlize-contact .channlize-counter{margin-left:auto;font-size:12px;color:var(--chz-muted);font-variant-numeric:tabular-nums;white-space:nowrap;}
735
+ .channlize-contact .channlize-counter[data-over="true"]{color:var(--chz-danger);}
736
+
737
+ .channlize-contact .channlize-error{display:flex;align-items:flex-start;gap:10px;padding:12px 14px;border-radius:var(--chz-radius);background:var(--chz-danger-bg);color:var(--chz-danger);font-size:14px;line-height:1.4;}
738
+ .channlize-contact .channlize-error svg{margin-top:1px;}
739
+
740
+ .channlize-contact .channlize-form-footer{display:flex;flex-wrap:wrap;align-items:center;gap:12px 16px;}
741
+ .channlize-contact .channlize-submit{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:10px 20px;border-radius:var(--chz-radius);background:var(--chz-primary);color:var(--chz-on-primary);font-size:15px;font-weight:600;line-height:1.2;transition:filter .15s ease,transform .15s ease,opacity .15s ease;}
742
+ .channlize-contact .channlize-submit:hover:not(:disabled){filter:brightness(1.08);}
743
+ .channlize-contact .channlize-submit:active:not(:disabled){transform:translateY(1px);}
744
+ .channlize-contact .channlize-submit:disabled{opacity:.75;}
745
+ .channlize-contact .channlize-submit:focus-visible{outline:2px solid var(--chz-primary);outline-offset:3px;}
746
+ @media (max-width: 480px){.channlize-contact .channlize-submit{width:100%;}}
747
+ .channlize-contact .channlize-footnote{font-size:12.5px;color:var(--chz-muted);}
748
+ .channlize-contact .channlize-form-spinner{width:14px;height:14px;flex-shrink:0;border-radius:999px;border:2px solid currentColor;border-right-color:transparent;animation:channlize-spin .8s linear infinite;}
749
+
750
+ .channlize-contact .channlize-honeypot{position:absolute !important;left:-10000px !important;top:auto !important;width:1px !important;height:1px !important;overflow:hidden !important;opacity:0 !important;pointer-events:none !important;}
751
+ .channlize-contact .channlize-visually-hidden{position:absolute !important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
752
+
753
+ .channlize-contact .channlize-form-success{display:flex;flex-direction:column;align-items:center;text-align:center;gap:8px;padding:24px 8px;animation:channlize-fade-in .25s ease;}
754
+ .channlize-contact .channlize-success-icon{display:inline-flex;align-items:center;justify-content:center;width:52px;height:52px;margin-bottom:6px;border-radius:999px;background:var(--chz-success-bg);color:var(--chz-success);}
755
+ .channlize-contact .channlize-success-title{font-size:20px;font-weight:650;line-height:1.3;color:var(--chz-text);outline:none;}
756
+ .channlize-contact .channlize-success-text{max-width:380px;font-size:15px;color:var(--chz-muted);}
757
+ .channlize-contact .channlize-secondary{margin-top:12px;display:inline-flex;align-items:center;justify-content:center;min-height:40px;padding:8px 16px;border-radius:var(--chz-radius);border:1px solid var(--chz-border);background:var(--chz-surface);color:var(--chz-text);font-size:14px;font-weight:600;transition:background-color .15s ease;}
758
+ .channlize-contact .channlize-secondary:hover{background:var(--chz-border-soft);}
759
+
760
+ @media (max-width: 640px), (pointer: coarse){
761
+ .channlize-contact .channlize-form-input,.channlize-contact .channlize-form-select,.channlize-contact .channlize-form-textarea{font-size:16px;}
762
+ }
763
+ @media (prefers-reduced-motion: reduce){
764
+ .channlize-contact,.channlize-contact *{transition-duration:.01ms !important;animation-duration:.01ms !important;animation-iteration-count:1 !important;}
765
+ }
766
+ @keyframes channlize-spin{to{transform:rotate(360deg);}}
767
+ @keyframes channlize-fade-in{from{opacity:0;}to{opacity:1;}}
768
+ `;
769
+
770
+ // src/components/Form.tsx
771
+ var import_jsx_runtime3 = (
772
+ // Static, trusted CSS; dangerouslySetInnerHTML keeps `>` intact in SSR.
773
+ require("react/jsx-runtime")
774
+ );
775
+ var defaultContactFormLabels = {
776
+ name: "Name",
777
+ email: "Email",
778
+ company: "Company",
779
+ subject: "Subject",
780
+ topic: "Topic",
781
+ message: "Message",
782
+ namePlaceholder: "Jane Doe",
783
+ emailPlaceholder: "you@company.com",
784
+ companyPlaceholder: "Acme Inc.",
785
+ subjectPlaceholder: "What is this about?",
786
+ topicPlaceholder: "Choose a topic",
787
+ messagePlaceholder: "How can we help?",
788
+ optional: "(optional)",
789
+ submit: "Send message",
790
+ submitting: "Sending\u2026",
791
+ successTitle: "Message sent",
792
+ successMessage: "Thanks for reaching out. We\u2019ll get back to you by email.",
793
+ sendAnother: "Send another message",
794
+ errorRequired: "Please fill in this field.",
795
+ errorSelect: "Please choose an option.",
796
+ errorEmail: "Enter a valid email address, like name@example.com.",
797
+ errorTooShort: "Please write at least {min} characters.",
798
+ errorTooLong: "Please keep this under {max} characters.",
799
+ errorSummary: "Please fix the highlighted fields.",
800
+ errorGeneric: "We couldn\u2019t send your message. Please try again.",
801
+ errorRateLimit: "Too many messages in a short time. Please wait a minute and try again.",
802
+ footnote: ""
803
+ };
804
+ var DEFAULT_PRIMARY = "#2563eb";
805
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
806
+ var EMPTY_VALUES = {
807
+ name: "",
808
+ email: "",
809
+ company: "",
810
+ subject: "",
811
+ topic: "",
812
+ message: ""
813
+ };
814
+ function normalizeTopics(topics) {
815
+ return (topics ?? []).map(
816
+ (t) => typeof t === "string" ? { value: t, label: t } : { value: t.value, label: t.label ?? t.value }
817
+ );
818
+ }
819
+ function matchTopic(topics, wanted) {
820
+ const w = wanted?.trim().toLowerCase();
821
+ if (!w) return "";
822
+ const hit = topics.find(
823
+ (t) => t.value.toLowerCase() === w || t.label.toLowerCase() === w
824
+ );
825
+ return hit ? hit.value : "";
826
+ }
367
827
  function Form({
368
828
  customFields = [],
369
829
  onSuccess,
370
830
  onError,
371
831
  className = "",
372
832
  styles = {},
373
- submitText = "Send Message",
374
- successMessage = "Thank you! Your message has been sent."
833
+ submitText,
834
+ successMessage,
835
+ theme = "light",
836
+ primaryColor,
837
+ primaryTextColor,
838
+ variant = "card",
839
+ unstyled = false,
840
+ fields = {},
841
+ topics,
842
+ defaultValues,
843
+ labels: labelOverrides,
844
+ minMessageLength = 10,
845
+ maxMessageLength = 5e3,
846
+ honeypot = true,
847
+ showSuccessState = true,
848
+ idPrefix = "channlize"
375
849
  }) {
376
- const [formData, setFormData] = (0, import_react5.useState)({
377
- name: "",
378
- email: "",
379
- subject: "",
380
- message: "",
381
- customFields: {}
850
+ const labels = {
851
+ ...defaultContactFormLabels,
852
+ ...labelOverrides,
853
+ ...submitText !== void 0 && { submit: submitText },
854
+ ...successMessage !== void 0 && { successMessage }
855
+ };
856
+ const topicOptions = (0, import_react5.useMemo)(() => normalizeTopics(topics), [topics]);
857
+ const modes = {
858
+ name: fields.name ?? "required",
859
+ company: fields.company ?? "hidden",
860
+ subject: fields.subject ?? "optional",
861
+ topic: topicOptions.length === 0 ? "hidden" : fields.topic ?? "optional"
862
+ };
863
+ const initialValues = () => ({
864
+ ...EMPTY_VALUES,
865
+ ...defaultValues,
866
+ topic: matchTopic(topicOptions, defaultValues?.topic)
382
867
  });
868
+ const [values, setValues] = (0, import_react5.useState)(initialValues);
869
+ const [custom, setCustom] = (0, import_react5.useState)({});
870
+ const [touched, setTouched] = (0, import_react5.useState)({});
871
+ const [showAllErrors, setShowAllErrors] = (0, import_react5.useState)(false);
872
+ const [trap, setTrap] = (0, import_react5.useState)("");
873
+ const [botSuccess, setBotSuccess] = (0, import_react5.useState)(false);
874
+ const [announcement, setAnnouncement] = (0, import_react5.useState)("");
875
+ const formRef = (0, import_react5.useRef)(null);
876
+ const successRef = (0, import_react5.useRef)(null);
877
+ const wantedTopic = defaultValues?.topic;
878
+ (0, import_react5.useEffect)(() => {
879
+ const matched = matchTopic(topicOptions, wantedTopic);
880
+ if (matched) setValues((v) => v.topic ? v : { ...v, topic: matched });
881
+ }, [wantedTopic, topicOptions]);
383
882
  const { submit, isSubmitting, isSuccess, error, reset } = useContactForm({
384
- onSuccess: () => {
385
- setFormData({
386
- name: "",
387
- email: "",
388
- subject: "",
389
- message: "",
390
- customFields: {}
391
- });
392
- onSuccess?.();
883
+ onSuccess: (response) => {
884
+ setValues((v) => ({
885
+ ...EMPTY_VALUES,
886
+ name: v.name,
887
+ email: v.email,
888
+ company: v.company
889
+ }));
890
+ setCustom({});
891
+ setTouched({});
892
+ setShowAllErrors(false);
893
+ if (!showSuccessState) setAnnouncement(labels.successTitle);
894
+ onSuccess?.(response);
393
895
  },
394
896
  onError
395
897
  });
898
+ const succeeded = (isSuccess || botSuccess) && showSuccessState;
899
+ (0, import_react5.useEffect)(() => {
900
+ if (succeeded) successRef.current?.focus();
901
+ }, [succeeded]);
902
+ const id = (name) => `${idPrefix}-${name}`;
903
+ const validate = (v, c) => {
904
+ const errors2 = {};
905
+ const need = (name, mode) => {
906
+ if (mode === "required" && !v[name].trim()) {
907
+ errors2[name] = name === "topic" ? labels.errorSelect : labels.errorRequired;
908
+ }
909
+ };
910
+ need("name", modes.name);
911
+ need("company", modes.company);
912
+ need("subject", modes.subject);
913
+ need("topic", modes.topic);
914
+ if (!v.email.trim()) errors2.email = labels.errorRequired;
915
+ else if (!EMAIL_PATTERN.test(v.email.trim()))
916
+ errors2.email = labels.errorEmail;
917
+ const message = v.message.trim();
918
+ if (!message) errors2.message = labels.errorRequired;
919
+ else if (message.length < minMessageLength) {
920
+ errors2.message = labels.errorTooShort.replace(
921
+ "{min}",
922
+ String(minMessageLength)
923
+ );
924
+ } else if (message.length > maxMessageLength) {
925
+ errors2.message = labels.errorTooLong.replace(
926
+ "{max}",
927
+ String(maxMessageLength)
928
+ );
929
+ }
930
+ for (const field of customFields) {
931
+ const value = (c[field.name] ?? "").trim();
932
+ if (field.required && !value) {
933
+ errors2[`custom:${field.name}`] = field.type === "select" ? labels.errorSelect : labels.errorRequired;
934
+ } else if (field.type === "email" && value && !EMAIL_PATTERN.test(value)) {
935
+ errors2[`custom:${field.name}`] = labels.errorEmail;
936
+ }
937
+ }
938
+ return errors2;
939
+ };
940
+ const errors = validate(values, custom);
941
+ const visibleError = (key) => showAllErrors || touched[key] ? errors[key] : void 0;
942
+ const handleBlur = (key) => (e) => {
943
+ const value = e.target.value;
944
+ if (value || touched[key] || showAllErrors)
945
+ setTouched((t) => ({ ...t, [key]: true }));
946
+ };
396
947
  const handleSubmit = async (e) => {
397
948
  e.preventDefault();
398
- if (!formData.name || !formData.email || !formData.message) {
949
+ if (isSubmitting) return;
950
+ if (Object.values(errors).some(Boolean)) {
951
+ setShowAllErrors(true);
952
+ setAnnouncement("");
953
+ setTimeout(() => setAnnouncement(labels.errorSummary), 0);
954
+ const order = [
955
+ "name",
956
+ "email",
957
+ "company",
958
+ "topic",
959
+ "subject",
960
+ ...customFields.map((f) => `custom:${f.name}`),
961
+ "message"
962
+ ];
963
+ const firstKey = order.find((key) => errors[key]);
964
+ if (firstKey) {
965
+ const target = formRef.current?.querySelector(
966
+ `[id="${id(firstKey.replace(/^custom:/, ""))}"]`
967
+ );
968
+ target?.focus();
969
+ }
970
+ return;
971
+ }
972
+ if (honeypot && trap) {
973
+ setBotSuccess(true);
399
974
  return;
400
975
  }
401
- await submit(formData);
976
+ const topic = topicOptions.find((t) => t.value === values.topic);
977
+ const customFieldValues = {};
978
+ for (const field of customFields) {
979
+ const v = (custom[field.name] ?? "").trim();
980
+ if (v) customFieldValues[field.name] = v;
981
+ }
982
+ if (modes.topic !== "hidden" && topic)
983
+ customFieldValues.topic = topic.label;
984
+ if (modes.company !== "hidden" && values.company.trim()) {
985
+ customFieldValues.company = values.company.trim();
986
+ }
987
+ const subject = modes.subject !== "hidden" && values.subject.trim() || topic?.label || void 0;
988
+ const data = {
989
+ name: modes.name === "hidden" ? "" : values.name.trim(),
990
+ email: values.email.trim(),
991
+ subject,
992
+ message: values.message.trim(),
993
+ ...Object.keys(customFieldValues).length > 0 && {
994
+ customFields: customFieldValues
995
+ }
996
+ };
997
+ await submit(data);
402
998
  };
403
- const handleInputChange = (field, value) => {
404
- setFormData((prev) => ({
405
- ...prev,
406
- [field]: value
407
- }));
999
+ const handleReset = () => {
1000
+ reset();
1001
+ setBotSuccess(false);
1002
+ setTrap("");
1003
+ setAnnouncement("");
408
1004
  };
409
- const handleCustomFieldChange = (fieldName, value) => {
410
- setFormData((prev) => ({
411
- ...prev,
412
- customFields: {
413
- ...prev.customFields,
414
- [fieldName]: value
415
- }
416
- }));
1005
+ const accent = primaryColor || DEFAULT_PRIMARY;
1006
+ const rootStyle = {
1007
+ ...primaryColor && { "--chz-primary": primaryColor },
1008
+ "--chz-on-primary": primaryTextColor || readableTextColor(accent)
417
1009
  };
418
- if (isSuccess) {
419
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1010
+ const rootProps = {
1011
+ className: `channlize-contact ${className}`.trim(),
1012
+ "data-theme": theme === "system" ? "auto" : theme,
1013
+ "data-variant": variant,
1014
+ style: rootStyle
1015
+ };
1016
+ const stylesheet = unstyled ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1017
+ "style",
1018
+ {
1019
+ "data-channlize-form": "",
1020
+ dangerouslySetInnerHTML: { __html: FORM_CSS }
1021
+ }
1022
+ );
1023
+ if (succeeded) {
1024
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ...rootProps, children: [
1025
+ stylesheet,
1026
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1027
+ "div",
1028
+ {
1029
+ className: "channlize-form-success",
1030
+ style: styles.success,
1031
+ role: "status",
1032
+ children: [
1033
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "channlize-success-icon", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CheckIcon, { size: 26 }) }),
1034
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1035
+ "h2",
1036
+ {
1037
+ ref: successRef,
1038
+ tabIndex: -1,
1039
+ className: "channlize-success-title",
1040
+ children: labels.successTitle
1041
+ }
1042
+ ),
1043
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "channlize-success-text", children: labels.successMessage }),
1044
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1045
+ "button",
1046
+ {
1047
+ type: "button",
1048
+ className: "channlize-secondary",
1049
+ onClick: handleReset,
1050
+ children: labels.sendAnother
1051
+ }
1052
+ )
1053
+ ]
1054
+ }
1055
+ )
1056
+ ] });
1057
+ }
1058
+ const label = (name, text, required) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { htmlFor: id(name), className: "channlize-label", style: styles.label, children: [
1059
+ text,
1060
+ !required && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "channlize-optional", children: labels.optional })
1061
+ ] });
1062
+ const fieldError = (key, errorId) => {
1063
+ const message = visibleError(key);
1064
+ if (!message) return null;
1065
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { id: errorId, className: "channlize-field-error", children: [
1066
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlertIcon, { size: 14 }),
1067
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: message })
1068
+ ] });
1069
+ };
1070
+ const a11y = (key, name, required, extraDescribedBy) => {
1071
+ const invalid = Boolean(visibleError(key));
1072
+ const describedBy = [invalid ? id(`${name}-error`) : null, extraDescribedBy].filter(Boolean).join(" ");
1073
+ return {
1074
+ id: id(name),
1075
+ "aria-invalid": invalid || void 0,
1076
+ "aria-required": required || void 0,
1077
+ "aria-describedby": describedBy || void 0
1078
+ };
1079
+ };
1080
+ const setValue = (name) => (e) => setValues((v) => ({ ...v, [name]: e.target.value }));
1081
+ const textField = (name, mode, inputProps) => {
1082
+ if (mode === "hidden") return null;
1083
+ const required = mode === "required";
1084
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
420
1085
  "div",
421
1086
  {
422
- className: `channlize-form-success ${className}`,
423
- style: styles.success,
1087
+ className: "channlize-field",
1088
+ "data-invalid": Boolean(visibleError(name)),
1089
+ style: styles.field,
424
1090
  children: [
425
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: successMessage }),
426
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", onClick: reset, style: styles.button, children: "Send Another Message" })
1091
+ label(name, labels[name], required),
1092
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1093
+ "input",
1094
+ {
1095
+ ...a11y(name, name, required),
1096
+ className: "channlize-form-input",
1097
+ name,
1098
+ type: inputProps.type,
1099
+ autoComplete: inputProps.autoComplete,
1100
+ inputMode: inputProps.inputMode,
1101
+ placeholder: labels[`${name}Placeholder`],
1102
+ value: values[name],
1103
+ onChange: setValue(name),
1104
+ onBlur: handleBlur(name),
1105
+ maxLength: name === "subject" ? 200 : 254,
1106
+ style: styles.input
1107
+ }
1108
+ ),
1109
+ fieldError(name, id(`${name}-error`))
427
1110
  ]
428
1111
  }
429
1112
  );
430
- }
431
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
432
- "form",
1113
+ };
1114
+ const topicField = modes.topic === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1115
+ "div",
433
1116
  {
434
- className: `channlize-form ${className}`,
435
- style: styles.form,
436
- onSubmit: handleSubmit,
1117
+ className: "channlize-field",
1118
+ "data-invalid": Boolean(visibleError("topic")),
1119
+ style: styles.field,
437
1120
  children: [
438
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "channlize-field", style: styles.field, children: [
439
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: "channlize-name", style: styles.label, children: "Name *" }),
440
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
441
- "input",
442
- {
443
- id: "channlize-name",
1121
+ label("topic", labels.topic, modes.topic === "required"),
1122
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1123
+ "select",
1124
+ {
1125
+ ...a11y("topic", "topic", modes.topic === "required"),
1126
+ className: "channlize-form-select",
1127
+ name: "topic",
1128
+ value: values.topic,
1129
+ "data-empty": !values.topic,
1130
+ onChange: (e) => {
1131
+ setValue("topic")(e);
1132
+ setTouched((t) => ({ ...t, topic: true }));
1133
+ },
1134
+ onBlur: handleBlur("topic"),
1135
+ style: styles.input,
1136
+ children: [
1137
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: labels.topicPlaceholder }),
1138
+ topicOptions.map((t) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: t.value, children: t.label }, t.value))
1139
+ ]
1140
+ }
1141
+ ),
1142
+ fieldError("topic", id("topic-error"))
1143
+ ]
1144
+ }
1145
+ );
1146
+ const messageLength = values.message.trim().length;
1147
+ const showCounter = messageLength > maxMessageLength * 0.8;
1148
+ const counterId = id("message-count");
1149
+ const submitError = error ? error.statusCode === 429 || error.code === "RATE_LIMIT_ERROR" ? labels.errorRateLimit : labels.errorGeneric : null;
1150
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ...rootProps, children: [
1151
+ stylesheet,
1152
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1153
+ "form",
1154
+ {
1155
+ ref: formRef,
1156
+ className: "channlize-form",
1157
+ style: styles.form,
1158
+ onSubmit: handleSubmit,
1159
+ noValidate: true,
1160
+ "aria-busy": isSubmitting || void 0,
1161
+ children: [
1162
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "channlize-form-row", children: [
1163
+ textField("name", modes.name, {
444
1164
  type: "text",
445
- value: formData.name || "",
446
- onChange: (e) => handleInputChange("name", e.target.value),
447
- required: true,
448
- style: styles.input
449
- }
450
- )
451
- ] }),
452
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "channlize-field", style: styles.field, children: [
453
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: "channlize-email", style: styles.label, children: "Email *" }),
454
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
455
- "input",
456
- {
457
- id: "channlize-email",
1165
+ autoComplete: "name"
1166
+ }),
1167
+ textField("email", "required", {
458
1168
  type: "email",
459
- value: formData.email || "",
460
- onChange: (e) => handleInputChange("email", e.target.value),
461
- required: true,
462
- style: styles.input
463
- }
464
- )
465
- ] }),
466
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "channlize-field", style: styles.field, children: [
467
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: "channlize-subject", style: styles.label, children: "Subject" }),
468
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
469
- "input",
470
- {
471
- id: "channlize-subject",
1169
+ autoComplete: "email",
1170
+ inputMode: "email"
1171
+ })
1172
+ ] }),
1173
+ (modes.company !== "hidden" || modes.topic !== "hidden") && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "channlize-form-row", children: [
1174
+ textField("company", modes.company, {
472
1175
  type: "text",
473
- value: formData.subject || "",
474
- onChange: (e) => handleInputChange("subject", e.target.value),
475
- style: styles.input
476
- }
477
- )
478
- ] }),
479
- customFields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "channlize-field", style: styles.field, children: [
480
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { htmlFor: `channlize-${field.name}`, style: styles.label, children: [
481
- field.label,
482
- " ",
483
- field.required && "*"
1176
+ autoComplete: "organization"
1177
+ }),
1178
+ topicField
484
1179
  ] }),
485
- field.type === "select" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
486
- "select",
1180
+ textField("subject", modes.subject, {
1181
+ type: "text",
1182
+ autoComplete: "off"
1183
+ }),
1184
+ customFields.map((field) => {
1185
+ const key = `custom:${field.name}`;
1186
+ const required = Boolean(field.required);
1187
+ const common = {
1188
+ ...a11y(key, field.name, required),
1189
+ name: field.name,
1190
+ value: custom[field.name] ?? "",
1191
+ onChange: (e) => {
1192
+ const value = e.target.value;
1193
+ setCustom((c) => ({ ...c, [field.name]: value }));
1194
+ },
1195
+ onBlur: handleBlur(key)
1196
+ };
1197
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1198
+ "div",
1199
+ {
1200
+ className: "channlize-field",
1201
+ "data-invalid": Boolean(visibleError(key)),
1202
+ style: styles.field,
1203
+ children: [
1204
+ label(field.name, field.label, required),
1205
+ field.type === "select" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1206
+ "select",
1207
+ {
1208
+ ...common,
1209
+ className: "channlize-form-select",
1210
+ "data-empty": !common.value,
1211
+ style: styles.input,
1212
+ children: [
1213
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: field.placeholder ?? labels.topicPlaceholder }),
1214
+ field.options?.map((option) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: option, children: option }, option))
1215
+ ]
1216
+ }
1217
+ ) : field.type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1218
+ "textarea",
1219
+ {
1220
+ ...common,
1221
+ className: "channlize-form-textarea",
1222
+ placeholder: field.placeholder,
1223
+ rows: 3,
1224
+ style: styles.textarea
1225
+ }
1226
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1227
+ "input",
1228
+ {
1229
+ ...common,
1230
+ className: "channlize-form-input",
1231
+ type: field.type,
1232
+ placeholder: field.placeholder,
1233
+ style: styles.input
1234
+ }
1235
+ ),
1236
+ fieldError(key, id(`${field.name}-error`))
1237
+ ]
1238
+ },
1239
+ field.name
1240
+ );
1241
+ }),
1242
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1243
+ "div",
487
1244
  {
488
- id: `channlize-${field.name}`,
489
- value: formData.customFields?.[field.name] || "",
490
- onChange: (e) => handleCustomFieldChange(field.name, e.target.value),
491
- required: field.required,
492
- style: styles.input,
1245
+ className: "channlize-field",
1246
+ "data-invalid": Boolean(visibleError("message")),
1247
+ style: styles.field,
493
1248
  children: [
494
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: "", children: "Select..." }),
495
- field.options?.map((option) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { value: option, children: option }, option))
1249
+ label("message", labels.message, true),
1250
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1251
+ "textarea",
1252
+ {
1253
+ ...a11y(
1254
+ "message",
1255
+ "message",
1256
+ true,
1257
+ showCounter ? counterId : void 0
1258
+ ),
1259
+ className: "channlize-form-textarea",
1260
+ name: "message",
1261
+ rows: 5,
1262
+ placeholder: labels.messagePlaceholder,
1263
+ value: values.message,
1264
+ onChange: setValue("message"),
1265
+ onBlur: handleBlur("message"),
1266
+ style: styles.textarea
1267
+ }
1268
+ ),
1269
+ (visibleError("message") || showCounter) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "channlize-field-meta", children: [
1270
+ fieldError("message", id("message-error")),
1271
+ showCounter && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1272
+ "span",
1273
+ {
1274
+ id: counterId,
1275
+ className: "channlize-counter",
1276
+ "data-over": messageLength > maxMessageLength,
1277
+ children: [
1278
+ messageLength,
1279
+ "/",
1280
+ maxMessageLength
1281
+ ]
1282
+ }
1283
+ )
1284
+ ] })
496
1285
  ]
497
1286
  }
498
- ) : field.type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
499
- "textarea",
500
- {
501
- id: `channlize-${field.name}`,
502
- value: formData.customFields?.[field.name] || "",
503
- onChange: (e) => handleCustomFieldChange(field.name, e.target.value),
504
- required: field.required,
505
- style: styles.textarea
506
- }
507
- ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
508
- "input",
509
- {
510
- id: `channlize-${field.name}`,
511
- type: field.type,
512
- value: formData.customFields?.[field.name] || "",
513
- onChange: (e) => handleCustomFieldChange(field.name, e.target.value),
514
- required: field.required,
515
- style: styles.input
516
- }
517
- )
518
- ] }, field.name)),
519
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "channlize-field", style: styles.field, children: [
520
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: "channlize-message", style: styles.label, children: "Message *" }),
521
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
522
- "textarea",
1287
+ ),
1288
+ honeypot && // Bots fill every input; people never see this one. No `id`, so
1289
+ // host-page form helpers keyed on ids skip it too.
1290
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "channlize-honeypot", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { children: [
1291
+ "Leave this field empty",
1292
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1293
+ "input",
1294
+ {
1295
+ type: "text",
1296
+ name: "website",
1297
+ tabIndex: -1,
1298
+ autoComplete: "off",
1299
+ value: trap,
1300
+ onChange: (e) => setTrap(e.target.value)
1301
+ }
1302
+ )
1303
+ ] }) }),
1304
+ submitError && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "channlize-error", role: "alert", style: styles.error, children: [
1305
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlertIcon, { size: 18 }),
1306
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: submitError })
1307
+ ] }),
1308
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "channlize-form-footer", children: [
1309
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1310
+ "button",
1311
+ {
1312
+ type: "submit",
1313
+ className: "channlize-submit",
1314
+ disabled: isSubmitting,
1315
+ style: styles.button,
1316
+ children: [
1317
+ isSubmitting && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "channlize-form-spinner", "aria-hidden": "true" }),
1318
+ isSubmitting ? labels.submitting : labels.submit
1319
+ ]
1320
+ }
1321
+ ),
1322
+ labels.footnote && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "channlize-footnote", children: labels.footnote })
1323
+ ] }),
1324
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1325
+ "p",
523
1326
  {
524
- id: "channlize-message",
525
- value: formData.message || "",
526
- onChange: (e) => handleInputChange("message", e.target.value),
527
- required: true,
528
- rows: 4,
529
- style: styles.textarea
1327
+ className: "channlize-visually-hidden",
1328
+ role: "status",
1329
+ "aria-live": "polite",
1330
+ children: announcement
530
1331
  }
531
1332
  )
532
- ] }),
533
- error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "channlize-error", style: styles.error, children: error.message }),
534
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "submit", disabled: isSubmitting, style: styles.button, children: isSubmitting ? "Sending..." : submitText })
535
- ]
536
- }
537
- );
1333
+ ]
1334
+ }
1335
+ )
1336
+ ] });
538
1337
  }
539
1338
 
540
1339
  // src/components/ContactForm.tsx
541
- var import_jsx_runtime3 = require("react/jsx-runtime");
1340
+ var import_jsx_runtime4 = require("react/jsx-runtime");
542
1341
  function ContactForm(props) {
543
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Form, { ...props });
1342
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Form, { ...props });
544
1343
  }
545
1344
 
546
1345
  // src/components/SupportChat.tsx
1346
+ var import_react7 = require("react");
1347
+ var import_core3 = require("@channlize/core");
1348
+
1349
+ // src/components/chat/internal.tsx
547
1350
  var import_react6 = require("react");
548
- var import_jsx_runtime4 = require("react/jsx-runtime");
1351
+
1352
+ // src/components/chat/styles.ts
1353
+ var WIDGET_CSS = `
1354
+ .channlize-widget{
1355
+ --chz-primary:#2563eb;
1356
+ --chz-on-primary:#ffffff;
1357
+ --chz-bg:#ffffff;
1358
+ --chz-surface:#f6f7f9;
1359
+ --chz-bubble:#eff1f4;
1360
+ --chz-text:#111827;
1361
+ --chz-muted:#5d6572;
1362
+ --chz-border:#e3e6eb;
1363
+ --chz-danger:#b91c1c;
1364
+ --chz-danger-bg:#fdecec;
1365
+ --chz-warn-bg:#fff6e0;
1366
+ --chz-warn-text:#7a4b00;
1367
+ --chz-online:#22c55e;
1368
+ --chz-away:#f59e0b;
1369
+ --chz-offline:#9ca3af;
1370
+ --chz-shadow:0 24px 48px -12px rgba(15,23,42,.28),0 0 0 1px rgba(15,23,42,.06);
1371
+ --chz-radius:18px;
1372
+ font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
1373
+ font-size:14px;
1374
+ line-height:1.45;
1375
+ font-weight:400;
1376
+ font-style:normal;
1377
+ letter-spacing:normal;
1378
+ text-transform:none;
1379
+ text-align:left;
1380
+ color:var(--chz-text);
1381
+ -webkit-font-smoothing:antialiased;
1382
+ -moz-osx-font-smoothing:grayscale;
1383
+ -webkit-tap-highlight-color:transparent;
1384
+ }
1385
+ .channlize-widget[data-theme="dark"]{
1386
+ --chz-bg:#16181d;
1387
+ --chz-surface:#1f2228;
1388
+ --chz-bubble:#2a2e36;
1389
+ --chz-text:#f2f4f7;
1390
+ --chz-muted:#a7aebb;
1391
+ --chz-border:#2f343d;
1392
+ --chz-danger:#ff9b9b;
1393
+ --chz-danger-bg:#3b1e22;
1394
+ --chz-warn-bg:#352a10;
1395
+ --chz-warn-text:#f6c768;
1396
+ --chz-offline:#6b7280;
1397
+ --chz-shadow:0 24px 48px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.06);
1398
+ }
1399
+ @media (prefers-color-scheme: dark){
1400
+ .channlize-widget[data-theme="auto"]{
1401
+ --chz-bg:#16181d;
1402
+ --chz-surface:#1f2228;
1403
+ --chz-bubble:#2a2e36;
1404
+ --chz-text:#f2f4f7;
1405
+ --chz-muted:#a7aebb;
1406
+ --chz-border:#2f343d;
1407
+ --chz-danger:#ff9b9b;
1408
+ --chz-danger-bg:#3b1e22;
1409
+ --chz-warn-bg:#352a10;
1410
+ --chz-warn-text:#f6c768;
1411
+ --chz-offline:#6b7280;
1412
+ --chz-shadow:0 24px 48px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.06);
1413
+ }
1414
+ }
1415
+ .channlize-widget *,.channlize-widget *::before,.channlize-widget *::after{box-sizing:border-box;letter-spacing:inherit;word-spacing:inherit;text-transform:inherit;text-indent:0;}
1416
+ .channlize-widget :where(h1,h2,h3,h4,p,ul,ol,li,figure,form,fieldset){margin:0;padding:0;border:0;font:inherit;color:inherit;background:none;}
1417
+ .channlize-widget :where(button,textarea,input){font:inherit;color:inherit;letter-spacing:inherit;text-transform:none;text-shadow:none;margin:0;}
1418
+ .channlize-widget :where(button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0;padding:0;box-shadow:none;cursor:pointer;width:auto;height:auto;min-width:0;min-height:0;line-height:inherit;text-align:center;text-decoration:none;}
1419
+ .channlize-widget :where(button:disabled){cursor:default;}
1420
+ .channlize-widget :where(img,svg){display:block;max-width:100%;border:0;}
1421
+ .channlize-widget :where(a){color:inherit;}
1422
+ .channlize-widget :where(:focus-visible){outline:2px solid var(--chz-primary);outline-offset:2px;}
1423
+
1424
+ /* ---------- Floating widget (ChannlizeChat) ---------- */
1425
+ .channlize-floating{position:static;}
1426
+ .channlize-floating .channlize-launcher-wrap,.channlize-floating .channlize-panel{position:fixed;z-index:var(--chz-z,2147483000);}
1427
+ .channlize-floating[data-position$="right"] .channlize-launcher-wrap,.channlize-floating[data-position$="right"] .channlize-panel{right:calc(var(--chz-x) + env(safe-area-inset-right,0px));}
1428
+ .channlize-floating[data-position$="left"] .channlize-launcher-wrap,.channlize-floating[data-position$="left"] .channlize-panel{left:calc(var(--chz-x) + env(safe-area-inset-left,0px));}
1429
+ .channlize-floating[data-position^="bottom"] .channlize-launcher-wrap,.channlize-floating[data-position^="bottom"] .channlize-panel{bottom:calc(var(--chz-y) + env(safe-area-inset-bottom,0px));}
1430
+ .channlize-floating[data-position^="top"] .channlize-launcher-wrap,.channlize-floating[data-position^="top"] .channlize-panel{top:calc(var(--chz-y) + env(safe-area-inset-top,0px));}
1431
+
1432
+ .channlize-launcher{position:relative;display:flex;align-items:center;justify-content:center;width:var(--chz-launcher-size,56px);height:var(--chz-launcher-size,56px);border-radius:999px;background:var(--chz-launcher-bg,var(--chz-primary));color:var(--chz-launcher-fg,var(--chz-on-primary));box-shadow:0 8px 24px -6px rgba(15,23,42,.35),0 2px 6px rgba(15,23,42,.12);transition:transform .2s cubic-bezier(.2,.8,.2,1),opacity .2s ease,box-shadow .2s ease,background-color .2s ease,visibility 0s;}
1433
+ .channlize-launcher:hover{transform:translateY(-2px) scale(1.04);background:var(--chz-launcher-hover,var(--chz-launcher-bg,var(--chz-primary)));box-shadow:0 12px 28px -6px rgba(15,23,42,.4),0 2px 6px rgba(15,23,42,.12);}
1434
+ .channlize-launcher:active{transform:scale(.96);}
1435
+ .channlize-launcher:focus-visible{outline:3px solid var(--chz-launcher-bg,var(--chz-primary));outline-offset:3px;}
1436
+ .channlize-launcher-custom{display:block;cursor:pointer;}
1437
+ .channlize-launcher-wrap{transition:opacity .2s ease,transform .2s cubic-bezier(.2,.8,.2,1),visibility 0s;}
1438
+ .channlize-floating[data-open="true"] .channlize-launcher-wrap{opacity:0;transform:scale(.6);visibility:hidden;pointer-events:none;transition:opacity .15s ease,transform .15s ease,visibility 0s linear .15s;}
1439
+ .channlize-badge{position:absolute;top:-4px;right:-4px;min-width:20px;height:20px;padding:0 6px;border-radius:999px;background:#d92d20;color:#ffffff;font-size:11px;font-weight:700;line-height:20px;text-align:center;box-shadow:0 0 0 2px var(--chz-bg);animation:channlize-pop .3s cubic-bezier(.2,.8,.2,1);}
1440
+
1441
+ .channlize-panel{display:flex;opacity:0;visibility:hidden;transform:translateY(16px) scale(.97);transform-origin:bottom right;transition:opacity .18s ease,transform .24s cubic-bezier(.2,.8,.2,1),visibility 0s linear .24s;}
1442
+ .channlize-floating[data-position="bottom-left"] .channlize-panel{transform-origin:bottom left;}
1443
+ .channlize-floating[data-position="top-right"] .channlize-panel{transform-origin:top right;transform:translateY(-16px) scale(.97);}
1444
+ .channlize-floating[data-position="top-left"] .channlize-panel{transform-origin:top left;transform:translateY(-16px) scale(.97);}
1445
+ .channlize-floating[data-open="true"] .channlize-panel{opacity:1;visibility:visible;transform:none;transition:opacity .18s ease,transform .24s cubic-bezier(.2,.8,.2,1),visibility 0s;}
1446
+ .channlize-panel .channlize-chat-container{width:var(--chz-panel-width,380px);height:640px;max-height:calc(100vh - var(--chz-y) * 2);max-height:calc(100dvh - var(--chz-y) * 2);min-height:0;border:0;box-shadow:var(--chz-shadow);}
1447
+
1448
+ .channlize-widget[data-animate="false"] *,.channlize-widget[data-animate="false"]{transition:none !important;animation:none !important;}
1449
+ @media (prefers-reduced-motion: reduce){
1450
+ .channlize-widget,.channlize-widget *{transition-duration:.01ms !important;animation-duration:.01ms !important;animation-iteration-count:1 !important;scroll-behavior:auto !important;}
1451
+ .channlize-panel{transform:none !important;}
1452
+ }
1453
+
1454
+ @media (max-width: 640px){
1455
+ .channlize-floating[data-hide-mobile="true"] .channlize-launcher-wrap{display:none;}
1456
+ .channlize-floating .channlize-panel{inset:0 !important;width:100%;height:100%;transform-origin:bottom center !important;}
1457
+ .channlize-panel .channlize-chat-container{width:100% !important;height:100% !important;max-height:none !important;border-radius:0 !important;box-shadow:none !important;}
1458
+ .channlize-panel .channlize-chat-header{padding-top:max(14px, env(safe-area-inset-top,0px));}
1459
+ .channlize-panel .channlize-chat-input-container{padding-bottom:max(12px, env(safe-area-inset-bottom,0px));}
1460
+ }
1461
+
1462
+ /* ---------- Chat container ---------- */
1463
+ .channlize-chat-container{position:relative;display:flex;flex-direction:column;width:100%;height:100%;min-height:420px;overflow:hidden;background:var(--chz-bg);color:var(--chz-text);border:1px solid var(--chz-border);border-radius:16px;isolation:isolate;}
1464
+
1465
+ /* Header */
1466
+ .channlize-chat-header{display:flex;align-items:center;gap:12px;padding:14px 12px 14px 16px;background:var(--chz-primary);color:var(--chz-on-primary);flex-shrink:0;}
1467
+ .channlize-header-text{flex:1;min-width:0;}
1468
+ .channlize-chat-title{font-size:15px;font-weight:650;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
1469
+ .channlize-chat-status{display:flex;align-items:center;gap:6px;margin-top:2px;font-size:12.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
1470
+ .channlize-chat-status>span:last-child{overflow:hidden;text-overflow:ellipsis;}
1471
+ .channlize-status-dot{flex-shrink:0;width:8px;height:8px;border-radius:999px;background:var(--chz-online);box-shadow:0 0 0 2px color-mix(in srgb, var(--chz-on-primary) 35%, transparent);}
1472
+ .channlize-chat-status[data-status="connecting"] .channlize-status-dot{background:var(--chz-away);animation:channlize-pulse 1.4s ease-in-out infinite;}
1473
+ .channlize-chat-status[data-status="offline"] .channlize-status-dot{background:var(--chz-offline);}
1474
+ .channlize-header-actions{display:flex;align-items:center;gap:2px;flex-shrink:0;}
1475
+ .channlize-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:10px;color:inherit;transition:background-color .15s ease;}
1476
+ .channlize-icon-btn:hover{background:rgba(127,127,127,.16);background:color-mix(in srgb, currentColor 14%, transparent);}
1477
+ .channlize-chat-header .channlize-icon-btn:focus-visible{outline-color:var(--chz-on-primary);outline-offset:0;}
1478
+
1479
+ /* Avatars */
1480
+ .channlize-avatar{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:28px;height:28px;border-radius:999px;overflow:hidden;background:var(--chz-primary);color:var(--chz-on-primary);font-size:11px;font-weight:650;line-height:1;user-select:none;}
1481
+ .channlize-avatar img{width:100%;height:100%;object-fit:cover;}
1482
+ .channlize-header-avatar{width:40px;height:40px;font-size:15px;background:rgba(255,255,255,.18);background:color-mix(in srgb, var(--chz-on-primary) 18%, transparent);color:inherit;box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--chz-on-primary) 20%, transparent);}
1483
+
1484
+ /* Overflow menu */
1485
+ .channlize-menu-wrap{position:relative;}
1486
+ .channlize-menu{position:absolute;top:calc(100% + 6px);right:0;z-index:3;min-width:200px;padding:6px;border-radius:12px;background:var(--chz-bg);color:var(--chz-text);border:1px solid var(--chz-border);box-shadow:0 12px 32px -8px rgba(15,23,42,.3);animation:channlize-fade-in .14s ease;}
1487
+ .channlize-menu-item{display:flex;align-items:center;gap:10px;width:100%;padding:9px 10px;border-radius:8px;text-align:left;font-size:14px;color:var(--chz-text);}
1488
+ .channlize-menu-item:hover,.channlize-menu-item:focus-visible{background:var(--chz-surface);outline:none;}
1489
+ .channlize-menu-item svg{color:var(--chz-muted);}
1490
+
1491
+ /* Banner */
1492
+ .channlize-banner{display:flex;align-items:center;gap:8px;padding:8px 14px;font-size:13px;line-height:1.35;background:var(--chz-warn-bg);color:var(--chz-warn-text);border-bottom:1px solid var(--chz-border);flex-shrink:0;animation:channlize-fade-in .2s ease;}
1493
+ .channlize-banner[data-tone="error"]{background:var(--chz-danger-bg);color:var(--chz-danger);}
1494
+ .channlize-banner-text{flex:1;min-width:0;}
1495
+ .channlize-banner-action{font-weight:600;text-decoration:underline;text-underline-offset:2px;border-radius:4px;padding:2px 4px;}
1496
+ .channlize-spinner{width:12px;height:12px;flex-shrink:0;border-radius:999px;border:2px solid currentColor;border-right-color:transparent;animation:channlize-spin .8s linear infinite;}
1497
+
1498
+ /* Messages */
1499
+ .channlize-messages{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:16px 14px 8px;background:var(--chz-bg);}
1500
+ .channlize-day{display:flex;align-items:center;gap:10px;margin:8px 0 12px;font-size:12px;font-weight:550;color:var(--chz-muted);}
1501
+ .channlize-day::before,.channlize-day::after{content:"";flex:1;height:1px;background:var(--chz-border);}
1502
+ .channlize-group{display:flex;align-items:flex-start;gap:8px;margin-bottom:14px;}
1503
+ .channlize-group[data-author="user"]{justify-content:flex-end;}
1504
+ .channlize-group-body{display:flex;flex-direction:column;align-items:flex-start;gap:3px;min-width:0;max-width:82%;}
1505
+ .channlize-group[data-author="user"] .channlize-group-body{align-items:flex-end;}
1506
+ .channlize-group-avatar{margin-top:20px;}
1507
+ .channlize-group-name{font-size:12px;font-weight:600;color:var(--chz-muted);padding:0 4px 1px;}
1508
+ .channlize-message{position:relative;max-width:100%;padding:9px 13px;border-radius:var(--chz-radius);font-size:14px;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;}
1509
+ .channlize-message-agent{background:var(--chz-bubble);color:var(--chz-text);}
1510
+ .channlize-message-user{background:var(--chz-primary);color:var(--chz-on-primary);}
1511
+ .channlize-message-agent[data-pos="first"]{border-bottom-left-radius:6px;}
1512
+ .channlize-message-agent[data-pos="middle"]{border-top-left-radius:6px;border-bottom-left-radius:6px;}
1513
+ .channlize-message-agent[data-pos="last"]{border-top-left-radius:6px;}
1514
+ .channlize-message-user[data-pos="first"]{border-bottom-right-radius:6px;}
1515
+ .channlize-message-user[data-pos="middle"]{border-top-right-radius:6px;border-bottom-right-radius:6px;}
1516
+ .channlize-message-user[data-pos="last"]{border-top-right-radius:6px;}
1517
+ .channlize-message[data-status="sending"]{opacity:.7;}
1518
+ .channlize-message[data-status="failed"]{opacity:.85;box-shadow:0 0 0 2px var(--chz-danger);}
1519
+ .channlize-message-content a{color:inherit;text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:1px;}
1520
+ .channlize-message-content a:hover{text-decoration-thickness:2px;}
1521
+ .channlize-message-content:empty{display:none;}
1522
+ .channlize-message-timestamp{font-size:11.5px;color:var(--chz-muted);padding:1px 4px 0;}
1523
+ .channlize-message-meta{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--chz-muted);padding:1px 4px 0;}
1524
+ .channlize-message-meta[data-status="failed"]{color:var(--chz-danger);}
1525
+ .channlize-link-btn{font-weight:600;text-decoration:underline;text-underline-offset:2px;border-radius:4px;padding:0 2px;color:inherit;}
1526
+
1527
+ /* Attachments */
1528
+ .channlize-attachments{display:flex;flex-direction:column;gap:6px;margin-top:6px;white-space:normal;}
1529
+ .channlize-message-content:empty+.channlize-attachments{margin-top:0;}
1530
+ .channlize-attachment-image{display:block;border-radius:12px;overflow:hidden;max-width:240px;background:rgba(127,127,127,.12);}
1531
+ .channlize-attachment-image img{width:100%;height:auto;max-height:220px;object-fit:cover;}
1532
+ .channlize-attachment-file{display:flex;align-items:center;gap:10px;min-width:0;max-width:260px;padding:8px 10px;border-radius:12px;background:rgba(127,127,127,.14);background:color-mix(in srgb, currentColor 10%, transparent);color:inherit;text-decoration:none;}
1533
+ a.channlize-attachment-file:hover{background:color-mix(in srgb, currentColor 16%, transparent);}
1534
+ .channlize-attachment-name{display:block;font-size:13px;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
1535
+ .channlize-attachment-size{display:block;font-size:11.5px;opacity:.8;}
1536
+ .channlize-attachment-text{min-width:0;}
1537
+
1538
+ /* Welcome / empty state */
1539
+ .channlize-welcome{display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;padding:28px 12px 12px;animation:channlize-fade-in .3s ease;}
1540
+ .channlize-welcome .channlize-avatar{width:52px;height:52px;font-size:18px;margin-bottom:8px;}
1541
+ .channlize-welcome-title{font-size:18px;font-weight:650;line-height:1.3;color:var(--chz-text);}
1542
+ .channlize-welcome-text{font-size:14px;color:var(--chz-muted);max-width:280px;}
1543
+ .channlize-notice{margin:4px 0 0;font-size:12.5px;color:var(--chz-muted);}
1544
+ .channlize-suggestions{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:16px;}
1545
+ .channlize-chip{padding:8px 14px;border-radius:999px;font-size:13.5px;font-weight:550;line-height:1.3;color:var(--chz-text);background:var(--chz-bg);border:1px solid var(--chz-border);transition:border-color .15s ease,background-color .15s ease;text-align:left;}
1546
+ .channlize-chip:hover:not(:disabled){border-color:var(--chz-primary);background:var(--chz-surface);}
1547
+ .channlize-chip:disabled{opacity:.55;}
1548
+
1549
+ /* Composer */
1550
+ .channlize-chat-input-container{flex-shrink:0;padding:10px 12px 12px;background:var(--chz-bg);border-top:1px solid var(--chz-border);}
1551
+ .channlize-composer{display:flex;align-items:flex-end;gap:6px;padding:6px 6px 6px 12px;border-radius:22px;background:var(--chz-surface);border:1px solid var(--chz-border);transition:border-color .15s ease,box-shadow .15s ease;}
1552
+ .channlize-composer:focus-within{border-color:var(--chz-primary);box-shadow:0 0 0 3px rgba(37,99,235,.15);box-shadow:0 0 0 3px color-mix(in srgb, var(--chz-primary) 18%, transparent);}
1553
+ .channlize-composer[data-has-attach="true"]{padding-left:4px;}
1554
+ .channlize-input{flex:1;min-width:0;display:block;resize:none;border:0;outline:none;background:transparent;color:var(--chz-text);padding:7px 0;max-height:140px;font-size:15px;line-height:1.4;overflow-y:auto;box-shadow:none;border-radius:0;}
1555
+ .channlize-input:focus,.channlize-input:focus-visible{outline:none;}
1556
+ .channlize-input::placeholder{color:var(--chz-muted);opacity:1;}
1557
+ .channlize-input:disabled{opacity:.7;}
1558
+ .channlize-send{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:34px;height:34px;border-radius:999px;background:var(--chz-primary);color:var(--chz-on-primary);transition:opacity .15s ease,transform .15s ease,background-color .15s ease;}
1559
+ .channlize-send:hover:not(:disabled){transform:scale(1.05);}
1560
+ .channlize-send:disabled{background:var(--chz-border);color:var(--chz-muted);}
1561
+ .channlize-composer .channlize-icon-btn{width:34px;height:34px;border-radius:999px;color:var(--chz-muted);}
1562
+ .channlize-composer .channlize-icon-btn:hover{color:var(--chz-text);}
1563
+ .channlize-file-input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;}
1564
+ .channlize-pending-files{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;}
1565
+ .channlize-pending-file{display:inline-flex;align-items:center;gap:6px;max-width:100%;padding:4px 4px 4px 10px;border-radius:999px;background:var(--chz-surface);border:1px solid var(--chz-border);font-size:12.5px;}
1566
+ .channlize-pending-file span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;}
1567
+ .channlize-pending-file button{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:999px;color:var(--chz-muted);}
1568
+ .channlize-pending-file button:hover{background:var(--chz-border);color:var(--chz-text);}
1569
+ .channlize-composer-footer{display:flex;justify-content:flex-end;padding:4px 6px 0;font-size:11.5px;color:var(--chz-muted);}
1570
+ .channlize-composer-footer[data-over="true"]{color:var(--chz-danger);}
1571
+ @media (max-width: 640px), (pointer: coarse){
1572
+ .channlize-input{font-size:16px;}
1573
+ }
1574
+
1575
+ /* End-chat confirmation sheet */
1576
+ .channlize-sheet-backdrop{position:absolute;inset:0;z-index:5;display:flex;align-items:flex-end;background:rgba(15,23,42,.35);animation:channlize-fade-in .15s ease;}
1577
+ .channlize-sheet{width:100%;padding:20px 18px max(18px, env(safe-area-inset-bottom,0px));background:var(--chz-bg);color:var(--chz-text);border-radius:18px 18px 0 0;box-shadow:0 -8px 30px rgba(15,23,42,.18);animation:channlize-slide-up .22s cubic-bezier(.2,.8,.2,1);}
1578
+ .channlize-sheet-title{font-size:16px;font-weight:650;}
1579
+ .channlize-sheet-text{margin-top:4px;font-size:14px;color:var(--chz-muted);}
1580
+ .channlize-sheet-actions{display:flex;gap:8px;margin-top:16px;}
1581
+ .channlize-btn{flex:1;padding:10px 14px;border-radius:12px;font-size:14px;font-weight:600;transition:background-color .15s ease,filter .15s ease;}
1582
+ .channlize-btn-secondary{background:var(--chz-surface);color:var(--chz-text);border:1px solid var(--chz-border);}
1583
+ .channlize-btn-secondary:hover{background:var(--chz-border);}
1584
+ .channlize-btn-danger{background:#b42318;color:#ffffff;}
1585
+ .channlize-btn-danger:hover{filter:brightness(.92);}
1586
+ .channlize-btn-primary{background:var(--chz-primary);color:var(--chz-on-primary);}
1587
+
1588
+ .channlize-visually-hidden{position:absolute !important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
1589
+
1590
+ @keyframes channlize-spin{to{transform:rotate(360deg);}}
1591
+ @keyframes channlize-pulse{0%,100%{opacity:1;}50%{opacity:.4;}}
1592
+ @keyframes channlize-fade-in{from{opacity:0;}to{opacity:1;}}
1593
+ @keyframes channlize-slide-up{from{transform:translateY(24px);opacity:0;}to{transform:none;opacity:1;}}
1594
+ @keyframes channlize-pop{from{transform:scale(.4);}to{transform:scale(1);}}
1595
+ `;
1596
+
1597
+ // src/components/chat/internal.tsx
1598
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1599
+ var StylesContext = (0, import_react6.createContext)(false);
1600
+ function WidgetStylesProvider({ children }) {
1601
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(StylesContext.Provider, { value: true, children: [
1602
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(WidgetStyles, {}),
1603
+ children
1604
+ ] });
1605
+ }
1606
+ function WidgetStyles({ ifMissing = false }) {
1607
+ const provided = (0, import_react6.useContext)(StylesContext);
1608
+ if (ifMissing && provided) return null;
1609
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1610
+ "style",
1611
+ {
1612
+ "data-channlize-widget": "",
1613
+ dangerouslySetInnerHTML: { __html: WIDGET_CSS }
1614
+ }
1615
+ );
1616
+ }
1617
+ var useIsomorphicLayoutEffect = typeof window !== "undefined" ? import_react6.useLayoutEffect : import_react6.useEffect;
1618
+ function useDelayedFlag(value, delayMs) {
1619
+ const [elapsed, setElapsed] = (0, import_react6.useState)(false);
1620
+ (0, import_react6.useEffect)(() => {
1621
+ if (!value) return;
1622
+ const timer = setTimeout(() => setElapsed(true), delayMs);
1623
+ return () => {
1624
+ clearTimeout(timer);
1625
+ setElapsed(false);
1626
+ };
1627
+ }, [value, delayMs]);
1628
+ return value && elapsed;
1629
+ }
1630
+ function Avatar({
1631
+ name,
1632
+ url,
1633
+ node,
1634
+ className = ""
1635
+ }) {
1636
+ const [failedUrl, setFailedUrl] = (0, import_react6.useState)(null);
1637
+ const src = safeUrl(url);
1638
+ if (node) {
1639
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: `channlize-avatar ${className}`, "aria-hidden": "true", children: node });
1640
+ }
1641
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: `channlize-avatar ${className}`, "aria-hidden": "true", children: src && failedUrl !== src ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1642
+ "img",
1643
+ {
1644
+ src,
1645
+ alt: "",
1646
+ referrerPolicy: "no-referrer",
1647
+ onError: () => setFailedUrl(src)
1648
+ }
1649
+ ) : initials(name) });
1650
+ }
1651
+
1652
+ // src/components/SupportChat.tsx
1653
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1654
+ var defaultSupportChatLabels = {
1655
+ online: "Online",
1656
+ connecting: "Connecting\u2026",
1657
+ reconnecting: "Reconnecting\u2026",
1658
+ offline: "Offline",
1659
+ connectionLost: "Connection lost. New replies may be delayed.",
1660
+ startFailed: "We couldn\u2019t start the chat.",
1661
+ retry: "Retry",
1662
+ today: "Today",
1663
+ yesterday: "Yesterday",
1664
+ you: "You",
1665
+ sending: "Sending\u2026",
1666
+ failed: "Not sent",
1667
+ remove: "Remove",
1668
+ moreOptions: "More options",
1669
+ minimize: "Minimize chat",
1670
+ endChat: "End conversation",
1671
+ endChatTitle: "End this conversation?",
1672
+ endChatDescription: "The chat history will be cleared from this device. You can start a new conversation at any time.",
1673
+ endChatConfirm: "End conversation",
1674
+ cancel: "Cancel",
1675
+ attach: "Attach files",
1676
+ removeAttachment: "Remove attachment",
1677
+ messages: "Messages",
1678
+ conversationEnded: "Conversation ended. Send a message to start a new one.",
1679
+ charactersLeft: "characters"
1680
+ };
1681
+ var DEFAULT_PRIMARY2 = "#2563eb";
1682
+ var MAX_FILES = 10;
1683
+ var STATUS_DELAY_MS = 3e3;
1684
+ var BANNER_DELAY_MS = 5e3;
1685
+ var localIdCounter = 0;
1686
+ function createPendingMessage(content, files) {
1687
+ localIdCounter += 1;
1688
+ return {
1689
+ localId: `local-${Date.now()}-${localIdCounter}`,
1690
+ content,
1691
+ files,
1692
+ timestamp: /* @__PURE__ */ new Date(),
1693
+ status: "sending"
1694
+ };
1695
+ }
549
1696
  function SupportChat({
550
1697
  autoConnect = true,
551
1698
  metadata,
@@ -554,17 +1701,55 @@ function SupportChat({
554
1701
  sessionId,
555
1702
  persistSession,
556
1703
  storageKey,
1704
+ debug,
557
1705
  className = "",
558
1706
  styles = {},
559
- placeholder = "Type your message...",
560
- title = "Support Chat",
561
- sendButtonText = "Send",
1707
+ placeholder = "Write a message\u2026",
1708
+ title = "Support",
1709
+ sendButtonText = "Send message",
562
1710
  showTimestamps = true,
563
- allowAttachments = false
1711
+ allowAttachments = false,
1712
+ avatarUrl,
1713
+ avatar,
1714
+ statusText = "Replies in a few minutes",
1715
+ greeting = "Hi there!",
1716
+ welcomeMessage = "Ask us anything. We\u2019re here to help.",
1717
+ suggestions,
1718
+ theme = "auto",
1719
+ primaryColor,
1720
+ primaryTextColor,
1721
+ locale,
1722
+ maxLength = 5e3,
1723
+ allowEndChat = true,
1724
+ onMinimize,
1725
+ labels: labelOverrides
564
1726
  }) {
565
- const [messageInput, setMessageInput] = (0, import_react6.useState)("");
566
- const [attachments, setAttachments] = (0, import_react6.useState)([]);
567
- const messagesEndRef = (0, import_react6.useRef)(null);
1727
+ const labels = (0, import_react7.useMemo)(
1728
+ () => ({ ...defaultSupportChatLabels, ...labelOverrides }),
1729
+ [labelOverrides]
1730
+ );
1731
+ const { config } = useChannlize();
1732
+ const baseUrl = config.baseUrl || import_core3.DEFAULT_BASE_URL;
1733
+ const [text, setText] = (0, import_react7.useState)("");
1734
+ const [files, setFiles] = (0, import_react7.useState)([]);
1735
+ const [pending, setPending] = (0, import_react7.useState)([]);
1736
+ const [sendError, setSendError] = (0, import_react7.useState)(null);
1737
+ const [menuOpen, setMenuOpen] = (0, import_react7.useState)(false);
1738
+ const [confirmOpen, setConfirmOpen] = (0, import_react7.useState)(false);
1739
+ const [ended, setEnded] = (0, import_react7.useState)(false);
1740
+ const rootId = (0, import_react7.useId)();
1741
+ const titleId = `${rootId}-title`;
1742
+ const menuId = `${rootId}-menu`;
1743
+ const confirmTitleId = `${rootId}-confirm-title`;
1744
+ const confirmTextId = `${rootId}-confirm-text`;
1745
+ const listRef = (0, import_react7.useRef)(null);
1746
+ const inputRef = (0, import_react7.useRef)(null);
1747
+ const fileInputRef = (0, import_react7.useRef)(null);
1748
+ const menuWrapRef = (0, import_react7.useRef)(null);
1749
+ const moreButtonRef = (0, import_react7.useRef)(null);
1750
+ const cancelRef = (0, import_react7.useRef)(null);
1751
+ const stickToBottomRef = (0, import_react7.useRef)(true);
1752
+ const startingRef = (0, import_react7.useRef)(null);
568
1753
  const {
569
1754
  session,
570
1755
  messages,
@@ -573,7 +1758,8 @@ function SupportChat({
573
1758
  error,
574
1759
  sendMessage,
575
1760
  startSession,
576
- endSession
1761
+ endSession,
1762
+ reconnect
577
1763
  } = useChatSession({
578
1764
  autoConnect,
579
1765
  metadata,
@@ -581,240 +1767,727 @@ function SupportChat({
581
1767
  onError,
582
1768
  sessionId,
583
1769
  persistSession,
584
- storageKey
1770
+ storageKey,
1771
+ debug
585
1772
  });
586
- (0, import_react6.useEffect)(() => {
587
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
588
- }, [messages]);
589
- const handleSubmit = async (e) => {
590
- e.preventDefault();
591
- if (!messageInput.trim() && attachments.length === 0) return;
592
- try {
593
- await sendMessage(
594
- messageInput,
595
- attachments.length > 0 ? attachments : void 0
1773
+ const sseFailed = error?.code === "SSE_ERROR";
1774
+ const ownSendError = !!error && error === sendError;
1775
+ const startFailed = !session && !!error && !isConnecting && !ownSendError;
1776
+ const streamDown = !!session && !isConnected && !sseFailed;
1777
+ const showConnecting = useDelayedFlag(
1778
+ isConnecting || streamDown,
1779
+ STATUS_DELAY_MS
1780
+ );
1781
+ const showReconnectBanner = useDelayedFlag(
1782
+ streamDown && !isConnecting,
1783
+ BANNER_DELAY_MS
1784
+ );
1785
+ const status = sseFailed || startFailed ? "offline" : showConnecting ? "connecting" : "online";
1786
+ const statusLabel = status === "offline" ? labels.offline : status === "connecting" ? session ? labels.reconnecting : labels.connecting : statusText ? `${labels.online} \xB7 ${statusText}` : labels.online;
1787
+ const displayMessages = (0, import_react7.useMemo)(() => {
1788
+ const fromServer = messages.map(
1789
+ (m, i) => ({
1790
+ key: m.id || `m-${i}`,
1791
+ author: m.author === "user" ? "user" : "agent",
1792
+ content: typeof m.content === "string" ? m.content : "",
1793
+ timestamp: isValidDate(m.timestamp) ? m.timestamp : new Date(m.timestamp),
1794
+ authorName: m.author === "user" ? void 0 : m.authorData?.name || title,
1795
+ avatarUrl: m.author === "user" ? void 0 : m.authorData?.avatarUrl,
1796
+ attachments: toDisplayAttachments(m.attachments, baseUrl),
1797
+ status: "sent",
1798
+ message: m
1799
+ })
1800
+ );
1801
+ const local = pending.map((p) => ({
1802
+ key: p.localId,
1803
+ localId: p.localId,
1804
+ author: "user",
1805
+ content: p.content,
1806
+ timestamp: p.timestamp,
1807
+ attachments: p.files.map((f, i) => ({
1808
+ key: `${p.localId}-${i}`,
1809
+ name: f.name,
1810
+ size: f.size,
1811
+ type: f.type
1812
+ })),
1813
+ status: p.status
1814
+ }));
1815
+ return [...fromServer, ...local];
1816
+ }, [messages, pending, title, baseUrl]);
1817
+ const sections = (0, import_react7.useMemo)(
1818
+ () => groupMessages(displayMessages),
1819
+ [displayMessages]
1820
+ );
1821
+ const lastMessage = displayMessages[displayMessages.length - 1];
1822
+ useIsomorphicLayoutEffect(() => {
1823
+ const el = listRef.current;
1824
+ if (!el) return;
1825
+ if (stickToBottomRef.current || lastMessage?.localId) {
1826
+ el.scrollTop = el.scrollHeight;
1827
+ }
1828
+ }, [lastMessage?.key, lastMessage?.status, displayMessages.length]);
1829
+ const handleScroll = () => {
1830
+ const el = listRef.current;
1831
+ if (!el) return;
1832
+ stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
1833
+ };
1834
+ const handleMediaLoad = () => {
1835
+ const el = listRef.current;
1836
+ if (el && stickToBottomRef.current) el.scrollTop = el.scrollHeight;
1837
+ };
1838
+ useIsomorphicLayoutEffect(() => {
1839
+ const el = inputRef.current;
1840
+ if (!el) return;
1841
+ el.style.height = "auto";
1842
+ el.style.height = `${Math.min(el.scrollHeight, 140)}px`;
1843
+ }, [text]);
1844
+ const deliver = (0, import_react7.useCallback)(
1845
+ async (item) => {
1846
+ setPending(
1847
+ (prev) => prev.map(
1848
+ (p) => p.localId === item.localId ? { ...p, status: "sending" } : p
1849
+ )
596
1850
  );
597
- setMessageInput("");
598
- setAttachments([]);
599
- } catch (err) {
600
- console.error("Failed to send message:", err);
1851
+ try {
1852
+ if (!session) {
1853
+ if (!startingRef.current) {
1854
+ startingRef.current = startSession().finally(() => {
1855
+ startingRef.current = null;
1856
+ });
1857
+ }
1858
+ await startingRef.current;
1859
+ }
1860
+ await sendMessage(
1861
+ item.content,
1862
+ item.files.length > 0 ? item.files : void 0
1863
+ );
1864
+ setPending((prev) => prev.filter((p) => p.localId !== item.localId));
1865
+ } catch (err) {
1866
+ setSendError(err);
1867
+ setPending(
1868
+ (prev) => prev.map(
1869
+ (p) => p.localId === item.localId ? { ...p, status: "failed" } : p
1870
+ )
1871
+ );
1872
+ }
1873
+ },
1874
+ [session, startSession, sendMessage]
1875
+ );
1876
+ const send = (raw, attached) => {
1877
+ const content = raw.trim() || attached.map((f) => f.name).join(", ");
1878
+ if (!content) return;
1879
+ const item = createPendingMessage(content, attached);
1880
+ stickToBottomRef.current = true;
1881
+ setEnded(false);
1882
+ setPending((prev) => [...prev, item]);
1883
+ void deliver(item);
1884
+ };
1885
+ const canSend = (!!text.trim() || files.length > 0) && !isConnecting;
1886
+ const handleSubmit = (e) => {
1887
+ e?.preventDefault();
1888
+ if (!canSend) return;
1889
+ send(text, files);
1890
+ setText("");
1891
+ setFiles([]);
1892
+ inputRef.current?.focus();
1893
+ };
1894
+ const handleKeyDown = (e) => {
1895
+ if (e.key !== "Enter" || e.shiftKey || e.altKey) return;
1896
+ if (e.nativeEvent.isComposing || e.keyCode === 229) return;
1897
+ if (typeof window !== "undefined" && window.matchMedia?.("(hover: none) and (pointer: coarse)").matches) {
1898
+ return;
601
1899
  }
1900
+ e.preventDefault();
1901
+ handleSubmit();
602
1902
  };
603
1903
  const handleFileChange = (e) => {
604
- if (e.target.files) {
605
- setAttachments(Array.from(e.target.files));
1904
+ const selected = e.target.files ? Array.from(e.target.files) : [];
1905
+ if (selected.length > 0) {
1906
+ setFiles((prev) => [...prev, ...selected].slice(0, MAX_FILES));
606
1907
  }
1908
+ e.target.value = "";
607
1909
  };
608
- const formatTimestamp = (date) => {
609
- if (!date || isNaN(date.getTime())) {
610
- return "";
611
- }
612
- try {
613
- return new Intl.DateTimeFormat("en-US", {
614
- hour: "numeric",
615
- minute: "2-digit",
616
- hour12: true
617
- }).format(date);
618
- } catch (error2) {
619
- console.warn("Failed to format timestamp:", error2);
620
- return "";
1910
+ const retry = (localId) => {
1911
+ const item = pending.find((p) => p.localId === localId);
1912
+ if (item) void deliver(item);
1913
+ };
1914
+ const removePending = (localId) => {
1915
+ setPending((prev) => prev.filter((p) => p.localId !== localId));
1916
+ };
1917
+ (0, import_react7.useEffect)(() => {
1918
+ if (!menuOpen) return;
1919
+ const onPointerDown = (e) => {
1920
+ if (!menuWrapRef.current?.contains(e.target)) {
1921
+ setMenuOpen(false);
1922
+ }
1923
+ };
1924
+ document.addEventListener("pointerdown", onPointerDown);
1925
+ return () => document.removeEventListener("pointerdown", onPointerDown);
1926
+ }, [menuOpen]);
1927
+ (0, import_react7.useEffect)(() => {
1928
+ if (confirmOpen) cancelRef.current?.focus();
1929
+ }, [confirmOpen]);
1930
+ const closeConfirm = () => {
1931
+ setConfirmOpen(false);
1932
+ moreButtonRef.current?.focus();
1933
+ };
1934
+ const confirmEnd = async () => {
1935
+ setConfirmOpen(false);
1936
+ await endSession();
1937
+ setPending([]);
1938
+ setEnded(true);
1939
+ inputRef.current?.focus();
1940
+ };
1941
+ const handleRootKeyDown = (e) => {
1942
+ if (e.key !== "Escape") return;
1943
+ if (confirmOpen) {
1944
+ e.stopPropagation();
1945
+ closeConfirm();
1946
+ } else if (menuOpen) {
1947
+ e.stopPropagation();
1948
+ setMenuOpen(false);
1949
+ moreButtonRef.current?.focus();
1950
+ } else if (onMinimize) {
1951
+ e.stopPropagation();
1952
+ onMinimize();
621
1953
  }
622
1954
  };
623
- if (!session && !isConnecting) {
624
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1955
+ let banner = null;
1956
+ if (sseFailed || startFailed) {
1957
+ banner = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
625
1958
  "div",
626
1959
  {
627
- className: `channlize-chat-container ${className}`,
628
- style: styles.container,
1960
+ className: "channlize-banner channlize-error",
1961
+ "data-tone": "error",
1962
+ role: "alert",
1963
+ style: styles.error,
629
1964
  children: [
630
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "channlize-chat-header", style: styles.header, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { children: title }) }),
631
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "channlize-chat-start", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { onClick: startSession, style: styles.button, children: "Start Chat" }) }),
632
- error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "channlize-error", style: styles.error, children: error.message })
1965
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AlertIcon, { size: 16 }),
1966
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "channlize-banner-text", children: sseFailed ? labels.connectionLost : labels.startFailed }),
1967
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1968
+ "button",
1969
+ {
1970
+ type: "button",
1971
+ className: "channlize-banner-action",
1972
+ onClick: () => sseFailed ? reconnect() : void startSession(),
1973
+ children: labels.retry
1974
+ }
1975
+ )
1976
+ ]
1977
+ }
1978
+ );
1979
+ } else if (error && !ownSendError) {
1980
+ banner = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1981
+ "div",
1982
+ {
1983
+ className: "channlize-banner channlize-error",
1984
+ "data-tone": "error",
1985
+ role: "alert",
1986
+ style: styles.error,
1987
+ children: [
1988
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AlertIcon, { size: 16 }),
1989
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "channlize-banner-text", children: error.message })
633
1990
  ]
634
1991
  }
635
1992
  );
1993
+ } else if (showReconnectBanner) {
1994
+ banner = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "channlize-banner", role: "status", children: [
1995
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "channlize-spinner", "aria-hidden": "true" }),
1996
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "channlize-banner-text", children: labels.reconnecting })
1997
+ ] });
636
1998
  }
637
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1999
+ const accent = primaryColor || DEFAULT_PRIMARY2;
2000
+ const themeVars = {
2001
+ ...primaryColor && { "--chz-primary": primaryColor },
2002
+ "--chz-on-primary": primaryTextColor || readableTextColor(accent)
2003
+ };
2004
+ const headerAvatar = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2005
+ Avatar,
2006
+ {
2007
+ name: title,
2008
+ url: avatarUrl,
2009
+ node: avatar,
2010
+ className: "channlize-header-avatar"
2011
+ }
2012
+ );
2013
+ const counterVisible = text.length > maxLength * 0.8;
2014
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
638
2015
  "div",
639
2016
  {
640
- className: `channlize-chat-container ${className}`,
641
- style: styles.container,
2017
+ className: `channlize-widget channlize-chat-container ${className}`,
2018
+ "data-theme": theme,
2019
+ style: { ...themeVars, ...styles.container },
2020
+ onKeyDown: handleRootKeyDown,
642
2021
  children: [
643
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "channlize-chat-header", style: styles.header, children: [
644
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { children: title }),
645
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
646
- "div",
647
- {
648
- className: "channlize-chat-status",
649
- style: { display: "flex", alignItems: "center", gap: "0.5rem" },
650
- children: [
651
- isConnecting && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
652
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
653
- "span",
654
- {
655
- style: {
656
- display: "inline-block",
657
- width: "8px",
658
- height: "8px",
659
- borderRadius: "50%",
660
- backgroundColor: "#f59e0b"
661
- }
662
- }
663
- ),
664
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "Connecting..." })
665
- ] }),
666
- isConnected && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
667
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
668
- "span",
669
- {
670
- style: {
671
- display: "inline-block",
672
- width: "8px",
673
- height: "8px",
674
- borderRadius: "50%",
675
- backgroundColor: "#10b981"
676
- }
677
- }
678
- ),
679
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "Connected" })
680
- ] }),
681
- !isConnected && !isConnecting && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
682
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
683
- "span",
2022
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(WidgetStyles, { ifMissing: true }),
2023
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "channlize-chat-header", style: styles.header, children: [
2024
+ headerAvatar,
2025
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "channlize-header-text", children: [
2026
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h2", { className: "channlize-chat-title", id: titleId, children: title }),
2027
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2028
+ "p",
2029
+ {
2030
+ className: "channlize-chat-status",
2031
+ "data-status": status,
2032
+ title: statusLabel,
2033
+ children: [
2034
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "channlize-status-dot", "aria-hidden": "true" }),
2035
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: statusLabel })
2036
+ ]
2037
+ }
2038
+ )
2039
+ ] }),
2040
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "channlize-header-actions", children: [
2041
+ allowEndChat && session && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "channlize-menu-wrap", ref: menuWrapRef, children: [
2042
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2043
+ "button",
2044
+ {
2045
+ ref: moreButtonRef,
2046
+ type: "button",
2047
+ className: "channlize-icon-btn",
2048
+ "aria-label": labels.moreOptions,
2049
+ title: labels.moreOptions,
2050
+ "aria-haspopup": "menu",
2051
+ "aria-expanded": menuOpen,
2052
+ "aria-controls": menuOpen ? menuId : void 0,
2053
+ onClick: () => setMenuOpen((o) => !o),
2054
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MoreIcon, { size: 20 })
2055
+ }
2056
+ ),
2057
+ menuOpen && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2058
+ "div",
2059
+ {
2060
+ className: "channlize-menu",
2061
+ role: "menu",
2062
+ id: menuId,
2063
+ "aria-label": labels.moreOptions,
2064
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2065
+ "button",
684
2066
  {
685
- style: {
686
- display: "inline-block",
687
- width: "8px",
688
- height: "8px",
689
- borderRadius: "50%",
690
- backgroundColor: "#ef4444"
691
- }
2067
+ type: "button",
2068
+ role: "menuitem",
2069
+ className: "channlize-menu-item",
2070
+ autoFocus: true,
2071
+ onClick: () => {
2072
+ setMenuOpen(false);
2073
+ setConfirmOpen(true);
2074
+ },
2075
+ children: [
2076
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ExitIcon, { size: 16 }),
2077
+ labels.endChat
2078
+ ]
692
2079
  }
693
- ),
694
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: "Disconnected" })
695
- ] })
696
- ]
697
- }
698
- ),
699
- session && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
700
- "button",
701
- {
702
- onClick: endSession,
703
- style: { ...styles.button, fontSize: "0.875rem" },
704
- type: "button",
705
- children: "End Chat"
706
- }
707
- )
2080
+ )
2081
+ }
2082
+ )
2083
+ ] }),
2084
+ onMinimize && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2085
+ "button",
2086
+ {
2087
+ type: "button",
2088
+ className: "channlize-icon-btn",
2089
+ "aria-label": labels.minimize,
2090
+ title: labels.minimize,
2091
+ onClick: onMinimize,
2092
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ChevronDownIcon, { size: 22 })
2093
+ }
2094
+ )
2095
+ ] })
708
2096
  ] }),
709
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "channlize-messages", style: styles.messagesContainer, children: [
710
- messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { textAlign: "center", padding: "2rem", color: "#666" }, children: "No messages yet. Start the conversation!" }),
711
- messages.map((message, index) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
712
- "div",
713
- {
714
- className: `channlize-message ${message.author === "user" ? "channlize-message-user" : "channlize-message-agent"}`,
715
- style: {
716
- ...styles.message,
717
- ...message.author === "user" ? styles.userMessage : styles.agentMessage
718
- },
719
- children: [
720
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "channlize-message-content", children: message.content }),
721
- showTimestamps && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2097
+ banner,
2098
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2099
+ "div",
2100
+ {
2101
+ ref: listRef,
2102
+ className: "channlize-messages channlize-chat-messages",
2103
+ style: styles.messagesContainer,
2104
+ role: "log",
2105
+ "aria-live": "polite",
2106
+ "aria-relevant": "additions",
2107
+ "aria-label": labels.messages,
2108
+ tabIndex: 0,
2109
+ onScroll: handleScroll,
2110
+ onLoadCapture: handleMediaLoad,
2111
+ children: displayMessages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "channlize-welcome", children: [
2112
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Avatar, { name: title, url: avatarUrl, node: avatar }),
2113
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "channlize-welcome-title", children: greeting }),
2114
+ welcomeMessage && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "channlize-welcome-text", children: welcomeMessage }),
2115
+ ended && !session && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "channlize-notice", role: "status", children: labels.conversationEnded }),
2116
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "channlize-suggestions", children: suggestions.map((s) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2117
+ "button",
2118
+ {
2119
+ type: "button",
2120
+ className: "channlize-chip",
2121
+ disabled: isConnecting,
2122
+ onClick: () => send(s, []),
2123
+ children: s
2124
+ },
2125
+ s
2126
+ )) })
2127
+ ] }) : sections.map((section) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "channlize-day-section", children: [
2128
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "channlize-day", role: "separator", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: formatDayLabel(section.date, labels, locale) }) }),
2129
+ section.groups.map((group) => {
2130
+ const last = group.messages[group.messages.length - 1];
2131
+ const isUser = group.author === "user";
2132
+ const speaker = isUser ? labels.you : group.authorName;
2133
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
722
2134
  "div",
723
2135
  {
724
- className: "channlize-message-timestamp",
725
- style: styles.timestamp,
726
- children: formatTimestamp(message.timestamp)
727
- }
728
- )
729
- ]
730
- },
731
- message.id || index
732
- )),
733
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: messagesEndRef })
734
- ] }),
735
- error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "channlize-error", style: styles.error, children: error.message }),
736
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2136
+ className: "channlize-group",
2137
+ "data-author": group.author,
2138
+ children: [
2139
+ !isUser && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2140
+ Avatar,
2141
+ {
2142
+ name: group.authorName,
2143
+ url: group.avatarUrl || (group.authorName === title ? avatarUrl : void 0),
2144
+ node: group.authorName === title ? avatar : void 0,
2145
+ className: "channlize-group-avatar"
2146
+ }
2147
+ ),
2148
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "channlize-group-body", children: [
2149
+ !isUser && group.authorName && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2150
+ "div",
2151
+ {
2152
+ className: "channlize-group-name",
2153
+ "aria-hidden": "true",
2154
+ children: group.authorName
2155
+ }
2156
+ ),
2157
+ group.messages.map((msg, i) => {
2158
+ const n = group.messages.length;
2159
+ const pos = n === 1 ? "single" : i === 0 ? "first" : i === n - 1 ? "last" : "middle";
2160
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2161
+ MessageBubble,
2162
+ {
2163
+ msg,
2164
+ pos,
2165
+ speaker,
2166
+ locale,
2167
+ labels,
2168
+ styles,
2169
+ onRetry: retry,
2170
+ onRemove: removePending
2171
+ },
2172
+ msg.key
2173
+ );
2174
+ }),
2175
+ showTimestamps && last && last.status === "sent" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2176
+ "div",
2177
+ {
2178
+ className: "channlize-message-timestamp",
2179
+ style: styles.timestamp,
2180
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2181
+ "time",
2182
+ {
2183
+ dateTime: isValidDate(last.timestamp) ? last.timestamp.toISOString() : void 0,
2184
+ children: formatTime(last.timestamp, locale)
2185
+ }
2186
+ )
2187
+ }
2188
+ )
2189
+ ] })
2190
+ ]
2191
+ },
2192
+ group.key
2193
+ );
2194
+ })
2195
+ ] }, section.key))
2196
+ }
2197
+ ),
2198
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
737
2199
  "form",
738
2200
  {
739
2201
  className: "channlize-chat-input-container",
740
2202
  style: styles.inputContainer,
741
2203
  onSubmit: handleSubmit,
742
2204
  children: [
743
- allowAttachments && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
744
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
745
- "input",
746
- {
747
- type: "file",
748
- multiple: true,
749
- onChange: handleFileChange,
750
- style: { fontSize: "0.875rem" }
751
- }
752
- ),
753
- attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontSize: "0.75rem", marginTop: "0.25rem" }, children: [
754
- attachments.length,
755
- " file(s) selected"
756
- ] })
757
- ] }),
758
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", gap: "0.5rem" }, children: [
759
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
760
- "input",
761
- {
762
- type: "text",
763
- value: messageInput,
764
- onChange: (e) => setMessageInput(e.target.value),
765
- placeholder,
766
- disabled: !session || isConnecting,
767
- style: { ...styles.input, flex: 1 }
768
- }
769
- ),
770
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2205
+ files.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "channlize-pending-files", children: files.map((f, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "channlize-pending-file", children: [
2206
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { title: f.name, children: f.name }),
2207
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
771
2208
  "button",
772
2209
  {
773
- type: "submit",
774
- disabled: !session || isConnecting || !messageInput.trim() && attachments.length === 0,
775
- style: styles.button,
776
- children: sendButtonText
2210
+ type: "button",
2211
+ "aria-label": `${labels.removeAttachment}: ${f.name}`,
2212
+ onClick: () => setFiles((prev) => prev.filter((_, j) => j !== i)),
2213
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, { size: 14 })
777
2214
  }
778
2215
  )
779
- ] })
2216
+ ] }, `${f.name}-${i}`)) }),
2217
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2218
+ "div",
2219
+ {
2220
+ className: "channlize-composer",
2221
+ "data-has-attach": allowAttachments ? "true" : void 0,
2222
+ children: [
2223
+ allowAttachments && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
2224
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2225
+ "button",
2226
+ {
2227
+ type: "button",
2228
+ className: "channlize-icon-btn",
2229
+ "aria-label": labels.attach,
2230
+ title: labels.attach,
2231
+ onClick: () => fileInputRef.current?.click(),
2232
+ disabled: files.length >= MAX_FILES,
2233
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PaperclipIcon, { size: 18 })
2234
+ }
2235
+ ),
2236
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2237
+ "input",
2238
+ {
2239
+ ref: fileInputRef,
2240
+ type: "file",
2241
+ multiple: true,
2242
+ className: "channlize-file-input",
2243
+ tabIndex: -1,
2244
+ "aria-hidden": "true",
2245
+ onChange: handleFileChange
2246
+ }
2247
+ )
2248
+ ] }),
2249
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2250
+ "textarea",
2251
+ {
2252
+ ref: inputRef,
2253
+ className: "channlize-input",
2254
+ rows: 1,
2255
+ value: text,
2256
+ maxLength,
2257
+ onChange: (e) => setText(e.target.value),
2258
+ onKeyDown: handleKeyDown,
2259
+ placeholder,
2260
+ "aria-label": placeholder,
2261
+ enterKeyHint: "send",
2262
+ style: styles.input
2263
+ }
2264
+ ),
2265
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2266
+ "button",
2267
+ {
2268
+ type: "submit",
2269
+ className: "channlize-send",
2270
+ disabled: !canSend,
2271
+ "aria-label": sendButtonText,
2272
+ title: sendButtonText,
2273
+ style: styles.button,
2274
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SendIcon, { size: 18 })
2275
+ }
2276
+ )
2277
+ ]
2278
+ }
2279
+ ),
2280
+ counterVisible && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2281
+ "div",
2282
+ {
2283
+ className: "channlize-composer-footer",
2284
+ "data-over": text.length >= maxLength ? "true" : void 0,
2285
+ "aria-live": "polite",
2286
+ children: [
2287
+ text.length,
2288
+ "/",
2289
+ maxLength,
2290
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "channlize-visually-hidden", children: [
2291
+ " ",
2292
+ labels.charactersLeft
2293
+ ] })
2294
+ ]
2295
+ }
2296
+ )
780
2297
  ]
781
2298
  }
2299
+ ),
2300
+ confirmOpen && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2301
+ "div",
2302
+ {
2303
+ className: "channlize-sheet-backdrop",
2304
+ onClick: (e) => {
2305
+ if (e.target === e.currentTarget) closeConfirm();
2306
+ },
2307
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2308
+ "div",
2309
+ {
2310
+ className: "channlize-sheet",
2311
+ role: "alertdialog",
2312
+ "aria-modal": "true",
2313
+ "aria-labelledby": confirmTitleId,
2314
+ "aria-describedby": confirmTextId,
2315
+ children: [
2316
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("h3", { className: "channlize-sheet-title", id: confirmTitleId, children: labels.endChatTitle }),
2317
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "channlize-sheet-text", id: confirmTextId, children: labels.endChatDescription }),
2318
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "channlize-sheet-actions", children: [
2319
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2320
+ "button",
2321
+ {
2322
+ ref: cancelRef,
2323
+ type: "button",
2324
+ className: "channlize-btn channlize-btn-secondary",
2325
+ onClick: closeConfirm,
2326
+ children: labels.cancel
2327
+ }
2328
+ ),
2329
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2330
+ "button",
2331
+ {
2332
+ type: "button",
2333
+ className: "channlize-btn channlize-btn-danger",
2334
+ onClick: () => void confirmEnd(),
2335
+ children: labels.endChatConfirm
2336
+ }
2337
+ )
2338
+ ] })
2339
+ ]
2340
+ }
2341
+ )
2342
+ }
782
2343
  )
783
2344
  ]
784
2345
  }
785
2346
  );
786
2347
  }
2348
+ function MessageBubble({
2349
+ msg,
2350
+ pos,
2351
+ speaker,
2352
+ locale,
2353
+ labels,
2354
+ styles,
2355
+ onRetry,
2356
+ onRemove
2357
+ }) {
2358
+ const isUser = msg.author === "user";
2359
+ const parts = (0, import_react7.useMemo)(() => linkify(msg.content), [msg.content]);
2360
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
2361
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2362
+ "div",
2363
+ {
2364
+ className: `channlize-message ${isUser ? "channlize-message-user" : "channlize-message-agent"}`,
2365
+ "data-pos": pos,
2366
+ "data-status": msg.status,
2367
+ title: formatFullDate(msg.timestamp, locale) || void 0,
2368
+ style: {
2369
+ ...styles.message,
2370
+ ...isUser ? styles.userMessage : styles.agentMessage
2371
+ },
2372
+ children: [
2373
+ speaker && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "channlize-visually-hidden", children: [
2374
+ speaker,
2375
+ ": "
2376
+ ] }),
2377
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "channlize-message-content", children: parts.map(
2378
+ (part, i) => part.type === "link" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2379
+ "a",
2380
+ {
2381
+ href: part.href,
2382
+ target: "_blank",
2383
+ rel: "noopener noreferrer nofollow",
2384
+ children: part.value
2385
+ },
2386
+ i
2387
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: part.value }, i)
2388
+ ) }),
2389
+ msg.attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "channlize-attachments", children: msg.attachments.map((a) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AttachmentView, { attachment: a }, a.key)) })
2390
+ ]
2391
+ }
2392
+ ),
2393
+ msg.status === "sending" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "channlize-message-meta", "data-status": "sending", children: labels.sending }),
2394
+ msg.status === "failed" && msg.localId && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2395
+ "div",
2396
+ {
2397
+ className: "channlize-message-meta",
2398
+ "data-status": "failed",
2399
+ role: "alert",
2400
+ children: [
2401
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AlertIcon, { size: 14 }),
2402
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: labels.failed }),
2403
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { "aria-hidden": "true", children: "\xB7" }),
2404
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2405
+ "button",
2406
+ {
2407
+ type: "button",
2408
+ className: "channlize-link-btn",
2409
+ onClick: () => onRetry(msg.localId),
2410
+ children: labels.retry
2411
+ }
2412
+ ),
2413
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { "aria-hidden": "true", children: "\xB7" }),
2414
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2415
+ "button",
2416
+ {
2417
+ type: "button",
2418
+ className: "channlize-link-btn",
2419
+ onClick: () => onRemove(msg.localId),
2420
+ children: labels.remove
2421
+ }
2422
+ )
2423
+ ]
2424
+ }
2425
+ )
2426
+ ] });
2427
+ }
2428
+ function AttachmentView({ attachment }) {
2429
+ const size = formatFileSize(attachment.size);
2430
+ if (attachment.url && isImage(attachment)) {
2431
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2432
+ "a",
2433
+ {
2434
+ className: "channlize-attachment-image",
2435
+ href: attachment.url,
2436
+ target: "_blank",
2437
+ rel: "noopener noreferrer",
2438
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2439
+ "img",
2440
+ {
2441
+ src: attachment.url,
2442
+ alt: attachment.name,
2443
+ loading: "lazy",
2444
+ referrerPolicy: "no-referrer"
2445
+ }
2446
+ )
2447
+ }
2448
+ );
2449
+ }
2450
+ const body = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
2451
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FileIcon, { size: 18 }),
2452
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "channlize-attachment-text", children: [
2453
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "channlize-attachment-name", children: attachment.name }),
2454
+ size && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "channlize-attachment-size", children: size })
2455
+ ] })
2456
+ ] });
2457
+ return attachment.url ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2458
+ "a",
2459
+ {
2460
+ className: "channlize-attachment-file",
2461
+ href: attachment.url,
2462
+ target: "_blank",
2463
+ rel: "noopener noreferrer",
2464
+ download: attachment.name,
2465
+ children: body
2466
+ }
2467
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "channlize-attachment-file", children: body });
2468
+ }
787
2469
 
788
2470
  // src/components/ChannlizeChat.tsx
789
- var import_react7 = require("react");
790
- var import_jsx_runtime5 = require("react/jsx-runtime");
2471
+ var import_react8 = require("react");
2472
+ var import_react_dom = require("react-dom");
2473
+ var import_jsx_runtime7 = require("react/jsx-runtime");
791
2474
  var emptySubscribe = () => () => {
792
2475
  };
793
- function subscribeDarkMode(onStoreChange) {
794
- const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
795
- mediaQuery.addEventListener("change", onStoreChange);
796
- return () => mediaQuery.removeEventListener("change", onStoreChange);
797
- }
798
- function getDarkModeSnapshot() {
799
- return window.matchMedia("(prefers-color-scheme: dark)").matches;
800
- }
801
2476
  function ChannlizeChat({
802
2477
  config,
803
2478
  position = "bottom-right",
804
- offset = { x: 24, y: 24 },
2479
+ offset,
805
2480
  buttonElement,
806
2481
  buttonIcon,
807
2482
  buttonSize = 56,
808
- buttonColors = {
809
- background: "#3b82f6",
810
- hover: "#2563eb",
811
- text: "#ffffff"
812
- },
2483
+ buttonColors,
813
2484
  chatProps = {},
2485
+ primaryColor,
2486
+ theme,
814
2487
  defaultOpen = false,
815
2488
  showBadge = false,
816
2489
  unreadCount = 0,
817
- zIndex = 1e3,
2490
+ zIndex = 2147483e3,
818
2491
  showOnMobile = true,
819
2492
  onOpen,
820
2493
  onClose,
@@ -822,301 +2495,173 @@ function ChannlizeChat({
822
2495
  buttonClassName = "",
823
2496
  chatClassName = ""
824
2497
  }) {
825
- const [isOpen, setIsOpen] = (0, import_react7.useState)(defaultOpen);
826
- const isMounted = (0, import_react7.useSyncExternalStore)(
2498
+ const [isOpen, setIsOpen] = (0, import_react8.useState)(defaultOpen);
2499
+ const [autoUnread, setAutoUnread] = (0, import_react8.useState)(0);
2500
+ const launcherRef = (0, import_react8.useRef)(null);
2501
+ const panelRef = (0, import_react8.useRef)(null);
2502
+ const isOpenRef = (0, import_react8.useRef)(isOpen);
2503
+ const userOnMessageRef = (0, import_react8.useRef)(chatProps.onMessage);
2504
+ const isMounted = (0, import_react8.useSyncExternalStore)(
827
2505
  emptySubscribe,
828
2506
  () => true,
829
2507
  () => false
830
2508
  );
831
- const isDarkMode = (0, import_react7.useSyncExternalStore)(
832
- subscribeDarkMode,
833
- getDarkModeSnapshot,
834
- () => false
835
- );
2509
+ (0, import_react8.useEffect)(() => {
2510
+ isOpenRef.current = isOpen;
2511
+ userOnMessageRef.current = chatProps.onMessage;
2512
+ });
2513
+ const handleMessage = (0, import_react8.useCallback)((message) => {
2514
+ if (!isOpenRef.current && message.author === "agent") {
2515
+ setAutoUnread((n) => n + 1);
2516
+ }
2517
+ userOnMessageRef.current?.(message);
2518
+ }, []);
2519
+ const open = (0, import_react8.useCallback)(() => {
2520
+ setIsOpen(true);
2521
+ setAutoUnread(0);
2522
+ onOpen?.();
2523
+ }, [onOpen]);
2524
+ const close = (0, import_react8.useCallback)(() => {
2525
+ const hadFocus = !!panelRef.current?.contains(document.activeElement);
2526
+ setIsOpen(false);
2527
+ onClose?.();
2528
+ if (hadFocus) {
2529
+ requestAnimationFrame(() => launcherRef.current?.focus());
2530
+ }
2531
+ }, [onClose]);
2532
+ (0, import_react8.useEffect)(() => {
2533
+ const panel = panelRef.current;
2534
+ if (!panel) return;
2535
+ panel.inert = !isOpen;
2536
+ if (!isOpen) return;
2537
+ const frame = requestAnimationFrame(() => {
2538
+ const coarse = window.matchMedia?.("(pointer: coarse)").matches;
2539
+ const input = panel.querySelector("textarea");
2540
+ if (input && !coarse) input.focus();
2541
+ else panel.focus();
2542
+ });
2543
+ return () => cancelAnimationFrame(frame);
2544
+ }, [isOpen, isMounted]);
836
2545
  if (!isMounted) {
837
2546
  return null;
838
2547
  }
839
- const handleToggle = () => {
840
- const newState = !isOpen;
841
- setIsOpen(newState);
842
- if (newState) {
843
- onOpen?.();
844
- } else {
845
- onClose?.();
2548
+ const accent = primaryColor || chatProps.primaryColor;
2549
+ const launcherBg = buttonColors?.background || accent;
2550
+ const badgeCount = showBadge ? unreadCount : autoUnread;
2551
+ const title = chatProps.title || "Support";
2552
+ const rootStyle = {
2553
+ "--chz-x": `${offset?.x ?? 24}px`,
2554
+ "--chz-y": `${offset?.y ?? 24}px`,
2555
+ "--chz-z": String(zIndex),
2556
+ "--chz-launcher-size": `${buttonSize}px`,
2557
+ ...accent && {
2558
+ "--chz-primary": accent,
2559
+ "--chz-on-primary": chatProps.primaryTextColor || readableTextColor(accent)
2560
+ },
2561
+ ...launcherBg && { "--chz-launcher-bg": launcherBg },
2562
+ ...buttonColors?.hover && { "--chz-launcher-hover": buttonColors.hover },
2563
+ ...(buttonColors?.text || launcherBg) && {
2564
+ "--chz-launcher-fg": buttonColors?.text || readableTextColor(launcherBg)
846
2565
  }
847
2566
  };
848
- const positionStyles = {
849
- position: "fixed",
850
- zIndex,
851
- ...position.includes("bottom") ? { bottom: `${offset.y}px` } : { top: `${offset.y}px` },
852
- ...position.includes("right") ? { right: `${offset.x}px` } : { left: `${offset.x}px` }
853
- };
854
- const buttonStyle = {
855
- width: `${buttonSize}px`,
856
- height: `${buttonSize}px`,
857
- borderRadius: "50%",
858
- backgroundColor: buttonColors.background,
859
- color: buttonColors.text,
860
- border: "none",
861
- cursor: "pointer",
862
- display: "flex",
863
- alignItems: "center",
864
- justifyContent: "center",
865
- boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
866
- transition: enableAnimations ? "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)" : "none",
867
- transform: enableAnimations && isOpen ? "scale(0.9)" : "scale(1)"
868
- };
869
- const chatContainerStyle = {
870
- ...positionStyles,
871
- opacity: isOpen ? 1 : 0,
872
- visibility: isOpen ? "visible" : "hidden",
873
- transform: enableAnimations ? isOpen ? "scale(1) translateY(0)" : "scale(0.95) translateY(10px)" : "none",
874
- transition: enableAnimations ? "opacity 0.2s, transform 0.2s, visibility 0.2s" : "none"
2567
+ const launcherLabel = badgeCount > 0 ? `Open chat, ${badgeCount} unread ${badgeCount === 1 ? "message" : "messages"}` : "Open chat";
2568
+ const badge = badgeCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "channlize-badge", "aria-hidden": "true", children: badgeCount > 99 ? "99+" : badgeCount }) : null;
2569
+ const onCustomLauncherKey = (e) => {
2570
+ if (e.key === "Enter" || e.key === " ") {
2571
+ e.preventDefault();
2572
+ open();
2573
+ }
875
2574
  };
876
- const defaultIcon = /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
877
- "svg",
2575
+ const widget = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChannlizeProvider, { config, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(WidgetStylesProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2576
+ "div",
878
2577
  {
879
- width: buttonSize * 0.5,
880
- height: buttonSize * 0.5,
881
- viewBox: "0 0 24 24",
882
- fill: "none",
883
- stroke: "currentColor",
884
- strokeWidth: "2",
885
- strokeLinecap: "round",
886
- strokeLinejoin: "round",
887
- children: isOpen ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
888
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
889
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
890
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
891
- /* @__PURE__ */ (0, import_jsx_runtime5.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" }),
892
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M8 10h.01" }),
893
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M12 10h.01" }),
894
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M16 10h.01" })
895
- ] })
896
- }
897
- );
898
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(ChannlizeProvider, { config, children: [
899
- !isOpen && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
900
- "div",
901
- {
902
- style: {
903
- ...positionStyles,
904
- display: !showOnMobile && typeof window !== "undefined" && window.innerWidth < 768 ? "none" : "block"
905
- },
906
- className: buttonClassName,
907
- children: buttonElement ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { onClick: handleToggle, style: { cursor: "pointer" }, children: buttonElement }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
908
- "button",
2578
+ className: "channlize-widget channlize-floating",
2579
+ "data-theme": theme || chatProps.theme || "auto",
2580
+ "data-position": position,
2581
+ "data-open": isOpen ? "true" : "false",
2582
+ "data-animate": enableAnimations ? "true" : "false",
2583
+ "data-hide-mobile": showOnMobile ? void 0 : "true",
2584
+ style: rootStyle,
2585
+ children: [
2586
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `channlize-launcher-wrap ${buttonClassName}`, children: buttonElement ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2587
+ "div",
909
2588
  {
910
- onClick: handleToggle,
911
- style: buttonStyle,
912
- onMouseEnter: (e) => {
913
- if (enableAnimations) {
914
- e.currentTarget.style.backgroundColor = buttonColors.hover || buttonColors.background || "#2563eb";
915
- e.currentTarget.style.transform = "scale(1.05)";
916
- }
2589
+ ref: (el) => {
2590
+ launcherRef.current = el;
917
2591
  },
918
- onMouseLeave: (e) => {
919
- if (enableAnimations) {
920
- e.currentTarget.style.backgroundColor = buttonColors.background || "#3b82f6";
921
- e.currentTarget.style.transform = "scale(1)";
922
- }
2592
+ className: "channlize-launcher-custom",
2593
+ role: "button",
2594
+ tabIndex: isOpen ? -1 : 0,
2595
+ "aria-label": launcherLabel,
2596
+ "aria-expanded": isOpen,
2597
+ onClick: open,
2598
+ onKeyDown: onCustomLauncherKey,
2599
+ children: buttonElement
2600
+ }
2601
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2602
+ "button",
2603
+ {
2604
+ ref: (el) => {
2605
+ launcherRef.current = el;
923
2606
  },
924
- "aria-label": isOpen ? "Close chat" : "Open chat",
2607
+ type: "button",
2608
+ className: "channlize-launcher",
2609
+ onClick: open,
2610
+ "aria-label": launcherLabel,
2611
+ "aria-expanded": isOpen,
2612
+ tabIndex: isOpen ? -1 : 0,
925
2613
  children: [
926
- buttonIcon || defaultIcon,
927
- showBadge && unreadCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
928
- "span",
929
- {
930
- style: {
931
- position: "absolute",
932
- top: "-4px",
933
- right: "-4px",
934
- backgroundColor: "#ef4444",
935
- color: "white",
936
- borderRadius: "10px",
937
- padding: "2px 6px",
938
- fontSize: "12px",
939
- fontWeight: "600",
940
- minWidth: "20px",
941
- textAlign: "center",
942
- boxShadow: "0 2px 4px rgba(0,0,0,0.2)"
943
- },
944
- children: unreadCount > 99 ? "99+" : unreadCount
945
- }
946
- )
2614
+ buttonIcon || /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ChatIcon, { size: Math.round(buttonSize * 0.46) }),
2615
+ badge
947
2616
  ]
948
2617
  }
949
- )
950
- }
951
- ),
952
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: chatContainerStyle, className: chatClassName, children: [
953
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
954
- SupportChat,
955
- {
956
- autoConnect: isOpen,
957
- persistSession: true,
958
- title: "Support Chat",
959
- placeholder: "Type your message...",
960
- showTimestamps: true,
961
- ...chatProps,
962
- styles: {
963
- container: {
964
- width: "384px",
965
- height: "600px",
966
- maxHeight: "calc(100vh - 120px)",
967
- borderRadius: "16px",
968
- overflow: "hidden",
969
- boxShadow: isDarkMode ? "0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.5)" : "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
970
- backgroundColor: isDarkMode ? "#1f2937" : "#ffffff",
971
- display: "flex",
972
- flexDirection: "column",
973
- position: "relative",
974
- ...chatProps.styles?.container
975
- },
976
- header: {
977
- background: "linear-gradient(135deg, #3b82f6 0%, #9333ea 100%)",
978
- padding: "20px",
979
- borderRadius: "16px 16px 0 0",
980
- color: "white",
981
- display: "flex",
982
- alignItems: "center",
983
- justifyContent: "space-between",
984
- ...chatProps.styles?.header
985
- },
986
- messagesContainer: {
987
- flex: 1,
988
- overflowY: "auto",
989
- padding: "20px",
990
- backgroundColor: isDarkMode ? "#111827" : "#f9fafb",
991
- ...chatProps.styles?.messagesContainer
992
- },
993
- inputContainer: {
994
- padding: "16px",
995
- backgroundColor: isDarkMode ? "#1f2937" : "white",
996
- borderTop: `1px solid ${isDarkMode ? "#374151" : "#e5e7eb"}`,
997
- ...chatProps.styles?.inputContainer
998
- },
999
- input: {
1000
- ...chatProps.styles?.input,
1001
- borderRadius: "12px",
1002
- padding: "12px 16px",
1003
- fontSize: "14px",
1004
- border: `1px solid ${isDarkMode ? "#374151" : "#d1d5db"}`,
1005
- outline: "none",
1006
- backgroundColor: isDarkMode ? "#374151" : "#ffffff",
1007
- color: isDarkMode ? "#f9fafb" : "#111827",
1008
- fontWeight: "400"
1009
- },
1010
- button: {
1011
- backgroundColor: "#3b82f6",
1012
- color: "white",
1013
- borderRadius: "12px",
1014
- padding: "12px 24px",
1015
- fontSize: "14px",
1016
- fontWeight: "500",
1017
- border: "none",
1018
- cursor: "pointer",
1019
- transition: "background-color 0.2s",
1020
- ...chatProps.styles?.button
1021
- },
1022
- userMessage: {
1023
- backgroundColor: "#3b82f6",
1024
- color: "white",
1025
- padding: "10px 14px",
1026
- borderRadius: "12px 12px 4px 12px",
1027
- marginBottom: "8px",
1028
- marginLeft: "auto",
1029
- maxWidth: "80%",
1030
- width: "fit-content",
1031
- ...chatProps.styles?.userMessage
1032
- },
1033
- agentMessage: {
1034
- backgroundColor: isDarkMode ? "#374151" : "white",
1035
- color: isDarkMode ? "#f9fafb" : "#1f2937",
1036
- padding: "10px 14px",
1037
- borderRadius: "12px 12px 12px 4px",
1038
- marginBottom: "8px",
1039
- marginRight: "auto",
1040
- maxWidth: "80%",
1041
- width: "fit-content",
1042
- boxShadow: isDarkMode ? "0 1px 2px 0 rgb(0 0 0 / 0.3)" : "0 1px 2px 0 rgb(0 0 0 / 0.05)",
1043
- ...chatProps.styles?.agentMessage
1044
- },
1045
- timestamp: {
1046
- fontSize: "11px",
1047
- opacity: 0.7,
1048
- marginTop: "4px",
1049
- color: isDarkMode ? "#d1d5db" : "inherit",
1050
- ...chatProps.styles?.timestamp
1051
- },
1052
- error: {
1053
- backgroundColor: isDarkMode ? "#7f1d1d" : "#fee2e2",
1054
- color: isDarkMode ? "#fca5a5" : "#991b1b",
1055
- padding: "12px",
1056
- margin: "8px 16px",
1057
- borderRadius: "8px",
1058
- fontSize: "14px",
1059
- ...chatProps.styles?.error
1060
- }
2618
+ ) }),
2619
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2620
+ "div",
2621
+ {
2622
+ ref: panelRef,
2623
+ className: `channlize-panel ${chatClassName}`,
2624
+ role: "dialog",
2625
+ "aria-modal": "false",
2626
+ "aria-label": title,
2627
+ "aria-hidden": !isOpen,
2628
+ tabIndex: -1,
2629
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2630
+ SupportChat,
2631
+ {
2632
+ persistSession: true,
2633
+ title,
2634
+ showTimestamps: true,
2635
+ ...chatProps,
2636
+ theme: theme || chatProps.theme,
2637
+ primaryColor: accent,
2638
+ autoConnect: isOpen,
2639
+ onMessage: handleMessage,
2640
+ onMinimize: close
2641
+ }
2642
+ )
1061
2643
  }
1062
- }
1063
- ),
1064
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1065
- "button",
1066
- {
1067
- onClick: handleToggle,
1068
- style: {
1069
- position: "absolute",
1070
- top: "16px",
1071
- right: "16px",
1072
- width: "32px",
1073
- height: "32px",
1074
- borderRadius: "50%",
1075
- backgroundColor: "rgba(255, 255, 255, 0.2)",
1076
- border: "none",
1077
- cursor: "pointer",
1078
- display: "flex",
1079
- alignItems: "center",
1080
- justifyContent: "center",
1081
- color: "white",
1082
- transition: "background-color 0.2s",
1083
- zIndex: 10
1084
- },
1085
- onMouseEnter: (e) => {
1086
- e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
1087
- },
1088
- onMouseLeave: (e) => {
1089
- e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.2)";
1090
- },
1091
- "aria-label": "Close chat",
1092
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1093
- "svg",
1094
- {
1095
- width: "16",
1096
- height: "16",
1097
- viewBox: "0 0 24 24",
1098
- fill: "none",
1099
- stroke: "currentColor",
1100
- strokeWidth: "2",
1101
- strokeLinecap: "round",
1102
- strokeLinejoin: "round",
1103
- children: [
1104
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1105
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1106
- ]
1107
- }
1108
- )
1109
- }
1110
- )
1111
- ] })
1112
- ] });
2644
+ )
2645
+ ]
2646
+ }
2647
+ ) }) });
2648
+ return typeof document !== "undefined" ? (0, import_react_dom.createPortal)(widget, document.body) : widget;
1113
2649
  }
2650
+
2651
+ // src/index.ts
2652
+ var import_core4 = require("@channlize/core");
1114
2653
  // Annotate the CommonJS export names for ESM import in node:
1115
2654
  0 && (module.exports = {
2655
+ AuthenticationError,
1116
2656
  ChannlizeChat,
2657
+ ChannlizeError,
1117
2658
  ChannlizeProvider,
1118
2659
  ContactForm,
2660
+ RateLimitError,
1119
2661
  SupportChat,
2662
+ ValidationError,
2663
+ defaultContactFormLabels,
2664
+ defaultSupportChatLabels,
1120
2665
  useChannlize,
1121
2666
  useChannlizeAnalytics,
1122
2667
  useChatSession,