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