@xcelsior/ui-chat 2.0.4 → 2.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.
Files changed (46) hide show
  1. package/dist/index.d.mts +58 -5
  2. package/dist/index.d.ts +58 -5
  3. package/dist/index.js +1042 -427
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1009 -397
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +2 -1
  8. package/src/components/BookingCancelledCard.tsx +103 -0
  9. package/src/components/BookingCards.stories.tsx +102 -0
  10. package/src/components/BookingConfirmationCard.tsx +170 -0
  11. package/src/components/BookingSlotPicker.stories.tsx +87 -0
  12. package/src/components/BookingSlotPicker.tsx +253 -0
  13. package/src/components/BrandIcons.stories.tsx +32 -1
  14. package/src/components/BrandIcons.tsx +21 -17
  15. package/src/components/Chat.tsx +43 -9
  16. package/src/components/ChatWidget.tsx +30 -2
  17. package/src/components/MessageItem.tsx +83 -72
  18. package/src/components/MessageList.tsx +4 -0
  19. package/src/hooks/useDraggablePosition.ts +147 -42
  20. package/src/hooks/useMessages.ts +106 -53
  21. package/src/hooks/useWebSocket.ts +17 -4
  22. package/src/index.tsx +11 -0
  23. package/src/types.ts +39 -2
  24. package/src/utils/api.ts +1 -0
  25. package/storybook-static/assets/BookingCancelledCard-XHuB-Ebp.js +31 -0
  26. package/storybook-static/assets/BookingCards.stories-DfJ482RS.js +66 -0
  27. package/storybook-static/assets/BookingSlotPicker-BkfssueW.js +1 -0
  28. package/storybook-static/assets/BookingSlotPicker.stories-fYlg1zLg.js +50 -0
  29. package/storybook-static/assets/BrandIcons-BsRAdWzL.js +4 -0
  30. package/storybook-static/assets/BrandIcons.stories-C6gBovfU.js +106 -0
  31. package/storybook-static/assets/Chat.stories-BrR7LHsz.js +830 -0
  32. package/storybook-static/assets/{Color-YHDXOIA2-CSuNIR0a.js → Color-YHDXOIA2-azE51u2m.js} +1 -1
  33. package/storybook-static/assets/{DocsRenderer-CFRXHY34-dpuOKTQp.js → DocsRenderer-CFRXHY34-jTmzKIDk.js} +3 -3
  34. package/storybook-static/assets/MessageItem-pEOwuLyh.js +34 -0
  35. package/storybook-static/assets/{MessageItem.stories-CsxqSqu-.js → MessageItem.stories-Cs5Vtkle.js} +2 -2
  36. package/storybook-static/assets/{entry-preview-C_-WO6GJ.js → entry-preview-vcpiajAT.js} +1 -1
  37. package/storybook-static/assets/globe-BtMvkLMD.js +31 -0
  38. package/storybook-static/assets/{iframe-BXTccXxS.js → iframe-Cx1n-SeE.js} +2 -2
  39. package/storybook-static/assets/{preview-Cyx3pE7Q.js → preview-Do3b3dZv.js} +2 -2
  40. package/storybook-static/iframe.html +1 -1
  41. package/storybook-static/index.json +1 -1
  42. package/storybook-static/project.json +1 -1
  43. package/storybook-static/assets/BrandIcons-Cjy5INAp.js +0 -4
  44. package/storybook-static/assets/BrandIcons.stories-BeVC6svr.js +0 -64
  45. package/storybook-static/assets/Chat.stories-BkbpOOSG.js +0 -830
  46. package/storybook-static/assets/MessageItem-Dlb6dSKL.js +0 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/ui-chat",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "license": "MIT",
@@ -36,6 +36,7 @@
36
36
  "@emoji-mart/react": "^1.1.1",
37
37
  "axios": "^1.6.7",
38
38
  "date-fns": "^3.3.1",
39
+ "lucide-react": "^0.477.0",
39
40
  "react-markdown": "^9.0.1"
40
41
  },
41
42
  "exports": {
@@ -0,0 +1,103 @@
1
+ import { CircleX, Calendar, Clock, MessageSquare } from 'lucide-react';
2
+ import type { IBookingCancelledData, IChatTheme } from '../types';
3
+
4
+ interface BookingCancelledCardProps {
5
+ data: IBookingCancelledData;
6
+ theme?: IChatTheme;
7
+ }
8
+
9
+ export function BookingCancelledCard({ data, theme }: BookingCancelledCardProps) {
10
+ const bgColor = theme?.background || '#00001a';
11
+ const isLightTheme = (() => {
12
+ if (!bgColor.startsWith('#')) return false;
13
+ const hex = bgColor.replace('#', '');
14
+ const r = parseInt(hex.substring(0, 2), 16);
15
+ const g = parseInt(hex.substring(2, 4), 16);
16
+ const b = parseInt(hex.substring(4, 6), 16);
17
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.5;
18
+ })();
19
+
20
+ const negativeColor = theme?.statusNegative || '#ef4444';
21
+ const textColor = theme?.text || (isLightTheme ? '#1a1a2e' : '#f7f7f8');
22
+ const textMuted = theme?.textMuted || (isLightTheme ? 'rgba(0,0,0,0.45)' : 'rgba(247,247,248,0.4)');
23
+
24
+ const cardStyle: React.CSSProperties = isLightTheme
25
+ ? {
26
+ backgroundColor: 'rgba(239,68,68,0.04)',
27
+ boxShadow: 'inset 0 0 0 1px rgba(239,68,68,0.12)',
28
+ borderRadius: '14px',
29
+ padding: '16px',
30
+ }
31
+ : {
32
+ backgroundColor: 'rgba(239,68,68,0.06)',
33
+ boxShadow: 'inset 0 0 0 0.5px rgba(239,68,68,0.18)',
34
+ borderRadius: '14px',
35
+ padding: '16px',
36
+ };
37
+
38
+ const strikethroughTextStyle: React.CSSProperties = {
39
+ fontSize: '13px',
40
+ color: textMuted,
41
+ lineHeight: 1.4,
42
+ textDecoration: 'line-through',
43
+ textDecorationColor: isLightTheme ? 'rgba(0,0,0,0.25)' : 'rgba(255,255,255,0.2)',
44
+ };
45
+
46
+ const detailRowStyle: React.CSSProperties = {
47
+ display: 'flex',
48
+ alignItems: 'flex-start',
49
+ gap: '8px',
50
+ marginBottom: '8px',
51
+ };
52
+
53
+ const cancelledByLabel = data.cancelledBy === 'visitor' ? 'Cancelled by you' : 'Cancelled by support';
54
+
55
+ return (
56
+ <div style={cardStyle}>
57
+ {/* Header: icon + title */}
58
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '14px' }}>
59
+ <CircleX size={22} color={negativeColor} aria-hidden="true" />
60
+ <div>
61
+ <p
62
+ style={{
63
+ fontSize: '14px',
64
+ fontWeight: '700',
65
+ color: textColor,
66
+ letterSpacing: '0.006em',
67
+ lineHeight: 1.3,
68
+ }}
69
+ >
70
+ Meeting Cancelled
71
+ </p>
72
+ <p
73
+ style={{
74
+ fontSize: '11px',
75
+ color: negativeColor,
76
+ letterSpacing: '0.01em',
77
+ marginTop: '1px',
78
+ opacity: 0.8,
79
+ }}
80
+ >
81
+ {cancelledByLabel}
82
+ </p>
83
+ </div>
84
+ </div>
85
+
86
+ {/* Details with strikethrough */}
87
+ <div>
88
+ <div style={detailRowStyle}>
89
+ <Calendar size={14} color={textMuted} aria-hidden="true" style={{ marginTop: '1px', flexShrink: 0 }} />
90
+ <span style={strikethroughTextStyle}>
91
+ {data.meetingDate} at {data.meetingTime}
92
+ </span>
93
+ </div>
94
+ <div style={{ ...detailRowStyle, marginBottom: '0' }}>
95
+ <MessageSquare size={14} color={textMuted} aria-hidden="true" style={{ marginTop: '1px', flexShrink: 0 }} />
96
+ <span style={strikethroughTextStyle}>
97
+ {data.meetingPurpose}
98
+ </span>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ );
103
+ }
@@ -0,0 +1,102 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { BookingConfirmationCard } from './BookingConfirmationCard';
3
+ import { BookingCancelledCard } from './BookingCancelledCard';
4
+ import type { IBookingConfirmationData, IBookingCancelledData } from '../types';
5
+
6
+ // --- BookingConfirmationCard ---
7
+
8
+ const confirmationMeta: Meta<typeof BookingConfirmationCard> = {
9
+ title: 'Components/BookingConfirmationCard',
10
+ component: BookingConfirmationCard,
11
+ parameters: { layout: 'padded' },
12
+ tags: ['autodocs'],
13
+ decorators: [
14
+ (Story) => (
15
+ <div style={{ maxWidth: 360, background: '#00001a', padding: 16, borderRadius: 12 }}>
16
+ <Story />
17
+ </div>
18
+ ),
19
+ ],
20
+ };
21
+
22
+ export default confirmationMeta;
23
+ type ConfirmationStory = StoryObj<typeof BookingConfirmationCard>;
24
+
25
+ const mockConfirmation: IBookingConfirmationData = {
26
+ bookingId: 'bk_abc123',
27
+ visitorName: 'John Doe',
28
+ meetingDate: '2026-03-26',
29
+ meetingTime: '10:00',
30
+ meetingDuration: 30,
31
+ meetingPurpose: 'Discuss mobile app development project',
32
+ meetLink: 'https://meet.google.com/abc-defg-hij',
33
+ calendarLink: 'https://calendar.google.com/event?eid=abc123',
34
+ timezone: 'Australia/Sydney',
35
+ };
36
+
37
+ export const Confirmed: ConfirmationStory = {
38
+ args: { data: mockConfirmation },
39
+ };
40
+
41
+ export const ConfirmedNoMeetLink: ConfirmationStory = {
42
+ args: {
43
+ data: { ...mockConfirmation, meetLink: undefined },
44
+ },
45
+ };
46
+
47
+ export const ConfirmedLightTheme: ConfirmationStory = {
48
+ args: {
49
+ data: mockConfirmation,
50
+ theme: {
51
+ primary: '#337eff',
52
+ background: '#ffffff',
53
+ text: '#1a1a2e',
54
+ textMuted: 'rgba(0,0,0,0.35)',
55
+ },
56
+ },
57
+ decorators: [
58
+ (Story) => (
59
+ <div style={{ maxWidth: 360, background: '#ffffff', padding: 16, borderRadius: 12 }}>
60
+ <Story />
61
+ </div>
62
+ ),
63
+ ],
64
+ };
65
+
66
+ // --- BookingCancelledCard (separate file but grouped for convenience) ---
67
+ // Note: Storybook only supports one default export per file.
68
+ // To view BookingCancelledCard stories, create a separate render.
69
+
70
+ export const CancelledByVisitor: StoryObj<typeof BookingCancelledCard> = {
71
+ render: (args) => (
72
+ <div style={{ maxWidth: 360, background: '#00001a', padding: 16, borderRadius: 12 }}>
73
+ <BookingCancelledCard {...args} />
74
+ </div>
75
+ ),
76
+ args: {
77
+ data: {
78
+ bookingId: 'bk_abc123',
79
+ meetingDate: '2026-03-26',
80
+ meetingTime: '10:00',
81
+ meetingPurpose: 'Discuss mobile app development project',
82
+ cancelledBy: 'visitor',
83
+ } as IBookingCancelledData,
84
+ },
85
+ };
86
+
87
+ export const CancelledByAdmin: StoryObj<typeof BookingCancelledCard> = {
88
+ render: (args) => (
89
+ <div style={{ maxWidth: 360, background: '#00001a', padding: 16, borderRadius: 12 }}>
90
+ <BookingCancelledCard {...args} />
91
+ </div>
92
+ ),
93
+ args: {
94
+ data: {
95
+ bookingId: 'bk_abc123',
96
+ meetingDate: '2026-03-26',
97
+ meetingTime: '10:00',
98
+ meetingPurpose: 'Discuss mobile app development project',
99
+ cancelledBy: 'admin',
100
+ } as IBookingCancelledData,
101
+ },
102
+ };
@@ -0,0 +1,170 @@
1
+ import { CircleCheck, Calendar, Clock, MessageSquare, Video, CalendarPlus, Globe } from 'lucide-react';
2
+ import type { IBookingConfirmationData, IChatTheme } from '../types';
3
+
4
+ interface BookingConfirmationCardProps {
5
+ data: IBookingConfirmationData;
6
+ theme?: IChatTheme;
7
+ }
8
+
9
+ export function BookingConfirmationCard({ data, theme }: BookingConfirmationCardProps) {
10
+ const bgColor = theme?.background || '#00001a';
11
+ const isLightTheme = (() => {
12
+ if (!bgColor.startsWith('#')) return false;
13
+ const hex = bgColor.replace('#', '');
14
+ const r = parseInt(hex.substring(0, 2), 16);
15
+ const g = parseInt(hex.substring(2, 4), 16);
16
+ const b = parseInt(hex.substring(4, 6), 16);
17
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.5;
18
+ })();
19
+
20
+ const primaryColor = theme?.primary || '#337eff';
21
+ const successColor = theme?.statusPositive || '#22c55e';
22
+ const textColor = theme?.text || (isLightTheme ? '#1a1a2e' : '#f7f7f8');
23
+ const textMuted = theme?.textMuted || (isLightTheme ? 'rgba(0,0,0,0.45)' : 'rgba(247,247,248,0.4)');
24
+
25
+ const cardStyle: React.CSSProperties = isLightTheme
26
+ ? {
27
+ backgroundColor: 'rgba(0,0,0,0.03)',
28
+ boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.08)',
29
+ borderRadius: '14px',
30
+ padding: '16px',
31
+ overflow: 'hidden',
32
+ }
33
+ : {
34
+ backgroundColor: 'rgba(255,255,255,0.04)',
35
+ boxShadow: 'inset 0 0 0 0.5px rgba(255,255,255,0.08), inset 0 1px 0 0 rgba(255,255,255,0.1)',
36
+ borderRadius: '14px',
37
+ padding: '16px',
38
+ overflow: 'hidden',
39
+ };
40
+
41
+ const detailRowStyle: React.CSSProperties = {
42
+ display: 'flex',
43
+ alignItems: 'flex-start',
44
+ gap: '8px',
45
+ marginBottom: '8px',
46
+ };
47
+
48
+ const dividerStyle: React.CSSProperties = {
49
+ height: '1px',
50
+ backgroundColor: isLightTheme ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.06)',
51
+ margin: '12px 0',
52
+ };
53
+
54
+ const actionButtonBase: React.CSSProperties = {
55
+ display: 'flex',
56
+ alignItems: 'center',
57
+ justifyContent: 'center',
58
+ gap: '6px',
59
+ flex: 1,
60
+ padding: '9px 12px',
61
+ borderRadius: '9px',
62
+ fontSize: '12px',
63
+ fontWeight: '600',
64
+ letterSpacing: '0.01em',
65
+ cursor: 'pointer',
66
+ textDecoration: 'none',
67
+ border: 'none',
68
+ transition: 'opacity 0.15s ease',
69
+ };
70
+
71
+ return (
72
+ <div style={cardStyle}>
73
+ {/* Header: icon + title */}
74
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '14px' }}>
75
+ <CircleCheck size={22} color={successColor} aria-hidden="true" />
76
+ <div>
77
+ <p
78
+ style={{
79
+ fontSize: '14px',
80
+ fontWeight: '700',
81
+ color: textColor,
82
+ letterSpacing: '0.006em',
83
+ lineHeight: 1.3,
84
+ }}
85
+ >
86
+ Meeting Confirmed
87
+ </p>
88
+ <p style={{ fontSize: '11px', color: successColor, letterSpacing: '0.01em', marginTop: '1px' }}>
89
+ You'll receive a calendar invite shortly
90
+ </p>
91
+ </div>
92
+ </div>
93
+
94
+ {/* Details */}
95
+ <div>
96
+ <div style={detailRowStyle}>
97
+ <Calendar size={14} color={primaryColor} aria-hidden="true" style={{ marginTop: '1px', flexShrink: 0 }} />
98
+ <span style={{ fontSize: '13px', color: textColor, lineHeight: 1.4 }}>
99
+ {data.meetingDate} at {data.meetingTime}
100
+ </span>
101
+ </div>
102
+ <div style={detailRowStyle}>
103
+ <Clock size={14} color={primaryColor} aria-hidden="true" style={{ marginTop: '1px', flexShrink: 0 }} />
104
+ <span style={{ fontSize: '13px', color: textColor, lineHeight: 1.4 }}>
105
+ {data.meetingDuration} minutes
106
+ </span>
107
+ </div>
108
+ <div style={{ ...detailRowStyle, marginBottom: '0' }}>
109
+ <MessageSquare size={14} color={primaryColor} aria-hidden="true" style={{ marginTop: '1px', flexShrink: 0 }} />
110
+ <span style={{ fontSize: '13px', color: textColor, lineHeight: 1.4 }}>
111
+ {data.meetingPurpose}
112
+ </span>
113
+ </div>
114
+ </div>
115
+
116
+ {/* Timezone */}
117
+ {data.timezone && (
118
+ <div style={{ display: 'flex', alignItems: 'center', gap: '5px', marginTop: '8px' }}>
119
+ <Globe size={11} color={textMuted} aria-hidden="true" />
120
+ <span style={{ fontSize: '11px', color: textMuted, letterSpacing: '0.01em' }}>
121
+ {data.timezone}
122
+ </span>
123
+ </div>
124
+ )}
125
+
126
+ {/* Action buttons */}
127
+ {(data.meetLink || data.calendarLink) && (
128
+ <>
129
+ <div style={dividerStyle} />
130
+ <div style={{ display: 'flex', gap: '8px' }}>
131
+ {data.meetLink && (
132
+ <a
133
+ href={data.meetLink}
134
+ target="_blank"
135
+ rel="noopener noreferrer"
136
+ style={{
137
+ ...actionButtonBase,
138
+ background: `linear-gradient(135deg, ${primaryColor}, ${theme?.primaryStrong || '#005eff'})`,
139
+ color: '#ffffff',
140
+ boxShadow: `0 2px 8px -2px ${primaryColor}50`,
141
+ }}
142
+ >
143
+ <Video size={13} aria-hidden="true" />
144
+ Join Meet
145
+ </a>
146
+ )}
147
+ {data.calendarLink && (
148
+ <a
149
+ href={data.calendarLink}
150
+ target="_blank"
151
+ rel="noopener noreferrer"
152
+ style={{
153
+ ...actionButtonBase,
154
+ backgroundColor: isLightTheme ? 'rgba(0,0,0,0.05)' : 'rgba(255,255,255,0.07)',
155
+ color: textColor,
156
+ boxShadow: isLightTheme
157
+ ? 'inset 0 0 0 1px rgba(0,0,0,0.1)'
158
+ : 'inset 0 0 0 0.5px rgba(255,255,255,0.12)',
159
+ }}
160
+ >
161
+ <CalendarPlus size={13} aria-hidden="true" />
162
+ Add to Calendar
163
+ </a>
164
+ )}
165
+ </div>
166
+ </>
167
+ )}
168
+ </div>
169
+ );
170
+ }
@@ -0,0 +1,87 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { BookingSlotPicker } from './BookingSlotPicker';
3
+ import type { IBookingData } from '../types';
4
+
5
+ const meta: Meta<typeof BookingSlotPicker> = {
6
+ title: 'Components/BookingSlotPicker',
7
+ component: BookingSlotPicker,
8
+ parameters: {
9
+ layout: 'padded',
10
+ },
11
+ tags: ['autodocs'],
12
+ decorators: [
13
+ (Story) => (
14
+ <div style={{ maxWidth: 360, background: '#00001a', padding: 16, borderRadius: 12 }}>
15
+ <Story />
16
+ </div>
17
+ ),
18
+ ],
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof BookingSlotPicker>;
23
+
24
+ const mockBookingData: IBookingData = {
25
+ availableDates: [
26
+ { date: '2026-03-25', dayLabel: 'Wed 25', dayName: 'Wednesday', slots: ['09:00', '09:30', '10:00', '10:30', '14:00', '14:30', '15:00'] },
27
+ { date: '2026-03-26', dayLabel: 'Thu 26', dayName: 'Thursday', slots: ['09:00', '10:00', '11:00', '13:00', '14:00'] },
28
+ { date: '2026-03-27', dayLabel: 'Fri 27', dayName: 'Friday', slots: ['09:30', '10:30', '11:30'] },
29
+ { date: '2026-03-28', dayLabel: 'Mon 30', dayName: 'Monday', slots: ['09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '13:00', '14:00', '15:00', '16:00'] },
30
+ { date: '2026-03-31', dayLabel: 'Tue 31', dayName: 'Tuesday', slots: ['14:00', '14:30', '15:00', '15:30', '16:00'] },
31
+ ],
32
+ timezone: 'Australia/Sydney',
33
+ meetingDuration: 30,
34
+ };
35
+
36
+ /** Default dark theme */
37
+ export const Default: Story = {
38
+ args: {
39
+ data: mockBookingData,
40
+ onSlotSelected: (date, time) => console.log('Selected:', date, time),
41
+ },
42
+ };
43
+
44
+ /** Light theme */
45
+ export const LightTheme: Story = {
46
+ args: {
47
+ data: mockBookingData,
48
+ onSlotSelected: (date, time) => console.log('Selected:', date, time),
49
+ theme: {
50
+ primary: '#337eff',
51
+ background: '#ffffff',
52
+ text: '#1a1a2e',
53
+ textMuted: 'rgba(0,0,0,0.35)',
54
+ },
55
+ },
56
+ decorators: [
57
+ (Story) => (
58
+ <div style={{ maxWidth: 360, background: '#ffffff', padding: 16, borderRadius: 12 }}>
59
+ <Story />
60
+ </div>
61
+ ),
62
+ ],
63
+ };
64
+
65
+ /** Disabled state (after selection) */
66
+ export const Disabled: Story = {
67
+ args: {
68
+ data: mockBookingData,
69
+ onSlotSelected: (date, time) => console.log('Selected:', date, time),
70
+ disabled: true,
71
+ },
72
+ };
73
+
74
+ /** Limited availability */
75
+ export const LimitedSlots: Story = {
76
+ args: {
77
+ data: {
78
+ availableDates: [
79
+ { date: '2026-03-26', dayLabel: 'Thu 26', dayName: 'Thursday', slots: ['14:00'] },
80
+ { date: '2026-03-27', dayLabel: 'Fri 27', dayName: 'Friday', slots: ['09:00', '15:30'] },
81
+ ],
82
+ timezone: 'Australia/Sydney',
83
+ meetingDuration: 30,
84
+ },
85
+ onSlotSelected: (date, time) => console.log('Selected:', date, time),
86
+ },
87
+ };