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