@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,16 +1,11 @@
1
+ // components/chat/ui/chat/AttachmentPreviewStrip.tsx
1
2
  "use client";
2
-
3
- import type { FC } from "react";
3
+ import clsx from "clsx";
4
+ import React from "react";
4
5
  import { FileIcon, ChatXIcon } from "../../icons";
5
- import { cn } from "../../utils/cn";
6
-
7
- /* =======================
8
- Helpers
9
- ======================= */
10
6
 
11
- const extColor = (ext: string): string => {
7
+ const extColor = (ext: string) => {
12
8
  const e = ext.toLowerCase();
13
-
14
9
  if (e === "pdf") {
15
10
  return "text-[#D93025]";
16
11
  }
@@ -20,143 +15,99 @@ const extColor = (ext: string): string => {
20
15
  if (e === "doc" || e === "docx") {
21
16
  return "text-[#2B579A]";
22
17
  }
23
-
24
18
  return "text-[#6B7280]";
25
19
  };
26
20
 
27
- /* =======================
28
- Types
29
- ======================= */
30
-
31
- export type FilePreview = {
21
+ export const FilePreviewChip: React.FC<{
32
22
  name: string;
33
23
  sizeMB: number;
34
24
  ext: string;
35
- href: string;
36
- downloadName?: string;
37
- };
25
+ onRemove: () => void;
26
+ }> = ({ name, sizeMB, ext, onRemove }) => (
27
+ <div className="mr-2 inline-flex items-center gap-3 whitespace-nowrap rounded-sm border border-[#e1e1e1] bg-white px-3 py-2 h-[65px] max-w-[185px]">
28
+ <div className="flex min-w-0 items-center gap-2">
29
+ <div className="min-w-0">
30
+ <div className="flex items-center gap-1">
31
+ <FileIcon className={clsx("h-[18px] w-[18px]", extColor(ext))} />{" "}
32
+ <div className="truncate text-sm font-normal text-black">{name}</div>
33
+ </div>
34
+ <div className="flex items-center gap-2 text-xs text-[#636363] mt-2">
35
+ <span>{sizeMB.toFixed(1)} MB</span>
36
+ <span className="h-3 w-px bg-[#e1e1e1]" />
37
+ <span className="uppercase">{ext}</span>
38
+ </div>
39
+ </div>
40
+ </div>
41
+ <button
42
+ type="button"
43
+ onClick={onRemove}
44
+ className="grid h-8 w-8 place-items-center rounded-full bg-white text-[#3D3D3D] shadow-[0px_2px_4px_0px_#A5A3AE4D] hover:bg-black/5"
45
+ title="Remove"
46
+ aria-label="Remove file"
47
+ >
48
+ <ChatXIcon className="h-[18px] w-[18px]" />
49
+ </button>
50
+ </div>
51
+ );
52
+
53
+ const ImageThumb: React.FC<{ url: string; onRemove: () => void }> = ({ url, onRemove }) => (
54
+ <div className="relative mr-2 inline-block h-[65px] w-[65px] rounded-sm border border-[#e1e1e1] bg-[#F7F7F7]">
55
+ <img src={url} alt="" className="h-full w-full object-cover rounded-sm" />
56
+ <button
57
+ type="button"
58
+ onClick={onRemove}
59
+ className="absolute left-1/2 top-1/2 z-10 grid h-6 w-6 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full bg-black/30 text-[#3D3D3D] shadow-[0px_2px_4px_0px_#A5A3AE4D]"
60
+ aria-label="Remove image"
61
+ title="Remove image"
62
+ >
63
+ <ChatXIcon className="h-4 w-4 text-white" />
64
+ </button>
65
+ </div>
66
+ );
38
67
 
39
- type FilePreviewChipProps = {
68
+ export type FilePreview = {
40
69
  name: string;
41
70
  sizeMB: number;
42
71
  ext: string;
43
- onRemove: () => void;
44
- };
45
-
46
- type ImageThumbProps = {
47
- url: string;
48
- onRemove: () => void;
72
+ href: string;
73
+ downloadName?: string;
49
74
  };
50
75
 
51
- type AttachmentPreviewStripProps = {
76
+ type Props = {
52
77
  imgPreviews: string[];
53
78
  filePreviews: FilePreview[];
54
79
  onRemoveImage: (index: number) => void;
55
80
  onRemoveFile: (index: number) => void;
56
81
  };
57
82
 
58
- /* =======================
59
- Sub Components
60
- ======================= */
61
-
62
- export const FilePreviewChip: FC<FilePreviewChipProps> = ({ name, sizeMB, ext, onRemove }) => {
63
- return (
64
- <div
65
- className={cn(
66
- "mr-2 inline-flex h-[65px] max-w-[185px] items-center gap-3 whitespace-nowrap rounded-sm",
67
- "border border-[#e1e1e1] bg-white px-3 py-2",
68
- )}
69
- >
70
- <div className="flex min-w-0 items-center gap-2">
71
- <div className="min-w-0">
72
- <div className="flex items-center gap-1">
73
- <FileIcon className={cn("h-[18px] w-[18px]", extColor(ext))} />
74
- <div className="truncate text-[13px] font-normal text-black">{name}</div>
75
- </div>
76
-
77
- <div className="mt-2 flex items-center gap-2 text-xs text-[#636363]">
78
- <span>{sizeMB.toFixed(1)} MB</span>
79
- <span className="h-3 w-px bg-[#e1e1e1]" />
80
- <span className="uppercase">{ext}</span>
81
- </div>
82
- </div>
83
- </div>
84
-
85
- <button
86
- type="button"
87
- onClick={onRemove}
88
- title="Remove"
89
- aria-label="Remove file"
90
- className={cn(
91
- "grid h-8 w-8 place-items-center rounded-full bg-white text-[#3D3D3D]",
92
- "shadow-[0px_2px_4px_0px_#A5A3AE4D] hover:bg-black/5",
93
- )}
94
- >
95
- <ChatXIcon className="h-[18px] w-[18px]" />
96
- </button>
97
- </div>
98
- );
99
- };
100
-
101
- const ImageThumb: FC<ImageThumbProps> = ({ url, onRemove }) => {
102
- return (
103
- <div
104
- className={cn(
105
- "relative mr-2 inline-block h-[65px] w-[65px] rounded-sm",
106
- "border border-[#e1e1e1] bg-[#F7F7F7]",
107
- )}
108
- >
109
- <img src={url} alt="" className="h-full w-full rounded-sm object-cover" loading="lazy" />
110
-
111
- <button
112
- type="button"
113
- onClick={onRemove}
114
- aria-label="Remove image"
115
- title="Remove image"
116
- className={cn(
117
- "absolute left-1/2 top-1/2 z-10 grid h-6 w-6 -translate-x-1/2 -translate-y-1/2 place-items-center",
118
- "rounded-full bg-black/30 text-white",
119
- "shadow-[0px_2px_4px_0px_#A5A3AE4D]",
120
- )}
121
- >
122
- <ChatXIcon className="h-4 w-4 text-white" />
123
- </button>
124
- </div>
125
- );
126
- };
127
-
128
- /* =======================
129
- Component
130
- ======================= */
131
-
132
83
  /** Single-row, horizontally scrollable preview strip */
133
- const AttachmentPreviewStrip: FC<AttachmentPreviewStripProps> = ({
84
+ const AttachmentPreviewStrip: React.FC<Props> = ({
134
85
  imgPreviews,
135
86
  filePreviews,
136
87
  onRemoveFile,
137
88
  onRemoveImage,
138
89
  }) => {
139
90
  const has = imgPreviews.length > 0 || filePreviews.length > 0;
140
-
141
91
  if (!has) {
142
92
  return null;
143
93
  }
144
94
 
145
95
  return (
146
- <div className={cn("mx-auto mb-2 max-w-[410px] overflow-x-auto custom-scroll")}>
96
+ <div className="mb-2 max-w-[410px] mx-auto overflow-x-auto custom-scroll">
147
97
  <div className="flex items-start whitespace-nowrap">
148
- {filePreviews.map((file, index) => (
98
+ {filePreviews.map((f, i) => (
149
99
  <FilePreviewChip
150
- key={`${file.name}-${index}`}
151
- name={file.name}
152
- sizeMB={file.sizeMB}
153
- ext={file.ext}
154
- onRemove={() => onRemoveFile(index)}
100
+ key={`${f.name}-${i}`}
101
+ name={f.name}
102
+ sizeMB={f.sizeMB}
103
+ ext={f.ext}
104
+ onRemove={() => onRemoveFile(i)}
155
105
  />
156
106
  ))}
157
-
158
- {imgPreviews.map((url, index) => (
159
- <ImageThumb key={`${url}-${index}`} url={url} onRemove={() => onRemoveImage(index)} />
107
+ {imgPreviews.map((u, i) => (
108
+ <div key={`${u}-${i}`}>
109
+ <ImageThumb url={u} onRemove={() => onRemoveImage(i)} />
110
+ </div>
160
111
  ))}
161
112
  </div>
162
113
  </div>
@@ -1,15 +1,11 @@
1
+ // components/chat/ui/chat/ChatComposerBar.tsx
1
2
  "use client";
2
3
 
3
- import { useRef, useState } from "react";
4
-
4
+ import clsx from "clsx";
5
+ import React, { useRef, useState } from "react";
5
6
  import { ArrowSendAngleIcon, ArrowSendIcon, RecordMicIcon, ChatXIcon } from "../../icons";
6
- import { cn } from "../../utils/cn";
7
-
8
- /* =======================
9
- Types
10
- ======================= */
11
7
 
12
- type ChatComposerBarProps = {
8
+ type Props = {
13
9
  recording: boolean;
14
10
  seconds: number;
15
11
 
@@ -34,21 +30,14 @@ type ChatComposerBarProps = {
34
30
  fmtTime: (s: number) => string;
35
31
  };
36
32
 
37
- /* =======================
38
- Gradients
39
- ======================= */
40
-
33
+ // gradients
41
34
  const idleGradient =
42
35
  "linear-gradient(90.85deg, rgba(51, 201, 212, 0.5) 0%, rgba(39, 83, 251, 0.5) 29.98%, rgba(39, 83, 251, 0.5) 49.97%, rgba(39, 83, 251, 0.5) 64.96%, rgba(235, 67, 255, 0.5) 99.94%)";
43
36
 
44
37
  const activeGradient =
45
38
  "linear-gradient(90.85deg, #33C9D4 0%, #2753FB 29.98%, #2753FB 49.97%, #2753FB 64.96%, #EB43FF 99.94%)";
46
39
 
47
- /* =======================
48
- Component
49
- ======================= */
50
-
51
- const ChatComposerBar = ({
40
+ const ChatComposerBar: React.FC<Props> = ({
52
41
  recording,
53
42
  seconds,
54
43
  isTyping,
@@ -63,10 +52,11 @@ const ChatComposerBar = ({
63
52
  sendText,
64
53
  sendAttachments,
65
54
  fmtTime,
66
- }: ChatComposerBarProps) => {
55
+ }) => {
67
56
  const composingRef = useRef(false);
68
57
  const [isFocused, setIsFocused] = useState(false);
69
58
 
59
+ // active border when focused or recording
70
60
  const isActiveBorder = isFocused || recording;
71
61
 
72
62
  if (!recording) {
@@ -84,7 +74,7 @@ const ChatComposerBar = ({
84
74
  <button
85
75
  type="button"
86
76
  onClick={startRecording}
87
- className="grid h-[46px] w-[46px] place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
77
+ className="grid h-[44px] w-[44px] place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
88
78
  title="Record voice"
89
79
  aria-label="Record voice"
90
80
  >
@@ -95,9 +85,9 @@ const ChatComposerBar = ({
95
85
  <textarea
96
86
  ref={textRef}
97
87
  rows={1}
98
- autoFocus
99
- placeholder="Type a message"
100
- className="custom-scroll-hidden max-h-[200px] flex-1 resize-none bg-transparent px-3 py-2 outline-none placeholder:text-[#777]"
88
+ placeholder="Type a message…"
89
+ autoFocus={true}
90
+ className="max-h-[200px] flex-1 resize-none bg-transparent px-3 py-2 outline-none custom-scroll-hidden"
101
91
  value={text}
102
92
  onChange={(e) => onTextChange(e.target.value)}
103
93
  onInput={onAutoGrow}
@@ -127,9 +117,7 @@ const ChatComposerBar = ({
127
117
  />
128
118
  </div>
129
119
 
130
- {!canSendArrow ? (
131
- <div className="grid h-full w-px place-items-center bg-[#E7E7E7]" />
132
- ) : null}
120
+ {!canSendArrow && <div className="grid h-full w-px place-items-center bg-[#E7E7E7]" />}
133
121
 
134
122
  <div className="px-2">
135
123
  {isTyping ? (
@@ -138,9 +126,8 @@ const ChatComposerBar = ({
138
126
  <button
139
127
  type="button"
140
128
  onClick={sendText}
141
- className={cn(
142
- "ms-1 grid h-[40px] w-[40px] place-items-center rounded-full",
143
- "text-[#ff5301] hover:bg-[#f8f8f8]",
129
+ className={clsx(
130
+ "ms-1 grid h-[40px] w-[40px] place-items-center rounded-full text-[#ff5301] hover:bg-[#f8f8f8]",
144
131
  )}
145
132
  title={hasAttachments ? "Send attachments" : "Send"}
146
133
  aria-label="Send"
@@ -155,7 +142,7 @@ const ChatComposerBar = ({
155
142
  type="button"
156
143
  onClick={sendAttachments}
157
144
  disabled={!hasAttachments}
158
- className={cn(
145
+ className={clsx(
159
146
  "ms-1 grid h-[40px] w-[40px] place-items-center rounded-full hover:bg-[#f8f8f8]",
160
147
  hasAttachments ? "text-[#ff5301]" : "text-[#B9C3D4]",
161
148
  )}
@@ -173,22 +160,19 @@ const ChatComposerBar = ({
173
160
  );
174
161
  }
175
162
 
176
- /* =======================
177
- Recording state UI
178
- ======================= */
179
-
163
+ // Recording state UI
180
164
  return (
181
165
  <div className="flex w-full items-stretch gap-2">
182
166
  <div className="w-full rounded-sm p-px" style={{ background: activeGradient }}>
183
- <div className="flex h-[52px] w-full items-center justify-between rounded-[3px] bg-white">
167
+ <div className="flex min-h-[50px] w-full items-center justify-between rounded-[3px] bg-white">
184
168
  <button
185
169
  type="button"
186
- className="ms-[3px] grid h-[46px] w-[46px] place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
170
+ className="grid ms-[3px] h-[46px] w-[46px] place-items-center rounded-xs bg-[#f8f8f8] text-[#ff5301] hover:brightness-95"
187
171
  aria-label="Recording"
188
172
  title="Recording"
189
173
  >
190
174
  <RecordMicIcon
191
- className={cn(
175
+ className={clsx(
192
176
  "h-6 w-6",
193
177
  seconds % 2 === 0
194
178
  ? "text-[#929292]"
@@ -197,7 +181,7 @@ const ChatComposerBar = ({
197
181
  />
198
182
  </button>
199
183
 
200
- <div className="px-3 text-[13px]">{fmtTime(seconds)}</div>
184
+ <div className="px-3 text-sm">{fmtTime(seconds)}</div>
201
185
 
202
186
  <div className="ml-auto flex items-center gap-3 pr-2">
203
187
  <button
@@ -216,7 +200,7 @@ const ChatComposerBar = ({
216
200
  type="button"
217
201
  onClick={() => stopRecording(true)}
218
202
  className="grid h-10 w-[40px] place-items-center rounded-full text-[#ff5301]"
219
- title={hasAttachments ? "Send attachments" : "Send"}
203
+ title="Send"
220
204
  aria-label="Send"
221
205
  >
222
206
  <ArrowSendAngleIcon className="h-6 w-6" />
@@ -11,7 +11,7 @@ import ChatComposerBar from "./ChatComposerBar";
11
11
  import BusinessCardDropup from "./drop-up/BusinessCardDropup";
12
12
  import EmojiDropup from "./drop-up/EmojiDropup";
13
13
  import ReplyCard from "./ReplyCard";
14
- import type { MessageRef } from "../../types";
14
+ import type { MessageRef } from "./types";
15
15
  import type { SendPayload } from "../../types";
16
16
 
17
17
  /* Simple tooltip wrapper */