@banbox/chat 1.0.4 → 1.0.5
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 +252 -412
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +2273 -0
- package/dist/index.d.cts +19 -13
- package/dist/index.d.ts +19 -13
- package/dist/index.js +252 -412
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
- package/src/chat/ChatRoot.tsx +15 -10
- package/src/chat/InboxPopup.tsx +105 -176
- package/src/chat/SinglePopup.tsx +39 -43
- package/src/index.ts +3 -0
- package/src/styles/index.css +231 -0
- package/src/ui/chat/ChatHeader.tsx +3 -14
- package/src/ui/chat/ChatListHeader.tsx +74 -103
- package/src/ui/chat/ChatScroll.tsx +5 -24
- package/src/ui/chat/TypingIndicator.tsx +8 -38
package/src/chat/SinglePopup.tsx
CHANGED
|
@@ -15,7 +15,9 @@ import TypingIndicator from "../ui/chat/TypingIndicator";
|
|
|
15
15
|
|
|
16
16
|
import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
17
17
|
import type { Message, MessageRef, Reference, Thread } from "../types";
|
|
18
|
+
import type { ChatTheme } from "./InboxPopup";
|
|
18
19
|
|
|
20
|
+
/* ─── Helpers ─── */
|
|
19
21
|
const GRADIENT_BORDER =
|
|
20
22
|
"linear-gradient(236.83deg, rgba(51,201,212,0.3) 0.4%, rgba(39,83,251,0.3) 30.28%, rgba(39,83,251,0.3) 50.2%, rgba(39,83,251,0.3) 65.14%, rgba(235,67,255,0.3) 100%)";
|
|
21
23
|
|
|
@@ -44,12 +46,31 @@ function toRef(m: Message): MessageRef {
|
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
function getThemeAttr(theme?: ChatTheme): string {
|
|
50
|
+
if (!theme || theme === "marketplace") return "marketplace";
|
|
51
|
+
if (theme === "admin") return "admin";
|
|
52
|
+
return "custom";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getThemeVars(theme?: ChatTheme): React.CSSProperties {
|
|
56
|
+
if (!theme || theme === "marketplace" || theme === "admin") return {};
|
|
57
|
+
const vars: Record<string, string> = {};
|
|
58
|
+
if (theme.primary) vars["--color-banbox-primary"] = theme.primary;
|
|
59
|
+
if (theme.primaryActive) vars["--color-banbox-primary-active"] = theme.primaryActive;
|
|
60
|
+
if (theme.surfaceLow) vars["--color-banbox-surface-container-low"] = theme.surfaceLow;
|
|
61
|
+
return vars as React.CSSProperties;
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
export type SinglePopupProps = {
|
|
48
65
|
adapter: ChatAdapter;
|
|
49
66
|
uiCallbacks?: ChatUICallbacks;
|
|
67
|
+
theme?: ChatTheme;
|
|
50
68
|
};
|
|
51
69
|
|
|
52
|
-
|
|
70
|
+
/* ══════════════════════════════════════════════════
|
|
71
|
+
Component
|
|
72
|
+
══════════════════════════════════════════════════ */
|
|
73
|
+
const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme }) => {
|
|
53
74
|
const { close, reference } = useChatUI();
|
|
54
75
|
|
|
55
76
|
const threads = adapter.threads.list(reference);
|
|
@@ -99,30 +120,29 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
99
120
|
void uiCallbacks;
|
|
100
121
|
|
|
101
122
|
return (
|
|
102
|
-
<div
|
|
123
|
+
<div className="fixed bottom-4 right-4 z-[10002]">
|
|
103
124
|
{/* Backdrop */}
|
|
104
125
|
<motion.button
|
|
105
126
|
aria-label="Close chat"
|
|
106
127
|
onClick={close}
|
|
107
|
-
|
|
128
|
+
className="fixed inset-0 cursor-auto!"
|
|
129
|
+
style={{ background: "transparent", border: "none" }}
|
|
108
130
|
initial={{ opacity: 0 }}
|
|
109
131
|
animate={{ opacity: 1 }}
|
|
110
132
|
exit={{ opacity: 0 }}
|
|
111
133
|
transition={{ type: "tween", duration: 0.25 }}
|
|
112
134
|
/>
|
|
113
135
|
|
|
114
|
-
{/* Outer gradient border */}
|
|
136
|
+
{/* Outer gradient border + theme root */}
|
|
115
137
|
<motion.div
|
|
116
138
|
role="dialog"
|
|
117
139
|
aria-modal="true"
|
|
140
|
+
data-theme={getThemeAttr(theme)}
|
|
141
|
+
className="banbox-chat-root relative h-[650px] w-[450px] rounded-[20px] p-[2px]"
|
|
118
142
|
style={{
|
|
119
|
-
position: "relative",
|
|
120
|
-
height: 650,
|
|
121
|
-
width: 450,
|
|
122
|
-
borderRadius: 20,
|
|
123
|
-
padding: 2,
|
|
124
143
|
boxShadow: "0px 2px 12px 0px #3B33331A",
|
|
125
144
|
background: GRADIENT_BORDER,
|
|
145
|
+
...getThemeVars(theme),
|
|
126
146
|
}}
|
|
127
147
|
initial={{ x: "110%" }}
|
|
128
148
|
animate={{ x: 0 }}
|
|
@@ -131,19 +151,11 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
131
151
|
>
|
|
132
152
|
{/* Inner white card */}
|
|
133
153
|
<div
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
flexDirection: "column",
|
|
137
|
-
height: "100%",
|
|
138
|
-
width: "100%",
|
|
139
|
-
overflow: "hidden",
|
|
140
|
-
borderRadius: 18,
|
|
141
|
-
backgroundColor: "#fff",
|
|
142
|
-
overscrollBehavior: "contain",
|
|
143
|
-
}}
|
|
154
|
+
className="flex h-full w-full flex-col overflow-hidden rounded-[18px] bg-white"
|
|
155
|
+
style={{ overscrollBehavior: "contain" }}
|
|
144
156
|
>
|
|
145
157
|
{/* Header — 64px */}
|
|
146
|
-
<div
|
|
158
|
+
<div className="h-[64px] shrink-0">
|
|
147
159
|
<ChatHeader
|
|
148
160
|
left={
|
|
149
161
|
<ChatIdentity
|
|
@@ -160,19 +172,7 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
160
172
|
<button
|
|
161
173
|
type="button"
|
|
162
174
|
onClick={close}
|
|
163
|
-
|
|
164
|
-
display: "flex",
|
|
165
|
-
alignItems: "center",
|
|
166
|
-
justifyContent: "center",
|
|
167
|
-
height: 34,
|
|
168
|
-
width: 34,
|
|
169
|
-
borderRadius: "50%",
|
|
170
|
-
backgroundColor: "#fff",
|
|
171
|
-
color: "#000",
|
|
172
|
-
border: "none",
|
|
173
|
-
boxShadow: "0px 2px 4px 0px #A5A3AE4D",
|
|
174
|
-
cursor: "pointer",
|
|
175
|
-
}}
|
|
175
|
+
className="flex h-[34px] w-[34px] items-center justify-center rounded-full bg-white text-black shadow-[0px_2px_4px_0px_#A5A3AE4D] hover:bg-black/5 hover:text-[var(--color-banbox-warning)] cursor-pointer border-none"
|
|
176
176
|
>
|
|
177
177
|
<ChatXIcon className="h-6 w-6" />
|
|
178
178
|
</button>
|
|
@@ -181,11 +181,11 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
181
181
|
</div>
|
|
182
182
|
|
|
183
183
|
{/* Messages — flex-1 */}
|
|
184
|
-
<div
|
|
184
|
+
<div className="flex-1 min-h-0">
|
|
185
185
|
<ChatScroll
|
|
186
|
+
className="h-full"
|
|
186
187
|
bottomAlignWhenShort={false}
|
|
187
188
|
scrollKey={scrollKey}
|
|
188
|
-
style={{ height: "100%", overflowY: "auto" }}
|
|
189
189
|
>
|
|
190
190
|
{messages.map((m, idx) => {
|
|
191
191
|
const mine = m.author === "you";
|
|
@@ -198,12 +198,8 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
198
198
|
time={m.time ?? ""}
|
|
199
199
|
authorInitial={typeof m.author === "string" ? m.author : "U"}
|
|
200
200
|
text={m.text ?? m.content}
|
|
201
|
-
businessCard={
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
addressCard={
|
|
205
|
-
m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]
|
|
206
|
-
}
|
|
201
|
+
businessCard={m.businessCard as Parameters<typeof ChatMessageItem>[0]["businessCard"]}
|
|
202
|
+
addressCard={m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]}
|
|
207
203
|
images={m.images}
|
|
208
204
|
files={m.files}
|
|
209
205
|
audio={m.audio}
|
|
@@ -217,14 +213,14 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
217
213
|
})}
|
|
218
214
|
|
|
219
215
|
{/* Typing indicator */}
|
|
220
|
-
<div
|
|
216
|
+
<div className="flex items-center justify-start">
|
|
221
217
|
<TypingIndicator />
|
|
222
218
|
</div>
|
|
223
219
|
</ChatScroll>
|
|
224
220
|
</div>
|
|
225
221
|
|
|
226
222
|
{/* Footer */}
|
|
227
|
-
<div
|
|
223
|
+
<div className="shrink-0">
|
|
228
224
|
<ChatFooter
|
|
229
225
|
variant="single"
|
|
230
226
|
replyTo={replyTo}
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
export { default as ChatRoot } from "./chat/ChatRoot";
|
|
9
9
|
export type { ChatRootProps } from "./chat/ChatRoot";
|
|
10
10
|
|
|
11
|
+
// ── Theme type ────────────────────────────────────────────────────────────────
|
|
12
|
+
export type { ChatTheme } from "./chat/InboxPopup";
|
|
13
|
+
|
|
11
14
|
// ── Adapter interfaces (implement these in your host app) ─────────────────────
|
|
12
15
|
export type { ChatAdapter, ChatUICallbacks, KebabMenuOpts, ToastOpts } from "./adapter/types";
|
|
13
16
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @banbox/chat — Compiled CSS
|
|
3
|
+
* Import once in your app: import "@banbox/chat/dist/index.css";
|
|
4
|
+
*
|
|
5
|
+
* Theme usage:
|
|
6
|
+
* <ChatUIProvider theme="marketplace"> — orange primary (#ff5300)
|
|
7
|
+
* <ChatUIProvider theme="admin"> — black primary (#1a1a1a)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
@import "tailwindcss";
|
|
11
|
+
|
|
12
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
13
|
+
THEME: Default / Marketplace / Retailers (orange primary)
|
|
14
|
+
═══════════════════════════════════════════════════════════════ */
|
|
15
|
+
.banbox-chat-root {
|
|
16
|
+
/* Primary */
|
|
17
|
+
--color-banbox-primary: #ff5300;
|
|
18
|
+
--color-banbox-primary-active: #dc4c07;
|
|
19
|
+
--color-banbox-secondary: #2079b0;
|
|
20
|
+
--color-banbox-tertiary: #74a380;
|
|
21
|
+
|
|
22
|
+
/* Primary containers */
|
|
23
|
+
--color-banbox-primary-container: #f9d2c4;
|
|
24
|
+
--color-banbox-on-primary-container: #9e3505;
|
|
25
|
+
--color-banbox-secondary-container: #b2e0fc;
|
|
26
|
+
--color-banbox-on-secondary-container: #17547b;
|
|
27
|
+
--color-banbox-tertiary-container: #ceefd6;
|
|
28
|
+
--color-banbox-on-tertiary-container: #3b7249;
|
|
29
|
+
|
|
30
|
+
/* Surfaces */
|
|
31
|
+
--color-banbox-surface-dim: #f1f1f1;
|
|
32
|
+
--color-banbox-surface: #f2f3f7;
|
|
33
|
+
--color-banbox-surface-bright: #fffcfa;
|
|
34
|
+
--color-banbox-surface-container-lowest: #ffffff;
|
|
35
|
+
--color-banbox-surface-container-low: #fff1ec;
|
|
36
|
+
--color-banbox-surface-container: #fceae5;
|
|
37
|
+
--color-banbox-surface-container-high: #f7e4df;
|
|
38
|
+
--color-banbox-surface-container-highest: #f1dfd9;
|
|
39
|
+
|
|
40
|
+
/* Text / outline */
|
|
41
|
+
--color-banbox-on-surface: #2c2c2c;
|
|
42
|
+
--color-banbox-text-dynamic: #636363;
|
|
43
|
+
--color-banbox-outline: #e0d6cf;
|
|
44
|
+
--color-banbox-outline-variant: #a89e97;
|
|
45
|
+
|
|
46
|
+
/* Semantic colors */
|
|
47
|
+
--color-banbox-blue: #0d99ff;
|
|
48
|
+
--color-banbox-blue-deep: #1078d8;
|
|
49
|
+
--color-banbox-blue-dim-deep: #006fd6;
|
|
50
|
+
--color-banbox-blue-h: #0a7acc;
|
|
51
|
+
--color-banbox-red: #eb2127;
|
|
52
|
+
--color-banbox-link: #005694;
|
|
53
|
+
--color-banbox-link-h: #004576;
|
|
54
|
+
--color-banbox-review: #fcb532;
|
|
55
|
+
--color-banbox-green: #28c76f;
|
|
56
|
+
--color-banbox-deep-green: #097000;
|
|
57
|
+
--color-banbox-warning: #ff5301;
|
|
58
|
+
--color-banbox-error: #eb2127;
|
|
59
|
+
|
|
60
|
+
/* Named neutrals */
|
|
61
|
+
--color-banbox-ed: #ededed;
|
|
62
|
+
--color-banbox-f8: #f8f8f8;
|
|
63
|
+
--color-banbox-e1: #e1e1e1;
|
|
64
|
+
--color-banbox-fc: #fcfcfc;
|
|
65
|
+
--color-banbox-ca: #cacaca;
|
|
66
|
+
--color-banbox-e5: #e5e5e5;
|
|
67
|
+
--color-banbox-92: #929292;
|
|
68
|
+
--color-banbox-63: #636363;
|
|
69
|
+
--color-banbox-2c: #2c2c2c;
|
|
70
|
+
--color-banbox-f1: #f1f1f1;
|
|
71
|
+
|
|
72
|
+
/* Borders */
|
|
73
|
+
--border-banbox-primary: #636363;
|
|
74
|
+
--border-banbox-e1: #e1e1e1;
|
|
75
|
+
--border-banbox-dark: #374151;
|
|
76
|
+
|
|
77
|
+
/* Radii */
|
|
78
|
+
--radius-banbox-sm: 4px;
|
|
79
|
+
--radius-banbox-md: 6px;
|
|
80
|
+
--radius-lg: 12px;
|
|
81
|
+
--radius-full: 100px;
|
|
82
|
+
|
|
83
|
+
/* Shadows */
|
|
84
|
+
--shadow-banbox-modal-primary: 0px 2px 12px 0px #3b33331a;
|
|
85
|
+
--shadow-banbox-card-primary: 0px 6px 12px 0px #3b33331a;
|
|
86
|
+
--shadow-banbox-card-secondary: 0px 2px 2px 0px #2f2f2f14;
|
|
87
|
+
--shadow-banbox-footer-primary: 0px -2px 4px 0px #0000000a;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
91
|
+
THEME: Marketplace (same as default, orange)
|
|
92
|
+
═══════════════════════════════════════════════════════════════ */
|
|
93
|
+
.banbox-chat-root[data-theme="marketplace"] {
|
|
94
|
+
--color-banbox-primary: #ff5300;
|
|
95
|
+
--color-banbox-primary-active: #dc4c07;
|
|
96
|
+
--color-banbox-surface-container-low: #fff1ec;
|
|
97
|
+
--color-banbox-surface-container: #fceae5;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
101
|
+
THEME: Admin (black primary)
|
|
102
|
+
═══════════════════════════════════════════════════════════════ */
|
|
103
|
+
.banbox-chat-root[data-theme="admin"] {
|
|
104
|
+
--color-banbox-primary: #1a1a1a;
|
|
105
|
+
--color-banbox-primary-active: #000000;
|
|
106
|
+
--color-banbox-secondary: #374151;
|
|
107
|
+
--color-banbox-surface-container-lowest: #ffffff;
|
|
108
|
+
--color-banbox-surface-container-low: #f5f5f5;
|
|
109
|
+
--color-banbox-surface-container: #eeeeee;
|
|
110
|
+
--color-banbox-surface-container-high: #e8e8e8;
|
|
111
|
+
--color-banbox-surface-container-highest: #e0e0e0;
|
|
112
|
+
--color-banbox-outline: #d4d4d4;
|
|
113
|
+
--color-banbox-outline-variant: #9e9e9e;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
117
|
+
CUSTOM UTILITY: Scrollbars
|
|
118
|
+
Same as marketplace globals.css .custom-scroll
|
|
119
|
+
═══════════════════════════════════════════════════════════════ */
|
|
120
|
+
.banbox-chat-root .custom-scroll,
|
|
121
|
+
.banbox-chat-root .custom-scroll-hidden {
|
|
122
|
+
--sb-size: 6px;
|
|
123
|
+
--sb-track: #f1f1f1;
|
|
124
|
+
--sb-thumb: #c1c1c1;
|
|
125
|
+
--sb-thumb-hover: #898989;
|
|
126
|
+
|
|
127
|
+
overflow-y: auto;
|
|
128
|
+
overscroll-behavior: contain;
|
|
129
|
+
scrollbar-width: thin;
|
|
130
|
+
scrollbar-color: #c1c1c1 #f1f1f1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.banbox-chat-root .custom-scroll::-webkit-scrollbar,
|
|
134
|
+
.banbox-chat-root .custom-scroll-hidden::-webkit-scrollbar {
|
|
135
|
+
width: 6px;
|
|
136
|
+
height: 6px;
|
|
137
|
+
display: block;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.banbox-chat-root .custom-scroll::-webkit-scrollbar-track,
|
|
141
|
+
.banbox-chat-root .custom-scroll-hidden::-webkit-scrollbar-track {
|
|
142
|
+
background: #f1f1f1;
|
|
143
|
+
border-radius: 9999px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.banbox-chat-root .custom-scroll::-webkit-scrollbar-thumb,
|
|
147
|
+
.banbox-chat-root .custom-scroll-hidden::-webkit-scrollbar-thumb {
|
|
148
|
+
background-color: #c1c1c1;
|
|
149
|
+
border-radius: 9999px;
|
|
150
|
+
border: 1px solid #f1f1f1;
|
|
151
|
+
min-height: 40px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.banbox-chat-root .custom-scroll::-webkit-scrollbar-thumb:hover,
|
|
155
|
+
.banbox-chat-root .custom-scroll-hidden::-webkit-scrollbar-thumb:hover {
|
|
156
|
+
background-color: #898989;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* hidden scrollbar variant (no visible track) */
|
|
160
|
+
.banbox-chat-root .custom-scroll-hidden {
|
|
161
|
+
scrollbar-width: none;
|
|
162
|
+
}
|
|
163
|
+
.banbox-chat-root .custom-scroll-hidden::-webkit-scrollbar {
|
|
164
|
+
display: none;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
168
|
+
no-scrollbar utility
|
|
169
|
+
═══════════════════════════════════════════════════════════════ */
|
|
170
|
+
.banbox-chat-root .no-scrollbar {
|
|
171
|
+
-ms-overflow-style: none;
|
|
172
|
+
scrollbar-width: none;
|
|
173
|
+
}
|
|
174
|
+
.banbox-chat-root .no-scrollbar::-webkit-scrollbar {
|
|
175
|
+
display: none;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
179
|
+
Chat-specific animations
|
|
180
|
+
═══════════════════════════════════════════════════════════════ */
|
|
181
|
+
@keyframes banbox-chat-fade-in {
|
|
182
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
183
|
+
to { opacity: 1; transform: translateY(0); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.banbox-chat-root .animate-fade-in {
|
|
187
|
+
animation: banbox-chat-fade-in 0.2s ease-out forwards;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
191
|
+
Chat bubble — mine (right) background uses primary color
|
|
192
|
+
═══════════════════════════════════════════════════════════════ */
|
|
193
|
+
.banbox-chat-root .chat-bubble-mine {
|
|
194
|
+
background-color: var(--color-banbox-primary);
|
|
195
|
+
color: #fff;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
199
|
+
Thread item — active state uses primary surface color
|
|
200
|
+
═══════════════════════════════════════════════════════════════ */
|
|
201
|
+
.banbox-chat-root .chat-thread-active {
|
|
202
|
+
background-color: var(--color-banbox-surface-container-low);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
206
|
+
Send button — primary color
|
|
207
|
+
═══════════════════════════════════════════════════════════════ */
|
|
208
|
+
.banbox-chat-root .chat-btn-send {
|
|
209
|
+
background-color: var(--color-banbox-primary);
|
|
210
|
+
color: #fff;
|
|
211
|
+
}
|
|
212
|
+
.banbox-chat-root .chat-btn-send:hover {
|
|
213
|
+
background-color: var(--color-banbox-primary-active);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
217
|
+
Unread badge — primary color
|
|
218
|
+
═══════════════════════════════════════════════════════════════ */
|
|
219
|
+
.banbox-chat-root .chat-badge-unread {
|
|
220
|
+
background-color: var(--color-banbox-primary);
|
|
221
|
+
color: #fff;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
225
|
+
Tailwind @layer utilities — so these work as Tailwind classes
|
|
226
|
+
═══════════════════════════════════════════════════════════════ */
|
|
227
|
+
@layer utilities {
|
|
228
|
+
.banbox-chat-root * {
|
|
229
|
+
box-sizing: border-box;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -8,22 +8,11 @@ type Props = {
|
|
|
8
8
|
className?: string;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
export default function ChatHeader({ left, right, below }: Props) {
|
|
11
|
+
export default function ChatHeader({ left, right, below, className }: Props) {
|
|
12
12
|
return (
|
|
13
13
|
<div>
|
|
14
|
-
<div
|
|
15
|
-
|
|
16
|
-
borderBottom: "1px solid #e1e1e1",
|
|
17
|
-
height: 64,
|
|
18
|
-
display: "flex",
|
|
19
|
-
alignItems: "flex-start",
|
|
20
|
-
justifyContent: "space-between",
|
|
21
|
-
paddingLeft: 16,
|
|
22
|
-
paddingRight: 16,
|
|
23
|
-
paddingTop: 10,
|
|
24
|
-
}}
|
|
25
|
-
>
|
|
26
|
-
<div style={{ display: "flex", alignItems: "flex-start", gap: 12 }}>{left}</div>
|
|
14
|
+
<div className={`border-b border-[#e1e1e1] h-[64px] flex items-start justify-between px-4 pt-2.5${className ? ` ${className}` : ""}`}>
|
|
15
|
+
<div className="flex items-start gap-3">{left}</div>
|
|
27
16
|
{right}
|
|
28
17
|
</div>
|
|
29
18
|
{below && <>{below}</>}
|
|
@@ -11,7 +11,7 @@ type Props = {
|
|
|
11
11
|
onSearchChange?: (value: string) => void;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
const ChatListHeader: React.FC<Props> = ({ onClose, onSearchChange }) => {
|
|
14
|
+
const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange }) => {
|
|
15
15
|
const [searching, setSearching] = React.useState(false);
|
|
16
16
|
const [q, setQ] = React.useState("");
|
|
17
17
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
@@ -43,113 +43,84 @@ const ChatListHeader: React.FC<Props> = ({ onClose, onSearchChange }) => {
|
|
|
43
43
|
outToRight: { opacity: 0, x: 24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
const btnStyle: React.CSSProperties = {
|
|
47
|
-
height: 36,
|
|
48
|
-
width: 36,
|
|
49
|
-
display: "flex",
|
|
50
|
-
alignItems: "center",
|
|
51
|
-
justifyContent: "center",
|
|
52
|
-
borderRadius: "50%",
|
|
53
|
-
border: "none",
|
|
54
|
-
background: "transparent",
|
|
55
|
-
cursor: "pointer",
|
|
56
|
-
};
|
|
57
|
-
|
|
58
46
|
return (
|
|
59
|
-
<div
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{!searching ? (
|
|
71
|
-
<motion.div
|
|
72
|
-
key="normal"
|
|
73
|
-
style={{ display: "flex", width: "100%", alignItems: "center", justifyContent: "space-between" }}
|
|
74
|
-
initial={{ opacity: 0, x: -24 }}
|
|
75
|
-
animate="inFromLeft"
|
|
76
|
-
exit="outToLeft"
|
|
77
|
-
variants={variants}
|
|
78
|
-
>
|
|
79
|
-
{/* Title */}
|
|
80
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8, color: "#2c2c2c" }}>
|
|
81
|
-
<MessageIcon className="w-6 h-6" />
|
|
82
|
-
<span style={{ fontSize: 22, fontWeight: 600 }}>Messenger</span>
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
{/* Actions */}
|
|
86
|
-
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
87
|
-
<button title="Search" onClick={() => setSearching(true)} style={btnStyle}>
|
|
88
|
-
<ChatSearchIcon className="w-5 h-5" />
|
|
89
|
-
</button>
|
|
90
|
-
<button title="Close" onClick={onClose} style={btnStyle}>
|
|
91
|
-
<ChatXIcon className="w-6 h-6" />
|
|
92
|
-
</button>
|
|
93
|
-
</div>
|
|
94
|
-
</motion.div>
|
|
95
|
-
) : (
|
|
96
|
-
<motion.div
|
|
97
|
-
key="search"
|
|
98
|
-
style={{ display: "flex", width: "100%", alignItems: "center", gap: 12 }}
|
|
99
|
-
initial={{ opacity: 0, x: 24 }}
|
|
100
|
-
animate="inFromRight"
|
|
101
|
-
exit="outToRight"
|
|
102
|
-
variants={variants}
|
|
103
|
-
>
|
|
104
|
-
<div
|
|
105
|
-
style={{
|
|
106
|
-
display: "flex",
|
|
107
|
-
flex: 1,
|
|
108
|
-
height: 40,
|
|
109
|
-
alignItems: "center",
|
|
110
|
-
borderRadius: 9999,
|
|
111
|
-
border: "1px solid #6A6A6A",
|
|
112
|
-
backgroundColor: "#fff",
|
|
113
|
-
padding: "0 4px",
|
|
114
|
-
gap: 6,
|
|
115
|
-
}}
|
|
47
|
+
<div className={`h-[64px] border-b border-[#ededed]${className ? ` ${className}` : ""}`}>
|
|
48
|
+
<div className="flex h-full items-center px-[20px]">
|
|
49
|
+
<AnimatePresence initial={false} mode="wait">
|
|
50
|
+
{!searching ? (
|
|
51
|
+
<motion.div
|
|
52
|
+
key="normal"
|
|
53
|
+
className="flex w-full items-center justify-between"
|
|
54
|
+
initial={{ opacity: 0, x: -24 }}
|
|
55
|
+
animate="inFromLeft"
|
|
56
|
+
exit="outToLeft"
|
|
57
|
+
variants={variants}
|
|
116
58
|
>
|
|
117
|
-
<div
|
|
118
|
-
<
|
|
59
|
+
<div className="flex items-center gap-3">
|
|
60
|
+
<div className="text-[#2c2c2c] flex items-center gap-2">
|
|
61
|
+
<MessageIcon className="w-6 h-6" />
|
|
62
|
+
<span className="text-[22px] font-semibold">Messenger</span>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className="flex items-center gap-2">
|
|
67
|
+
<button
|
|
68
|
+
title="Search"
|
|
69
|
+
onClick={() => setSearching(true)}
|
|
70
|
+
className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center cursor-pointer border-none bg-transparent"
|
|
71
|
+
>
|
|
119
72
|
<ChatSearchIcon className="w-5 h-5" />
|
|
120
|
-
</
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}}
|
|
129
|
-
placeholder="Search"
|
|
130
|
-
style={{
|
|
131
|
-
flex: 1,
|
|
132
|
-
height: "100%",
|
|
133
|
-
background: "transparent",
|
|
134
|
-
fontSize: 15,
|
|
135
|
-
outline: "none",
|
|
136
|
-
border: "none",
|
|
137
|
-
color: "#2c2c2c",
|
|
138
|
-
}}
|
|
139
|
-
/>
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
title="Close"
|
|
76
|
+
onClick={onClose}
|
|
77
|
+
className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center cursor-pointer border-none bg-transparent"
|
|
78
|
+
>
|
|
79
|
+
<ChatXIcon className="w-6 h-6" />
|
|
80
|
+
</button>
|
|
140
81
|
</div>
|
|
82
|
+
</motion.div>
|
|
83
|
+
) : (
|
|
84
|
+
<motion.div
|
|
85
|
+
key="search"
|
|
86
|
+
className="flex w-full items-center gap-3"
|
|
87
|
+
initial={{ opacity: 0, x: 24 }}
|
|
88
|
+
animate="inFromRight"
|
|
89
|
+
exit="outToRight"
|
|
90
|
+
variants={variants}
|
|
91
|
+
>
|
|
92
|
+
<div className="relative flex-1">
|
|
93
|
+
<div className="flex h-10 w-full items-center rounded-full border border-[#6A6A6A] bg-white px-[4px] gap-1.5">
|
|
94
|
+
<div className="flex items-center ms-[12px] w-full">
|
|
95
|
+
<span className="mr-2 grid h-6 w-6 shrink-0 place-items-center text-[#929292]">
|
|
96
|
+
<ChatSearchIcon className="w-5 h-5" />
|
|
97
|
+
</span>
|
|
98
|
+
<span className="mr-2 h-6 w-px shrink-0 bg-[#e1e1e1]" />
|
|
99
|
+
<input
|
|
100
|
+
ref={inputRef}
|
|
101
|
+
value={q}
|
|
102
|
+
onChange={(e) => {
|
|
103
|
+
setQ(e.target.value);
|
|
104
|
+
onSearchChange?.(e.target.value);
|
|
105
|
+
}}
|
|
106
|
+
placeholder="Search"
|
|
107
|
+
className="h-full w-full flex-1 bg-transparent text-[15px] outline-none placeholder:text-[#9C9C9C] border-none"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
141
110
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
111
|
+
<button
|
|
112
|
+
title="Close search"
|
|
113
|
+
onClick={() => { setSearching(false); setQ(""); onSearchChange?.(""); }}
|
|
114
|
+
className="grid h-8 w-8 place-items-center rounded-full text-xl hover:bg-black/5 cursor-pointer border-none bg-transparent"
|
|
115
|
+
>
|
|
116
|
+
<ChatXIcon className="w-5 h-5" />
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</motion.div>
|
|
121
|
+
)}
|
|
122
|
+
</AnimatePresence>
|
|
123
|
+
</div>
|
|
153
124
|
</div>
|
|
154
125
|
);
|
|
155
126
|
};
|