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