@sesionario/blocks 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  "use client";
2
2
  import * as React6 from 'react';
3
- import { createContext, useContext, useState, useCallback, useEffect, useRef, useMemo } from 'react';
4
- import { ChevronsUpDown, CheckIcon, Smile, SearchIcon, Search, Clock, Flag, Hash, Lightbulb, Trophy, Plane, Apple, Leaf, Hand, Loader2, AlertTriangle, Inbox, Monitor, Sun, Moon, Building2, Check, Plus, Languages, Upload, Move, ZoomOut, ZoomIn, RotateCcw, RotateCw, Grid3X3, RefreshCw, X, User, Pencil, Play, AlertCircle, VolumeX, Pause, Volume1, Volume2, Settings, PictureInPicture2, Minimize, Maximize, BadgeCheck, ArrowDownIcon, MessageCircleDashedIcon, XIcon } from 'lucide-react';
3
+ import { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react';
4
+ import { ChevronsUpDown, CheckIcon, Smile, SearchIcon, Search, Clock, Flag, Hash, Lightbulb, Trophy, Plane, Apple, Leaf, Hand, Loader2, AlertTriangle, Inbox, X, ChevronLeft, ChevronRight, Monitor, Sun, Moon, Building2, Check, Plus, Languages, Upload, Move, ZoomOut, ZoomIn, RotateCcw, RotateCw, Grid3X3, RefreshCw, User, Pencil, Play, AlertCircle, VolumeX, Pause, Volume1, Volume2, Settings, PictureInPicture2, Minimize, Maximize, BadgeCheck, ArrowDownIcon, MessageCircleDashedIcon, MessageCircle, XIcon } from 'lucide-react';
5
5
  import { clsx } from 'clsx';
6
6
  import { twMerge } from 'tailwind-merge';
7
7
  import { cva } from 'class-variance-authority';
8
8
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
9
  import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
10
10
  import { Slot } from '@radix-ui/react-slot';
11
+ import { createPortal } from 'react-dom';
11
12
  import { useTheme } from 'next-themes';
12
13
  import { useReducedMotion, AnimatePresence, motion } from 'framer-motion';
13
14
  import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
@@ -424,6 +425,301 @@ function ConfirmDestructiveDialog({
424
425
  ] })
425
426
  ] }) });
426
427
  }
428
+ var MIN_SCALE = 1;
429
+ var MAX_SCALE = 4;
430
+ var DOUBLE_TAP_MS = 300;
431
+ var IDENTITY = { scale: 1, tx: 0, ty: 0 };
432
+ function clamp(value, min, max) {
433
+ return Math.min(max, Math.max(min, value));
434
+ }
435
+ function zoomToward(prev, nextScale, originX, originY) {
436
+ const scale = clamp(nextScale, MIN_SCALE, MAX_SCALE);
437
+ if (scale === MIN_SCALE) return IDENTITY;
438
+ const ratio = scale / prev.scale;
439
+ return {
440
+ scale,
441
+ tx: originX - (originX - prev.tx) * ratio,
442
+ ty: originY - (originY - prev.ty) * ratio
443
+ };
444
+ }
445
+ function pointerDistance(a, b) {
446
+ return Math.hypot(b.x - a.x, b.y - a.y);
447
+ }
448
+ function ImageLightbox({
449
+ images,
450
+ index,
451
+ onIndexChange,
452
+ onClose,
453
+ labels
454
+ }) {
455
+ const ariaLabel = labels?.ariaLabel ?? "Visor de im\xE1genes";
456
+ const closeLabel = labels?.close ?? "Cerrar";
457
+ const previousLabel = labels?.previous ?? "Anterior";
458
+ const nextLabel = labels?.next ?? "Siguiente";
459
+ const hasMultiple = images.length > 1;
460
+ const [transform, setTransform] = useState(IDENTITY);
461
+ const transformRef = useRef(transform);
462
+ transformRef.current = transform;
463
+ const stageRef = useRef(null);
464
+ const pointersRef = useRef(/* @__PURE__ */ new Map());
465
+ const pinchStartRef = useRef(null);
466
+ const panRef = useRef(null);
467
+ const lastTapRef = useRef(
468
+ null
469
+ );
470
+ const resetZoom = useCallback(() => {
471
+ setTransform(IDENTITY);
472
+ }, []);
473
+ useEffect(() => {
474
+ resetZoom();
475
+ }, [index, images[index], resetZoom]);
476
+ function goPrev() {
477
+ onIndexChange((index - 1 + images.length) % images.length);
478
+ }
479
+ function goNext() {
480
+ onIndexChange((index + 1) % images.length);
481
+ }
482
+ useEffect(() => {
483
+ function handleKeyDown(e) {
484
+ if (e.key === "Escape") {
485
+ onClose();
486
+ return;
487
+ }
488
+ if (!hasMultiple) return;
489
+ if (e.key === "ArrowLeft") {
490
+ onIndexChange((index - 1 + images.length) % images.length);
491
+ } else if (e.key === "ArrowRight") {
492
+ onIndexChange((index + 1) % images.length);
493
+ }
494
+ }
495
+ window.addEventListener("keydown", handleKeyDown);
496
+ return () => window.removeEventListener("keydown", handleKeyDown);
497
+ }, [index, images.length, hasMultiple, onClose, onIndexChange]);
498
+ useEffect(() => {
499
+ const previousOverflow = document.body.style.overflow;
500
+ document.body.style.overflow = "hidden";
501
+ return () => {
502
+ document.body.style.overflow = previousOverflow;
503
+ };
504
+ }, []);
505
+ const stageOrigin = useCallback((clientX, clientY) => {
506
+ const stage = stageRef.current;
507
+ if (!stage) return { x: 0, y: 0 };
508
+ const rect = stage.getBoundingClientRect();
509
+ return {
510
+ x: clientX - rect.left - rect.width / 2,
511
+ y: clientY - rect.top - rect.height / 2
512
+ };
513
+ }, []);
514
+ useEffect(() => {
515
+ const stage = stageRef.current;
516
+ if (!stage) return;
517
+ const handleWheel = (e) => {
518
+ e.preventDefault();
519
+ e.stopPropagation();
520
+ const prev = transformRef.current;
521
+ const factor = e.deltaY > 0 ? 0.9 : 1.1;
522
+ const { x, y } = stageOrigin(e.clientX, e.clientY);
523
+ setTransform(zoomToward(prev, prev.scale * factor, x, y));
524
+ };
525
+ stage.addEventListener("wheel", handleWheel, { passive: false });
526
+ return () => stage.removeEventListener("wheel", handleWheel);
527
+ }, [stageOrigin]);
528
+ function beginPinch() {
529
+ const pts = [...pointersRef.current.values()];
530
+ const [a, b] = pts;
531
+ if (!a || !b) return;
532
+ const midX = (a.x + b.x) / 2;
533
+ const midY = (a.y + b.y) / 2;
534
+ const { x, y } = stageOrigin(midX, midY);
535
+ pinchStartRef.current = {
536
+ distance: Math.max(pointerDistance(a, b), 1),
537
+ originX: x,
538
+ originY: y,
539
+ transform: { ...transformRef.current }
540
+ };
541
+ panRef.current = null;
542
+ }
543
+ function onPointerDown(e) {
544
+ e.stopPropagation();
545
+ const stage = stageRef.current;
546
+ if (!stage) return;
547
+ stage.setPointerCapture(e.pointerId);
548
+ pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
549
+ if (pointersRef.current.size === 2) {
550
+ beginPinch();
551
+ return;
552
+ }
553
+ if (transformRef.current.scale > MIN_SCALE) {
554
+ panRef.current = {
555
+ pointerId: e.pointerId,
556
+ startX: e.clientX,
557
+ startY: e.clientY,
558
+ originTx: transformRef.current.tx,
559
+ originTy: transformRef.current.ty,
560
+ moved: false
561
+ };
562
+ }
563
+ }
564
+ function onPointerMove(e) {
565
+ if (!pointersRef.current.has(e.pointerId)) return;
566
+ pointersRef.current.set(e.pointerId, { x: e.clientX, y: e.clientY });
567
+ const pinchStart = pinchStartRef.current;
568
+ if (pointersRef.current.size >= 2 && pinchStart) {
569
+ const pts = [...pointersRef.current.values()];
570
+ const [a, b] = pts;
571
+ if (!a || !b) return;
572
+ const distance = Math.max(pointerDistance(a, b), 1);
573
+ const nextScale = pinchStart.transform.scale * (distance / pinchStart.distance);
574
+ setTransform(
575
+ zoomToward(
576
+ pinchStart.transform,
577
+ nextScale,
578
+ pinchStart.originX,
579
+ pinchStart.originY
580
+ )
581
+ );
582
+ return;
583
+ }
584
+ const pan = panRef.current;
585
+ if (pan && pan.pointerId === e.pointerId) {
586
+ const dx = e.clientX - pan.startX;
587
+ const dy = e.clientY - pan.startY;
588
+ if (Math.abs(dx) > 2 || Math.abs(dy) > 2) pan.moved = true;
589
+ setTransform((prev) => ({
590
+ ...prev,
591
+ tx: pan.originTx + dx,
592
+ ty: pan.originTy + dy
593
+ }));
594
+ }
595
+ }
596
+ function onPointerUp(e) {
597
+ const stage = stageRef.current;
598
+ if (stage?.hasPointerCapture(e.pointerId)) {
599
+ stage.releasePointerCapture(e.pointerId);
600
+ }
601
+ pointersRef.current.delete(e.pointerId);
602
+ if (pointersRef.current.size < 2) {
603
+ pinchStartRef.current = null;
604
+ }
605
+ if (pointersRef.current.size === 1) {
606
+ const remaining = [...pointersRef.current.entries()][0];
607
+ if (remaining && transformRef.current.scale > MIN_SCALE) {
608
+ const [pointerId, point] = remaining;
609
+ panRef.current = {
610
+ pointerId,
611
+ startX: point.x,
612
+ startY: point.y,
613
+ originTx: transformRef.current.tx,
614
+ originTy: transformRef.current.ty,
615
+ moved: true
616
+ };
617
+ }
618
+ }
619
+ const pan = panRef.current;
620
+ if (pan && pan.pointerId === e.pointerId) {
621
+ const wasMoved = pan.moved;
622
+ panRef.current = null;
623
+ if (wasMoved) return;
624
+ }
625
+ if (pointersRef.current.size === 0 && e.button === 0) {
626
+ const now = Date.now();
627
+ const last = lastTapRef.current;
628
+ if (last && now - last.time < DOUBLE_TAP_MS && Math.hypot(e.clientX - last.x, e.clientY - last.y) < 24) {
629
+ lastTapRef.current = null;
630
+ resetZoom();
631
+ return;
632
+ }
633
+ lastTapRef.current = { time: now, x: e.clientX, y: e.clientY };
634
+ }
635
+ }
636
+ function stopDialogClose(e) {
637
+ e.stopPropagation();
638
+ }
639
+ const { scale, tx, ty } = transform;
640
+ return createPortal(
641
+ /* @__PURE__ */ jsxs(
642
+ "div",
643
+ {
644
+ className: "fixed inset-x-0 top-0 z-50 flex h-dvh items-center justify-center bg-black/90 p-4",
645
+ onClick: onClose,
646
+ role: "dialog",
647
+ "aria-modal": "true",
648
+ "aria-label": ariaLabel,
649
+ children: [
650
+ /* @__PURE__ */ jsx(
651
+ "button",
652
+ {
653
+ type: "button",
654
+ onClick: onClose,
655
+ "aria-label": closeLabel,
656
+ className: "absolute right-4 top-4 z-10 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70",
657
+ children: /* @__PURE__ */ jsx(X, { className: "h-5 w-5" })
658
+ }
659
+ ),
660
+ hasMultiple && /* @__PURE__ */ jsx(
661
+ "button",
662
+ {
663
+ type: "button",
664
+ onClick: (e) => {
665
+ e.stopPropagation();
666
+ goPrev();
667
+ },
668
+ "aria-label": previousLabel,
669
+ className: "absolute left-4 top-1/2 z-10 -translate-y-1/2 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70",
670
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "h-6 w-6" })
671
+ }
672
+ ),
673
+ /* @__PURE__ */ jsx(
674
+ "div",
675
+ {
676
+ ref: stageRef,
677
+ className: "relative flex max-h-full max-w-full touch-none items-center justify-center",
678
+ "data-scale": scale,
679
+ onClick: stopDialogClose,
680
+ onPointerDown,
681
+ onPointerMove,
682
+ onPointerUp,
683
+ onPointerCancel: onPointerUp,
684
+ children: /* @__PURE__ */ jsx(
685
+ "img",
686
+ {
687
+ src: images[index],
688
+ alt: "",
689
+ draggable: false,
690
+ className: "max-h-[calc(100dvh-2rem)] max-w-full select-none rounded-lg object-contain",
691
+ style: {
692
+ transform: `translate(${tx}px, ${ty}px) scale(${scale})`,
693
+ cursor: scale > MIN_SCALE ? "grab" : "default"
694
+ }
695
+ }
696
+ )
697
+ }
698
+ ),
699
+ hasMultiple && /* @__PURE__ */ jsx(
700
+ "button",
701
+ {
702
+ type: "button",
703
+ onClick: (e) => {
704
+ e.stopPropagation();
705
+ goNext();
706
+ },
707
+ "aria-label": nextLabel,
708
+ className: "absolute right-4 top-1/2 z-10 -translate-y-1/2 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70",
709
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "h-6 w-6" })
710
+ }
711
+ ),
712
+ hasMultiple && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-4 left-1/2 z-10 -translate-x-1/2 rounded-full bg-black/50 px-3 py-1 text-xs text-white", children: [
713
+ index + 1,
714
+ " / ",
715
+ images.length
716
+ ] })
717
+ ]
718
+ }
719
+ ),
720
+ document.body
721
+ );
722
+ }
427
723
  var miniMenuContentClassName = "max-h-none overflow-x-visible overflow-y-visible";
428
724
  var miniMenuContentDefaults = {
429
725
  sideOffset: 8,
@@ -5040,7 +5336,636 @@ function ChatMessageList({
5040
5336
  /* @__PURE__ */ jsx(MessageScrollerButton, {})
5041
5337
  ] }) });
5042
5338
  }
5339
+ function useMediaQuery2(query) {
5340
+ const [matches, setMatches] = useState(false);
5341
+ useEffect(() => {
5342
+ if (typeof window === "undefined" || !window.matchMedia) return;
5343
+ const mql = window.matchMedia(query);
5344
+ const onChange = () => setMatches(mql.matches);
5345
+ onChange();
5346
+ mql.addEventListener("change", onChange);
5347
+ return () => mql.removeEventListener("change", onChange);
5348
+ }, [query]);
5349
+ return matches;
5350
+ }
5351
+ var RECONNECT_DELAY_MS = 2e3;
5352
+ function useSupportConversation({
5353
+ client,
5354
+ getWsUrl,
5355
+ conversationId: conversationIdProp,
5356
+ open = null,
5357
+ enabled = true,
5358
+ onConversationReady,
5359
+ onIncomingMessage
5360
+ }) {
5361
+ const [conversationId, setConversationId] = useState(
5362
+ conversationIdProp
5363
+ );
5364
+ const [messages, setMessages] = useState([]);
5365
+ const [loading, setLoading] = useState(false);
5366
+ const [sending, setSending] = useState(false);
5367
+ const [error, setError] = useState(null);
5368
+ const socketRef = useRef(null);
5369
+ const reconnectRef = useRef(null);
5370
+ const closedByUser = useRef(false);
5371
+ const onReadyRef = useRef(onConversationReady);
5372
+ const onIncomingRef = useRef(onIncomingMessage);
5373
+ onReadyRef.current = onConversationReady;
5374
+ onIncomingRef.current = onIncomingMessage;
5375
+ const load = useCallback(async () => {
5376
+ if (!enabled) return;
5377
+ setLoading(true);
5378
+ setError(null);
5379
+ try {
5380
+ let id = conversationIdProp;
5381
+ if (!id && open) {
5382
+ const conv = await client.findOrCreateConversation(open);
5383
+ id = conv.id;
5384
+ }
5385
+ if (!id) {
5386
+ setConversationId(null);
5387
+ setMessages([]);
5388
+ return;
5389
+ }
5390
+ setConversationId(id);
5391
+ onReadyRef.current?.(id);
5392
+ const msgs = await client.listMessages(id);
5393
+ setMessages(msgs);
5394
+ await client.markRead(id);
5395
+ } catch (err) {
5396
+ setError(err instanceof Error ? err.message : "Chat load failed");
5397
+ } finally {
5398
+ setLoading(false);
5399
+ }
5400
+ }, [client, conversationIdProp, enabled, open]);
5401
+ useEffect(() => {
5402
+ void load();
5403
+ }, [load]);
5404
+ useEffect(() => {
5405
+ closedByUser.current = false;
5406
+ if (!enabled || !conversationId) return;
5407
+ let cancelled = false;
5408
+ const connect = async () => {
5409
+ const url = await getWsUrl(conversationId);
5410
+ if (!url || cancelled || closedByUser.current) return;
5411
+ const socket = new WebSocket(url);
5412
+ socketRef.current = socket;
5413
+ socket.onmessage = (event) => {
5414
+ let data;
5415
+ try {
5416
+ data = JSON.parse(String(event.data));
5417
+ } catch {
5418
+ return;
5419
+ }
5420
+ if (data.type === "message") {
5421
+ setMessages((prev) => {
5422
+ if (prev.some((m) => m.id === data.id)) return prev;
5423
+ return [...prev, data];
5424
+ });
5425
+ onIncomingRef.current?.(data);
5426
+ } else if (data.type === "deleted") {
5427
+ setMessages(
5428
+ (prev) => prev.map(
5429
+ (m) => m.id === data.id ? { ...m, deleted: true, body: "" } : m
5430
+ )
5431
+ );
5432
+ }
5433
+ };
5434
+ socket.onclose = () => {
5435
+ if (cancelled || closedByUser.current) return;
5436
+ reconnectRef.current = setTimeout(() => {
5437
+ void connect();
5438
+ }, RECONNECT_DELAY_MS);
5439
+ };
5440
+ };
5441
+ void connect();
5442
+ return () => {
5443
+ cancelled = true;
5444
+ closedByUser.current = true;
5445
+ if (reconnectRef.current) clearTimeout(reconnectRef.current);
5446
+ socketRef.current?.close();
5447
+ socketRef.current = null;
5448
+ };
5449
+ }, [conversationId, enabled, getWsUrl]);
5450
+ const send = useCallback(
5451
+ async (text) => {
5452
+ const trimmed = text.trim();
5453
+ if (!conversationId || !trimmed || sending) return;
5454
+ setSending(true);
5455
+ setError(null);
5456
+ try {
5457
+ const msg = await client.sendMessage(conversationId, trimmed);
5458
+ setMessages((prev) => {
5459
+ if (prev.some((m) => m.id === msg.id)) return prev;
5460
+ return [...prev, msg];
5461
+ });
5462
+ await client.markRead(conversationId);
5463
+ } catch (err) {
5464
+ setError(err instanceof Error ? err.message : "Send failed");
5465
+ throw err;
5466
+ } finally {
5467
+ setSending(false);
5468
+ }
5469
+ },
5470
+ [client, conversationId, sending]
5471
+ );
5472
+ return {
5473
+ conversationId,
5474
+ messages,
5475
+ loading,
5476
+ sending,
5477
+ error,
5478
+ send,
5479
+ reload: load
5480
+ };
5481
+ }
5482
+ var SupportMessengerContext = createContext(null);
5483
+ function useSupportMessenger() {
5484
+ const ctx = useContext(SupportMessengerContext);
5485
+ if (!ctx) {
5486
+ throw new Error(
5487
+ "useSupportMessenger must be used within SupportMessengerProvider"
5488
+ );
5489
+ }
5490
+ return ctx;
5491
+ }
5492
+ function useSupportMessengerOptional() {
5493
+ return useContext(SupportMessengerContext);
5494
+ }
5495
+ function ThreadPanel({
5496
+ thread,
5497
+ client,
5498
+ getWsUrl,
5499
+ currentUserId,
5500
+ labels,
5501
+ emojiLocale,
5502
+ onClose,
5503
+ onBack,
5504
+ showBack,
5505
+ onConversationReady,
5506
+ className
5507
+ }) {
5508
+ const [draft, setDraft] = useState("");
5509
+ const { messages, loading, sending, error, send } = useSupportConversation({
5510
+ client,
5511
+ getWsUrl,
5512
+ conversationId: thread.conversationId ?? null,
5513
+ open: thread.open ?? null,
5514
+ onConversationReady
5515
+ });
5516
+ async function onSubmit(e) {
5517
+ e.preventDefault();
5518
+ const text = draft.trim();
5519
+ if (!text) return;
5520
+ setDraft("");
5521
+ try {
5522
+ await send(text);
5523
+ } catch {
5524
+ setDraft(text);
5525
+ }
5526
+ }
5527
+ return /* @__PURE__ */ jsxs(
5528
+ "div",
5529
+ {
5530
+ className: cn(
5531
+ "flex h-full min-h-0 flex-col overflow-hidden rounded-xl border bg-card shadow-lg",
5532
+ className
5533
+ ),
5534
+ children: [
5535
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 border-b px-2 py-2", children: [
5536
+ showBack && onBack ? /* @__PURE__ */ jsx(
5537
+ Button,
5538
+ {
5539
+ type: "button",
5540
+ variant: "ghost",
5541
+ size: "icon-sm",
5542
+ onClick: onBack,
5543
+ "aria-label": labels.back,
5544
+ children: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
5545
+ }
5546
+ ) : null,
5547
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 px-1", children: [
5548
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold", children: thread.title }),
5549
+ thread.subtitle ? /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: thread.subtitle }) : null
5550
+ ] }),
5551
+ /* @__PURE__ */ jsx(
5552
+ Button,
5553
+ {
5554
+ type: "button",
5555
+ variant: "ghost",
5556
+ size: "icon-sm",
5557
+ onClick: onClose,
5558
+ "aria-label": labels.close,
5559
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
5560
+ }
5561
+ )
5562
+ ] }),
5563
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: loading ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center p-6", children: /* @__PURE__ */ jsx(LoadingState, { label: labels.loading, className: "py-0" }) }) : error ? /* @__PURE__ */ jsx("p", { className: "p-4 text-sm text-destructive", role: "alert", children: error }) : /* @__PURE__ */ jsx(
5564
+ ChatMessageList,
5565
+ {
5566
+ messages,
5567
+ currentUserId,
5568
+ emptyDescription: labels.emptyThread
5569
+ }
5570
+ ) }),
5571
+ /* @__PURE__ */ jsxs("form", { className: "flex gap-2 border-t p-3", onSubmit, children: [
5572
+ /* @__PURE__ */ jsx(
5573
+ EmojiTextarea,
5574
+ {
5575
+ locale: emojiLocale,
5576
+ value: draft,
5577
+ onChange: (e) => setDraft(e.target.value.slice(0, 2e3)),
5578
+ placeholder: labels.placeholder,
5579
+ "aria-label": labels.placeholder,
5580
+ disabled: loading || sending,
5581
+ maxLength: 2e3,
5582
+ rows: 2,
5583
+ className: "min-h-10 flex-1"
5584
+ }
5585
+ ),
5586
+ /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !draft.trim() || sending || loading, children: labels.send })
5587
+ ] })
5588
+ ]
5589
+ }
5590
+ );
5591
+ }
5592
+ function ListPanel({
5593
+ items,
5594
+ labels,
5595
+ tabs,
5596
+ activeTab,
5597
+ onTabChange,
5598
+ onSelect,
5599
+ onClose,
5600
+ searchThreshold,
5601
+ className
5602
+ }) {
5603
+ const [query, setQuery] = useState("");
5604
+ const showSearch = items.length >= searchThreshold;
5605
+ const filtered = useMemo(() => {
5606
+ const q = query.trim().toLowerCase();
5607
+ if (!q) return items;
5608
+ return items.filter(
5609
+ (item) => item.title.toLowerCase().includes(q) || (item.subtitle?.toLowerCase().includes(q) ?? false)
5610
+ );
5611
+ }, [items, query]);
5612
+ return /* @__PURE__ */ jsxs(
5613
+ "div",
5614
+ {
5615
+ className: cn(
5616
+ "flex h-full min-h-0 flex-col overflow-hidden rounded-xl border bg-card shadow-lg",
5617
+ className
5618
+ ),
5619
+ children: [
5620
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b px-3 py-2", children: [
5621
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold", children: labels.listTitle }),
5622
+ /* @__PURE__ */ jsx(
5623
+ Button,
5624
+ {
5625
+ type: "button",
5626
+ variant: "ghost",
5627
+ size: "icon-sm",
5628
+ onClick: onClose,
5629
+ "aria-label": labels.close,
5630
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
5631
+ }
5632
+ )
5633
+ ] }),
5634
+ tabs && tabs.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex gap-1 border-b p-2", children: tabs.map((tab) => /* @__PURE__ */ jsx(
5635
+ Button,
5636
+ {
5637
+ type: "button",
5638
+ size: "sm",
5639
+ variant: activeTab === tab.id ? "default" : "outline",
5640
+ onClick: () => onTabChange?.(tab.id),
5641
+ children: tab.label
5642
+ },
5643
+ tab.id
5644
+ )) }) : null,
5645
+ showSearch ? /* @__PURE__ */ jsx("div", { className: "border-b p-2", children: /* @__PURE__ */ jsxs("label", { className: "relative block", children: [
5646
+ /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground" }),
5647
+ /* @__PURE__ */ jsx(
5648
+ "input",
5649
+ {
5650
+ value: query,
5651
+ onChange: (e) => setQuery(e.target.value),
5652
+ placeholder: labels.searchPlaceholder ?? "Buscar\u2026",
5653
+ className: "h-8 w-full rounded-md border bg-transparent pr-2 pl-8 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
5654
+ }
5655
+ )
5656
+ ] }) }) : null,
5657
+ /* @__PURE__ */ jsx("ul", { className: "min-h-0 flex-1 overflow-y-auto p-1", children: filtered.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-3 py-8 text-center text-sm text-muted-foreground", children: labels.emptyList }) : filtered.map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
5658
+ "button",
5659
+ {
5660
+ type: "button",
5661
+ className: "flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left transition-colors hover:bg-muted/80",
5662
+ onClick: () => onSelect(item),
5663
+ children: [
5664
+ /* @__PURE__ */ jsx(
5665
+ "span",
5666
+ {
5667
+ className: "flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-full text-xs font-semibold text-white",
5668
+ style: item.avatarUrl ? void 0 : avatarFallbackStyle(item.title),
5669
+ children: item.avatarUrl ? /* @__PURE__ */ jsx(
5670
+ "img",
5671
+ {
5672
+ src: item.avatarUrl,
5673
+ alt: "",
5674
+ className: "size-full object-cover"
5675
+ }
5676
+ ) : getInitials(item.title)
5677
+ }
5678
+ ),
5679
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
5680
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-sm font-medium", children: item.title }),
5681
+ item.subtitle ? /* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-muted-foreground", children: item.subtitle }) : null
5682
+ ] }),
5683
+ (item.unreadCount ?? 0) > 0 ? /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold text-primary-foreground", children: (item.unreadCount ?? 0) > 99 ? "99+" : item.unreadCount }) : null
5684
+ ]
5685
+ }
5686
+ ) }, item.key)) })
5687
+ ]
5688
+ }
5689
+ );
5690
+ }
5691
+ function SupportMessengerProvider({
5692
+ children,
5693
+ client,
5694
+ getWsUrl,
5695
+ currentUserId,
5696
+ items,
5697
+ labels,
5698
+ enabled = true,
5699
+ forceMinimized = false,
5700
+ emojiLocale = "es",
5701
+ tabs,
5702
+ activeTab,
5703
+ onTabChange,
5704
+ onRefreshItems,
5705
+ searchThreshold = 8,
5706
+ className
5707
+ }) {
5708
+ const isMobile = useMediaQuery2("(max-width: 767px)");
5709
+ const [listOpen, setListOpen] = useState(false);
5710
+ const [threads, setThreads] = useState([]);
5711
+ const [mobileThreadKey, setMobileThreadKey] = useState(null);
5712
+ const totalUnread = useMemo(
5713
+ () => items.reduce((sum, item) => sum + (item.unreadCount ?? 0), 0),
5714
+ [items]
5715
+ );
5716
+ useEffect(() => {
5717
+ if (!forceMinimized) return;
5718
+ setListOpen(false);
5719
+ setThreads([]);
5720
+ setMobileThreadKey(null);
5721
+ }, [forceMinimized]);
5722
+ const openList = useCallback(() => {
5723
+ setListOpen(true);
5724
+ void onRefreshItems?.();
5725
+ }, [onRefreshItems]);
5726
+ const closeList = useCallback(() => setListOpen(false), []);
5727
+ const toggleList = useCallback(() => {
5728
+ setListOpen((prev) => {
5729
+ const next = !prev;
5730
+ if (next) void onRefreshItems?.();
5731
+ return next;
5732
+ });
5733
+ }, [onRefreshItems]);
5734
+ const openThread = useCallback((req) => {
5735
+ setListOpen(false);
5736
+ setThreads((prev) => {
5737
+ const existing = prev.find((t) => t.key === req.key);
5738
+ if (existing) {
5739
+ return [
5740
+ ...prev.filter((t) => t.key !== req.key),
5741
+ { ...existing, ...req }
5742
+ ];
5743
+ }
5744
+ const next = [...prev, req];
5745
+ return next.slice(-2);
5746
+ });
5747
+ setMobileThreadKey(req.key);
5748
+ }, []);
5749
+ const closeThread = useCallback((key) => {
5750
+ setThreads((prev) => prev.filter((t) => t.key !== key));
5751
+ setMobileThreadKey((prev) => prev === key ? null : prev);
5752
+ }, []);
5753
+ const closeAll = useCallback(() => {
5754
+ setListOpen(false);
5755
+ setThreads([]);
5756
+ setMobileThreadKey(null);
5757
+ }, []);
5758
+ const ctx = useMemo(
5759
+ () => ({
5760
+ listOpen,
5761
+ openList,
5762
+ closeList,
5763
+ toggleList,
5764
+ openThread,
5765
+ closeThread,
5766
+ closeAll,
5767
+ activeThreadKey: mobileThreadKey ?? threads[threads.length - 1]?.key ?? null,
5768
+ totalUnread
5769
+ }),
5770
+ [
5771
+ listOpen,
5772
+ openList,
5773
+ closeList,
5774
+ toggleList,
5775
+ openThread,
5776
+ closeThread,
5777
+ closeAll,
5778
+ mobileThreadKey,
5779
+ threads,
5780
+ totalUnread
5781
+ ]
5782
+ );
5783
+ const handleSelectItem = useCallback(
5784
+ (item) => {
5785
+ openThread({
5786
+ key: item.key,
5787
+ title: item.title,
5788
+ subtitle: item.subtitle,
5789
+ conversationId: item.conversationId,
5790
+ open: item.open
5791
+ });
5792
+ },
5793
+ [openThread]
5794
+ );
5795
+ const activeMobileThread = threads.find((t) => t.key === mobileThreadKey);
5796
+ return /* @__PURE__ */ jsxs(SupportMessengerContext.Provider, { value: ctx, children: [
5797
+ children,
5798
+ enabled ? /* @__PURE__ */ jsxs(
5799
+ "div",
5800
+ {
5801
+ className: cn(
5802
+ "pointer-events-none fixed right-4 bottom-4 z-50 flex flex-col items-end gap-3",
5803
+ className
5804
+ ),
5805
+ children: [
5806
+ !isMobile ? /* @__PURE__ */ jsxs("div", { className: "pointer-events-auto flex items-end gap-3", children: [
5807
+ threads.map((thread) => /* @__PURE__ */ jsx("div", { className: "h-[28rem] w-[22rem]", children: /* @__PURE__ */ jsx(
5808
+ ThreadPanel,
5809
+ {
5810
+ thread,
5811
+ client,
5812
+ getWsUrl,
5813
+ currentUserId,
5814
+ labels,
5815
+ emojiLocale,
5816
+ onClose: () => closeThread(thread.key),
5817
+ onConversationReady: () => void onRefreshItems?.()
5818
+ }
5819
+ ) }, thread.key)),
5820
+ listOpen ? /* @__PURE__ */ jsx("div", { className: "h-[28rem] w-[20rem]", children: /* @__PURE__ */ jsx(
5821
+ ListPanel,
5822
+ {
5823
+ items,
5824
+ labels,
5825
+ tabs,
5826
+ activeTab,
5827
+ onTabChange,
5828
+ onSelect: handleSelectItem,
5829
+ onClose: closeList,
5830
+ searchThreshold
5831
+ }
5832
+ ) }) : null
5833
+ ] }) : null,
5834
+ isMobile ? /* @__PURE__ */ jsx(
5835
+ Drawer,
5836
+ {
5837
+ open: listOpen || Boolean(activeMobileThread),
5838
+ onOpenChange: (open) => {
5839
+ if (!open) closeAll();
5840
+ },
5841
+ children: /* @__PURE__ */ jsx(DrawerContent, { className: "pointer-events-auto mx-auto flex h-[85vh] max-w-lg flex-col", children: activeMobileThread ? /* @__PURE__ */ jsxs(Fragment, { children: [
5842
+ /* @__PURE__ */ jsx(DrawerHeader, { className: "sr-only", children: /* @__PURE__ */ jsx(DrawerTitle, { children: activeMobileThread.title }) }),
5843
+ /* @__PURE__ */ jsx(
5844
+ ThreadPanel,
5845
+ {
5846
+ thread: activeMobileThread,
5847
+ client,
5848
+ getWsUrl,
5849
+ currentUserId,
5850
+ labels,
5851
+ emojiLocale,
5852
+ showBack: true,
5853
+ onBack: () => {
5854
+ closeThread(activeMobileThread.key);
5855
+ setListOpen(true);
5856
+ },
5857
+ onClose: closeAll,
5858
+ onConversationReady: () => void onRefreshItems?.(),
5859
+ className: "h-full rounded-none border-0 shadow-none"
5860
+ }
5861
+ )
5862
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
5863
+ /* @__PURE__ */ jsx(DrawerHeader, { className: "sr-only", children: /* @__PURE__ */ jsx(DrawerTitle, { children: labels.listTitle }) }),
5864
+ /* @__PURE__ */ jsx(
5865
+ ListPanel,
5866
+ {
5867
+ items,
5868
+ labels,
5869
+ tabs,
5870
+ activeTab,
5871
+ onTabChange,
5872
+ onSelect: handleSelectItem,
5873
+ onClose: closeAll,
5874
+ searchThreshold,
5875
+ className: "h-full rounded-none border-0 shadow-none"
5876
+ }
5877
+ )
5878
+ ] }) })
5879
+ }
5880
+ ) : null,
5881
+ /* @__PURE__ */ jsxs(
5882
+ "button",
5883
+ {
5884
+ type: "button",
5885
+ className: cn(
5886
+ "pointer-events-auto relative flex size-14 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg transition-[transform,opacity] motion-safe:hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none",
5887
+ listOpen && !forceMinimized && "ring-2 ring-ring ring-offset-2"
5888
+ ),
5889
+ "aria-label": totalUnread > 0 && labels.unreadBadgeAria ? `${labels.launcherAria}. ${labels.unreadBadgeAria.replace("{count}", String(totalUnread))}` : labels.launcherAria,
5890
+ onClick: () => {
5891
+ if (isMobile) {
5892
+ if (activeMobileThread) {
5893
+ closeAll();
5894
+ } else {
5895
+ toggleList();
5896
+ }
5897
+ return;
5898
+ }
5899
+ toggleList();
5900
+ },
5901
+ children: [
5902
+ /* @__PURE__ */ jsx(MessageCircle, { className: "size-6" }),
5903
+ totalUnread > 0 ? /* @__PURE__ */ jsx("span", { className: "absolute -top-0.5 -right-0.5 flex size-5 items-center justify-center rounded-full bg-destructive text-[10px] font-bold text-white", children: totalUnread > 99 ? "99+" : totalUnread }) : null
5904
+ ]
5905
+ }
5906
+ )
5907
+ ]
5908
+ }
5909
+ ) : null
5910
+ ] });
5911
+ }
5912
+
5913
+ // src/lib/support-chat.ts
5914
+ async function parseJson(res) {
5915
+ const body = await res.json();
5916
+ if (!res.ok) {
5917
+ throw new Error(
5918
+ typeof body === "object" && body && "error" in body && body.error ? String(body.error) : `Chat request failed (${res.status})`
5919
+ );
5920
+ }
5921
+ return body;
5922
+ }
5923
+ function createSupportChatClient(basePath = "/api/chat-svc") {
5924
+ const root = basePath.replace(/\/$/, "");
5925
+ return {
5926
+ basePath: root,
5927
+ async listConversations() {
5928
+ const res = await fetch(`${root}/me/conversations`);
5929
+ const body = await parseJson(res);
5930
+ return body.result ?? [];
5931
+ },
5932
+ async findOrCreateConversation(input) {
5933
+ const res = await fetch(`${root}/conversations`, {
5934
+ method: "POST",
5935
+ headers: { "content-type": "application/json" },
5936
+ body: JSON.stringify(input)
5937
+ });
5938
+ const body = await parseJson(res);
5939
+ return body.result;
5940
+ },
5941
+ async listMessages(conversationId) {
5942
+ const res = await fetch(
5943
+ `${root}/conversations/${encodeURIComponent(conversationId)}/messages`
5944
+ );
5945
+ const body = await parseJson(res);
5946
+ return body.result ?? [];
5947
+ },
5948
+ async sendMessage(conversationId, text) {
5949
+ const res = await fetch(
5950
+ `${root}/conversations/${encodeURIComponent(conversationId)}/messages`,
5951
+ {
5952
+ method: "POST",
5953
+ headers: { "content-type": "application/json" },
5954
+ body: JSON.stringify({ body: text })
5955
+ }
5956
+ );
5957
+ const body = await parseJson(res);
5958
+ return body.result;
5959
+ },
5960
+ async markRead(conversationId) {
5961
+ await fetch(
5962
+ `${root}/conversations/${encodeURIComponent(conversationId)}/read`,
5963
+ { method: "POST" }
5964
+ );
5965
+ }
5966
+ };
5967
+ }
5043
5968
 
5044
- export { AvatarEditor, AvatarEditorDialog, Bubble, BubbleContent, BubbleGroup, BubbleReactions, ChatMessageList, ConfirmDestructiveDialog, EmojiInput, EmojiPicker, EmojiPickerPopover, EmojiTextarea, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyState, EmptyTitle, ErrorState, LanguageContext, LanguageSwitcher, LoadingState, MEXICO_STATES, MEXICO_STATE_PATHS, Marker, MarkerContent, MarkerIcon, Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader, MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, MexicoMap, OrgSwitcher, PhoneInput, SearchCommand, ThemeSwitcher, VerifiedProfessionalBadge, VideoPlayer, avatarFallbackStyle, bubbleVariants, cn, createLiveHlsConfig, createVodHlsConfig, defaultLanguages, describePlaybackError, fuzzyMatch, getAvatarColors, getInitials, insertAtCaret, markerVariants, normalizeEmojiQuery, shouldRetryLiveEdge404, useDebouncedValue, useDebouncedValueStrict, useHlsPlayback, useLanguageContext, usePlaybackStats, useSearchHotkey };
5969
+ export { AvatarEditor, AvatarEditorDialog, Bubble, BubbleContent, BubbleGroup, BubbleReactions, ChatMessageList, ConfirmDestructiveDialog, EmojiInput, EmojiPicker, EmojiPickerPopover, EmojiTextarea, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyState, EmptyTitle, ErrorState, ImageLightbox, LanguageContext, LanguageSwitcher, LoadingState, MEXICO_STATES, MEXICO_STATE_PATHS, Marker, MarkerContent, MarkerIcon, Message, MessageAvatar, MessageContent, MessageFooter, MessageGroup, MessageHeader, MessageScroller, MessageScrollerButton, MessageScrollerContent, MessageScrollerItem, MessageScrollerProvider, MessageScrollerViewport, MexicoMap, OrgSwitcher, PhoneInput, SearchCommand, SupportMessengerProvider, ThemeSwitcher, VerifiedProfessionalBadge, VideoPlayer, avatarFallbackStyle, bubbleVariants, cn, createLiveHlsConfig, createSupportChatClient, createVodHlsConfig, defaultLanguages, describePlaybackError, fuzzyMatch, getAvatarColors, getInitials, insertAtCaret, markerVariants, normalizeEmojiQuery, shouldRetryLiveEdge404, useDebouncedValue, useDebouncedValueStrict, useHlsPlayback, useLanguageContext, usePlaybackStats, useSearchHotkey, useSupportConversation, useSupportMessenger, useSupportMessengerOptional };
5045
5970
  //# sourceMappingURL=index.js.map
5046
5971
  //# sourceMappingURL=index.js.map