@banbox/chat 1.0.3 → 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.
@@ -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
+ }
@@ -1,24 +1,21 @@
1
- "use client";
2
- import clsx from "clsx";
3
- import React from "react";
4
-
5
- type Props = {
6
- left: React.ReactNode; // header left content (avatar/title)
7
- right?: React.ReactNode; // header right content (icons/actions)
8
- below?: React.ReactNode; // optional bar below the header (e.g., Inquiry ID)
9
- className?: string;
10
- };
11
-
12
- export default function ChatHeader({ left, right, below, className }: Props) {
13
- return (
14
- <div>
15
- <div className={clsx("border-b border-[#e1e1e1] h-[64px]", className)}>
16
- <div className="flex items-start justify-between px-4 pt-2.5">
17
- <div className="flex items-start gap-3">{left}</div>
18
- {right}
19
- </div>
20
- </div>
21
- {below && <>{below}</>}
22
- </div>
23
- );
24
- }
1
+ "use client";
2
+ import React from "react";
3
+
4
+ type Props = {
5
+ left: React.ReactNode;
6
+ right?: React.ReactNode;
7
+ below?: React.ReactNode;
8
+ className?: string;
9
+ };
10
+
11
+ export default function ChatHeader({ left, right, below, className }: Props) {
12
+ return (
13
+ <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>
16
+ {right}
17
+ </div>
18
+ {below && <>{below}</>}
19
+ </div>
20
+ );
21
+ }
@@ -1,156 +1,128 @@
1
- "use client";
2
-
3
- import clsx from "clsx";
4
- import type { Variants } from "framer-motion";
5
- import { AnimatePresence, motion } from "framer-motion";
6
- import React from "react";
7
- import { MessageIcon, ChatSearchIcon, ChatXIcon } from "../../icons";
8
-
9
- type Props = {
10
- className?: string;
11
- onClose?: () => void;
12
- onSearchChange?: (value: string) => void;
13
- };
14
-
15
- const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange }) => {
16
- const [searching, setSearching] = React.useState(false);
17
- const [q, setQ] = React.useState("");
18
- const inputRef = React.useRef<HTMLInputElement>(null);
19
-
20
- React.useEffect(() => {
21
- // AnimatePresence mode="wait" delays mounting; wait for enter animation
22
- const timer = searching
23
- ? setTimeout(() => {
24
- inputRef.current?.focus();
25
- }, 220)
26
- : undefined;
27
- return () => {
28
- clearTimeout(timer);
29
- };
30
- }, [searching]);
31
-
32
- React.useEffect(() => {
33
- if (!searching) {
34
- return;
35
- }
36
- const onKey = (e: KeyboardEvent) => {
37
- if (e.key === "Escape") {
38
- setSearching(false);
39
- setQ("");
40
- onSearchChange?.("");
41
- }
42
- };
43
- window.addEventListener("keydown", onKey);
44
- return () => {
45
- window.removeEventListener("keydown", onKey);
46
- };
47
- }, [searching, onSearchChange]);
48
-
49
- const _clearInside = () => {
50
- setQ("");
51
- onSearchChange?.("");
52
- inputRef.current?.focus();
53
- };
54
-
55
- // Use cubic-bezier tuples to satisfy the Transition type
56
- const variants: Variants = {
57
- inFromRight: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
58
- outToLeft: { opacity: 0, x: -24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
59
- inFromLeft: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
60
- outToRight: { opacity: 0, x: 24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
61
- };
62
-
63
- return (
64
- <div className={clsx("h-[64px] border-b border-[#ededed]", className)}>
65
- <div className="flex h-full items-center px-[20px]">
66
- <AnimatePresence initial={false} mode="wait">
67
- {!searching ? (
68
- // NORMAL (title) — appears from left when closing search
69
- <motion.div
70
- key="normal"
71
- className="flex w-full items-center justify-between"
72
- initial={{ opacity: 0, x: -24 }}
73
- animate="inFromLeft"
74
- exit="outToLeft"
75
- variants={variants}
76
- >
77
- <div className="flex items-center gap-3">
78
- <div className="text-[#2c2c2c] flex items-center gap-2">
79
- <MessageIcon className="w-6 h-6" />
80
- <span className="text-[22px] font-semibold">
81
- Messenger
82
- </span>
83
- </div>
84
- </div>
85
-
86
- <div className="flex items-center gap-2">
87
- <button
88
- title="Search"
89
- onClick={() => setSearching(true)}
90
- className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center"
91
- >
92
- <ChatSearchIcon className="w-5 h-5" />
93
- </button>
94
-
95
- <button
96
- title="Close"
97
- onClick={onClose}
98
- className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center"
99
- >
100
- <ChatXIcon className="w-6 h-6" />
101
- </button>
102
- </div>
103
- </motion.div>
104
- ) : (
105
- // SEARCH — enters from right, exits to right
106
- <motion.div
107
- key="search"
108
- className="flex w-full items-center gap-3"
109
- initial={{ opacity: 0, x: 24 }}
110
- animate="inFromRight"
111
- exit="outToRight"
112
- variants={variants}
113
- >
114
- <div className="relative flex-1">
115
- <div className="flex h-10 w-full items-center rounded-full border border-[#6A6A6A] bg-white px-[4px] gap-1.5">
116
- <div className="flex items-center ms-[12px] w-full">
117
- <span className="mr-2 grid h-6 w-6 shrink-0 place-items-center text-[#929292]">
118
- <ChatSearchIcon className="w-5 h-5" />
119
- </span>
120
- <span className="mr-2 h-6 w-px shrink-0 bg-[#e1e1e1]" />
121
- <input
122
- ref={inputRef}
123
- value={q}
124
- onChange={(e) => {
125
- setQ(e.target.value);
126
- onSearchChange?.(e.target.value);
127
- }}
128
- placeholder="Search"
129
- className="h-full w-full flex-1 bg-transparent text-[15px] outline-none placeholder:text-[#9C9C9C]"
130
- />
131
- </div>
132
-
133
- <div>
134
- <button
135
- title="Close search"
136
- onClick={() => {
137
- setSearching(false);
138
- setQ("");
139
- onSearchChange?.("");
140
- }}
141
- className="grid h-8 w-8 place-items-center rounded-full text-xl hover:bg-black/5"
142
- >
143
- <ChatXIcon className="w-5 h-5" />
144
- </button>
145
- </div>
146
- </div>
147
- </div>
148
- </motion.div>
149
- )}
150
- </AnimatePresence>
151
- </div>
152
- </div>
153
- );
154
- };
155
-
156
- export default ChatListHeader;
1
+ "use client";
2
+
3
+ import type { Variants } from "framer-motion";
4
+ import { AnimatePresence, motion } from "framer-motion";
5
+ import React from "react";
6
+ import { ChatSearchIcon, ChatXIcon, MessageIcon } from "../../icons";
7
+
8
+ type Props = {
9
+ className?: string;
10
+ onClose?: () => void;
11
+ onSearchChange?: (value: string) => void;
12
+ };
13
+
14
+ const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange }) => {
15
+ const [searching, setSearching] = React.useState(false);
16
+ const [q, setQ] = React.useState("");
17
+ const inputRef = React.useRef<HTMLInputElement>(null);
18
+
19
+ React.useEffect(() => {
20
+ const timer = searching
21
+ ? setTimeout(() => { inputRef.current?.focus(); }, 220)
22
+ : undefined;
23
+ return () => { clearTimeout(timer); };
24
+ }, [searching]);
25
+
26
+ React.useEffect(() => {
27
+ if (!searching) return;
28
+ const onKey = (e: KeyboardEvent) => {
29
+ if (e.key === "Escape") {
30
+ setSearching(false);
31
+ setQ("");
32
+ onSearchChange?.("");
33
+ }
34
+ };
35
+ window.addEventListener("keydown", onKey);
36
+ return () => { window.removeEventListener("keydown", onKey); };
37
+ }, [searching, onSearchChange]);
38
+
39
+ const variants: Variants = {
40
+ inFromRight: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
41
+ outToLeft: { opacity: 0, x: -24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
42
+ inFromLeft: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
43
+ outToRight: { opacity: 0, x: 24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
44
+ };
45
+
46
+ return (
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}
58
+ >
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
+ >
72
+ <ChatSearchIcon className="w-5 h-5" />
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>
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>
110
+
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>
124
+ </div>
125
+ );
126
+ };
127
+
128
+ export default ChatListHeader;