@happyvertical/smrt-chat 0.30.0

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 (169) hide show
  1. package/AGENTS.md +35 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +163 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/chunks/ChatService-Dpzc1Pa5.js +2044 -0
  8. package/dist/chunks/ChatService-Dpzc1Pa5.js.map +1 -0
  9. package/dist/collections/AgentSessionCollection.d.ts +57 -0
  10. package/dist/collections/AgentSessionCollection.d.ts.map +1 -0
  11. package/dist/collections/ChatMessageCollection.d.ts +79 -0
  12. package/dist/collections/ChatMessageCollection.d.ts.map +1 -0
  13. package/dist/collections/ChatParticipantCollection.d.ts +26 -0
  14. package/dist/collections/ChatParticipantCollection.d.ts.map +1 -0
  15. package/dist/collections/ChatReactionCollection.d.ts +23 -0
  16. package/dist/collections/ChatReactionCollection.d.ts.map +1 -0
  17. package/dist/collections/ChatRoomCollection.d.ts +43 -0
  18. package/dist/collections/ChatRoomCollection.d.ts.map +1 -0
  19. package/dist/collections/ChatThreadCollection.d.ts +9 -0
  20. package/dist/collections/ChatThreadCollection.d.ts.map +1 -0
  21. package/dist/index.d.ts +5 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +18 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/internal/agent-runtime.d.ts +16 -0
  26. package/dist/internal/agent-runtime.d.ts.map +1 -0
  27. package/dist/internal/agent-runtime.js +5 -0
  28. package/dist/internal/agent-runtime.js.map +1 -0
  29. package/dist/manifest.json +2811 -0
  30. package/dist/models/AgentSession.d.ts +70 -0
  31. package/dist/models/AgentSession.d.ts.map +1 -0
  32. package/dist/models/ChatMessage.d.ts +55 -0
  33. package/dist/models/ChatMessage.d.ts.map +1 -0
  34. package/dist/models/ChatParticipant.d.ts +32 -0
  35. package/dist/models/ChatParticipant.d.ts.map +1 -0
  36. package/dist/models/ChatReaction.d.ts +19 -0
  37. package/dist/models/ChatReaction.d.ts.map +1 -0
  38. package/dist/models/ChatRoom.d.ts +44 -0
  39. package/dist/models/ChatRoom.d.ts.map +1 -0
  40. package/dist/models/ChatThread.d.ts +24 -0
  41. package/dist/models/ChatThread.d.ts.map +1 -0
  42. package/dist/models/index.d.ts +7 -0
  43. package/dist/models/index.d.ts.map +1 -0
  44. package/dist/playground.d.ts +2 -0
  45. package/dist/playground.d.ts.map +1 -0
  46. package/dist/playground.js +166 -0
  47. package/dist/playground.js.map +1 -0
  48. package/dist/services/ChatService.d.ts +390 -0
  49. package/dist/services/ChatService.d.ts.map +1 -0
  50. package/dist/services/index.d.ts +2 -0
  51. package/dist/services/index.d.ts.map +1 -0
  52. package/dist/smrt-knowledge.json +1507 -0
  53. package/dist/svelte/components/agent/AgentChat.svelte +542 -0
  54. package/dist/svelte/components/agent/AgentChat.svelte.d.ts +21 -0
  55. package/dist/svelte/components/agent/AgentChat.svelte.d.ts.map +1 -0
  56. package/dist/svelte/components/agent/AgentSelector.svelte +175 -0
  57. package/dist/svelte/components/agent/AgentSelector.svelte.d.ts +11 -0
  58. package/dist/svelte/components/agent/AgentSelector.svelte.d.ts.map +1 -0
  59. package/dist/svelte/components/agent/AgentSessionPanel.svelte +322 -0
  60. package/dist/svelte/components/agent/AgentSessionPanel.svelte.d.ts +15 -0
  61. package/dist/svelte/components/agent/AgentSessionPanel.svelte.d.ts.map +1 -0
  62. package/dist/svelte/components/agent/ToolCallDisplay.svelte +335 -0
  63. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts +9 -0
  64. package/dist/svelte/components/agent/ToolCallDisplay.svelte.d.ts.map +1 -0
  65. package/dist/svelte/components/agent/message-blocks.d.ts +12 -0
  66. package/dist/svelte/components/agent/message-blocks.d.ts.map +1 -0
  67. package/dist/svelte/components/agent/message-blocks.js +41 -0
  68. package/dist/svelte/components/agent/message-blocks.test.js +31 -0
  69. package/dist/svelte/components/dialogs/RoomCreateDialog.svelte +403 -0
  70. package/dist/svelte/components/dialogs/RoomCreateDialog.svelte.d.ts +16 -0
  71. package/dist/svelte/components/dialogs/RoomCreateDialog.svelte.d.ts.map +1 -0
  72. package/dist/svelte/components/dialogs/SearchMessages.svelte +457 -0
  73. package/dist/svelte/components/dialogs/SearchMessages.svelte.d.ts +17 -0
  74. package/dist/svelte/components/dialogs/SearchMessages.svelte.d.ts.map +1 -0
  75. package/dist/svelte/components/layout/ChatLayout.svelte +150 -0
  76. package/dist/svelte/components/layout/ChatLayout.svelte.d.ts +18 -0
  77. package/dist/svelte/components/layout/ChatLayout.svelte.d.ts.map +1 -0
  78. package/dist/svelte/components/layout/MemberList.svelte +389 -0
  79. package/dist/svelte/components/layout/MemberList.svelte.d.ts +11 -0
  80. package/dist/svelte/components/layout/MemberList.svelte.d.ts.map +1 -0
  81. package/dist/svelte/components/layout/RoomHeader.svelte +241 -0
  82. package/dist/svelte/components/layout/RoomHeader.svelte.d.ts +15 -0
  83. package/dist/svelte/components/layout/RoomHeader.svelte.d.ts.map +1 -0
  84. package/dist/svelte/components/layout/RoomList.svelte +471 -0
  85. package/dist/svelte/components/layout/RoomList.svelte.d.ts +15 -0
  86. package/dist/svelte/components/layout/RoomList.svelte.d.ts.map +1 -0
  87. package/dist/svelte/components/messages/MessageInput.svelte +232 -0
  88. package/dist/svelte/components/messages/MessageInput.svelte.d.ts +20 -0
  89. package/dist/svelte/components/messages/MessageInput.svelte.d.ts.map +1 -0
  90. package/dist/svelte/components/messages/MessageItem.svelte +431 -0
  91. package/dist/svelte/components/messages/MessageItem.svelte.d.ts +19 -0
  92. package/dist/svelte/components/messages/MessageItem.svelte.d.ts.map +1 -0
  93. package/dist/svelte/components/messages/MessageList.svelte +129 -0
  94. package/dist/svelte/components/messages/MessageList.svelte.d.ts +17 -0
  95. package/dist/svelte/components/messages/MessageList.svelte.d.ts.map +1 -0
  96. package/dist/svelte/components/messages/ThreadPanel.svelte +156 -0
  97. package/dist/svelte/components/messages/ThreadPanel.svelte.d.ts +17 -0
  98. package/dist/svelte/components/messages/ThreadPanel.svelte.d.ts.map +1 -0
  99. package/dist/svelte/components/messages/__tests__/MessageInput.test.js +38 -0
  100. package/dist/svelte/components/shared/Avatar.svelte +30 -0
  101. package/dist/svelte/components/shared/Avatar.svelte.d.ts +14 -0
  102. package/dist/svelte/components/shared/Avatar.svelte.d.ts.map +1 -0
  103. package/dist/svelte/components/shared/FileUpload.svelte +382 -0
  104. package/dist/svelte/components/shared/FileUpload.svelte.d.ts +14 -0
  105. package/dist/svelte/components/shared/FileUpload.svelte.d.ts.map +1 -0
  106. package/dist/svelte/components/shared/LinkPreview.svelte +108 -0
  107. package/dist/svelte/components/shared/LinkPreview.svelte.d.ts +18 -0
  108. package/dist/svelte/components/shared/LinkPreview.svelte.d.ts.map +1 -0
  109. package/dist/svelte/components/shared/MentionAutocomplete.svelte +168 -0
  110. package/dist/svelte/components/shared/MentionAutocomplete.svelte.d.ts +18 -0
  111. package/dist/svelte/components/shared/MentionAutocomplete.svelte.d.ts.map +1 -0
  112. package/dist/svelte/components/shared/MessageBubble.svelte +81 -0
  113. package/dist/svelte/components/shared/MessageBubble.svelte.d.ts +16 -0
  114. package/dist/svelte/components/shared/MessageBubble.svelte.d.ts.map +1 -0
  115. package/dist/svelte/components/shared/ReactionPicker.svelte +103 -0
  116. package/dist/svelte/components/shared/ReactionPicker.svelte.d.ts +10 -0
  117. package/dist/svelte/components/shared/ReactionPicker.svelte.d.ts.map +1 -0
  118. package/dist/svelte/components/shared/ReadReceipts.svelte +127 -0
  119. package/dist/svelte/components/shared/ReadReceipts.svelte.d.ts +13 -0
  120. package/dist/svelte/components/shared/ReadReceipts.svelte.d.ts.map +1 -0
  121. package/dist/svelte/components/shared/TypingIndicator.svelte +90 -0
  122. package/dist/svelte/components/shared/TypingIndicator.svelte.d.ts +12 -0
  123. package/dist/svelte/components/shared/TypingIndicator.svelte.d.ts.map +1 -0
  124. package/dist/svelte/components/shared/UserPresence.svelte +65 -0
  125. package/dist/svelte/components/shared/UserPresence.svelte.d.ts +13 -0
  126. package/dist/svelte/components/shared/UserPresence.svelte.d.ts.map +1 -0
  127. package/dist/svelte/components/shared/__tests__/Avatar.test.js +20 -0
  128. package/dist/svelte/components/shared/__tests__/LinkPreview.test.js +29 -0
  129. package/dist/svelte/components/shared/__tests__/MessageBubble.test.js +21 -0
  130. package/dist/svelte/components/shared/__tests__/ReactionPicker.test.js +35 -0
  131. package/dist/svelte/components/shared/__tests__/ReadReceipts.test.js +28 -0
  132. package/dist/svelte/components/shared/__tests__/TypingIndicator.test.js +27 -0
  133. package/dist/svelte/components/shared/__tests__/UserPresence.test.js +23 -0
  134. package/dist/svelte/components/tabs/ChatTab.svelte +240 -0
  135. package/dist/svelte/components/tabs/ChatTab.svelte.d.ts +21 -0
  136. package/dist/svelte/components/tabs/ChatTab.svelte.d.ts.map +1 -0
  137. package/dist/svelte/components/tabs/ChatTabList.svelte +158 -0
  138. package/dist/svelte/components/tabs/ChatTabList.svelte.d.ts +13 -0
  139. package/dist/svelte/components/tabs/ChatTabList.svelte.d.ts.map +1 -0
  140. package/dist/svelte/components/tabs/ChatTabs.svelte +88 -0
  141. package/dist/svelte/components/tabs/ChatTabs.svelte.d.ts +21 -0
  142. package/dist/svelte/components/tabs/ChatTabs.svelte.d.ts.map +1 -0
  143. package/dist/svelte/components/tabs/MiniChat.svelte +253 -0
  144. package/dist/svelte/components/tabs/MiniChat.svelte.d.ts +15 -0
  145. package/dist/svelte/components/tabs/MiniChat.svelte.d.ts.map +1 -0
  146. package/dist/svelte/i18n.d.ts +51 -0
  147. package/dist/svelte/i18n.d.ts.map +1 -0
  148. package/dist/svelte/i18n.js +72 -0
  149. package/dist/svelte/i18n.messages.d.ts +50 -0
  150. package/dist/svelte/i18n.messages.d.ts.map +1 -0
  151. package/dist/svelte/i18n.messages.js +69 -0
  152. package/dist/svelte/index.d.ts +48 -0
  153. package/dist/svelte/index.d.ts.map +1 -0
  154. package/dist/svelte/index.js +117 -0
  155. package/dist/svelte/playground.d.ts +171 -0
  156. package/dist/svelte/playground.d.ts.map +1 -0
  157. package/dist/svelte/playground.js +161 -0
  158. package/dist/svelte/types.d.ts +116 -0
  159. package/dist/svelte/types.d.ts.map +1 -0
  160. package/dist/svelte/types.js +1 -0
  161. package/dist/types.d.ts +99 -0
  162. package/dist/types.d.ts.map +1 -0
  163. package/dist/types.js +2 -0
  164. package/dist/types.js.map +1 -0
  165. package/dist/ui.d.ts +4 -0
  166. package/dist/ui.d.ts.map +1 -0
  167. package/dist/ui.js +92 -0
  168. package/dist/ui.js.map +1 -0
  169. package/package.json +95 -0
@@ -0,0 +1,35 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for ReactionPicker via the shared S11 harness (#1416).
4
+ */
5
+ import { expectNoA11yViolations, render, screen, userEvent, within, } from '@happyvertical/smrt-vitest/svelte';
6
+ import { describe, expect, it, vi } from 'vitest';
7
+ import ReactionPicker from '../ReactionPicker.svelte';
8
+ // S12 a11y remediation (#1417): the picker is now a labelled `role="group"` of
9
+ // buttons (was an invalid `role="grid"` with button children, which tripped axe's
10
+ // `aria-required-children`), so axe is asserted on the open state.
11
+ describe('ReactionPicker', () => {
12
+ it('renders a labelled group of emoji buttons when open', () => {
13
+ render(ReactionPicker, { props: { onreact: vi.fn(), isOpen: true } });
14
+ const group = screen.getByRole('group', { name: 'Emoji reactions' });
15
+ expect(within(group).getAllByRole('button').length).toBeGreaterThan(0);
16
+ });
17
+ it('reports the chosen emoji through onreact', async () => {
18
+ const onreact = vi.fn();
19
+ render(ReactionPicker, { props: { onreact, isOpen: true } });
20
+ const [first] = within(screen.getByRole('group', { name: 'Emoji reactions' })).getAllByRole('button');
21
+ await userEvent.click(first);
22
+ expect(onreact).toHaveBeenCalledTimes(1);
23
+ expect(onreact.mock.calls[0][0]).toBeTruthy();
24
+ });
25
+ it('renders nothing when closed', () => {
26
+ render(ReactionPicker, { props: { onreact: vi.fn(), isOpen: false } });
27
+ expect(screen.queryByRole('group')).toBeNull();
28
+ });
29
+ it('is axe-clean', async () => {
30
+ const { container } = render(ReactionPicker, {
31
+ props: { onreact: vi.fn(), isOpen: true },
32
+ });
33
+ await expectNoA11yViolations(container);
34
+ });
35
+ });
@@ -0,0 +1,28 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for ReadReceipts via the shared S11 harness (#1416).
4
+ */
5
+ import { expectNoA11yViolations, render, screen, } from '@happyvertical/smrt-vitest/svelte';
6
+ import { describe, expect, it } from 'vitest';
7
+ import ReadReceipts from '../ReadReceipts.svelte';
8
+ describe('ReadReceipts', () => {
9
+ it('summarises how many participants have read', () => {
10
+ render(ReadReceipts, {
11
+ props: {
12
+ readBy: [{ name: 'Ada' }, { name: 'Bob' }],
13
+ totalParticipants: 3,
14
+ },
15
+ });
16
+ expect(screen.getByLabelText('2 of 3 read')).toBeInTheDocument();
17
+ });
18
+ it('renders the sent (unread) state with no readers', () => {
19
+ render(ReadReceipts, { props: { readBy: [], totalParticipants: 3 } });
20
+ expect(screen.getByLabelText('0 of 3 read')).toBeInTheDocument();
21
+ });
22
+ it('is axe-clean', async () => {
23
+ const { container } = render(ReadReceipts, {
24
+ props: { readBy: [{ name: 'Ada' }], totalParticipants: 2 },
25
+ });
26
+ await expectNoA11yViolations(container);
27
+ });
28
+ });
@@ -0,0 +1,27 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for TypingIndicator via the shared S11 harness (#1416).
4
+ */
5
+ import { expectNoA11yViolations, render, screen, } from '@happyvertical/smrt-vitest/svelte';
6
+ import { describe, expect, it } from 'vitest';
7
+ import TypingIndicator from '../TypingIndicator.svelte';
8
+ describe('TypingIndicator', () => {
9
+ it('names a single typist', () => {
10
+ render(TypingIndicator, { props: { names: ['Ada'] } });
11
+ expect(screen.getByText('Ada is typing')).toBeInTheDocument();
12
+ });
13
+ it('names two typists', () => {
14
+ render(TypingIndicator, { props: { names: ['Ada', 'Bob'] } });
15
+ expect(screen.getByText('Ada and Bob are typing')).toBeInTheDocument();
16
+ });
17
+ it('renders nothing when nobody is typing', () => {
18
+ const { container } = render(TypingIndicator, { props: { names: [] } });
19
+ expect(container.querySelector('.typing')).toBeNull();
20
+ });
21
+ it('is axe-clean', async () => {
22
+ const { container } = render(TypingIndicator, {
23
+ props: { names: ['Ada'] },
24
+ });
25
+ await expectNoA11yViolations(container);
26
+ });
27
+ });
@@ -0,0 +1,23 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * Component coverage for UserPresence via the shared S11 harness (#1416).
4
+ */
5
+ import { expectNoA11yViolations, render, screen, } from '@happyvertical/smrt-vitest/svelte';
6
+ import { describe, expect, it } from 'vitest';
7
+ import UserPresence from '../UserPresence.svelte';
8
+ describe('UserPresence', () => {
9
+ it('exposes the status as an accessible label', () => {
10
+ render(UserPresence, { props: { status: 'online' } });
11
+ expect(screen.getByLabelText('Online')).toBeInTheDocument();
12
+ });
13
+ it('shows a visible label when requested', () => {
14
+ render(UserPresence, { props: { status: 'dnd', showLabel: true } });
15
+ expect(screen.getByText('Do not disturb')).toBeInTheDocument();
16
+ });
17
+ it('is axe-clean', async () => {
18
+ const { container } = render(UserPresence, {
19
+ props: { status: 'away', showLabel: true },
20
+ });
21
+ await expectNoA11yViolations(container);
22
+ });
23
+ });
@@ -0,0 +1,240 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ChatTab - Individual collapsible chat window
4
+ * Compact header with title, collapse/close buttons. Contains message list and input.
5
+ * Expands upward from the bottom tab bar.
6
+ */
7
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
8
+ import { M } from '../../i18n.messages.js';
9
+ import type { ChatMessageData, ChatTabState } from '../../types.js';
10
+ import Avatar from '../shared/Avatar.svelte';
11
+ import MiniChat from './MiniChat.svelte';
12
+
13
+ const { t } = useI18n();
14
+
15
+ export interface Props {
16
+ /** Tab state */
17
+ tab: ChatTabState;
18
+ /** Messages in this chat */
19
+ messages: ChatMessageData[];
20
+ /** Current user's profile ID */
21
+ currentProfileId: string;
22
+ /** Send a message */
23
+ onsend: (content: string) => void;
24
+ /** Collapse this tab */
25
+ oncollapse: () => void;
26
+ /** Close this tab */
27
+ onclose: () => void;
28
+ /** Expand this tab */
29
+ onexpand: () => void;
30
+ }
31
+
32
+ const {
33
+ tab,
34
+ messages,
35
+ currentProfileId,
36
+ onsend,
37
+ oncollapse,
38
+ onclose,
39
+ onexpand,
40
+ }: Props = $props();
41
+ </script>
42
+
43
+ {#if tab.isExpanded}
44
+ <div class="chat-tab chat-tab--expanded" aria-label={t(M['chat.chat_tab.chat_with'], { name: tab.room.name })}>
45
+ <div class="chat-tab__header">
46
+ <button
47
+ class="chat-tab__header-btn"
48
+ type="button"
49
+ onclick={oncollapse}
50
+ aria-label={t(M['chat.chat_tab.collapse'])}
51
+ >
52
+ <Avatar
53
+ name={tab.room.name}
54
+ avatarUrl={tab.room.avatarUrl}
55
+ size="sm"
56
+ />
57
+ <span class="chat-tab__name">{tab.room.name}</span>
58
+ </button>
59
+
60
+ <div class="chat-tab__actions">
61
+ <button
62
+ class="chat-tab__icon-btn"
63
+ type="button"
64
+ onclick={oncollapse}
65
+ aria-label={t(M['chat.chat_tab.minimize'])}
66
+ title={t(M['chat.chat_tab.minimize'])}
67
+ >
68
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
69
+ <rect x="3" y="12" width="10" height="2" rx="1" />
70
+ </svg>
71
+ </button>
72
+ <button
73
+ class="chat-tab__icon-btn"
74
+ type="button"
75
+ onclick={onclose}
76
+ aria-label={t(M['chat.chat_tab.close'])}
77
+ title={t(M['chat.chat_tab.close'])}
78
+ >
79
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
80
+ <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
81
+ </svg>
82
+ </button>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="chat-tab__body">
87
+ <MiniChat
88
+ {messages}
89
+ {currentProfileId}
90
+ {onsend}
91
+ maxHeight={320}
92
+ />
93
+ </div>
94
+ </div>
95
+ {:else}
96
+ <button
97
+ class="chat-tab chat-tab--collapsed"
98
+ type="button"
99
+ onclick={onexpand}
100
+ aria-label={t(M['chat.chat_tab.open_chat_with'], { name: tab.room.name })}
101
+ >
102
+ <Avatar
103
+ name={tab.room.name}
104
+ avatarUrl={tab.room.avatarUrl}
105
+ size="sm"
106
+ />
107
+ <span class="chat-tab__name">{tab.room.name}</span>
108
+ {#if tab.unreadCount > 0}
109
+ <span class="chat-tab__badge" aria-label={t(M['chat.chat_tab.unread'], { count: tab.unreadCount })}>{tab.unreadCount}</span>
110
+ {/if}
111
+ </button>
112
+ {/if}
113
+
114
+ <style>
115
+ .chat-tab--expanded {
116
+ display: flex;
117
+ flex-direction: column;
118
+ width: 328px;
119
+ background: var(--smrt-color-surface, #fefbff);
120
+ border-radius: var(--smrt-radius-large, 12px) var(--smrt-radius-large, 12px) 0 0;
121
+ box-shadow: var(--smrt-elevation-3, 0 4px 8px rgba(0, 0, 0, 0.15));
122
+ overflow: hidden;
123
+ }
124
+
125
+ .chat-tab__header {
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: space-between;
129
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
130
+ background: var(--smrt-color-primary, #005ac1);
131
+ color: var(--smrt-color-on-primary, #ffffff);
132
+ min-height: 44px;
133
+ }
134
+
135
+ .chat-tab__header-btn {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: var(--smrt-spacing-2, 8px);
139
+ background: none;
140
+ border: none;
141
+ color: inherit;
142
+ cursor: pointer;
143
+ padding: 0;
144
+ min-width: 0;
145
+ }
146
+
147
+ .chat-tab__header-btn:focus-visible {
148
+ outline: 2px solid var(--smrt-color-on-primary, #ffffff);
149
+ outline-offset: 2px;
150
+ border-radius: var(--smrt-radius-small, 4px);
151
+ }
152
+
153
+ .chat-tab__name {
154
+ font: var(--smrt-typography-label-large-font, 500 0.875rem/1.25 sans-serif);
155
+ white-space: nowrap;
156
+ overflow: hidden;
157
+ text-overflow: ellipsis;
158
+ }
159
+
160
+ .chat-tab__actions {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: var(--smrt-spacing-1, 4px);
164
+ flex-shrink: 0;
165
+ }
166
+
167
+ .chat-tab__icon-btn {
168
+ display: inline-flex;
169
+ align-items: center;
170
+ justify-content: center;
171
+ width: 28px;
172
+ height: 28px;
173
+ border: none;
174
+ background: none;
175
+ color: inherit;
176
+ cursor: pointer;
177
+ border-radius: var(--smrt-radius-full, 9999px);
178
+ transition: background var(--smrt-duration-short2, 150ms);
179
+ }
180
+
181
+ .chat-tab__icon-btn:hover {
182
+ background: color-mix(in srgb, var(--smrt-color-on-primary) 15%, transparent);
183
+ }
184
+
185
+ .chat-tab__icon-btn:focus-visible {
186
+ outline: 2px solid var(--smrt-color-on-primary, #ffffff);
187
+ outline-offset: -2px;
188
+ }
189
+
190
+ .chat-tab__body {
191
+ flex: 1;
192
+ min-height: 0;
193
+ }
194
+
195
+ .chat-tab--collapsed {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: var(--smrt-spacing-2, 8px);
199
+ padding: var(--smrt-spacing-2, 8px) var(--smrt-spacing-3, 12px);
200
+ background: var(--smrt-color-surface-container, #f0f0f4);
201
+ border: none;
202
+ border-radius: var(--smrt-radius-large, 12px) var(--smrt-radius-large, 12px) 0 0;
203
+ cursor: pointer;
204
+ box-shadow: var(--smrt-elevation-2, 0 2px 6px rgba(0, 0, 0, 0.1));
205
+ color: var(--smrt-color-on-surface, #1a1c1e);
206
+ transition: background var(--smrt-duration-short2, 150ms);
207
+ white-space: nowrap;
208
+ }
209
+
210
+ .chat-tab--collapsed:hover {
211
+ background: var(--smrt-color-surface-container-high, #e6e6ea);
212
+ }
213
+
214
+ .chat-tab--collapsed:focus-visible {
215
+ outline: 2px solid var(--smrt-color-primary, #005ac1);
216
+ outline-offset: -2px;
217
+ }
218
+
219
+ .chat-tab__badge {
220
+ display: inline-flex;
221
+ align-items: center;
222
+ justify-content: center;
223
+ min-width: 18px;
224
+ height: 18px;
225
+ padding: 0 var(--smrt-spacing-1, 4px);
226
+ border-radius: var(--smrt-radius-full, 9999px);
227
+ background: var(--smrt-color-error, #ba1a1a);
228
+ color: var(--smrt-color-on-error, #ffffff);
229
+ font-size: var(--smrt-typography-label-small-size, 0.6875rem);
230
+ font-weight: var(--smrt-typography-weight-semibold, 600);
231
+ line-height: 1;
232
+ }
233
+
234
+ @media (prefers-reduced-motion: reduce) {
235
+ .chat-tab--collapsed,
236
+ .chat-tab__icon-btn {
237
+ transition: none;
238
+ }
239
+ }
240
+ </style>
@@ -0,0 +1,21 @@
1
+ import type { ChatMessageData, ChatTabState } from '../../types.js';
2
+ export interface Props {
3
+ /** Tab state */
4
+ tab: ChatTabState;
5
+ /** Messages in this chat */
6
+ messages: ChatMessageData[];
7
+ /** Current user's profile ID */
8
+ currentProfileId: string;
9
+ /** Send a message */
10
+ onsend: (content: string) => void;
11
+ /** Collapse this tab */
12
+ oncollapse: () => void;
13
+ /** Close this tab */
14
+ onclose: () => void;
15
+ /** Expand this tab */
16
+ onexpand: () => void;
17
+ }
18
+ declare const ChatTab: import("svelte").Component<Props, {}, "">;
19
+ type ChatTab = ReturnType<typeof ChatTab>;
20
+ export default ChatTab;
21
+ //# sourceMappingURL=ChatTab.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatTab.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/tabs/ChatTab.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKpE,MAAM,WAAW,KAAK;IACpB,gBAAgB;IAChB,GAAG,EAAE,YAAY,CAAC;IAClB,4BAA4B;IAC5B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,gCAAgC;IAChC,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB;IACrB,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,wBAAwB;IACxB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,qBAAqB;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,sBAAsB;IACtB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AA+DD,QAAA,MAAM,OAAO,2CAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ChatTabList - Minimized tab overview bar
4
+ * Horizontal bar of small avatars/names at bottom. Shows unread badges.
5
+ */
6
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
7
+ import { M } from '../../i18n.messages.js';
8
+ import type { ChatTabState } from '../../types.js';
9
+ import Avatar from '../shared/Avatar.svelte';
10
+
11
+ const { t } = useI18n();
12
+
13
+ export interface Props {
14
+ /** Collapsed/minimized tabs */
15
+ tabs: ChatTabState[];
16
+ /** Select (expand) a tab */
17
+ onselect: (roomId: string) => void;
18
+ /** Close a tab */
19
+ onclose: (roomId: string) => void;
20
+ }
21
+
22
+ const { tabs, onselect, onclose }: Props = $props();
23
+ </script>
24
+
25
+ {#if tabs.length > 0}
26
+ <nav class="tab-list" aria-label={t(M['chat.chat_tab_list.minimized_chats'])}>
27
+ {#each tabs as tab (tab.roomId)}
28
+ <div class="tab-list__item">
29
+ <button
30
+ class="tab-list__btn"
31
+ type="button"
32
+ onclick={() => onselect(tab.roomId)}
33
+ aria-label={t(M['chat.chat_tab_list.open_chat_with'], { name: tab.room.name })}
34
+ title={tab.room.name}
35
+ >
36
+ <Avatar
37
+ name={tab.room.name}
38
+ avatarUrl={tab.room.avatarUrl}
39
+ size="sm"
40
+ />
41
+ {#if tab.unreadCount > 0}
42
+ <span class="tab-list__badge" aria-label={t(M['chat.chat_tab_list.unread'], { count: tab.unreadCount })}>
43
+ {tab.unreadCount > 99 ? '99+' : tab.unreadCount}
44
+ </span>
45
+ {/if}
46
+ </button>
47
+ <button
48
+ class="tab-list__close"
49
+ type="button"
50
+ onclick={(e) => { e.stopPropagation(); onclose(tab.roomId); }}
51
+ aria-label={t(M['chat.chat_tab_list.close_chat_with'], { name: tab.room.name })}
52
+ title={t(M['chat.chat_tab_list.close'])}
53
+ >
54
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
55
+ <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z" />
56
+ </svg>
57
+ </button>
58
+ </div>
59
+ {/each}
60
+ </nav>
61
+ {/if}
62
+
63
+ <style>
64
+ .tab-list {
65
+ display: flex;
66
+ align-items: flex-end;
67
+ gap: var(--smrt-spacing-1, 4px);
68
+ pointer-events: auto;
69
+ }
70
+
71
+ .tab-list__item {
72
+ position: relative;
73
+ display: flex;
74
+ flex-direction: column;
75
+ align-items: center;
76
+ }
77
+
78
+ .tab-list__btn {
79
+ position: relative;
80
+ display: inline-flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ padding: var(--smrt-spacing-2, 8px);
84
+ border: none;
85
+ background: var(--smrt-color-surface-container, #f0f0f4);
86
+ border-radius: var(--smrt-radius-full, 9999px);
87
+ cursor: pointer;
88
+ transition: background var(--smrt-duration-short2, 150ms),
89
+ box-shadow var(--smrt-duration-short2, 150ms);
90
+ box-shadow: var(--smrt-elevation-1, 0 1px 3px rgba(0, 0, 0, 0.1));
91
+ }
92
+
93
+ .tab-list__btn:hover {
94
+ background: var(--smrt-color-surface-container-high, #e6e6ea);
95
+ box-shadow: var(--smrt-elevation-2, 0 2px 6px rgba(0, 0, 0, 0.15));
96
+ }
97
+
98
+ .tab-list__btn:focus-visible {
99
+ outline: 2px solid var(--smrt-color-primary, #005ac1);
100
+ outline-offset: 2px;
101
+ }
102
+
103
+ .tab-list__badge {
104
+ position: absolute;
105
+ top: -2px;
106
+ right: -2px;
107
+ display: inline-flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ min-width: 16px;
111
+ height: 16px;
112
+ padding: 0 var(--smrt-spacing-1, 4px);
113
+ border-radius: var(--smrt-radius-full, 9999px);
114
+ background: var(--smrt-color-error, #ba1a1a);
115
+ color: var(--smrt-color-on-error, #ffffff);
116
+ font-size: var(--smrt-typography-label-small-size, 0.625rem);
117
+ font-weight: var(--smrt-typography-weight-semibold, 600);
118
+ line-height: 1;
119
+ border: 2px solid var(--smrt-color-surface, #fefbff);
120
+ }
121
+
122
+ .tab-list__close {
123
+ display: none;
124
+ position: absolute;
125
+ top: -4px;
126
+ right: -4px;
127
+ width: 18px;
128
+ height: 18px;
129
+ align-items: center;
130
+ justify-content: center;
131
+ border: none;
132
+ background: var(--smrt-color-surface-variant, #e1e2ec);
133
+ color: var(--smrt-color-on-surface-variant, #43474e);
134
+ border-radius: var(--smrt-radius-full, 9999px);
135
+ cursor: pointer;
136
+ padding: 0;
137
+ font-size: 0;
138
+ }
139
+
140
+ .tab-list__item:hover .tab-list__close {
141
+ display: inline-flex;
142
+ }
143
+
144
+ .tab-list__item:hover .tab-list__badge {
145
+ display: none;
146
+ }
147
+
148
+ .tab-list__close:hover {
149
+ background: var(--smrt-color-error-container, #ffdad6);
150
+ color: var(--smrt-color-on-error-container, #410002);
151
+ }
152
+
153
+ @media (prefers-reduced-motion: reduce) {
154
+ .tab-list__btn {
155
+ transition: none;
156
+ }
157
+ }
158
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { ChatTabState } from '../../types.js';
2
+ export interface Props {
3
+ /** Collapsed/minimized tabs */
4
+ tabs: ChatTabState[];
5
+ /** Select (expand) a tab */
6
+ onselect: (roomId: string) => void;
7
+ /** Close a tab */
8
+ onclose: (roomId: string) => void;
9
+ }
10
+ declare const ChatTabList: import("svelte").Component<Props, {}, "">;
11
+ type ChatTabList = ReturnType<typeof ChatTabList>;
12
+ export default ChatTabList;
13
+ //# sourceMappingURL=ChatTabList.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatTabList.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/tabs/ChatTabList.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAInD,MAAM,WAAW,KAAK;IACpB,+BAA+B;IAC/B,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,4BAA4B;IAC5B,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,kBAAkB;IAClB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAwCD,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ChatTabs - Bottom bar with expandable chat tabs (Facebook Messenger style)
4
+ * Fixed to viewport bottom. Shows collapsed tab headers that expand upward on click.
5
+ */
6
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
7
+ import { M } from '../../i18n.messages.js';
8
+ import type { ChatMessageData, ChatTabState } from '../../types.js';
9
+ import ChatTab from './ChatTab.svelte';
10
+ import ChatTabList from './ChatTabList.svelte';
11
+
12
+ const { t } = useI18n();
13
+
14
+ export interface Props {
15
+ /** Active chat tabs */
16
+ tabs: ChatTabState[];
17
+ /** Messages keyed by roomId */
18
+ messagesByRoom?: Record<string, ChatMessageData[]>;
19
+ /** Current user's profile ID */
20
+ currentProfileId?: string;
21
+ /** Expand a tab */
22
+ onexpand: (roomId: string) => void;
23
+ /** Collapse a tab */
24
+ oncollapse: (roomId: string) => void;
25
+ /** Close a tab */
26
+ onclose: (roomId: string) => void;
27
+ /** Send a message in a tab */
28
+ onsend?: (roomId: string, content: string) => void;
29
+ }
30
+
31
+ const {
32
+ tabs,
33
+ messagesByRoom = {},
34
+ currentProfileId = '',
35
+ onexpand,
36
+ oncollapse,
37
+ onclose,
38
+ onsend,
39
+ }: Props = $props();
40
+
41
+ const expandedTabs = $derived(tabs.filter((t) => t.isExpanded));
42
+ const collapsedTabs = $derived(tabs.filter((t) => !t.isExpanded));
43
+ </script>
44
+
45
+ <div class="chat-tabs" aria-label={t(M['chat.chat_tabs.tabs_label'])}>
46
+ <div class="chat-tabs__expanded">
47
+ {#each expandedTabs as tab (tab.roomId)}
48
+ <ChatTab
49
+ {tab}
50
+ messages={messagesByRoom[tab.roomId] ?? []}
51
+ {currentProfileId}
52
+ onsend={(content) => onsend?.(tab.roomId, content)}
53
+ oncollapse={() => oncollapse(tab.roomId)}
54
+ onclose={() => onclose(tab.roomId)}
55
+ onexpand={() => onexpand(tab.roomId)}
56
+ />
57
+ {/each}
58
+ </div>
59
+
60
+ {#if collapsedTabs.length > 0}
61
+ <ChatTabList
62
+ tabs={collapsedTabs}
63
+ onselect={(roomId) => onexpand(roomId)}
64
+ onclose={(roomId) => onclose(roomId)}
65
+ />
66
+ {/if}
67
+ </div>
68
+
69
+ <style>
70
+ .chat-tabs {
71
+ position: fixed;
72
+ bottom: 0;
73
+ right: 0;
74
+ display: flex;
75
+ align-items: flex-end;
76
+ gap: var(--smrt-spacing-2, 8px);
77
+ padding: 0 var(--smrt-spacing-4, 16px);
78
+ z-index: var(--smrt-z-index-sticky, 1100);
79
+ pointer-events: none;
80
+ }
81
+
82
+ .chat-tabs__expanded {
83
+ display: flex;
84
+ align-items: flex-end;
85
+ gap: var(--smrt-spacing-2, 8px);
86
+ pointer-events: auto;
87
+ }
88
+ </style>
@@ -0,0 +1,21 @@
1
+ import type { ChatMessageData, ChatTabState } from '../../types.js';
2
+ export interface Props {
3
+ /** Active chat tabs */
4
+ tabs: ChatTabState[];
5
+ /** Messages keyed by roomId */
6
+ messagesByRoom?: Record<string, ChatMessageData[]>;
7
+ /** Current user's profile ID */
8
+ currentProfileId?: string;
9
+ /** Expand a tab */
10
+ onexpand: (roomId: string) => void;
11
+ /** Collapse a tab */
12
+ oncollapse: (roomId: string) => void;
13
+ /** Close a tab */
14
+ onclose: (roomId: string) => void;
15
+ /** Send a message in a tab */
16
+ onsend?: (roomId: string, content: string) => void;
17
+ }
18
+ declare const ChatTabs: import("svelte").Component<Props, {}, "">;
19
+ type ChatTabs = ReturnType<typeof ChatTabs>;
20
+ export default ChatTabs;
21
+ //# sourceMappingURL=ChatTabs.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChatTabs.svelte.d.ts","sourceRoot":"","sources":["../../../../src/svelte/components/tabs/ChatTabs.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKpE,MAAM,WAAW,KAAK;IACpB,uBAAuB;IACvB,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,+BAA+B;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAAC;IACnD,gCAAgC;IAChC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mBAAmB;IACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,qBAAqB;IACrB,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,kBAAkB;IAClB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,8BAA8B;IAC9B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD;AA0CD,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}