customer-module-frontend 1.0.1-beta.1

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.
Files changed (87) hide show
  1. package/.cursor/rules/context.md +306 -0
  2. package/.cursor/rules/guardrails.md +35 -0
  3. package/.env +1 -0
  4. package/.github/workflows/publish-to-npm-beta.yml +51 -0
  5. package/.github/workflows/publish-to-npm.yml +58 -0
  6. package/README.md +73 -0
  7. package/eslint.config.js +23 -0
  8. package/index.html +13 -0
  9. package/package.json +43 -0
  10. package/postcss-unwrap-layers.js +31 -0
  11. package/postcss.config.js +11 -0
  12. package/public/vite.svg +1 -0
  13. package/src/App.css +40 -0
  14. package/src/App.tsx +58 -0
  15. package/src/assets/accounts.svg +3 -0
  16. package/src/assets/at_the_rate.svg +10 -0
  17. package/src/assets/buildings.svg +3 -0
  18. package/src/assets/chat.svg +3 -0
  19. package/src/assets/close.svg +3 -0
  20. package/src/assets/contacts.svg +3 -0
  21. package/src/assets/conversation.svg +10 -0
  22. package/src/assets/customers.svg +10 -0
  23. package/src/assets/details.svg +3 -0
  24. package/src/assets/domain.svg +10 -0
  25. package/src/assets/edit.svg +15 -0
  26. package/src/assets/email.svg +3 -0
  27. package/src/assets/google.svg +8 -0
  28. package/src/assets/inbox.svg +0 -0
  29. package/src/assets/message.svg +3 -0
  30. package/src/assets/no-data.svg +9 -0
  31. package/src/assets/open_in_a_new_tab.svg +10 -0
  32. package/src/assets/phone.svg +3 -0
  33. package/src/assets/react.svg +1 -0
  34. package/src/assets/search.svg +3 -0
  35. package/src/assets/search_typing.svg +4 -0
  36. package/src/assets/sm_contacts.svg +3 -0
  37. package/src/assets/sm_inbox.svg +3 -0
  38. package/src/assets/sm_slider.svg +3 -0
  39. package/src/assets/status-resolved.svg +3 -0
  40. package/src/assets/status-snoozed.svg +4 -0
  41. package/src/assets/status_open.svg +1 -0
  42. package/src/components/AccountContacts/index.tsx +107 -0
  43. package/src/components/AccountDetails/index.tsx +102 -0
  44. package/src/components/AccountsConversation/index.tsx +75 -0
  45. package/src/components/Avatar/constants.tsx +45 -0
  46. package/src/components/Avatar/index.tsx +42 -0
  47. package/src/components/BreadcrumbsSection/index.tsx +16 -0
  48. package/src/components/Card/index.tsx +31 -0
  49. package/src/components/Card/types.ts +10 -0
  50. package/src/components/ContactConversation/Converation.tsx +14 -0
  51. package/src/components/ContactConversation/index.tsx +81 -0
  52. package/src/components/ContactDetails/index.tsx +111 -0
  53. package/src/components/Contacts/EditContact.tsx +213 -0
  54. package/src/components/Contacts/constants/index.tsx +24 -0
  55. package/src/components/Contacts/index.tsx +171 -0
  56. package/src/components/ConversationBox/constants.tsx +99 -0
  57. package/src/components/ConversationBox/index.tsx +147 -0
  58. package/src/components/ConversationBox/types.ts +20 -0
  59. package/src/components/CustomersLayout/index.tsx +20 -0
  60. package/src/components/DetailsCard/index.tsx +31 -0
  61. package/src/components/DetailsCard/types.ts +10 -0
  62. package/src/components/EmptyData/NoDataFound.tsx +31 -0
  63. package/src/components/Header/index.tsx +55 -0
  64. package/src/components/Icon/index.tsx +93 -0
  65. package/src/components/Icon/types.ts +13 -0
  66. package/src/components/Listing/AccountTable.tsx +47 -0
  67. package/src/components/Listing/ContactTable.tsx +76 -0
  68. package/src/components/RightPanel/AccountPanel.tsx +123 -0
  69. package/src/components/RightPanel/ContactPanel.tsx +142 -0
  70. package/src/components/RightPanel/index.tsx +167 -0
  71. package/src/components/Search/SearchDialog.tsx +150 -0
  72. package/src/components/TabsSection/index.tsx +49 -0
  73. package/src/constants/index.tsx +645 -0
  74. package/src/hooks/useBreadcrumb.tsx +93 -0
  75. package/src/index.css +315 -0
  76. package/src/main.tsx +14 -0
  77. package/src/pages/AccountDetail.tsx +68 -0
  78. package/src/pages/Accounts.tsx +12 -0
  79. package/src/pages/ContactDetail.tsx +55 -0
  80. package/src/pages/Contacts.tsx +12 -0
  81. package/src/stores/count.tsx +17 -0
  82. package/src/types/index.ts +0 -0
  83. package/tailwind.config.js +179 -0
  84. package/tsconfig.app.json +36 -0
  85. package/tsconfig.json +7 -0
  86. package/tsconfig.node.json +26 -0
  87. package/vite.config.ts +31 -0
@@ -0,0 +1,213 @@
1
+ import { useState } from "react";
2
+ import { Dialog, Button, TextField } from "hiver-ui-kit-extended";
3
+ import Avatar from "../Avatar";
4
+ import closeIcon from "../../assets/close.svg";
5
+ import Icon from "../Icon";
6
+
7
+ interface Contact {
8
+ id: string;
9
+ name: string;
10
+ email: string;
11
+ phone_number?: string;
12
+ }
13
+
14
+ interface EditContactProps {
15
+ contact?: Contact;
16
+ open?: boolean;
17
+ onClose?: () => void;
18
+ }
19
+
20
+ const EditContact = ({ contact, open = false, onClose }: EditContactProps) => {
21
+ const [dialogOpen, setDialogOpen] = useState(open);
22
+ const [formData, setFormData] = useState({
23
+ name: contact?.name || "",
24
+ email: contact?.email || "",
25
+ phone_number: contact?.phone_number || "",
26
+ });
27
+
28
+ const [errors, setErrors] = useState({
29
+ name: "",
30
+ email: "",
31
+ phone_number: "",
32
+ });
33
+
34
+ const handleClick = () => {
35
+ setDialogOpen(true);
36
+ };
37
+
38
+ const handleClose = () => {
39
+ setDialogOpen(false);
40
+ setFormData({
41
+ name: contact?.name || "",
42
+ email: contact?.email || "",
43
+ phone_number: contact?.phone_number || "",
44
+ });
45
+ setErrors({ name: "", email: "", phone_number: "" });
46
+ onClose?.();
47
+ };
48
+
49
+ const validateForm = () => {
50
+ const newErrors = {
51
+ name: "",
52
+ email: "",
53
+ phone_number: "",
54
+ };
55
+
56
+ let isValid = true;
57
+
58
+ if (!formData.name.trim()) {
59
+ newErrors.name = "Name is required";
60
+ isValid = false;
61
+ }
62
+
63
+ if (!formData.email.trim()) {
64
+ newErrors.email = "Email is required";
65
+ isValid = false;
66
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
67
+ newErrors.email = "Please enter a valid email";
68
+ isValid = false;
69
+ }
70
+
71
+ setErrors(newErrors);
72
+ return isValid;
73
+ };
74
+
75
+ const handleSubmit = () => {
76
+ if (validateForm()) {
77
+ console.log("Form submitted:", formData);
78
+ alert("Contact updated successfully!");
79
+ handleClose();
80
+ }
81
+ };
82
+
83
+ const handleInputChange = (field: keyof typeof formData, value: string) => {
84
+ setFormData((prev) => ({ ...prev, [field]: value }));
85
+ // Clear error when user starts typing
86
+ if (errors[field]) {
87
+ setErrors((prev) => ({ ...prev, [field]: "" }));
88
+ }
89
+ };
90
+
91
+ const renderHeader = () => {
92
+ return (
93
+ <div className="cm:flex cm:items-center cm:justify-between cm:w-full">
94
+ <div className="cm:text-slate-text-title cm:text-base cm:font-semibold cm:leading-6">
95
+ {contact ? "Edit Contact" : "Add Contact"}
96
+ </div>
97
+ <Button
98
+ onClick={handleClose}
99
+ variant="text"
100
+ icon={closeIcon}
101
+ size="small"
102
+ color="primary"
103
+ className="cm:p-0"
104
+ />
105
+ </div>
106
+ );
107
+ };
108
+
109
+ const renderContent = () => {
110
+ return (
111
+ <div className="" onClick={(e) => e.stopPropagation()}>
112
+ {/* Contact Avatar and Info */}
113
+ {contact && (
114
+ <div className="cm:flex cm:items-center cm:gap-3 cm:mb-6">
115
+ <Avatar name={formData.name} labelName={formData.name} />
116
+ <div className="cm:flex cm:flex-col">
117
+ <p className="cm:text-slate-text-title cm:text-sm cm:font-medium cm:leading-5 cm:m-0">
118
+ {contact.name}
119
+ </p>
120
+ <p className="cm:text-slate-text-subtle cm:text-xs cm:font-normal cm:leading-[18px] cm:m-0">
121
+ {contact.email}
122
+ </p>
123
+ </div>
124
+ </div>
125
+ )}
126
+
127
+ {/* Form Fields */}
128
+ <div className="cm:flex cm:flex-col cm:gap-4 cm:mb-6">
129
+ {/* Name Field */}
130
+ <div className="cm:flex cm:flex-col cm:gap-1.5">
131
+ <label className="cm:text-slate-text-title cm:text-sm cm:font-medium cm:leading-5">
132
+ Name <span className="cm:text-error">*</span>
133
+ </label>
134
+ <TextField
135
+ placeholder="Enter contact name"
136
+ value={formData.name}
137
+ onChange={(e) => handleInputChange("name", e.target.value)}
138
+ className="cm:w-full"
139
+ error={!!errors.name}
140
+ />
141
+ {errors.name && (
142
+ <span className="cm:text-error cm:text-xs cm:font-normal cm:leading-[18px]">
143
+ {errors.name}
144
+ </span>
145
+ )}
146
+ </div>
147
+
148
+ {/* Email Field */}
149
+ <div className="cm:flex cm:flex-col cm:gap-1.5">
150
+ <label className="cm:text-slate-text-title cm:text-sm cm:font-medium cm:leading-5">
151
+ Email <span className="cm:text-error">*</span>
152
+ </label>
153
+ <TextField
154
+ placeholder="Enter email address"
155
+ value={formData.email}
156
+ onChange={(e) => handleInputChange("email", e.target.value)}
157
+ className="cm:w-full"
158
+ error={!!errors.email}
159
+ />
160
+ {errors.email && (
161
+ <span className="cm:text-error cm:text-xs cm:font-normal cm:leading-[18px]">
162
+ {errors.email}
163
+ </span>
164
+ )}
165
+ </div>
166
+
167
+ {/* Phone Field */}
168
+ <div className="cm:flex cm:flex-col cm:gap-1.5">
169
+ <label className="cm:text-slate-text-title cm:text-sm cm:font-medium cm:leading-5">
170
+ Phone Number
171
+ </label>
172
+ <TextField
173
+ placeholder="Enter phone number"
174
+ value={formData.phone_number}
175
+ onChange={(e) => handleInputChange("phone_number", e.target.value)}
176
+ className="cm:w-full"
177
+ />
178
+ </div>
179
+ </div>
180
+
181
+ {/* Action Buttons */}
182
+
183
+ </div>
184
+ );
185
+ };
186
+
187
+ return (
188
+ <div>
189
+
190
+ <Icon name="edit" size={16} type="active" color="#0F172A" onClick={handleClick}/>
191
+
192
+ <Dialog
193
+ open={dialogOpen}
194
+ onClose={handleClose}
195
+ maxWidth="sm"
196
+ title={renderHeader()}
197
+ className="cm:bg-slate-surface-white cm:w-[400px]"
198
+ footer={ <div className="cm:flex cm:gap-3 cm:items-center cm:justify-end cm:w-full">
199
+ <Button variant="outlined" size="medium" onClick={handleClose}>
200
+ Cancel
201
+ </Button>
202
+ <Button onClick={handleSubmit} size="medium">
203
+ Save
204
+ </Button>
205
+ </div>}
206
+ >
207
+ {renderContent()}
208
+ </Dialog>
209
+ </div>
210
+ );
211
+ };
212
+
213
+ export default EditContact;
@@ -0,0 +1,24 @@
1
+ export const mockContacts = [
2
+ {
3
+ id: '1',
4
+ name: 'Bob Johnson',
5
+ email: 'bobjohnson@company.com',
6
+ initial: 'B',
7
+ avatarColor: 'bg-[#ac80b7]', // pastel-violet
8
+ },
9
+ {
10
+ id: '2',
11
+ name: 'Martin George',
12
+ email: 'martingeorge@company.com',
13
+ initial: 'M',
14
+ avatarColor: 'bg-[#d04b4f]', // pastel-red
15
+ isSelected: true,
16
+ },
17
+ {
18
+ id: '3',
19
+ name: 'Alice Smith',
20
+ email: 'alicesmith@company.com',
21
+ initial: 'A',
22
+ avatarColor: 'bg-[#5398cf]', // pastel-light-blue
23
+ },
24
+ ];
@@ -0,0 +1,171 @@
1
+ import { useState } from "react";
2
+ import { Dialog, Button, TextField } from "hiver-ui-kit-extended";
3
+ import { mockContacts } from "./constants";
4
+ import Avatar from "../Avatar";
5
+ import closeIcon from "../../assets/close.svg";
6
+ import Icon from "../Icon";
7
+
8
+ type DialogState = "add-primary-contact" | "primary-contact-confirmation";
9
+
10
+ interface Contact {
11
+ id: string;
12
+ name: string;
13
+ email: string;
14
+ isSelected?: boolean;
15
+ }
16
+
17
+ const PrimaryContactsDialog = () => {
18
+ const [dialogOpen, setDialogOpen] = useState(false);
19
+ const [dialogState, setDialogState] = useState<DialogState>("add-primary-contact");
20
+ const [searchValue, setSearchValue] = useState("");
21
+ const [selectedContact, setSelectedContact] = useState<Contact | null>(null);
22
+
23
+ const handleClick = () => {
24
+ setDialogOpen(true);
25
+ };
26
+
27
+ const handleClose = () => {
28
+ setDialogOpen(false);
29
+ setDialogState("add-primary-contact");
30
+ setSearchValue("");
31
+ setSelectedContact(null);
32
+ };
33
+
34
+ const filteredContacts = mockContacts.filter(
35
+ (contact) =>
36
+ contact.name.toLowerCase().includes(searchValue.toLowerCase()) ||
37
+ contact.email.toLowerCase().includes(searchValue.toLowerCase())
38
+ );
39
+
40
+ const handleContactSelect = (contact: Contact) => {
41
+ setSelectedContact(contact);
42
+ setDialogState("primary-contact-confirmation");
43
+ };
44
+
45
+ const renderHeader = () => {
46
+ if (dialogState === "add-primary-contact") {
47
+ return (
48
+ <div className="flex items-center justify-between w-full">
49
+ <div>Change Primary Contact</div>
50
+ <Button
51
+ onClick={handleClose}
52
+ variant="text"
53
+ icon={closeIcon}
54
+ size="small"
55
+ color="primary"
56
+ className="p-0"
57
+ />
58
+ </div>
59
+ );
60
+ }
61
+ return <div>Primary Contact Confirmation</div>;
62
+ };
63
+
64
+ const renderContent = () => {
65
+ if (dialogState === "primary-contact-confirmation") {
66
+ return (
67
+ <div className="w-[380px]" onClick={(e) => e.stopPropagation()}>
68
+ <p className="font-['Hanken_Grotesk',sans-serif] font-normal leading-5 text-sm text-[var(--slate-text-subtle,#64758b)] m-0 w-full mb-4">
69
+ Are you sure you want to change the contact to {selectedContact?.name}? This will replace
70
+ the existing contact for this conversation.
71
+ </p>
72
+ <div className="flex gap-3 items-center justify-end w-full">
73
+ <Button
74
+ variant="outlined"
75
+ size="medium"
76
+ onClick={() => setDialogState("add-primary-contact")}
77
+ >
78
+ Cancel
79
+ </Button>
80
+ <Button
81
+ onClick={() => {
82
+ alert("Contact changed!");
83
+ handleClose();
84
+ }}
85
+ size="medium"
86
+ >
87
+ Change
88
+ </Button>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <div className="w-[380px]" onClick={(e) => e.stopPropagation()}>
96
+ <div className="w-full mb-4">
97
+ <TextField
98
+ placeholder="Search name or email"
99
+ value={searchValue}
100
+ onChange={(e) => setSearchValue(e.target.value)}
101
+ />
102
+ </div>
103
+ <div className="flex flex-col gap-2 items-start w-full">
104
+ <p className="font-['Hanken_Grotesk',sans-serif] font-normal leading-5 text-sm text-[#64758b]">
105
+ Contacts from this conversation
106
+ </p>
107
+ <div className="flex flex-col items-start w-full gap-0">
108
+ {filteredContacts.map((contact: Contact) => (
109
+ <ContactItem
110
+ key={contact.id}
111
+ contact={contact}
112
+ onSelect={() => handleContactSelect(contact)}
113
+ />
114
+ ))}
115
+ </div>
116
+ </div>
117
+ </div>
118
+ );
119
+ };
120
+
121
+ return (
122
+ <div>
123
+
124
+ <Icon name="edit" size={16} type="active" color="#0F172A" onClick={handleClick} />
125
+ <Dialog
126
+ open={dialogOpen}
127
+ variant="confirmation"
128
+ onClose={handleClose}
129
+ onBack={() => setDialogState("add-primary-contact")}
130
+ maxWidth="sm"
131
+ title={renderHeader()}
132
+ >
133
+ {renderContent()}
134
+ </Dialog>
135
+ </div>
136
+ );
137
+ };
138
+
139
+ export default PrimaryContactsDialog;
140
+
141
+ const ContactItem = ({
142
+ contact,
143
+ onSelect,
144
+ }: {
145
+ contact: Contact;
146
+ onSelect: () => void;
147
+ }) => {
148
+ return (
149
+ <div
150
+ className="bg-white flex gap-3 items-center px-0 py-2 rounded-lg w-full cursor-pointer hover:bg-gray-50 transition-colors"
151
+ onClick={onSelect}
152
+ >
153
+ <div className="flex gap-3 items-center flex-1 min-w-0">
154
+ <Avatar name={contact.name} labelName={contact.name} />
155
+ <div className="flex flex-col items-start justify-center flex-1 min-w-0">
156
+ <p className="font-['Hanken_Grotesk',sans-serif] font-normal leading-5 text-sm text-[#334155] truncate w-full">
157
+ {contact.name}
158
+ </p>
159
+ <p className="font-['Hanken_Grotesk',sans-serif] font-normal leading-[18px] text-xs text-[#64758b] truncate w-full">
160
+ {contact.email}
161
+ </p>
162
+ </div>
163
+ </div>
164
+ {contact.isSelected && (
165
+ <div className="overflow-clip relative shrink-0 w-3.5 h-3.5">
166
+ <i className="pi pi-check text-[#276cf0] text-sm"></i>
167
+ </div>
168
+ )}
169
+ </div>
170
+ );
171
+ };
@@ -0,0 +1,99 @@
1
+ import React from 'react';
2
+ import type { ConversationStatus, ConversationBoxSize } from "./types";
3
+ import statusOpenIcon from '../../assets/status_open.svg';
4
+ import statusPendingIcon from '../../assets/status-snoozed.svg';
5
+ import statusClosedIcon from '../../assets/status-resolved.svg';
6
+ import conversationIcon from '../../assets/conversation.svg';
7
+ import emailIcon from '../../assets/email.svg';
8
+ import sharedMailboxIcon from '../../assets/sm_inbox.svg';
9
+
10
+ // Status configuration with colors and icons
11
+ export const statusConfig: Record<ConversationStatus, { label: string; color: string; iconColor: string; icon: string }> = {
12
+ open: { label: 'Open', color: '#E37144', iconColor: '#E3692C', icon: statusOpenIcon },
13
+ pending: { label: 'Pending', color: '#5398CF', iconColor: '#EC8A54', icon: statusPendingIcon },
14
+ closed: { label: 'Closed', color: '#2DBB6D', iconColor: '#276CF0', icon: statusClosedIcon },
15
+ };
16
+
17
+ // Size configuration
18
+ export const sizeConfig: Record<ConversationBoxSize, {
19
+ padding: { email: string; chat: string };
20
+ gap: string;
21
+ iconSize: string;
22
+ fontSize: { title: string; body: string; timestamp: string };
23
+ avatarSize: string;
24
+ avatarText: string;
25
+ maxWidth: string;
26
+ dividerHeight: string;
27
+ }> = {
28
+ default: {
29
+ padding: {
30
+ email: 'cm:px-5 cm:py-4',
31
+ chat: 'cm:px-5 cm:py-4',
32
+ },
33
+ gap: 'cm:gap-1.5',
34
+ iconSize: 'cm:w-3.5 cm:h-3.5',
35
+ fontSize: {
36
+ title: 'cm:text-sm',
37
+ body: 'cm:text-sm',
38
+ timestamp: 'cm:text-[13px]',
39
+ },
40
+ avatarSize: 'cm:w-4 cm:h-4',
41
+ avatarText: 'cm:text-[9px]',
42
+ maxWidth: 'cm:max-w-[914px]',
43
+ dividerHeight: 'cm:h-4',
44
+ },
45
+ small: {
46
+ padding: {
47
+ email: 'cm:p-4',
48
+ chat: 'cm:p-4',
49
+ },
50
+ gap: 'cm:gap-1',
51
+ iconSize: 'cm:w-3 cm:h-3',
52
+ fontSize: {
53
+ title: 'cm:text-xs',
54
+ body: 'cm:text-xs',
55
+ timestamp: 'cm:text-xs',
56
+ },
57
+ avatarSize: 'cm:w-3 cm:h-3',
58
+ avatarText: 'cm:text-[8px]',
59
+ maxWidth: 'cm:max-w-[330px]',
60
+ dividerHeight: 'cm:h-3',
61
+ },
62
+ };
63
+
64
+ // Icon components
65
+ interface IconProps {
66
+ iconSize: string;
67
+ }
68
+
69
+ export const ChatIcon: React.FC<IconProps> = ({ iconSize }) => (
70
+ <img
71
+ src={conversationIcon}
72
+ alt="Chat"
73
+ className={iconSize}
74
+ />
75
+ );
76
+
77
+ export const EnvelopeIcon: React.FC<IconProps> = ({ iconSize }) => (
78
+ <img
79
+ src={emailIcon}
80
+ alt="Email"
81
+ className={iconSize}
82
+ />
83
+ );
84
+
85
+ export const SharedMailboxIcon: React.FC<IconProps> = ({ iconSize }) => (
86
+ <img
87
+ src={sharedMailboxIcon}
88
+ alt="Shared Mailbox"
89
+ className={iconSize}
90
+ />
91
+ );
92
+
93
+ export const StatusIcon: React.FC<IconProps & { icon: string; label: string }> = ({ iconSize, icon, label }) => (
94
+ <img
95
+ src={icon}
96
+ alt={label}
97
+ className={iconSize}
98
+ />
99
+ );
@@ -0,0 +1,147 @@
1
+ import React, { useState } from 'react';
2
+ import type { ConversationBoxProps } from './types';
3
+ import { statusConfig, sizeConfig, ChatIcon, EnvelopeIcon, SharedMailboxIcon, StatusIcon } from './constants';
4
+ import Avatar from '../Avatar';
5
+ import openInANewTab from '../../assets/open_in_a_new_tab.svg';
6
+
7
+
8
+ const ConversationBox: React.FC<ConversationBoxProps> = ({
9
+ type,
10
+ status,
11
+ size = 'default',
12
+ senderName,
13
+ timestamp,
14
+ messagePreview,
15
+ heading,
16
+ mailboxName,
17
+ assignedAgent,
18
+ onClick,
19
+ }) => {
20
+ const [isHovered, setIsHovered] = useState(false);
21
+ const statusInfo = statusConfig[status];
22
+ const sizeStyles = sizeConfig[size];
23
+
24
+ // Divider line (vertical)
25
+ const Divider = () => (
26
+ <div className={`${sizeStyles.dividerHeight} cm:w-px cm:bg-slate-border-light cm:shrink-0`} />
27
+ );
28
+
29
+ const handleActionClick = (e: React.MouseEvent) => {
30
+ e.stopPropagation(); // Prevent triggering the onClick handler
31
+ // Handle action button click (e.g., show menu)
32
+ console.log('Action button clicked');
33
+ };
34
+
35
+ return (
36
+ <div
37
+ className={`cm:border cm:p-3.5 cm:border-slate-border-light cm:rounded-[10px] cm:flex cm:flex-col ${sizeStyles.gap} cm:w-full ${sizeStyles.maxWidth} cm:cursor-pointer hover:cm:bg-slate-surface-subtle_100 cm:transition-colors cm:relative ${type === 'email' ? sizeStyles.padding.email : sizeStyles.padding.chat
38
+ }`}
39
+ onClick={onClick}
40
+ onMouseEnter={() => setIsHovered(true)}
41
+ onMouseLeave={() => setIsHovered(false)}
42
+ >
43
+ {/* Top row: Sender, Status, Timestamp */}
44
+ <div className={`cm:flex ${size === 'small' ? 'cm:gap-2' : 'cm:gap-2.5'} cm:items-center cm:w-full`}>
45
+ <div className={`cm:flex-1 cm:flex ${size === 'small' ? 'cm:gap-2' : 'cm:gap-2.5'} cm:items-center cm:min-w-0`}>
46
+ {/* Icon and Sender Name */}
47
+ <div className={`cm:flex ${size === 'small' ? 'cm:gap-1.5' : 'cm:gap-2'} cm:items-center cm:shrink-0`}>
48
+ <div className={`${sizeStyles.iconSize} cm:shrink-0 cm:flex cm:items-center cm:justify-center cm:text-slate-icons-subtle`}>
49
+ {type === 'chat' ? <ChatIcon iconSize={sizeStyles.iconSize} /> : <EnvelopeIcon iconSize={sizeStyles.iconSize} />}
50
+ </div>
51
+ <div className="cm:flex cm:gap-1 cm:items-center">
52
+ <p className={`cm:font-medium cm:leading-5 ${sizeStyles.fontSize.title} cm:text-slate-text-title cm:whitespace-nowrap`}>
53
+ {senderName}
54
+ </p>
55
+ </div>
56
+ </div>
57
+
58
+ {/* Divider */}
59
+ <Divider />
60
+
61
+ {/* Status Chip */}
62
+ <div className={`cm:flex ${size === 'small' ? 'cm:gap-1.5' : 'cm:gap-2'} cm:items-center cm:justify-center cm:rounded-2xl cm:shrink-0`}>
63
+ <div className={`${sizeStyles.iconSize} cm:shrink-0 cm:flex cm:items-center cm:justify-center`}>
64
+ <StatusIcon iconSize={sizeStyles.iconSize} icon={statusInfo.icon} label={statusInfo.label} />
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ {/* Timestamp or Action Button - fixed width container to prevent layout shift */}
70
+ <div className="cm:relative cm:shrink-0 cm:min-w-[100px] cm:flex cm:items-center cm:justify-end">
71
+ {/* Timestamp - always rendered but hidden on hover */}
72
+ <p
73
+ className={`cm:font-normal cm:leading-[18px] ${sizeStyles.fontSize.timestamp} cm:text-slate-text-subtle cm:whitespace-nowrap cm:text-right cm:transition-opacity ${
74
+ isHovered ? 'cm:opacity-0' : 'cm:opacity-100'
75
+ }`}
76
+ >
77
+ {timestamp}
78
+ </p>
79
+
80
+ {/* Action Button - absolutely positioned, shown on hover */}
81
+ <button
82
+ onClick={handleActionClick}
83
+ className={`cm:absolute cm:right-0 cm:bg-slate-surface-white cm:border cm:border-slate-border-light cm:rounded-[6px] cm:flex cm:items-center cm:justify-center cm:p-1.5 cm:w-7 cm:h-7 cm:hover:bg-slate-surface-subtle cm:transition-all ${
84
+ isHovered ? 'cm:opacity-100 cm:pointer-events-auto' : 'cm:opacity-0 cm:pointer-events-none'
85
+ }`}
86
+ aria-label="Open in a new tab"
87
+ >
88
+ <div className="cm:w-3.5 cm:h-3.5 cm:flex cm:items-center cm:justify-center">
89
+ <img src={openInANewTab} alt="Open in a new tab" className="cm:w-3.5 cm:h-3.5" />
90
+ </div>
91
+ </button>
92
+ </div>
93
+ </div>
94
+
95
+ {/* Content Section */}
96
+ <div className="cm:flex cm:flex-col cm:items-start cm:w-full">
97
+ {/* Heading (only for email) */}
98
+ {type === 'email' && heading && (
99
+ <div className={`cm:flex ${size === 'small' ? 'cm:gap-2' : 'cm:gap-3'} cm:items-center cm:w-full`}>
100
+ <div className={`cm:flex-1 cm:flex cm:flex-col cm:font-normal cm:justify-center cm:leading-none cm:overflow-hidden ${sizeStyles.fontSize.body} cm:text-slate-text-body`}>
101
+ <p className="cm:leading-5 cm:text-left cm:overflow-ellipsis cm:overflow-hidden">
102
+ {heading}
103
+ </p>
104
+ </div>
105
+ </div>
106
+ )}
107
+
108
+ {/* Message Preview and Footer */}
109
+ <div className={`cm:flex ${size === 'small' ? 'cm:flex-col cm:gap-1' : 'cm:flex-row cm:items-center cm:justify-between'} cm:w-full`}>
110
+ <p className={`cm:font-normal cm:leading-5 cm:overflow-ellipsis cm:overflow-hidden ${sizeStyles.fontSize.body} cm:text-slate-text-subtle cm:whitespace-nowrap ${size === 'small' ? 'cm:w-full' : 'cm:max-w-[560px]'}`}>
111
+ {messagePreview}
112
+ </p>
113
+
114
+ {/* MailboxName and Assigned Agent */}
115
+ <div className={`cm:flex ${size === 'small' ? 'cm:gap-2 cm:w-full' : 'cm:gap-2.5'} cm:items-center cm:shrink-0`}>
116
+ {/* mailboxName */}
117
+ {mailboxName && (
118
+ <>
119
+ <div className="cm:flex cm:gap-1 cm:items-center cm:justify-center cm:py-0.5 cm:rounded cm:shrink-0">
120
+ <div className={`${sizeStyles.iconSize} cm:overflow-hidden cm:shrink-0 cm:flex cm:items-center cm:justify-center cm:text-slate-icons-subtle`}>
121
+ <SharedMailboxIcon iconSize={sizeStyles.iconSize} />
122
+ </div>
123
+ <p className={`cm:font-normal cm:leading-5 ${sizeStyles.fontSize.body} cm:text-slate-text-subtle cm:whitespace-nowrap`}>
124
+ {mailboxName}
125
+ </p>
126
+ </div>
127
+ <Divider />
128
+ </>
129
+ )}
130
+
131
+ {/* Assigned Agent */}
132
+ {assignedAgent && (
133
+ <div className={`cm:flex ${size === 'small' ? 'cm:gap-1' : 'cm:gap-1.5'} cm:items-center cm:shrink-0`}>
134
+ <Avatar name={assignedAgent.name} size={size} />
135
+ <p className={`cm:font-normal cm:leading-5 ${sizeStyles.fontSize.body} cm:text-slate-text-subtle cm:whitespace-nowrap`}>
136
+ {assignedAgent.name}
137
+ </p>
138
+ </div>
139
+ )}
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ );
145
+ };
146
+
147
+ export default ConversationBox;
@@ -0,0 +1,20 @@
1
+ export type ConversationType = 'chat' | 'email';
2
+ export type ConversationStatus = 'open' | 'pending' | 'closed';
3
+ export type ConversationBoxSize = 'default' | 'small';
4
+
5
+ export interface ConversationBoxProps {
6
+ type: ConversationType;
7
+ status: ConversationStatus;
8
+ size?: ConversationBoxSize;
9
+ senderName: string;
10
+ timestamp: string;
11
+ messagePreview: string;
12
+ heading?: string; // Only used for email type
13
+ mailboxName?: string;
14
+ assignedAgent?: {
15
+ name: string;
16
+ avatarColor?: string;
17
+ avatarInitial?: string;
18
+ };
19
+ onClick?: () => void;
20
+ }