@banbox/chat 1.0.1 → 1.0.2

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,140 +1,133 @@
1
- "use client";
2
-
3
- import { BlueBadgeIcon } from "../../icons";
4
- import { cn } from "../../utils/cn";
5
-
6
- /* =======================
7
- Types
8
- ======================= */
9
-
10
- export type ChatThreadStatus =
11
- | { kind: "seen" }
12
- | { kind: "delivered" }
13
- | { kind: "new"; count: number };
14
-
15
- type Props = {
16
- active?: boolean;
17
- pinned?: boolean;
18
- online?: boolean;
19
- verified?: boolean;
20
-
21
- title: string;
22
- preview: string;
23
- time: string;
24
-
25
- status: ChatThreadStatus;
26
-
27
- avatarText: string;
28
- avatarSrc?: string;
29
- size?: number;
30
- avatarBg?: string;
31
- className?: string;
32
- onClick?: () => void;
33
- };
34
-
35
- /* =======================
36
- Helpers
37
- ======================= */
38
-
39
- const formatTwoDigits = (value: number): string => {
40
- return String(Math.max(0, value)).padStart(2, "0");
41
- };
42
-
43
- /* =======================
44
- Component
45
- ======================= */
46
-
47
- const ChatThreadItem = ({
48
- active = false,
49
- pinned = false,
50
- online = false,
51
- verified = false,
52
- title,
53
- preview,
54
- time,
55
- status,
56
- avatarText,
57
- avatarSrc,
58
- avatarBg = "#FFE5DA",
59
- className,
60
- onClick,
61
- }: Props) => {
62
- const statusEl = (() => {
63
- switch (status.kind) {
64
- case "seen":
65
- return <span className="text-[#005694]">Seen</span>;
66
-
67
- case "delivered":
68
- return <span className="text-[#929292]">Delivered</span>;
69
-
70
- case "new":
71
- return <span className="text-[#EB2127]">New {formatTwoDigits(status.count)}</span>;
72
- }
73
- })();
74
-
75
- return (
76
- <button
77
- type="button"
78
- onClick={onClick}
79
- className={cn(
80
- "relative w-full px-5 py-2 text-left focus:outline-none border-b border-[#f1f1f1]",
81
- "hover:bg-[#f8f8f8]",
82
- active && "bg-[#f8f8f8]",
83
- className,
84
- )}
85
- >
86
- {/* Pinned corner */}
87
- {pinned ? (
88
- <span className="absolute right-0 top-0 h-0 w-0 border-l-16 border-t-16 border-l-transparent border-t-[#FFD2BD]" />
89
- ) : null}
90
-
91
- <div className="flex items-start gap-3">
92
- <div className="relative shrink-0" style={{ width: 36, height: 36 }}>
93
- {avatarSrc ? (
94
- <div className="h-9 w-9 overflow-hidden rounded-[2px] border border-[#f1f1f1]">
95
- <img src={avatarSrc} alt={title} className="h-full w-full rounded-[2px] object-cover" />
96
- </div>
97
- ) : (
98
- <div
99
- className="grid h-9 w-9 place-items-center rounded-[2px] border border-[#f1f1f1] text-[15px] font-semibold text-[#2c2c2c]"
100
- style={{ backgroundColor: avatarBg }}
101
- >
102
- {avatarText}
103
- </div>
104
- )}
105
-
106
- <span
107
- className={cn(
108
- "absolute rounded-full",
109
- online ? "bg-[#74A380]" : "bg-[#EB2127]",
110
- )}
111
- style={{ bottom: -1.5, right: -1.5, width: 11.25, height: 11.25, border: "1px solid #FFFFFF" }}
112
- />
113
- </div>
114
-
115
- {/* Content */}
116
- <div className="min-w-0 flex-1">
117
- <div className="flex items-center gap-1 text-[14px]">
118
- <span className="truncate font-medium text-black">{title}</span>
119
- {verified ? (
120
- <span>
121
- <BlueBadgeIcon />
122
- </span>
123
- ) : null}
124
- </div>
125
-
126
- <div className="truncate text-xs font-normal text-[#2c2c2c]">
127
- {preview}
128
- </div>
129
-
130
- <div className="mt-0.5 flex items-center justify-between text-[12px]">
131
- <div>{statusEl}</div>
132
- <span className="font-light text-[#636363] tracking-[0.5px]">{time}</span>
133
- </div>
134
- </div>
135
- </div>
136
- </button>
137
- );
138
- };
139
-
140
- export default ChatThreadItem;
1
+ "use client";
2
+
3
+ import clsx from "clsx";
4
+ import React from "react";
5
+ import { BlueBadgeIcon } from "../../icons";
6
+
7
+ export type ChatThreadStatus =
8
+ | { kind: "seen" }
9
+ | { kind: "delivered" }
10
+ | { kind: "new"; count: number };
11
+
12
+ type Props = {
13
+ active?: boolean;
14
+ pinned?: boolean;
15
+ online?: boolean;
16
+ verified?: boolean;
17
+
18
+ title: string;
19
+ preview: string; // last message snippet
20
+ time: string; // "29 Jul 2025 16:51"
21
+ status: ChatThreadStatus;
22
+
23
+ avatarText: string; // e.g. "A"
24
+ avatarSrc?: string; // e.g. "A"
25
+ _size?: number;
26
+ avatarBg?: string; // default soft peach
27
+ className?: string;
28
+ onClick?: () => void;
29
+ };
30
+
31
+ const ChatThreadItem: React.FC<Props> = ({
32
+ active,
33
+ pinned,
34
+ online,
35
+ verified,
36
+ title,
37
+ preview,
38
+ time,
39
+ status,
40
+ avatarText,
41
+ avatarSrc,
42
+ _size = 46,
43
+ avatarBg = "#FFF1EC",
44
+ className,
45
+ onClick,
46
+ }) => {
47
+ const count = status.kind === "new" ? String(Math.max(0, status.count)).padStart(2, "0") : "";
48
+
49
+ const statusEl = (() => {
50
+ switch (status.kind) {
51
+ case "seen":
52
+ return <span className="text-[#0D5EA8]">Seen</span>;
53
+
54
+ case "delivered":
55
+ return <span className="text-[#B7B7B7]">Delivered</span>;
56
+
57
+ case "new":
58
+ return (
59
+ <span className="text-[#E63946]">
60
+ {count} New
61
+ </span>
62
+ );
63
+
64
+ default:
65
+ return null;
66
+ }
67
+ })();
68
+
69
+ return (
70
+ <button
71
+ onClick={onClick}
72
+ className={clsx(
73
+ "relative w-full text-left px-5 py-2 hover:bg-[#f8f8f8] focus:outline-none h-[75px]",
74
+ active && "bg-[#f8f8f8]",
75
+ className,
76
+ )}
77
+ >
78
+ {/* Pinned corner triangle */}
79
+ {pinned && (
80
+ <span className="absolute right-0 top-0 h-0 w-0 border-l-16 border-t-16 border-l-transparent border-t-[#FFD2BD]" />
81
+ )}
82
+
83
+ <div className="flex items-start gap-3 border-b border-[#f8f8f8] pb-2">
84
+ {/* Avatar + online */}
85
+ <div className="relative mt-[2px]">
86
+ {avatarSrc ? (
87
+ <div className="grid h-9 w-9 place-items-center rounded-xs font-semibold text-2xl border border-[#f1f1f1] relative overflow-hidden">
88
+ <img
89
+ src={avatarSrc}
90
+ alt={title}
91
+ className="h-full w-full rounded-xs object-cover border border-[#f1f1f1]"
92
+ />
93
+ </div>
94
+ ) : (
95
+ <div
96
+ className="grid h-9 w-9 place-items-center rounded-xs font-semibold text-[#2c2c2c] text-2xl border border-[#f1f1f1]"
97
+ style={{ backgroundColor: avatarBg }}
98
+ >
99
+ {avatarText}
100
+ </div>
101
+ )}
102
+
103
+ <span
104
+ className={`absolute bottom-0 right-0 h-2.5 w-2.5 rounded-full ${
105
+ online ? "bg-[#328545]" : "bg-[#eb2127]"
106
+ } ring-1 ring-white`}
107
+ />
108
+ </div>
109
+
110
+ {/* Text block */}
111
+ <div className="min-w-0 flex-1">
112
+ <div className="flex items-center gap-1 text-sm">
113
+ <span className="truncate font-medium">{title}</span>
114
+ {verified && (
115
+ <span>
116
+ <BlueBadgeIcon className="mt-[2px]" />
117
+ </span>
118
+ )}
119
+ </div>
120
+
121
+ <div className="truncate text-xs font-normal text-[#2c2c2c]">{preview}</div>
122
+
123
+ <div className="mt-0.5 flex items-center justify-between text-xs font-normal">
124
+ <div>{statusEl}</div>
125
+ <span className="text-[#636363]">{time}</span>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </button>
130
+ );
131
+ };
132
+
133
+ export default ChatThreadItem;
@@ -1,120 +1,136 @@
1
- "use client";
2
-
3
- import React from "react";
4
-
5
- import { MessageReplayIcon } from "../../icons";
6
- import { NewLanguageIcon } from "../../icons";
7
- import { cn } from "../../utils/cn";
8
-
9
- /* =======================
10
- Types
11
- ======================= */
12
-
13
- type ItemButton = "replay" | "translate";
14
-
15
- type Props = {
16
- mine: boolean;
17
- onReply?: () => void;
18
- onTranslate?: () => void;
19
- children: React.ReactNode;
20
- className?: string;
21
- alwaysVisible?: boolean;
22
-
23
- /** Which buttons to show (omit => show all) */
24
- isItemButton?: ItemButton[];
25
- /** Which buttons are “active/on” for styling */
26
- activeButtons?: ItemButton[];
27
- };
28
-
29
- /* =======================
30
- Component
31
- ======================= */
32
-
33
- const MessageHoverActions: React.FC<Props> = ({
34
- mine,
35
- onReply,
36
- onTranslate,
37
- children,
38
- className,
39
- alwaysVisible = false,
40
- isItemButton,
41
- activeButtons,
42
- }) => {
43
- const sidePos = mine ? "right-full" : "left-full";
44
- const railNudge = mine ? "-translate-x-1.5" : "translate-x-1.5";
45
-
46
- const showReplay = !isItemButton || isItemButton.includes("replay");
47
- const showTranslate = !isItemButton || isItemButton.includes("translate");
48
- const hasAny = showReplay || showTranslate;
49
-
50
- const isActive = (k: ItemButton) => Boolean(activeButtons?.includes(k));
51
-
52
- return (
53
- <div className={cn("relative inline-flex group/message", className)}>
54
- {children}
55
-
56
- {hasAny ? (
57
- <div
58
- aria-hidden
59
- className={cn(
60
- "pointer-events-auto absolute inset-y-0 w-2",
61
- mine ? "right-full" : "left-full",
62
- )}
63
- />
64
- ) : null}
65
-
66
- {hasAny ? (
67
- <div
68
- className={cn(
69
- "pointer-events-auto absolute bottom-0 transition-opacity",
70
- sidePos,
71
- railNudge,
72
- alwaysVisible ? "opacity-100" : "opacity-0 group-hover/message:opacity-100",
73
- )}
74
- >
75
- <div className="flex gap-2 pb-[2px]">
76
- {showReplay ? (
77
- <button
78
- type="button"
79
- onClick={(e) => {
80
- e.stopPropagation();
81
- onReply?.();
82
- }}
83
- className={cn(
84
- "inline-flex h-[22px] w-[22px] items-center justify-center rounded-xs bg-white ",
85
- "shadow-[0_1px_3px_rgba(0,0,0,0.08)] hover:bg-[#f8f8f8]",
86
- isActive("replay") ? "bg-[#636363] text-white" : "text-[#2c2c2c]",
87
- )}
88
- title="Reply"
89
- aria-label="Reply"
90
- >
91
- <MessageReplayIcon className="h-[14px] w-[14px]" />
92
- </button>
93
- ) : null}
94
-
95
- {showTranslate ? (
96
- <button
97
- type="button"
98
- onClick={(e) => {
99
- e.stopPropagation();
100
- onTranslate?.();
101
- }}
102
- className={cn(
103
- "inline-flex h-[22px] w-[22px] items-center justify-center rounded-xs bg-white ",
104
- "shadow-banbox-card-secondary hover:bg-[#f8f8f8]",
105
- isActive("translate") ? "bg-[#636363]! text-white" : "text-[#2c2c2c]",
106
- )}
107
- title="Translate"
108
- aria-label="Translate"
109
- >
110
- <NewLanguageIcon className="h-[14px] w-[14px]" />
111
- </button>
112
- ) : null}
113
- </div>
114
- </div>
115
- ) : null}
116
- </div>
117
- );
118
- };
119
-
120
- export default MessageHoverActions;
1
+ // components/ui/chat/MessageHoverActions.tsx
2
+ "use client";
3
+
4
+ import clsx from "clsx";
5
+ import React from "react";
6
+ import { MessageReplayIcon, NewLanguageIcon } from "../../icons";
7
+
8
+ type ItemButton = "replay" | "translate";
9
+
10
+ type Props = {
11
+ mine: boolean;
12
+ onReply?: () => void;
13
+ onTranslate?: () => void;
14
+ children: React.ReactNode;
15
+ className?: string;
16
+ alwaysVisible?: boolean;
17
+
18
+ /** Which buttons to show (omit => show all) */
19
+ isItemButton?: ItemButton[];
20
+ /** Which buttons are "active/on" for styling */
21
+ activeButtons?: ItemButton[];
22
+ };
23
+
24
+ const MessageHoverActions: React.FC<Props> = ({
25
+ mine,
26
+ onReply,
27
+ onTranslate,
28
+ children,
29
+ className,
30
+ alwaysVisible = false,
31
+ isItemButton,
32
+ activeButtons,
33
+ }) => {
34
+ const sidePos = mine ? "right-full" : "left-full";
35
+ const railNudge = mine ? "-translate-x-1.5" : "translate-x-1.5";
36
+
37
+ const showReplay = !isItemButton || isItemButton.includes("replay");
38
+ const showTranslate = !isItemButton || isItemButton.includes("translate");
39
+ const hasAny = showReplay || showTranslate;
40
+
41
+ const isActive = (k: ItemButton) => !!activeButtons?.includes(k);
42
+
43
+ return (
44
+ <div className={clsx("relative inline-flex group/message", className)}>
45
+ {children}
46
+
47
+ {hasAny && (
48
+ <div
49
+ aria-hidden
50
+ className={clsx(
51
+ "absolute inset-y-0 w-2",
52
+ mine ? "right-full" : "left-full",
53
+ "pointer-events-auto",
54
+ )}
55
+ />
56
+ )}
57
+
58
+ {hasAny && (
59
+ <div
60
+ className={clsx(
61
+ "absolute bottom-0 pointer-events-auto transition-opacity",
62
+ sidePos,
63
+ railNudge,
64
+ alwaysVisible ? "opacity-100" : "opacity-0 group-hover/message:opacity-100",
65
+ )}
66
+ >
67
+ <div className="flex gap-2 pb-[2px]">
68
+ {showReplay && (
69
+ <div className="relative group/replay">
70
+ {/* Tooltip */}
71
+ <div className="absolute bottom-full right-0 mb-0.5 pointer-events-none opacity-0 group-hover/replay:opacity-100 transition-opacity duration-150">
72
+ <div className="bg-[#636363] rounded-[6px] px-2 h-[28px] flex items-center whitespace-nowrap">
73
+ <span className="text-[12px] text-white font-normal tracking-[0.4px]">
74
+ Reply
75
+ </span>
76
+ </div>
77
+ {/* Caret — right-aligned */}
78
+ <div className="flex justify-end pr-1.5">
79
+ <div className="w-0 h-0 border-l-[5px] border-l-transparent border-r-[5px] border-r-transparent border-t-[5px] border-t-[#636363]" />
80
+ </div>
81
+ </div>
82
+
83
+ <button
84
+ type="button"
85
+ onClick={(e) => {
86
+ e.stopPropagation();
87
+ onReply?.();
88
+ }}
89
+ className={clsx(
90
+ "inline-flex h-[22px] w-[22px] items-center justify-center rounded-sm bg-white text-[#2c2c2c] shadow-[0_1px_3px_rgba(0,0,0,0.08)] hover:bg-[#f8f8f8]",
91
+ isActive("replay") && "bg-[#636363] text-white",
92
+ )}
93
+ >
94
+ <MessageReplayIcon className="h-[14px] w-[14px]" />
95
+ </button>
96
+ </div>
97
+ )}
98
+
99
+ {showTranslate && (
100
+ <div className="relative group/translate">
101
+ {/* Tooltip */}
102
+ <div className="absolute bottom-full right-0 mb-0.5 pointer-events-none opacity-0 group-hover/translate:opacity-100 transition-opacity duration-150">
103
+ <div className="bg-[#636363] rounded-[6px] px-2 h-[28px] flex items-center whitespace-nowrap">
104
+ <span className="text-[12px] text-white font-normal leading-4 tracking-[0.4px]">
105
+ {isActive("translate") ? "Back to Original" : "Translate"}
106
+ </span>
107
+ </div>
108
+ {/* Caret — right-aligned */}
109
+ <div className="flex justify-end pr-1.5">
110
+ <div className="w-0 h-0 border-l-[5px] border-l-transparent border-r-[5px] border-r-transparent border-t-[5px] border-t-[#636363]" />
111
+ </div>
112
+ </div>
113
+
114
+ <button
115
+ type="button"
116
+ onClick={(e) => {
117
+ e.stopPropagation();
118
+ onTranslate?.();
119
+ }}
120
+ className={clsx(
121
+ "inline-flex h-[22px] w-[22px] items-center justify-center rounded-sm bg-white text-[#2c2c2c] shadow-[0_1px_3px_rgba(0,0,0,0.08)] hover:bg-[#f8f8f8]",
122
+ isActive("translate") && "!bg-[#636363] text-white",
123
+ )}
124
+ >
125
+ <NewLanguageIcon className="h-[14px] w-[14px]" />
126
+ </button>
127
+ </div>
128
+ )}
129
+ </div>
130
+ </div>
131
+ )}
132
+ </div>
133
+ );
134
+ };
135
+
136
+ export default MessageHoverActions;
@@ -1,7 +1,9 @@
1
1
  "use client";
2
2
 
3
+ import Lottie from "lottie-react";
3
4
  import React from "react";
4
5
  import { cn } from "../../utils/cn";
6
+ import dots from "../../lottie/typingdotanimation2.json";
5
7
 
6
8
  /* =======================
7
9
  Types
@@ -9,51 +11,24 @@ import { cn } from "../../utils/cn";
9
11
 
10
12
  type Props = {
11
13
  /** Pixel box for the animation area */
12
- size?: number;
13
- loop?: boolean;
14
- autoplay?: boolean;
14
+ size?: number; // default 18 (inside badge)
15
+ loop?: boolean; // default true
16
+ autoplay?: boolean; // default true
15
17
  className?: string;
16
18
  ariaLabel?: string;
19
+
17
20
  /** Avatar size in px */
18
- avatarSize?: number;
21
+ avatarSize?: number; // default 40
19
22
  };
20
23
 
21
- /* =======================
22
- CSS Typing Dots (no external dependency)
23
- ======================= */
24
-
25
- const TypingDots: React.FC = () => (
26
- <>
27
- <style>{`
28
- @keyframes banbox-typing-bounce {
29
- 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30
- 30% { transform: translateY(-5px); opacity: 1; }
31
- }
32
- .banbox-typing-dot {
33
- width: 7px;
34
- height: 7px;
35
- border-radius: 50%;
36
- background: #888;
37
- display: inline-block;
38
- animation: banbox-typing-bounce 1.2s infinite ease-in-out;
39
- }
40
- .banbox-typing-dot:nth-child(1) { animation-delay: 0s; }
41
- .banbox-typing-dot:nth-child(2) { animation-delay: 0.2s; }
42
- .banbox-typing-dot:nth-child(3) { animation-delay: 0.4s; }
43
- `}</style>
44
- <span style={{ display: "inline-flex", gap: "4px", alignItems: "center", height: "16px" }}>
45
- <span className="banbox-typing-dot" />
46
- <span className="banbox-typing-dot" />
47
- <span className="banbox-typing-dot" />
48
- </span>
49
- </>
50
- );
51
-
52
24
  /* =======================
53
25
  Component
54
26
  ======================= */
55
27
 
56
28
  const TypingIndicator: React.FC<Props> = ({
29
+ size = 18,
30
+ loop = true,
31
+ autoplay = true,
57
32
  className,
58
33
  ariaLabel = "Typing…",
59
34
  avatarSize = 40,
@@ -62,7 +37,7 @@ const TypingIndicator: React.FC<Props> = ({
62
37
 
63
38
  return (
64
39
  <div
65
- className={cn("flex items-end gap-[6px]", className)}
40
+ className={cn("relative flex items-end gap-[6px]", className)}
66
41
  role="status"
67
42
  aria-label={ariaLabel}
68
43
  >
@@ -77,15 +52,20 @@ const TypingIndicator: React.FC<Props> = ({
77
52
  className="h-full w-full rounded-full object-cover"
78
53
  />
79
54
 
80
- {isOnline ? (
81
- <span className="absolute bottom-0 right-0 h-[11.25px] w-[11.25px] rounded-full bg-[#328545] ring-1 ring-white" />
82
- ) : null}
55
+ {isOnline && (
56
+ <span className="absolute bottom-[0px] right-[0px] h-[11.25px] w-[11.25px] rounded-full bg-[#328545] ring-1 ring-white" />
57
+ )}
83
58
  </div>
84
59
 
85
- {/* Typing dots inline bubble next to avatar */}
86
- <div className="flex items-center rounded-[18px] bg-[#f0f0f0] px-3 py-2">
87
- <TypingDots />
88
- </div>
60
+ {/* typing lottie at bottom-right */}
61
+ <span className="absolute bottom-[-13px] left-[30px]">
62
+ <Lottie
63
+ animationData={dots}
64
+ loop={loop}
65
+ autoplay={autoplay}
66
+ style={{ width: "80px", height: "38px" }}
67
+ />
68
+ </span>
89
69
  </div>
90
70
  );
91
71
  };