@channlize/react 0.1.1 → 0.3.0

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