@banbox/chat 1.0.1 → 1.0.3

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.
@@ -1,145 +1,152 @@
1
- "use client";
2
-
3
- import { BlueBadgeIcon } from "../../icons";
4
- import { cn } from "../../utils/cn";
5
-
6
- /* =======================
7
- Types
8
- ======================= */
9
-
10
- type SubtitleVariant = "live" | "muted";
11
- // Variant = "avatar" | "initial" | "logo" — inferred from union props below
12
-
13
- type BaseProps = {
14
- title: string;
15
- subtitle?: string;
16
- subtitleVariant?: SubtitleVariant;
17
- online?: boolean;
18
- verified?: boolean;
19
- /** Circle size in px (default 46) */
20
- size?: number;
21
- className?: string;
22
- };
23
-
24
- type AvatarVariant = BaseProps & {
25
- variant: "avatar";
26
- src: string;
27
- };
28
-
29
- type InitialVariant = BaseProps & {
30
- variant: "initial";
31
- initial: string;
32
- initialSrc?: string;
33
- bg?: string;
34
- textClassName?: string;
35
- };
36
-
37
- type LogoVariant = BaseProps & {
38
- variant: "logo";
39
- src: string;
40
- ringColor?: string;
41
- };
42
-
43
- export type ChatIdentityProps = AvatarVariant | InitialVariant | LogoVariant;
44
-
45
- /* =======================
46
- Component
47
- ======================= */
48
-
49
- const ChatIdentity = (props: ChatIdentityProps) => {
50
- const {
51
- title,
52
- subtitle,
53
- subtitleVariant = "muted",
54
- online = false,
55
- verified = false,
56
- size = 46,
57
- className,
58
- } = props;
59
-
60
- const subtitleClass = cn(
61
- "text-[10px] font-medium",
62
- subtitleVariant === "live" ? "text-[#1E9E6A]" : "text-[#929292]",
63
- );
64
-
65
- return (
66
- <div className={cn("flex items-start gap-3", className)}>
67
- <div className="relative" style={{ width: size, height: size }}>
68
- {/* Avatar */}
69
- {props.variant === "avatar" ? (
70
- <img
71
- src={props.src}
72
- alt={title}
73
- className="h-full w-full rounded-xs object-cover border border-[#f1f1f1]"
74
- />
75
- ) : null}
76
-
77
- {/* Initial */}
78
- {props.variant === "initial" ? (
79
- <div
80
- className={cn(
81
- "grid h-full w-full place-items-center rounded-xs text-[15px] font-semibold text-[#2c2c2c]",
82
- props.textClassName,
83
- )}
84
- style={{ backgroundColor: props.bg ?? "#FFE5DA" }}
85
- >
86
- {props.initial}
87
- </div>
88
- ) : null}
89
-
90
- {/* Logo */}
91
- {props.variant === "logo" ? (
92
- <div
93
- className="grid h-full w-full place-items-center rounded-xs"
94
- style={{ boxShadow: `0 0 0 1px ${props.ringColor ?? "#EDEDED"} inset` }}
95
- >
96
- <img src={props.src} alt={title} className="h-full w-full rounded-xs object-cover" />
97
- </div>
98
- ) : null}
99
-
100
- {/* Online / Offline dot */}
101
- <span
102
- className={cn(
103
- "absolute rounded-full ring-1 ring-white bottom-[-2px] right-[-2px]",
104
- online ? "bg-[#328545]" : "bg-[#eb2127]",
105
- )}
106
- style={{
107
- width: "15px",
108
- height: "15px",
109
- right: 0,
110
- bottom: 0,
111
- }}
112
- />
113
- </div>
114
-
115
- <div>
116
- <div className="flex items-start gap-1 text-[14px] font-medium text-black">
117
- <span className="max-w-[300px] truncate">{title}</span>
118
- {verified ? (
119
- <span>
120
- <BlueBadgeIcon />
121
- </span>
122
- ) : null}
123
- </div>
124
-
125
- {subtitle ? (
126
- title.toLowerCase() === "banbox.com" ? (
127
- <div className="flex items-center">
128
- <img
129
- src="/chat/globe.gif"
130
- alt="globe"
131
- className="h-[12px] w-auto shrink-0 object-contain"
132
- style={{ mixBlendMode: "multiply" }}
133
- />
134
- <div className={subtitleClass}>{subtitle}</div>
135
- </div>
136
- ) : (
137
- <div className={subtitleClass}>{subtitle}</div>
138
- )
139
- ) : null}
140
- </div>
141
- </div>
142
- );
143
- };
144
-
145
- export default ChatIdentity;
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import _Lottie from "lottie-react";
5
+ // Handle ESM/CJS interop: when bundled by tsup the default export may be wrapped
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const Lottie = ((_Lottie as any).default ?? _Lottie) as typeof _Lottie;
8
+
9
+ import React from "react";
10
+ import { BlueBadgeIcon } from "../../icons";
11
+ import globe from "../../lottie/banbox-chat-globe.json";
12
+
13
+ type SubtitleVariant = "live" | "muted";
14
+
15
+ type BaseProps = {
16
+ title: string;
17
+ subtitle?: string;
18
+ subtitleVariant?: SubtitleVariant;
19
+ online?: boolean;
20
+ verified?: boolean;
21
+ /** Circle size in px (default 46) */
22
+ size?: number;
23
+ className?: string;
24
+ };
25
+
26
+ type AvatarVariant = BaseProps & {
27
+ variant: "avatar";
28
+ src: string; // person photo
29
+ };
30
+
31
+ type InitialVariant = BaseProps & {
32
+ variant: "initial";
33
+ initial: string; // e.g. "K"
34
+ initialSrc?: string;
35
+ bg?: string; // circle background
36
+ textClassName?: string;
37
+ };
38
+
39
+ type LogoVariant = BaseProps & {
40
+ variant: "logo";
41
+ src: string; // brand logo
42
+ ringColor?: string;
43
+ };
44
+
45
+ export type ChatIdentityProps = AvatarVariant | InitialVariant | LogoVariant;
46
+
47
+ const ChatIdentity: React.FC<ChatIdentityProps> = (props) => {
48
+ const {
49
+ title,
50
+ subtitle,
51
+ subtitleVariant = "muted",
52
+ online = false,
53
+ verified = false,
54
+ size = 46,
55
+ className,
56
+ } = props;
57
+
58
+ const subtitleClass = clsx(
59
+ "text-[10px] font-medium",
60
+ subtitleVariant === "live" ? "text-[#1E9E6A]" : "text-[#929292]",
61
+ );
62
+
63
+ return (
64
+ <div className={clsx("flex items-start gap-3", className)}>
65
+ <div className="relative" style={{ width: size, height: size }}>
66
+ {/* Leading visual by variant */}
67
+ {props.variant === "avatar" && (
68
+ <img
69
+ src={props.src}
70
+ alt={title}
71
+ className="h-full w-full rounded-xs object-cover border border-[#f1f1f1]"
72
+ />
73
+ )}
74
+
75
+ {props.variant === "initial" && (
76
+ <div
77
+ className={clsx(
78
+ "grid h-full w-full place-items-center rounded-xs text-[15px] font-semibold text-[#2c2c2c]",
79
+ props.textClassName,
80
+ )}
81
+ style={{ backgroundColor: props.bg ?? "#FFE7DB" }}
82
+ >
83
+ {props.initial}
84
+ </div>
85
+ )}
86
+
87
+ {props.variant === "logo" && (
88
+ <div
89
+ className="grid h-full w-full place-items-center rounded-xs"
90
+ style={{ boxShadow: `0 0 0 1px ${props.ringColor ?? "#EDEDED"} inset` }}
91
+ >
92
+ <img
93
+ src={props.src}
94
+ alt={title}
95
+ className="h-full w-full rounded-xs object-cover"
96
+ />
97
+ </div>
98
+ )}
99
+
100
+ {/* Online dot RIGHT SIDE */}
101
+ {online ? (
102
+ <span
103
+ className="absolute rounded-full bg-[#1E8E3E] ring-1 ring-white"
104
+ style={{
105
+ width: "15px",
106
+ height: "15px",
107
+ right: 0,
108
+ bottom: 0,
109
+ }}
110
+ />
111
+ ) : (
112
+ <span
113
+ className="absolute rounded-full bg-[#eb2127] ring-1 ring-white"
114
+ style={{
115
+ width: "15px",
116
+ height: "15px",
117
+ right: 0,
118
+ bottom: 0,
119
+ }}
120
+ />
121
+ )}
122
+ </div>
123
+
124
+ <div>
125
+ <div className="text-[16px] font-semibold text-[#2c2c2c] flex items-center gap-1">
126
+ <span className="max-w-[300px] truncate">{title}</span>
127
+ {verified && (
128
+ <span>
129
+ <BlueBadgeIcon />
130
+ </span>
131
+ )}
132
+ </div>
133
+ {subtitle &&
134
+ (title === "banbox.com" ? (
135
+ <div className="flex items-center -ml-[5px]">
136
+ <Lottie
137
+ animationData={globe}
138
+ loop={true}
139
+ autoplay={true}
140
+ style={{ width: "22px", marginBottom: "1.5px" }}
141
+ />
142
+ <div className={subtitleClass}>{subtitle}</div>
143
+ </div>
144
+ ) : (
145
+ <div className={subtitleClass}>{subtitle}</div>
146
+ ))}
147
+ </div>
148
+ </div>
149
+ );
150
+ };
151
+
152
+ export default ChatIdentity;
@@ -1,16 +1,10 @@
1
- // components/ui/chat/ChatListHeader.tsx
2
1
  "use client";
3
2
 
4
- import { useEffect, useRef, useState } from "react";
3
+ import clsx from "clsx";
5
4
  import type { Variants } from "framer-motion";
6
5
  import { AnimatePresence, motion } from "framer-motion";
7
-
6
+ import React from "react";
8
7
  import { MessageIcon, ChatSearchIcon, ChatXIcon } from "../../icons";
9
- import { cn } from "../../utils/cn";
10
-
11
- /* =======================
12
- Types
13
- ======================= */
14
8
 
15
9
  type Props = {
16
10
  className?: string;
@@ -18,27 +12,27 @@ type Props = {
18
12
  onSearchChange?: (value: string) => void;
19
13
  };
20
14
 
21
- /* =======================
22
- Component
23
- ======================= */
24
-
25
- const ChatListHeader = ({ className, onClose, onSearchChange }: Props) => {
26
- const [searching, setSearching] = useState(false);
27
- const [q, setQ] = useState("");
28
- const inputRef = useRef<HTMLInputElement>(null);
29
-
30
- useEffect(() => {
31
- if (searching) {
32
- const timer = setTimeout(() => inputRef.current?.focus(), 200);
33
- return () => clearTimeout(timer);
34
- }
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
+ };
35
30
  }, [searching]);
36
31
 
37
- useEffect(() => {
32
+ React.useEffect(() => {
38
33
  if (!searching) {
39
34
  return;
40
35
  }
41
-
42
36
  const onKey = (e: KeyboardEvent) => {
43
37
  if (e.key === "Escape") {
44
38
  setSearching(false);
@@ -46,48 +40,32 @@ const ChatListHeader = ({ className, onClose, onSearchChange }: Props) => {
46
40
  onSearchChange?.("");
47
41
  }
48
42
  };
49
-
50
43
  window.addEventListener("keydown", onKey);
51
- return () => window.removeEventListener("keydown", onKey);
44
+ return () => {
45
+ window.removeEventListener("keydown", onKey);
46
+ };
52
47
  }, [searching, onSearchChange]);
53
48
 
54
- // const clearInside = () => {
55
- // setQ("");
56
- // onSearchChange?.("");
57
- // inputRef.current?.focus();
58
- // };
49
+ const _clearInside = () => {
50
+ setQ("");
51
+ onSearchChange?.("");
52
+ inputRef.current?.focus();
53
+ };
59
54
 
55
+ // Use cubic-bezier tuples to satisfy the Transition type
60
56
  const variants: Variants = {
61
- inFromRight: {
62
- opacity: 1,
63
- x: 0,
64
- transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] },
65
- },
66
- outToLeft: {
67
- opacity: 0,
68
- x: -24,
69
- transition: { duration: 0.16, ease: [0.4, 0, 1, 1] },
70
- },
71
- inFromLeft: {
72
- opacity: 1,
73
- x: 0,
74
- transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] },
75
- },
76
- outToRight: {
77
- opacity: 0,
78
- x: 24,
79
- transition: { duration: 0.16, ease: [0.4, 0, 1, 1] },
80
- },
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] } },
81
61
  };
82
62
 
83
63
  return (
84
- <div className={cn("h-[64px] border-b border-[#ededed]", className)}>
64
+ <div className={clsx("h-[64px] border-b border-[#ededed]", className)}>
85
65
  <div className="flex h-full items-center px-[20px]">
86
66
  <AnimatePresence initial={false} mode="wait">
87
67
  {!searching ? (
88
- /* =======================
89
- Normal header
90
- ======================= */
68
+ // NORMAL (title) — appears from left when closing search
91
69
  <motion.div
92
70
  key="normal"
93
71
  className="flex w-full items-center justify-between"
@@ -97,36 +75,34 @@ const ChatListHeader = ({ className, onClose, onSearchChange }: Props) => {
97
75
  variants={variants}
98
76
  >
99
77
  <div className="flex items-center gap-3">
100
- <div className="flex items-center gap-2 text-[#2c2c2c]">
101
- <MessageIcon className="h-6 w-6" />
102
- <span className="text-[22px] font-semibold">Messenger</span>
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>
103
83
  </div>
104
84
  </div>
105
85
 
106
86
  <div className="flex items-center gap-2">
107
87
  <button
108
- type="button"
109
88
  title="Search"
110
89
  onClick={() => setSearching(true)}
111
- className="flex h-9 w-9 items-center justify-center rounded-full hover:bg-black/5"
90
+ className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center"
112
91
  >
113
- <ChatSearchIcon className="h-6 w-6" />
92
+ <ChatSearchIcon className="w-5 h-5" />
114
93
  </button>
115
94
 
116
95
  <button
117
- type="button"
118
96
  title="Close"
119
97
  onClick={onClose}
120
- className="flex h-9 w-9 items-center justify-center rounded-full hover:bg-black/5"
98
+ className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center"
121
99
  >
122
- <ChatXIcon className="h-6 w-6" />
100
+ <ChatXIcon className="w-6 h-6" />
123
101
  </button>
124
102
  </div>
125
103
  </motion.div>
126
104
  ) : (
127
- /* =======================
128
- Search header
129
- ======================= */
105
+ // SEARCH — enters from right, exits to right
130
106
  <motion.div
131
107
  key="search"
132
108
  className="flex w-full items-center gap-3"
@@ -136,10 +112,10 @@ const ChatListHeader = ({ className, onClose, onSearchChange }: Props) => {
136
112
  variants={variants}
137
113
  >
138
114
  <div className="relative flex-1">
139
- <div className="flex justify-between h-10 w-full items-center gap-1.5 rounded-full border border-[#6A6A6A] bg-white px-[4px]">
140
- <div className="ms-[12px] flex items-center">
141
- <span className="mr-2 grid h-6 w-6 shrink-0 place-items-center text-#929292]">
142
- <ChatSearchIcon className="h-5 w-5" />
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" />
143
119
  </span>
144
120
  <span className="mr-2 h-6 w-px shrink-0 bg-[#e1e1e1]" />
145
121
  <input
@@ -154,18 +130,19 @@ const ChatListHeader = ({ className, onClose, onSearchChange }: Props) => {
154
130
  />
155
131
  </div>
156
132
 
157
- <button
158
- type="button"
159
- title="Close search"
160
- onClick={() => {
161
- setSearching(false);
162
- setQ("");
163
- onSearchChange?.("");
164
- }}
165
- className="flex h-8! w-8! items-center justify-center rounded-full hover:bg-black/5"
166
- >
167
- <ChatXIcon className="h-5 w-5" />
168
- </button>
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>
169
146
  </div>
170
147
  </div>
171
148
  </motion.div>