@banbox/chat 1.0.8 → 1.0.9

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.
Files changed (42) hide show
  1. package/dist/index.cjs +1166 -202
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +11 -1
  4. package/dist/index.d.ts +11 -1
  5. package/dist/index.js +1090 -127
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/chat/InboxPopup.tsx +30 -19
  9. package/src/chat/SinglePopup.tsx +59 -14
  10. package/src/icons/index.tsx +55 -0
  11. package/src/index.ts +14 -12
  12. package/src/modals/ChatAddressModal.tsx +844 -0
  13. package/src/modals/{chat/ChatConfirmModal.tsx → ChatConfirmModal.tsx} +2 -2
  14. package/src/modals/ChatTranslateSettingsModal.tsx +182 -0
  15. package/src/styles/index.build.css +15 -0
  16. package/src/styles/index.css +10 -2
  17. package/src/ui/{chat/AttachmentPreviewStrip.tsx → AttachmentPreviewStrip.tsx} +2 -2
  18. package/src/ui/{chat/ChatComposerBar.tsx → ChatComposerBar.tsx} +2 -2
  19. package/src/ui/{chat/ChatFooter.tsx → ChatFooter.tsx} +102 -8
  20. package/src/ui/{chat/ChatIdentity.tsx → ChatIdentity.tsx} +2 -2
  21. package/src/ui/{chat/ChatInquiryBar.tsx → ChatInquiryBar.tsx} +1 -1
  22. package/src/ui/ChatKebabMenu.tsx +125 -0
  23. package/src/ui/{chat/ChatListHeader.tsx → ChatListHeader.tsx} +1 -1
  24. package/src/ui/{chat/ChatMessageItem.tsx → ChatMessageItem.tsx} +1 -1
  25. package/src/ui/{chat/ChatScroll.tsx → ChatScroll.tsx} +1 -1
  26. package/src/ui/{chat/ChatSpinner.tsx → ChatSpinner.tsx} +1 -1
  27. package/src/ui/{chat/ChatThreadItem.tsx → ChatThreadItem.tsx} +1 -1
  28. package/src/ui/{chat/MessageHoverActions.tsx → MessageHoverActions.tsx} +2 -2
  29. package/src/ui/{chat/ReplyCard.tsx → ReplyCard.tsx} +2 -2
  30. package/src/ui/{chat/TypingIndicator.tsx → TypingIndicator.tsx} +1 -1
  31. package/src/ui/{chat/drop-up → drop-up}/BusinessCardDropup.tsx +15 -3
  32. package/src/ui/{chat/drop-up → drop-up}/EmojiDropup.tsx +1 -1
  33. package/src/ui/{chat/message-items → message-items}/ChatAddressCard.tsx +4 -4
  34. package/src/ui/{chat/message-items → message-items}/ChatBubbleFiles.tsx +1 -1
  35. package/src/ui/{chat/message-items → message-items}/ChatBubbleImages.tsx +2 -2
  36. package/src/ui/{chat/message-items → message-items}/ChatBusinessCard.tsx +1 -1
  37. package/src/ui/{chat/scrollToMessage.ts → scrollToMessage.ts} +1 -1
  38. package/src/ui/{chat/types.ts → types.ts} +2 -2
  39. package/src/modals/chat/ChatTranslateSettingsModal.tsx +0 -180
  40. /package/src/ui/{chat/ChatHeader.tsx → ChatHeader.tsx} +0 -0
  41. /package/src/ui/{chat/message-items → message-items}/ChatBubbleAudio.tsx +0 -0
  42. /package/src/ui/{chat/message-items → message-items}/ChatBubbleText.tsx +0 -0
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { cn } from "../../utils/cn";
3
- import Button from "../../ui/Button";
2
+ import { cn } from "../utils/cn";
3
+ import Button from "../ui/Button";
4
4
 
5
5
  type Props = {
6
6
  open: boolean;
@@ -0,0 +1,182 @@
1
+ // modals/ChatTranslateSettingsModal.tsx
2
+ "use client";
3
+
4
+ import clsx from "clsx";
5
+ import React, { useState } from "react";
6
+
7
+ import { ChatInfoIcon } from "../icons";
8
+ import Button from "../ui/Button";
9
+ import Select from "../ui/Select";
10
+
11
+ /* ───────── Types ───────── */
12
+
13
+ export type TranslateSettings = {
14
+ incomingTarget?: string;
15
+ autoIncoming: boolean;
16
+ enableOutgoing: boolean;
17
+ outgoingFrom: string;
18
+ outgoingTo: string;
19
+ };
20
+
21
+ type Props = {
22
+ open: boolean;
23
+ onClose: () => void;
24
+ onSave: (settings: TranslateSettings) => void;
25
+ initial?: Partial<TranslateSettings>;
26
+ className?: string;
27
+ /** Controls layout / placement */
28
+ variant?: "single" | "group";
29
+ };
30
+
31
+ /* Language options */
32
+ const LANG_OPTIONS = [
33
+ "English",
34
+ "Bangla",
35
+ "Arabic",
36
+ "Chinese",
37
+ "French",
38
+ "German",
39
+ "Hindi",
40
+ "Italian",
41
+ "Japanese",
42
+ "Korean",
43
+ "Portuguese",
44
+ "Russian",
45
+ "Spanish",
46
+ "Turkish",
47
+ "Urdu",
48
+ ].map((l) => ({ label: l, value: l }));
49
+
50
+ const ChatTranslateSettingsModal: React.FC<Props> = ({
51
+ open,
52
+ onClose,
53
+ onSave,
54
+ initial,
55
+ className,
56
+ variant = "group",
57
+ }) => {
58
+ const [incomingTarget, setIncomingTarget] = useState(
59
+ initial?.incomingTarget ?? "",
60
+ );
61
+
62
+ // ESC to close
63
+ React.useEffect(() => {
64
+ if (!open) return;
65
+ const onKey = (e: KeyboardEvent) => {
66
+ if (e.key === "Escape") onClose();
67
+ };
68
+ window.addEventListener("keydown", onKey);
69
+ return () => window.removeEventListener("keydown", onKey);
70
+ }, [open, onClose]);
71
+
72
+ if (!open) return null;
73
+
74
+ const handleSave = () => {
75
+ onSave({
76
+ incomingTarget,
77
+ autoIncoming: true,
78
+ enableOutgoing: false,
79
+ outgoingFrom: "English",
80
+ outgoingTo: "Bangla",
81
+ });
82
+ };
83
+
84
+ // Use absolute inset-0 to overlay the nearest positioned ancestor (the chat panel).
85
+ // This works for both SinglePopup and InboxPopup because the parent containers
86
+ // already establish a stacking context.
87
+ // No createPortal needed — the parent's `position: relative` is the containing block.
88
+ return (
89
+ <div
90
+ className={clsx(
91
+ "absolute inset-0 z-[9999] flex items-center justify-center",
92
+ variant === "single" && "rounded-[inherit]",
93
+ )}
94
+ onClick={(e) => {
95
+ e.stopPropagation();
96
+ onClose();
97
+ }}
98
+ >
99
+ {/* Semi-transparent backdrop over the chat panel */}
100
+ <div className="absolute inset-0 bg-black/30 rounded-[inherit]" />
101
+
102
+ {/* Modal card */}
103
+ <div
104
+ role="dialog"
105
+ aria-modal="true"
106
+ aria-labelledby="translate-settings-title"
107
+ onClick={(e) => e.stopPropagation()}
108
+ className={clsx(
109
+ "relative w-[460px] max-w-[95%]",
110
+ "overflow-clip rounded-[6px] bg-white shadow-[0px_2px_12px_0px_rgba(59,51,51,0.1)]",
111
+ className,
112
+ )}
113
+ >
114
+ {/* Header */}
115
+ <div className="h-[44px] px-6 py-2 flex items-center w-full shadow-[0px_2px_2px_0px_rgba(47,47,47,0.08)] bg-[#f8f8f8] rounded-t-[6px]">
116
+ <h2
117
+ id="translate-settings-title"
118
+ className="text-[20px] font-semibold text-black capitalize tracking-[0.5px] truncate leading-normal"
119
+ >
120
+ Translation Settings
121
+ </h2>
122
+ </div>
123
+
124
+ {/* Body */}
125
+ <div className="px-6 pt-4 pb-6">
126
+ <div className="flex flex-col gap-[24px]">
127
+ {/* Language select */}
128
+ <div className="flex flex-col gap-[6px]">
129
+ <span className="text-[12px] font-medium text-black tracking-[0.25px] leading-normal">
130
+ Translate message into
131
+ </span>
132
+
133
+ <Select
134
+ options={LANG_OPTIONS}
135
+ value={incomingTarget}
136
+ onChange={setIncomingTarget}
137
+ placeholder="Select Language"
138
+ size={34}
139
+ />
140
+ </div>
141
+
142
+ {/* Info section */}
143
+ <div className="flex gap-[6px] items-start text-[#ff5200]">
144
+ <span className="pt-[2px] shrink-0">
145
+ <ChatInfoIcon className="w-4 h-4" />
146
+ </span>
147
+ <p className="text-[12px] font-normal tracking-[0.25px] leading-normal">
148
+ Automatically translate incoming messages. The language you save here will be used
149
+ to display all incoming messages. You can choose from Spanish, Russian, French,
150
+ Arabic, Portuguese, Turkish, Bangla, and among others.
151
+ </p>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ {/* Footer */}
157
+ <div className="px-6 py-[9px] bg-[#f0f4ff] flex items-center justify-end rounded-b-[6px]">
158
+ <div className="flex items-center justify-end gap-6">
159
+ <Button
160
+ onClick={onClose}
161
+ variant="outlined"
162
+ color="black"
163
+ size="34"
164
+ >
165
+ Cancel
166
+ </Button>
167
+ <Button
168
+ onClick={handleSave}
169
+ variant="filled"
170
+ color="black"
171
+ size="34"
172
+ >
173
+ Update
174
+ </Button>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ );
180
+ };
181
+
182
+ export default ChatTranslateSettingsModal;
@@ -0,0 +1,15 @@
1
+ /*
2
+ * @banbox/chat — Build-time CSS
3
+ * Used by tsup during `npm run build` only.
4
+ * Includes @import "tailwindcss" so the compiled dist/index.js
5
+ * bundle has all Tailwind utilities injected via injectStyle.
6
+ *
7
+ * Do NOT import this in src/index.ts — it is only referenced by tsup.config.ts.
8
+ */
9
+
10
+ @import "tailwindcss";
11
+
12
+ /* ═══════════════════════════════════════════════════════════════
13
+ Re-export the main package styles
14
+ ═══════════════════════════════════════════════════════════════ */
15
+ @import "./index.css";
@@ -1,6 +1,13 @@
1
1
  /*
2
- * @banbox/chat — Compiled CSS
3
- * Import once in your app: import "@banbox/chat/dist/index.css";
2
+ * @banbox/chat — Package CSS
3
+ *
4
+ * When used via dist/: tsup's injectStyle pre-compiles this with @import "tailwindcss"
5
+ * so all Tailwind utilities are bundled into the injected JS.
6
+ *
7
+ * When used via src/ alias in dev: the seller app's vite config strips this
8
+ * @import via a custom plugin to avoid double-injecting Tailwind.
9
+ * The seller app's @tailwindcss/vite then scans banbox-chat/src files
10
+ * and generates all needed utilities automatically.
4
11
  *
5
12
  * Theme usage:
6
13
  * <ChatUIProvider theme="marketplace"> — orange primary (#ff5300)
@@ -9,6 +16,7 @@
9
16
 
10
17
  @import "tailwindcss";
11
18
 
19
+
12
20
  /* ═══════════════════════════════════════════════════════════════
13
21
  THEME: Default / Marketplace / Retailers (orange primary)
14
22
  ═══════════════════════════════════════════════════════════════ */
@@ -1,8 +1,8 @@
1
- // components/chat/ui/chat/AttachmentPreviewStrip.tsx
1
+ // ui/AttachmentPreviewStrip.tsx
2
2
  "use client";
3
3
  import clsx from "clsx";
4
4
  import React from "react";
5
- import { FileIcon, ChatXIcon } from "../../icons";
5
+ import { FileIcon, ChatXIcon } from "../icons";
6
6
 
7
7
  const extColor = (ext: string) => {
8
8
  const e = ext.toLowerCase();
@@ -1,9 +1,9 @@
1
- // components/chat/ui/chat/ChatComposerBar.tsx
1
+ // ui/ChatComposerBar.tsx
2
2
  "use client";
3
3
 
4
4
  import clsx from "clsx";
5
5
  import React, { useRef, useState } from "react";
6
- import { ArrowSendAngleIcon, ArrowSendIcon, RecordMicIcon, ChatXIcon } from "../../icons";
6
+ import { ArrowSendAngleIcon, ArrowSendIcon, RecordMicIcon, ChatXIcon } from "../icons";
7
7
 
8
8
  type Props = {
9
9
  recording: boolean;
@@ -1,9 +1,10 @@
1
- // ui/chat/ChatFooter.tsx
1
+ // ui/ChatFooter.tsx
2
2
  "use client";
3
3
 
4
- import { AttachIcon, ChatInfoIcon, SmileIcon, ChatXIcon, ProfileCardIcon, NewLanguageIcon } from "../../icons";
5
- import ChatTranslateSettingsModal from "../../modals/chat/ChatTranslateSettingsModal";
6
- import type { TranslateSettings } from "../../modals/chat/ChatTranslateSettingsModal";
4
+ import { AttachIcon, ChatInfoIcon, SmileIcon, ChatXIcon, ProfileCardIcon, NewLanguageIcon, MapIcon2 } from "../icons";
5
+ import ChatTranslateSettingsModal from "../modals/ChatTranslateSettingsModal";
6
+ import type { TranslateSettings } from "../modals/ChatTranslateSettingsModal";
7
+ import ChatAddressModal from "../modals/ChatAddressModal";
7
8
  import clsx from "clsx";
8
9
  import React, { useEffect, useMemo, useRef, useState } from "react";
9
10
  import AttachmentPreviewStrip, { type FilePreview as PreviewFile } from "./AttachmentPreviewStrip";
@@ -12,7 +13,7 @@ import BusinessCardDropup from "./drop-up/BusinessCardDropup";
12
13
  import EmojiDropup from "./drop-up/EmojiDropup";
13
14
  import ReplyCard from "./ReplyCard";
14
15
  import type { MessageRef } from "./types";
15
- import type { SendPayload } from "../../types";
16
+ import type { SendPayload } from "../types";
16
17
 
17
18
  /* Simple tooltip wrapper */
18
19
  const Tooltip = ({ children, text }: { children: React.ReactNode; text?: string }) => (
@@ -39,6 +40,8 @@ type Props = {
39
40
  onAfterSend?: () => void;
40
41
 
41
42
  actions?: FooterAction[];
43
+ /** Keys of actions to hide from the toolbar, e.g. ["businessCard", "addressCard"] */
44
+ hiddenActionKeys?: string[];
42
45
  className?: string;
43
46
  maxRows?: number;
44
47
 
@@ -68,10 +71,11 @@ const ChatFooter: React.FC<Props> = ({
68
71
  maxRows = 4,
69
72
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
70
73
  actions: _actions,
74
+ hiddenActionKeys = [],
71
75
  replyTo,
72
76
  clearReply,
73
77
  }) => {
74
- const actionData: FooterAction[] = [
78
+ const allActions: FooterAction[] = [
75
79
  { key: "attachment", title: "Attach file", icon: <AttachIcon className="h-4 w-4" /> },
76
80
  { key: "emoji", title: "Add emoji", icon: <SmileIcon className="h-4 w-4" /> },
77
81
  {
@@ -79,6 +83,11 @@ const ChatFooter: React.FC<Props> = ({
79
83
  title: "Share business card",
80
84
  icon: <ProfileCardIcon className="h-4 w-4" />,
81
85
  },
86
+ {
87
+ key: "addressCard",
88
+ title: "Share address",
89
+ icon: <MapIcon2 className="h-4 w-4" />,
90
+ },
82
91
  {
83
92
  key: "translate",
84
93
  title: "Translation settings",
@@ -86,6 +95,11 @@ const ChatFooter: React.FC<Props> = ({
86
95
  },
87
96
  ];
88
97
 
98
+ // Filter hidden actions
99
+ const actionData = hiddenActionKeys.length
100
+ ? allActions.filter((a) => !hiddenActionKeys.includes(a.key))
101
+ : allActions;
102
+
89
103
  const textRef = useRef<HTMLTextAreaElement>(null);
90
104
  const emojiBtnRef = useRef<HTMLButtonElement | null>(null);
91
105
  const [text, setText] = useState("");
@@ -113,11 +127,19 @@ const ChatFooter: React.FC<Props> = ({
113
127
  outgoingTo: "Bangla",
114
128
  });
115
129
 
130
+ // Business card dropup + edit modal
116
131
  const bizBtnRef = useRef<HTMLButtonElement | null>(null);
117
132
  const [showBiz, setShowBiz] = useState(false);
133
+ const [showBizEdit, setShowBizEdit] = useState(false);
134
+
135
+ const handleOpenBizEdit = () => {
136
+ setShowBiz(false);
137
+ setShowBizEdit(true);
138
+ };
118
139
 
140
+ // Address card modal
119
141
  const addrBtnRef = useRef<HTMLButtonElement | null>(null);
120
- const [, setShowAddress] = useState(false);
142
+ const [showAddress, setShowAddress] = useState(false);
121
143
 
122
144
  // insert emoji at caret
123
145
  const insertEmoji = (emoji: string) => {
@@ -390,7 +412,8 @@ const ChatFooter: React.FC<Props> = ({
390
412
  open={showBiz}
391
413
  onClose={() => setShowBiz(false)}
392
414
  anchorRef={bizBtnRef}
393
- onSend={async (card) => {
415
+ onEdit={handleOpenBizEdit}
416
+ onSend={(card) => {
394
417
  onSend({ type: "businessCard", card, replyTo });
395
418
  clearReply?.();
396
419
  onAfterSend?.();
@@ -435,6 +458,77 @@ const ChatFooter: React.FC<Props> = ({
435
458
  setShowTranslate(false);
436
459
  }}
437
460
  />
461
+
462
+ <ChatAddressModal
463
+ variant={variant}
464
+ open={showAddress}
465
+ onClose={() => setShowAddress(false)}
466
+ onSend={(card) => {
467
+ onSend({ type: "addressCard", card, replyTo });
468
+ clearReply?.();
469
+ onAfterSend?.();
470
+ }}
471
+ />
472
+
473
+ {/* Business card edit modal — simple inline form */}
474
+ {showBizEdit && (
475
+ <div
476
+ className="fixed inset-0 z-[10003] flex items-center justify-center"
477
+ onClick={() => setShowBizEdit(false)}
478
+ >
479
+ <div className="fixed inset-0 bg-black/30" />
480
+ <div
481
+ role="dialog"
482
+ aria-modal="true"
483
+ onClick={(e) => e.stopPropagation()}
484
+ className="relative z-[10004] w-[480px] max-w-[95vw] overflow-hidden rounded-md bg-white shadow-[0_12px_30px_rgba(0,0,0,0.18)]"
485
+ >
486
+ <div className="flex h-[44px] items-center justify-between bg-[#f8f8f8] px-6">
487
+ <h2 className="text-xl font-semibold text-black">Business Info</h2>
488
+ <button
489
+ type="button"
490
+ onClick={() => setShowBizEdit(false)}
491
+ className="flex h-8 w-8 items-center justify-center rounded-full hover:bg-black/10 cursor-pointer"
492
+ >
493
+ <ChatXIcon className="h-5 w-5" />
494
+ </button>
495
+ </div>
496
+ <div className="p-6 space-y-3 text-sm text-[#374151]">
497
+ <p className="text-[#6b7280]">
498
+ Update your business card information below.
499
+ </p>
500
+ {/* For now this is a placeholder — integrate your full BusinessAccountForm here */}
501
+ <div className="grid grid-cols-2 gap-3">
502
+ {["First Name", "Last Name", "Business Name", "Mobile", "Email", "Website"].map((label) => (
503
+ <div key={label} className="flex flex-col gap-1">
504
+ <label className="text-xs font-medium text-[#374151]">{label}</label>
505
+ <input
506
+ placeholder={label}
507
+ className="rounded-md border border-[#d1d5db] px-3 py-2 text-sm text-black placeholder-[#9ca3af] outline-none focus:border-[#ff5200] focus:ring-1 focus:ring-[#ff5200]/30"
508
+ />
509
+ </div>
510
+ ))}
511
+ </div>
512
+ </div>
513
+ <div className="flex h-[52px] items-center justify-end gap-3 bg-[#f0f4ff] px-6">
514
+ <button
515
+ type="button"
516
+ onClick={() => setShowBizEdit(false)}
517
+ className="h-[34px] cursor-pointer rounded-[4px] border border-[#d1d5db] bg-white px-4 text-[13px] font-medium text-black hover:bg-[#f9fafb]"
518
+ >
519
+ Cancel
520
+ </button>
521
+ <button
522
+ type="button"
523
+ onClick={() => setShowBizEdit(false)}
524
+ className="h-[34px] cursor-pointer rounded-[4px] border-none bg-[#ff5200] px-4 text-[13px] font-medium text-white hover:bg-[#e64a00]"
525
+ >
526
+ Save
527
+ </button>
528
+ </div>
529
+ </div>
530
+ </div>
531
+ )}
438
532
  </div>
439
533
  );
440
534
  };
@@ -7,8 +7,8 @@ import _Lottie from "lottie-react";
7
7
  const Lottie = ((_Lottie as any).default ?? _Lottie) as typeof _Lottie;
8
8
 
9
9
  import React from "react";
10
- import { BlueBadgeIcon } from "../../icons";
11
- import globe from "../../lottie/banbox-chat-globe.json";
10
+ import { BlueBadgeIcon } from "../icons";
11
+ import globe from "../lottie/banbox-chat-globe.json";
12
12
 
13
13
  type SubtitleVariant = "live" | "muted";
14
14
 
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import clsx from "clsx";
3
3
  import React from "react";
4
- import { RightArrow } from "../../icons";
4
+ import { RightArrow } from "../icons";
5
5
 
6
6
  type Props = {
7
7
  id?: string; // if undefined, don't render
@@ -0,0 +1,125 @@
1
+ // ui/ChatKebabMenu.tsx
2
+ // Self-contained kebab (⋮) menu for the chat header.
3
+ // 100% clone of the marketplace ChatKebabMenu — no external deps.
4
+ "use client";
5
+
6
+ import clsx from "clsx";
7
+ import React, { useEffect, useRef, useState } from "react";
8
+
9
+ import { MenuIcon, PinIcon, PinOffIcon, TrashIcon } from "../icons";
10
+
11
+ /* ─── Types ─── */
12
+
13
+ export type ChatKebabMenuProps = {
14
+ pinned?: boolean;
15
+ onPinToggle?: () => void;
16
+ onDelete?: () => void;
17
+ className?: string;
18
+ };
19
+
20
+ /* ─── Shared item class — exact marketplace style ─── */
21
+
22
+ const baseItem =
23
+ "flex w-full items-center gap-3 px-3 py-2 text-sm hover:bg-black/5 cursor-pointer";
24
+
25
+ /* ─── Component ─── */
26
+
27
+ const ChatKebabMenu: React.FC<ChatKebabMenuProps> = ({
28
+ pinned,
29
+ onPinToggle,
30
+ onDelete,
31
+ className,
32
+ }) => {
33
+ const [open, setOpen] = useState(false);
34
+ const btnRef = useRef<HTMLButtonElement>(null);
35
+ const menuId = "chat-kebab-menu";
36
+
37
+ /* Close on outside click */
38
+ useEffect(() => {
39
+ if (!open) return;
40
+
41
+ const onDoc = (e: MouseEvent) => {
42
+ const menu = document.getElementById(menuId);
43
+ if (!menu || !btnRef.current) return;
44
+ const target = e.target as Node;
45
+ if (menu.contains(target) || btnRef.current.contains(target)) return;
46
+ setOpen(false);
47
+ };
48
+
49
+ document.addEventListener("mousedown", onDoc);
50
+ return () => document.removeEventListener("mousedown", onDoc);
51
+ }, [open]);
52
+
53
+ /* Close on ESC */
54
+ useEffect(() => {
55
+ if (!open) return;
56
+ const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false);
57
+ document.addEventListener("keydown", onKey);
58
+ return () => document.removeEventListener("keydown", onKey);
59
+ }, [open]);
60
+
61
+ return (
62
+ <div className={clsx("relative", className)}>
63
+ {/* Trigger */}
64
+ <button
65
+ ref={btnRef}
66
+ type="button"
67
+ className="grid h-9 w-9 place-items-center rounded-full hover:bg-black/5 cursor-pointer"
68
+ onClick={() => setOpen((v) => !v)}
69
+ aria-haspopup="menu"
70
+ aria-expanded={open}
71
+ title="More options"
72
+ >
73
+ <MenuIcon className="w-6 h-6" />
74
+ </button>
75
+
76
+ {/* Dropdown */}
77
+ {open && (
78
+ <div
79
+ id={menuId}
80
+ role="menu"
81
+ className="absolute right-0 z-10 mt-2 w-44 rounded-xs bg-white py-1 shadow-[0_4px_16px_rgba(0,0,0,0.08)]"
82
+ >
83
+ {/* Speech-bubble caret */}
84
+ <div className="pointer-events-none absolute -z-[1] -top-[6px] right-4 h-3 w-3 rotate-45 bg-white" />
85
+
86
+ {/* Pin / Unpin */}
87
+ <button
88
+ role="menuitem"
89
+ type="button"
90
+ className={baseItem}
91
+ onClick={() => {
92
+ onPinToggle?.();
93
+ setOpen(false);
94
+ }}
95
+ >
96
+ {pinned ? (
97
+ <PinOffIcon className="h-[18px] w-[18px] text-[#636363]" />
98
+ ) : (
99
+ <PinIcon className="h-[18px] w-[18px] text-[#636363]" />
100
+ )}
101
+ <span className="text-[#161616]">
102
+ {pinned ? "Unpin" : "Pin on top"}
103
+ </span>
104
+ </button>
105
+
106
+ {/* Delete */}
107
+ <button
108
+ role="menuitem"
109
+ type="button"
110
+ className={baseItem}
111
+ onClick={() => {
112
+ onDelete?.();
113
+ setOpen(false);
114
+ }}
115
+ >
116
+ <TrashIcon className="h-[18px] w-[18px] text-[#636363]" />
117
+ <span className="text-[#161616]">Delete</span>
118
+ </button>
119
+ </div>
120
+ )}
121
+ </div>
122
+ );
123
+ };
124
+
125
+ export default ChatKebabMenu;
@@ -4,7 +4,7 @@ import clsx from "clsx";
4
4
  import type { Variants } from "framer-motion";
5
5
  import { AnimatePresence, motion } from "framer-motion";
6
6
  import React from "react";
7
- import { ChatSearchIcon, ChatXIcon, MessageIcon } from "../../icons";
7
+ import { ChatSearchIcon, ChatXIcon, MessageIcon } from "../icons";
8
8
 
9
9
  type Props = {
10
10
  className?: string;
@@ -1,4 +1,4 @@
1
- // components/ui/chat/ChatMessageItem.tsx
1
+ // ui/ChatMessageItem.tsx
2
2
  "use client";
3
3
 
4
4
  import clsx from "clsx";
@@ -1,4 +1,4 @@
1
- // components/ui/chat/ChatScroll.tsx
1
+ // ui/ChatScroll.tsx
2
2
  "use client";
3
3
  import clsx from "clsx";
4
4
  import React from "react";
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { cn } from "../../utils/cn";
2
+ import { cn } from "../utils/cn";
3
3
 
4
4
  /* ===========================================================
5
5
  ChatSpinner
@@ -2,7 +2,7 @@
2
2
 
3
3
  import clsx from "clsx";
4
4
  import React from "react";
5
- import { BlueBadgeIcon } from "../../icons";
5
+ import { BlueBadgeIcon } from "../icons";
6
6
 
7
7
  export type ChatThreadStatus =
8
8
  | { kind: "seen" }
@@ -1,9 +1,9 @@
1
- // components/ui/chat/MessageHoverActions.tsx
1
+ // ui/MessageHoverActions.tsx
2
2
  "use client";
3
3
 
4
4
  import clsx from "clsx";
5
5
  import React from "react";
6
- import { MessageReplayIcon, NewLanguageIcon } from "../../icons";
6
+ import { MessageReplayIcon, NewLanguageIcon } from "../icons";
7
7
 
8
8
  type ItemButton = "replay" | "translate";
9
9
 
@@ -1,9 +1,9 @@
1
- // components/chat/ui/chat/ReplyCard.tsx
1
+ // ui/ReplyCard.tsx
2
2
  "use client";
3
3
 
4
4
  import clsx from "clsx";
5
5
  import React from "react";
6
- import { ArrowBackUpIcon, FileIcon, ChatXIcon } from "../../icons";
6
+ import { ArrowBackUpIcon, FileIcon, ChatXIcon } from "../icons";
7
7
  import { scrollToMessageById } from "./scrollToMessage";
8
8
  import type { MessageRef } from "./types";
9
9
 
@@ -6,7 +6,7 @@ import _Lottie from "lottie-react";
6
6
  const Lottie = ((_Lottie as any).default ?? _Lottie) as typeof _Lottie;
7
7
 
8
8
  import React from "react";
9
- import dots from "../../lottie/typingdotanimation2.json";
9
+ import dots from "../lottie/typingdotanimation2.json";
10
10
 
11
11
  type Props = {
12
12
  size?: number;