@banbox/chat 1.0.7 → 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.
- package/dist/index.cjs +1236 -235
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +1160 -160
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/InboxPopup.tsx +105 -42
- package/src/chat/SinglePopup.tsx +59 -14
- package/src/icons/index.tsx +55 -0
- package/src/index.ts +14 -12
- package/src/modals/ChatAddressModal.tsx +844 -0
- package/src/modals/{chat/ChatConfirmModal.tsx → ChatConfirmModal.tsx} +2 -2
- package/src/modals/ChatTranslateSettingsModal.tsx +182 -0
- package/src/styles/index.build.css +15 -0
- package/src/styles/index.css +10 -2
- package/src/ui/{chat/AttachmentPreviewStrip.tsx → AttachmentPreviewStrip.tsx} +2 -2
- package/src/ui/{chat/ChatComposerBar.tsx → ChatComposerBar.tsx} +2 -2
- package/src/ui/{chat/ChatFooter.tsx → ChatFooter.tsx} +102 -8
- package/src/ui/{chat/ChatHeader.tsx → ChatHeader.tsx} +7 -4
- package/src/ui/{chat/ChatIdentity.tsx → ChatIdentity.tsx} +2 -2
- package/src/ui/{chat/ChatInquiryBar.tsx → ChatInquiryBar.tsx} +1 -1
- package/src/ui/ChatKebabMenu.tsx +125 -0
- package/src/ui/{chat/ChatListHeader.tsx → ChatListHeader.tsx} +49 -25
- package/src/ui/{chat/ChatMessageItem.tsx → ChatMessageItem.tsx} +1 -1
- package/src/ui/ChatScroll.tsx +59 -0
- package/src/ui/{chat/ChatSpinner.tsx → ChatSpinner.tsx} +1 -1
- package/src/ui/{chat/ChatThreadItem.tsx → ChatThreadItem.tsx} +9 -16
- package/src/ui/{chat/MessageHoverActions.tsx → MessageHoverActions.tsx} +2 -2
- package/src/ui/{chat/ReplyCard.tsx → ReplyCard.tsx} +2 -2
- package/src/ui/{chat/TypingIndicator.tsx → TypingIndicator.tsx} +1 -1
- package/src/ui/{chat/drop-up → drop-up}/BusinessCardDropup.tsx +15 -3
- package/src/ui/{chat/drop-up → drop-up}/EmojiDropup.tsx +1 -1
- package/src/ui/{chat/message-items → message-items}/ChatAddressCard.tsx +4 -4
- package/src/ui/{chat/message-items → message-items}/ChatBubbleFiles.tsx +1 -1
- package/src/ui/{chat/message-items → message-items}/ChatBubbleImages.tsx +2 -2
- package/src/ui/{chat/message-items → message-items}/ChatBusinessCard.tsx +1 -1
- package/src/ui/{chat/scrollToMessage.ts → scrollToMessage.ts} +1 -1
- package/src/ui/{chat/types.ts → types.ts} +2 -2
- package/src/modals/chat/ChatTranslateSettingsModal.tsx +0 -180
- package/src/ui/chat/ChatScroll.tsx +0 -52
- /package/src/ui/{chat/message-items → message-items}/ChatBubbleAudio.tsx +0 -0
- /package/src/ui/{chat/message-items → message-items}/ChatBubbleText.tsx +0 -0
|
@@ -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";
|
package/src/styles/index.css
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* @banbox/chat —
|
|
3
|
-
*
|
|
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
|
-
//
|
|
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 "
|
|
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
|
-
//
|
|
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 "
|
|
6
|
+
import { ArrowSendAngleIcon, ArrowSendIcon, RecordMicIcon, ChatXIcon } from "../icons";
|
|
7
7
|
|
|
8
8
|
type Props = {
|
|
9
9
|
recording: boolean;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// ui/
|
|
1
|
+
// ui/ChatFooter.tsx
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import { AttachIcon, ChatInfoIcon, SmileIcon, ChatXIcon, ProfileCardIcon, NewLanguageIcon } from "
|
|
5
|
-
import ChatTranslateSettingsModal from "
|
|
6
|
-
import type { TranslateSettings } from "
|
|
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 "
|
|
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
|
|
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
|
-
|
|
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
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import clsx from "clsx";
|
|
2
3
|
import React from "react";
|
|
3
4
|
|
|
4
5
|
type Props = {
|
|
@@ -10,10 +11,12 @@ type Props = {
|
|
|
10
11
|
|
|
11
12
|
export default function ChatHeader({ left, right, below, className }: Props) {
|
|
12
13
|
return (
|
|
13
|
-
<div
|
|
14
|
-
<div className="
|
|
15
|
-
<div className="flex items-center
|
|
16
|
-
|
|
14
|
+
<div>
|
|
15
|
+
<div className={clsx("border-b border-[#ededed] h-[64px]", className)}>
|
|
16
|
+
<div className="flex items-center justify-between px-4 pt-2.5">
|
|
17
|
+
<div className="flex items-center gap-3">{left}</div>
|
|
18
|
+
{right}
|
|
19
|
+
</div>
|
|
17
20
|
</div>
|
|
18
21
|
{below && <>{below}</>}
|
|
19
22
|
</div>
|
|
@@ -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 "
|
|
11
|
-
import globe from "
|
|
10
|
+
import { BlueBadgeIcon } from "../icons";
|
|
11
|
+
import globe from "../lottie/banbox-chat-globe.json";
|
|
12
12
|
|
|
13
13
|
type SubtitleVariant = "live" | "muted";
|
|
14
14
|
|
|
@@ -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;
|