@happyvertical/smrt-messages 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 (153) hide show
  1. package/AGENTS.md +31 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +103 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/collections/AccountCollection.d.ts +42 -0
  8. package/dist/collections/AccountCollection.d.ts.map +1 -0
  9. package/dist/collections/AttachmentCollection.d.ts +67 -0
  10. package/dist/collections/AttachmentCollection.d.ts.map +1 -0
  11. package/dist/collections/BlacklistCollection.d.ts +14 -0
  12. package/dist/collections/BlacklistCollection.d.ts.map +1 -0
  13. package/dist/collections/EmailAccountCollection.d.ts +74 -0
  14. package/dist/collections/EmailAccountCollection.d.ts.map +1 -0
  15. package/dist/collections/EmailAttachmentCollection.d.ts +38 -0
  16. package/dist/collections/EmailAttachmentCollection.d.ts.map +1 -0
  17. package/dist/collections/EmailCollection.d.ts +81 -0
  18. package/dist/collections/EmailCollection.d.ts.map +1 -0
  19. package/dist/collections/EmailFolderCollection.d.ts +85 -0
  20. package/dist/collections/EmailFolderCollection.d.ts.map +1 -0
  21. package/dist/collections/MessageCollection.d.ts +74 -0
  22. package/dist/collections/MessageCollection.d.ts.map +1 -0
  23. package/dist/collections/WhitelistCollection.d.ts +18 -0
  24. package/dist/collections/WhitelistCollection.d.ts.map +1 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +3068 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/manifest.json +10576 -0
  30. package/dist/models/Account.d.ts +47 -0
  31. package/dist/models/Account.d.ts.map +1 -0
  32. package/dist/models/Attachment.d.ts +48 -0
  33. package/dist/models/Attachment.d.ts.map +1 -0
  34. package/dist/models/Blacklist.d.ts +21 -0
  35. package/dist/models/Blacklist.d.ts.map +1 -0
  36. package/dist/models/Email.d.ts +98 -0
  37. package/dist/models/Email.d.ts.map +1 -0
  38. package/dist/models/EmailAccount.d.ts +65 -0
  39. package/dist/models/EmailAccount.d.ts.map +1 -0
  40. package/dist/models/EmailAttachment.d.ts +19 -0
  41. package/dist/models/EmailAttachment.d.ts.map +1 -0
  42. package/dist/models/EmailFolder.d.ts +65 -0
  43. package/dist/models/EmailFolder.d.ts.map +1 -0
  44. package/dist/models/Message.d.ts +105 -0
  45. package/dist/models/Message.d.ts.map +1 -0
  46. package/dist/models/SlackAccount.d.ts +13 -0
  47. package/dist/models/SlackAccount.d.ts.map +1 -0
  48. package/dist/models/SlackMessage.d.ts +34 -0
  49. package/dist/models/SlackMessage.d.ts.map +1 -0
  50. package/dist/models/Tweet.d.ts +31 -0
  51. package/dist/models/Tweet.d.ts.map +1 -0
  52. package/dist/models/TwitterAccount.d.ts +12 -0
  53. package/dist/models/TwitterAccount.d.ts.map +1 -0
  54. package/dist/models/Whitelist.d.ts +21 -0
  55. package/dist/models/Whitelist.d.ts.map +1 -0
  56. package/dist/playground.d.ts +2 -0
  57. package/dist/playground.d.ts.map +1 -0
  58. package/dist/playground.js +176 -0
  59. package/dist/playground.js.map +1 -0
  60. package/dist/senders/EmailSender.d.ts +13 -0
  61. package/dist/senders/EmailSender.d.ts.map +1 -0
  62. package/dist/senders/SlackSender.d.ts +11 -0
  63. package/dist/senders/SlackSender.d.ts.map +1 -0
  64. package/dist/senders/TweetSender.d.ts +11 -0
  65. package/dist/senders/TweetSender.d.ts.map +1 -0
  66. package/dist/smrt-knowledge.json +4234 -0
  67. package/dist/svelte/components/AccountAvatar.svelte +107 -0
  68. package/dist/svelte/components/AccountAvatar.svelte.d.ts +12 -0
  69. package/dist/svelte/components/AccountAvatar.svelte.d.ts.map +1 -0
  70. package/dist/svelte/components/AccountCard.svelte +173 -0
  71. package/dist/svelte/components/AccountCard.svelte.d.ts +12 -0
  72. package/dist/svelte/components/AccountCard.svelte.d.ts.map +1 -0
  73. package/dist/svelte/components/AccountList.svelte +90 -0
  74. package/dist/svelte/components/AccountList.svelte.d.ts +12 -0
  75. package/dist/svelte/components/AccountList.svelte.d.ts.map +1 -0
  76. package/dist/svelte/components/AttachmentChip.svelte +99 -0
  77. package/dist/svelte/components/AttachmentChip.svelte.d.ts +12 -0
  78. package/dist/svelte/components/AttachmentChip.svelte.d.ts.map +1 -0
  79. package/dist/svelte/components/AttachmentUpload.svelte +160 -0
  80. package/dist/svelte/components/AttachmentUpload.svelte.d.ts +11 -0
  81. package/dist/svelte/components/AttachmentUpload.svelte.d.ts.map +1 -0
  82. package/dist/svelte/components/ComposeForm.svelte +387 -0
  83. package/dist/svelte/components/ComposeForm.svelte.d.ts +13 -0
  84. package/dist/svelte/components/ComposeForm.svelte.d.ts.map +1 -0
  85. package/dist/svelte/components/EmailAccountManager.svelte +690 -0
  86. package/dist/svelte/components/EmailAccountManager.svelte.d.ts +15 -0
  87. package/dist/svelte/components/EmailAccountManager.svelte.d.ts.map +1 -0
  88. package/dist/svelte/components/EmailFilterManager.svelte +687 -0
  89. package/dist/svelte/components/EmailFilterManager.svelte.d.ts +14 -0
  90. package/dist/svelte/components/EmailFilterManager.svelte.d.ts.map +1 -0
  91. package/dist/svelte/components/FolderNav.svelte +171 -0
  92. package/dist/svelte/components/FolderNav.svelte.d.ts +11 -0
  93. package/dist/svelte/components/FolderNav.svelte.d.ts.map +1 -0
  94. package/dist/svelte/components/ForwardForm.svelte +166 -0
  95. package/dist/svelte/components/ForwardForm.svelte.d.ts +10 -0
  96. package/dist/svelte/components/ForwardForm.svelte.d.ts.map +1 -0
  97. package/dist/svelte/components/MessageCard.svelte +336 -0
  98. package/dist/svelte/components/MessageCard.svelte.d.ts +20 -0
  99. package/dist/svelte/components/MessageCard.svelte.d.ts.map +1 -0
  100. package/dist/svelte/components/MessageDetail.svelte +309 -0
  101. package/dist/svelte/components/MessageDetail.svelte.d.ts +18 -0
  102. package/dist/svelte/components/MessageDetail.svelte.d.ts.map +1 -0
  103. package/dist/svelte/components/MessageFilters.svelte +228 -0
  104. package/dist/svelte/components/MessageFilters.svelte.d.ts +13 -0
  105. package/dist/svelte/components/MessageFilters.svelte.d.ts.map +1 -0
  106. package/dist/svelte/components/MessageList.svelte +101 -0
  107. package/dist/svelte/components/MessageList.svelte.d.ts +23 -0
  108. package/dist/svelte/components/MessageList.svelte.d.ts.map +1 -0
  109. package/dist/svelte/components/MessageStatusIndicator.svelte +82 -0
  110. package/dist/svelte/components/MessageStatusIndicator.svelte.d.ts +11 -0
  111. package/dist/svelte/components/MessageStatusIndicator.svelte.d.ts.map +1 -0
  112. package/dist/svelte/components/MessageToolbar.svelte +131 -0
  113. package/dist/svelte/components/MessageToolbar.svelte.d.ts +14 -0
  114. package/dist/svelte/components/MessageToolbar.svelte.d.ts.map +1 -0
  115. package/dist/svelte/components/MessageTypeBadge.svelte +59 -0
  116. package/dist/svelte/components/MessageTypeBadge.svelte.d.ts +9 -0
  117. package/dist/svelte/components/MessageTypeBadge.svelte.d.ts.map +1 -0
  118. package/dist/svelte/components/RecipientInput.svelte +150 -0
  119. package/dist/svelte/components/RecipientInput.svelte.d.ts +11 -0
  120. package/dist/svelte/components/RecipientInput.svelte.d.ts.map +1 -0
  121. package/dist/svelte/components/ReplyForm.svelte +159 -0
  122. package/dist/svelte/components/ReplyForm.svelte.d.ts +11 -0
  123. package/dist/svelte/components/ReplyForm.svelte.d.ts.map +1 -0
  124. package/dist/svelte/components/SendStatusBadge.svelte +64 -0
  125. package/dist/svelte/components/SendStatusBadge.svelte.d.ts +8 -0
  126. package/dist/svelte/components/SendStatusBadge.svelte.d.ts.map +1 -0
  127. package/dist/svelte/components/ThreadView.svelte +240 -0
  128. package/dist/svelte/components/ThreadView.svelte.d.ts +12 -0
  129. package/dist/svelte/components/ThreadView.svelte.d.ts.map +1 -0
  130. package/dist/svelte/i18n.d.ts +42 -0
  131. package/dist/svelte/i18n.d.ts.map +1 -0
  132. package/dist/svelte/i18n.js +60 -0
  133. package/dist/svelte/i18n.messages.d.ts +32 -0
  134. package/dist/svelte/i18n.messages.d.ts.map +1 -0
  135. package/dist/svelte/i18n.messages.js +46 -0
  136. package/dist/svelte/index.d.ts +54 -0
  137. package/dist/svelte/index.d.ts.map +1 -0
  138. package/dist/svelte/index.js +44 -0
  139. package/dist/svelte/playground.d.ts +341 -0
  140. package/dist/svelte/playground.d.ts.map +1 -0
  141. package/dist/svelte/playground.js +171 -0
  142. package/dist/svelte/types.d.ts +195 -0
  143. package/dist/svelte/types.d.ts.map +1 -0
  144. package/dist/svelte/types.js +6 -0
  145. package/dist/types.d.ts +316 -0
  146. package/dist/types.d.ts.map +1 -0
  147. package/dist/types.js +2 -0
  148. package/dist/types.js.map +1 -0
  149. package/dist/ui.d.ts +4 -0
  150. package/dist/ui.d.ts.map +1 -0
  151. package/dist/ui.js +103 -0
  152. package/dist/ui.js.map +1 -0
  153. package/package.json +104 -0
@@ -0,0 +1,20 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { AccountData, MessageData } from '../types.js';
3
+ export interface Props {
4
+ message: MessageData;
5
+ selected?: boolean;
6
+ compact?: boolean;
7
+ showAccount?: boolean;
8
+ showType?: boolean;
9
+ onclick?: (message: MessageData) => void;
10
+ onselect?: (message: MessageData) => void;
11
+ onflag?: (message: MessageData) => void;
12
+ account?: AccountData;
13
+ typeContent?: Snippet<[{
14
+ message: MessageData;
15
+ }]>;
16
+ }
17
+ declare const MessageCard: import("svelte").Component<Props, {}, "">;
18
+ type MessageCard = ReturnType<typeof MessageCard>;
19
+ export default MessageCard;
20
+ //# sourceMappingURL=MessageCard.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageCard.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/MessageCard.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK5D,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC1C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC,CAAC;CACnD;AA6ID,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,309 @@
1
+ <script lang="ts">
2
+ /**
3
+ * MessageDetail - Full message view with type-adaptive sections
4
+ */
5
+
6
+ import { Card } from '@happyvertical/smrt-ui/ui';
7
+ import type { Snippet } from 'svelte';
8
+ import type { AccountData, AttachmentData, MessageData } from '../types.js';
9
+ import AttachmentChip from './AttachmentChip.svelte';
10
+ import MessageStatusIndicator from './MessageStatusIndicator.svelte';
11
+ import MessageTypeBadge from './MessageTypeBadge.svelte';
12
+
13
+ export interface Props {
14
+ message: MessageData;
15
+ attachments?: AttachmentData[];
16
+ account?: AccountData;
17
+ showHtml?: boolean;
18
+ onreply?: (message: MessageData) => void;
19
+ onforward?: (message: MessageData) => void;
20
+ ondelete?: (message: MessageData) => void;
21
+ typeDetail?: Snippet<[{ message: MessageData }]>;
22
+ }
23
+
24
+ const {
25
+ message,
26
+ attachments = [],
27
+ account,
28
+ showHtml = false,
29
+ onreply,
30
+ onforward,
31
+ ondelete,
32
+ typeDetail,
33
+ }: Props = $props();
34
+
35
+ const _formattedDate = $derived.by(() => {
36
+ if (!message.date) return '';
37
+ const d =
38
+ typeof message.date === 'string' ? new Date(message.date) : message.date;
39
+ return d.toLocaleDateString('en-US', {
40
+ weekday: 'long',
41
+ year: 'numeric',
42
+ month: 'long',
43
+ day: 'numeric',
44
+ hour: 'numeric',
45
+ minute: '2-digit',
46
+ });
47
+ });
48
+
49
+ const _recipients = $derived.by(() => {
50
+ return message.recipientAddresses
51
+ .map((r) => (r.name ? `${r.name} <${r.address}>` : r.address))
52
+ .join(', ');
53
+ });
54
+
55
+ const _ccRecipients = $derived.by(() => {
56
+ if (!message.ccAddresses || message.ccAddresses.length === 0) return '';
57
+ return message.ccAddresses
58
+ .map((r) => (r.name ? `${r.name} <${r.address}>` : r.address))
59
+ .join(', ');
60
+ });
61
+
62
+ const _tweetMeta = $derived.by(() => {
63
+ if (message.type !== 'tweet') return null;
64
+ return message.meta || {};
65
+ });
66
+
67
+ const _slackMeta = $derived.by(() => {
68
+ if (message.type !== 'slack') return null;
69
+ return message.meta || {};
70
+ });
71
+
72
+ const _sanitizedHtml = $derived.by(() => {
73
+ if (!message.htmlBody) return '';
74
+ return message.htmlBody
75
+ .replace(/<script[\s\S]*?<\/script>/gi, '')
76
+ .replace(/<iframe[\s\S]*?<\/iframe>/gi, '')
77
+ .replace(/\son\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]*)/gi, '')
78
+ .replace(/javascript\s*:/gi, 'blocked:')
79
+ .replace(/<link[^>]*>/gi, '')
80
+ .replace(/<style[\s\S]*?<\/style>/gi, '');
81
+ });
82
+ </script>
83
+
84
+ <article class="message-detail">
85
+ <Card padding="lg">
86
+ <header class="detail-header">
87
+ <div class="header-top">
88
+ <MessageTypeBadge type={message.type} size="md" />
89
+ <MessageStatusIndicator
90
+ isRead={message.isRead}
91
+ isFlagged={message.isFlagged}
92
+ hasAttachments={message.hasAttachments}
93
+ />
94
+ </div>
95
+
96
+ <h2 class="subject">{message.subject || '(no subject)'}</h2>
97
+
98
+ <div class="meta-grid">
99
+ <div class="meta-row">
100
+ <span class="meta-label">From</span>
101
+ <span class="meta-value">
102
+ {message.senderName ? `${message.senderName} <${message.senderAddress}>` : message.senderAddress}
103
+ </span>
104
+ </div>
105
+ <div class="meta-row">
106
+ <span class="meta-label">To</span>
107
+ <span class="meta-value">{_recipients}</span>
108
+ </div>
109
+ {#if _ccRecipients}
110
+ <div class="meta-row">
111
+ <span class="meta-label">Cc</span>
112
+ <span class="meta-value">{_ccRecipients}</span>
113
+ </div>
114
+ {/if}
115
+ <div class="meta-row">
116
+ <span class="meta-label">Date</span>
117
+ <time class="meta-value">{_formattedDate}</time>
118
+ </div>
119
+ {#if account}
120
+ <div class="meta-row">
121
+ <span class="meta-label">Account</span>
122
+ <span class="meta-value">{account.name}</span>
123
+ </div>
124
+ {/if}
125
+ </div>
126
+ </header>
127
+
128
+ {#if typeDetail}
129
+ <div class="type-detail">
130
+ {@render typeDetail({ message })}
131
+ </div>
132
+ {:else if _tweetMeta}
133
+ <div class="type-specific">
134
+ <span>🔄 {_tweetMeta.retweetCount || 0}</span>
135
+ <span>❤ {_tweetMeta.likeCount || 0}</span>
136
+ <span>💬 {_tweetMeta.replyCount || 0}</span>
137
+ </div>
138
+ {:else if _slackMeta}
139
+ <div class="type-specific">
140
+ {#if _slackMeta.channelName}
141
+ <span>#{_slackMeta.channelName}</span>
142
+ {/if}
143
+ </div>
144
+ {/if}
145
+
146
+ <div class="body">
147
+ {#if showHtml && message.htmlBody}
148
+ {@html _sanitizedHtml}
149
+ {:else}
150
+ <pre class="body-text">{message.body}</pre>
151
+ {/if}
152
+ </div>
153
+
154
+ {#if attachments.length > 0}
155
+ <div class="attachments">
156
+ <h3 class="attachments-label">Attachments ({attachments.length})</h3>
157
+ <div class="attachments-list">
158
+ {#each attachments as attachment (attachment.id)}
159
+ <AttachmentChip {attachment} />
160
+ {/each}
161
+ </div>
162
+ </div>
163
+ {/if}
164
+
165
+ {#if onreply || onforward || ondelete}
166
+ <div class="actions">
167
+ {#if onreply}
168
+ <button class="action-btn" type="button" onclick={() => onreply?.(message)}>
169
+ ↩ Reply
170
+ </button>
171
+ {/if}
172
+ {#if onforward}
173
+ <button class="action-btn" type="button" onclick={() => onforward?.(message)}>
174
+ ↪ Forward
175
+ </button>
176
+ {/if}
177
+ {#if ondelete}
178
+ <button class="action-btn action-btn--danger" type="button" onclick={() => ondelete?.(message)}>
179
+ 🗑 Delete
180
+ </button>
181
+ {/if}
182
+ </div>
183
+ {/if}
184
+ </Card>
185
+ </article>
186
+
187
+ <style>
188
+ .message-detail {
189
+ max-width: 100%;
190
+ }
191
+
192
+ .detail-header {
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: 0.75rem;
196
+ padding-bottom: 1rem;
197
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6d0);
198
+ }
199
+
200
+ .header-top {
201
+ display: flex;
202
+ align-items: center;
203
+ justify-content: space-between;
204
+ }
205
+
206
+ .subject {
207
+ margin: 0;
208
+ font: var(--smrt-typography-title-large-font, 600 1.375rem / 1.25 sans-serif);
209
+ color: var(--smrt-color-on-surface, #1a1c1e);
210
+ }
211
+
212
+ .meta-grid {
213
+ display: flex;
214
+ flex-direction: column;
215
+ gap: 0.25rem;
216
+ }
217
+
218
+ .meta-row {
219
+ display: flex;
220
+ gap: 0.75rem;
221
+ font: var(--smrt-typography-body-small-font, 0.75rem / 1.33 sans-serif);
222
+ }
223
+
224
+ .meta-label {
225
+ flex-shrink: 0;
226
+ width: 4rem;
227
+ color: var(--smrt-color-on-surface-variant, #43474e);
228
+ font-weight: var(--smrt-typography-weight-medium, 500);
229
+ }
230
+
231
+ .meta-value {
232
+ color: var(--smrt-color-on-surface, #1a1c1e);
233
+ overflow: hidden;
234
+ text-overflow: ellipsis;
235
+ }
236
+
237
+ .type-specific {
238
+ display: flex;
239
+ gap: 1rem;
240
+ padding: 0.75rem 0;
241
+ font: var(--smrt-typography-body-medium-font, 0.875rem / 1.25 sans-serif);
242
+ color: var(--smrt-color-on-surface-variant, #43474e);
243
+ }
244
+
245
+ .type-detail {
246
+ padding: 0.75rem 0;
247
+ }
248
+
249
+ .body {
250
+ padding: 1rem 0;
251
+ }
252
+
253
+ .body-text {
254
+ font: var(--smrt-typography-body-large-font, 1rem / 1.5 sans-serif);
255
+ color: var(--smrt-color-on-surface, #1a1c1e);
256
+ white-space: pre-wrap;
257
+ word-break: break-word;
258
+ margin: 0;
259
+ font-family: inherit;
260
+ }
261
+
262
+ .attachments {
263
+ padding-top: 0.75rem;
264
+ border-top: 1px solid var(--smrt-color-outline-variant, #c4c6d0);
265
+ }
266
+
267
+ .attachments-label {
268
+ margin: 0 0 0.5rem;
269
+ font: var(--smrt-typography-label-large-font, 500 0.875rem / 1.25 sans-serif);
270
+ color: var(--smrt-color-on-surface-variant, #43474e);
271
+ }
272
+
273
+ .attachments-list {
274
+ display: flex;
275
+ flex-wrap: wrap;
276
+ gap: 0.375rem;
277
+ }
278
+
279
+ .actions {
280
+ display: flex;
281
+ gap: 0.5rem;
282
+ padding-top: 1rem;
283
+ border-top: 1px solid var(--smrt-color-outline-variant, #c4c6d0);
284
+ }
285
+
286
+ .action-btn {
287
+ padding: 0.5rem 1rem;
288
+ border: 1px solid var(--smrt-color-outline, #72787e);
289
+ border-radius: var(--smrt-radius-small, 0.25rem);
290
+ background: var(--smrt-color-surface, #fefbff);
291
+ color: var(--smrt-color-on-surface, #1a1c1e);
292
+ font: var(--smrt-typography-label-large-font, 500 0.875rem / 1.25 sans-serif);
293
+ cursor: pointer;
294
+ transition: background var(--smrt-duration-short2, 150ms);
295
+ }
296
+
297
+ .action-btn:hover {
298
+ background: var(--smrt-color-surface-variant, #e1e2ec);
299
+ }
300
+
301
+ .action-btn--danger {
302
+ color: var(--smrt-color-error, #ba1a1a);
303
+ border-color: var(--smrt-color-error, #ba1a1a);
304
+ }
305
+
306
+ .action-btn--danger:hover {
307
+ background: var(--smrt-color-error-container, #ffdad6);
308
+ }
309
+ </style>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { AccountData, AttachmentData, MessageData } from '../types.js';
3
+ export interface Props {
4
+ message: MessageData;
5
+ attachments?: AttachmentData[];
6
+ account?: AccountData;
7
+ showHtml?: boolean;
8
+ onreply?: (message: MessageData) => void;
9
+ onforward?: (message: MessageData) => void;
10
+ ondelete?: (message: MessageData) => void;
11
+ typeDetail?: Snippet<[{
12
+ message: MessageData;
13
+ }]>;
14
+ }
15
+ declare const MessageDetail: import("svelte").Component<Props, {}, "">;
16
+ type MessageDetail = ReturnType<typeof MessageDetail>;
17
+ export default MessageDetail;
18
+ //# sourceMappingURL=MessageDetail.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageDetail.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/MessageDetail.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM5E,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAC/B,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC3C,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,CAAC;IAC1C,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC,CAAC;CAClD;AA+KD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -0,0 +1,228 @@
1
+ <script lang="ts">
2
+ /**
3
+ * MessageFilters - Filter/sort controls bar
4
+ */
5
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
6
+ import { M } from '../i18n.messages.js';
7
+ import type {
8
+ AccountData,
9
+ MessageFilterState,
10
+ MessageSort,
11
+ MessageType,
12
+ } from '../types.js';
13
+
14
+ const { t } = useI18n();
15
+
16
+ export interface Props {
17
+ filters?: MessageFilterState;
18
+ sort?: MessageSort;
19
+ accounts?: AccountData[];
20
+ availableTypes?: MessageType[];
21
+ onfilterchange?: (filters: MessageFilterState) => void;
22
+ onsearch?: (query: string) => void;
23
+ }
24
+
25
+ const {
26
+ filters = {},
27
+ sort = { field: 'date', direction: 'desc' },
28
+ accounts = [],
29
+ availableTypes = ['email', 'tweet', 'slack'],
30
+ onfilterchange,
31
+ onsearch,
32
+ }: Props = $props();
33
+
34
+ function getInitialFilterState() {
35
+ return {
36
+ searchValue: filters.search ?? '',
37
+ filters,
38
+ };
39
+ }
40
+
41
+ const initialFilterState = getInitialFilterState();
42
+ let searchValue = $state(initialFilterState.searchValue);
43
+ let appliedFilters: MessageFilterState | undefined = initialFilterState.filters;
44
+
45
+ $effect(() => {
46
+ if (appliedFilters === filters) {
47
+ return;
48
+ }
49
+
50
+ appliedFilters = filters;
51
+ searchValue = filters.search ?? '';
52
+ });
53
+
54
+ function updateFilter(key: keyof MessageFilterState, value: any) {
55
+ const updated = { ...filters, [key]: value || undefined };
56
+ onfilterchange?.(updated);
57
+ }
58
+
59
+ function handleSearch() {
60
+ onsearch?.(searchValue);
61
+ updateFilter('search', searchValue);
62
+ }
63
+
64
+ function clearFilters() {
65
+ searchValue = '';
66
+ onfilterchange?.({});
67
+ }
68
+
69
+ const _hasActiveFilters = $derived(
70
+ !!filters.type ||
71
+ !!filters.accountId ||
72
+ filters.isRead !== undefined ||
73
+ filters.isFlagged !== undefined ||
74
+ !!filters.search,
75
+ );
76
+ </script>
77
+
78
+ <div class="message-filters" role="search" aria-label={t(M['messages.message_filters.filters_label'])}>
79
+ <div class="search-row">
80
+ <input
81
+ type="search"
82
+ class="search-input"
83
+ placeholder={t(M['messages.message_filters.search_placeholder'])}
84
+ bind:value={searchValue}
85
+ onkeydown={(e) => { if (e.key === 'Enter') handleSearch(); }}
86
+ aria-label={t(M['messages.message_filters.search_label'])}
87
+ />
88
+ <button class="search-btn" type="button" onclick={handleSearch}>Search</button>
89
+ </div>
90
+
91
+ <div class="filter-row">
92
+ <select
93
+ class="filter-select"
94
+ value={filters.type || ''}
95
+ onchange={(e) => updateFilter('type', (e.target as HTMLSelectElement).value)}
96
+ aria-label={t(M['messages.message_filters.filter_by_type'])}
97
+ >
98
+ <option value="">{t(M['messages.message_filters.all_types'])}</option>
99
+ {#each availableTypes as type}
100
+ <option value={type}>{type === 'email' ? 'Email' : type === 'tweet' ? 'Tweet' : type === 'slack' ? 'Slack' : type}</option>
101
+ {/each}
102
+ </select>
103
+
104
+ {#if accounts.length > 0}
105
+ <select
106
+ class="filter-select"
107
+ value={filters.accountId || ''}
108
+ onchange={(e) => updateFilter('accountId', (e.target as HTMLSelectElement).value)}
109
+ aria-label={t(M['messages.message_filters.filter_by_account'])}
110
+ >
111
+ <option value="">{t(M['messages.message_filters.all_accounts'])}</option>
112
+ {#each accounts as account}
113
+ <option value={account.id}>{account.name}</option>
114
+ {/each}
115
+ </select>
116
+ {/if}
117
+
118
+ <select
119
+ class="filter-select"
120
+ value={filters.isRead === undefined ? '' : filters.isRead ? 'read' : 'unread'}
121
+ onchange={(e) => {
122
+ const val = (e.target as HTMLSelectElement).value;
123
+ updateFilter('isRead', val === '' ? undefined : val === 'read');
124
+ }}
125
+ aria-label={t(M['messages.message_filters.filter_by_read_status'])}
126
+ >
127
+ <option value="">Read & unread</option>
128
+ <option value="unread">{t(M['messages.message_filters.unread_only'])}</option>
129
+ <option value="read">{t(M['messages.message_filters.read_only'])}</option>
130
+ </select>
131
+
132
+ <label class="filter-checkbox">
133
+ <input
134
+ type="checkbox"
135
+ checked={filters.isFlagged === true}
136
+ onchange={(e) => updateFilter('isFlagged', (e.target as HTMLInputElement).checked ? true : undefined)}
137
+ />
138
+ Flagged
139
+ </label>
140
+
141
+ {#if _hasActiveFilters}
142
+ <button class="clear-btn" type="button" onclick={clearFilters}>
143
+ {t(M['messages.message_filters.clear_filters'])}
144
+ </button>
145
+ {/if}
146
+ </div>
147
+ </div>
148
+
149
+ <style>
150
+ .message-filters {
151
+ display: flex;
152
+ flex-direction: column;
153
+ gap: 0.5rem;
154
+ padding: 0.75rem;
155
+ background: var(--smrt-color-surface, #fefbff);
156
+ border: 1px solid var(--smrt-color-outline-variant, #c4c6d0);
157
+ border-radius: var(--smrt-radius-medium, 0.5rem);
158
+ }
159
+
160
+ .search-row {
161
+ display: flex;
162
+ gap: 0.5rem;
163
+ }
164
+
165
+ .search-input {
166
+ flex: 1;
167
+ padding: 0.5rem 0.75rem;
168
+ border: 1px solid var(--smrt-color-outline, #72787e);
169
+ border-radius: var(--smrt-radius-small, 0.25rem);
170
+ font: var(--smrt-typography-body-medium-font, 0.875rem / 1.25 sans-serif);
171
+ background: var(--smrt-color-surface, #fefbff);
172
+ color: var(--smrt-color-on-surface, #1a1c1e);
173
+ }
174
+
175
+ .search-input:focus {
176
+ outline: 2px solid var(--smrt-color-primary, #005ac1);
177
+ outline-offset: -1px;
178
+ }
179
+
180
+ .search-btn {
181
+ padding: 0.5rem 1rem;
182
+ border: none;
183
+ border-radius: var(--smrt-radius-small, 0.25rem);
184
+ background: var(--smrt-color-primary, #005ac1);
185
+ color: var(--smrt-color-on-primary, #fff);
186
+ font: var(--smrt-typography-label-large-font, 500 0.875rem / 1.25 sans-serif);
187
+ cursor: pointer;
188
+ }
189
+
190
+ .search-btn:hover {
191
+ opacity: 0.9;
192
+ }
193
+
194
+ .filter-row {
195
+ display: flex;
196
+ flex-wrap: wrap;
197
+ align-items: center;
198
+ gap: 0.5rem;
199
+ }
200
+
201
+ .filter-select {
202
+ padding: 0.375rem 0.5rem;
203
+ border: 1px solid var(--smrt-color-outline, #72787e);
204
+ border-radius: var(--smrt-radius-small, 0.25rem);
205
+ font: var(--smrt-typography-body-small-font, 0.75rem / 1.33 sans-serif);
206
+ background: var(--smrt-color-surface, #fefbff);
207
+ color: var(--smrt-color-on-surface, #1a1c1e);
208
+ }
209
+
210
+ .filter-checkbox {
211
+ display: flex;
212
+ align-items: center;
213
+ gap: 0.25rem;
214
+ font: var(--smrt-typography-body-small-font, 0.75rem / 1.33 sans-serif);
215
+ color: var(--smrt-color-on-surface, #1a1c1e);
216
+ cursor: pointer;
217
+ }
218
+
219
+ .clear-btn {
220
+ padding: 0.25rem 0.5rem;
221
+ border: none;
222
+ background: none;
223
+ font: var(--smrt-typography-label-medium-font, 500 0.75rem / 1.33 sans-serif);
224
+ color: var(--smrt-color-primary, #005ac1);
225
+ cursor: pointer;
226
+ text-decoration: underline;
227
+ }
228
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { AccountData, MessageFilterState, MessageSort, MessageType } from '../types.js';
2
+ export interface Props {
3
+ filters?: MessageFilterState;
4
+ sort?: MessageSort;
5
+ accounts?: AccountData[];
6
+ availableTypes?: MessageType[];
7
+ onfilterchange?: (filters: MessageFilterState) => void;
8
+ onsearch?: (query: string) => void;
9
+ }
10
+ declare const MessageFilters: import("svelte").Component<Props, {}, "">;
11
+ type MessageFilters = ReturnType<typeof MessageFilters>;
12
+ export default MessageFilters;
13
+ //# sourceMappingURL=MessageFilters.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MessageFilters.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/MessageFilters.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAClB,WAAW,EACX,WAAW,EACZ,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,KAAK;IACpB,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,WAAW,EAAE,CAAC;IAC/B,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACvD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAgHD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -0,0 +1,101 @@
1
+ <script lang="ts">
2
+ /**
3
+ * MessageList - Unified message list with selection
4
+ */
5
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
6
+ import type { Snippet } from 'svelte';
7
+ import { M } from '../i18n.messages.js';
8
+ import type { AccountData, MessageData } from '../types.js';
9
+ import MessageCard from './MessageCard.svelte';
10
+
11
+ const { t } = useI18n();
12
+
13
+ export interface Props {
14
+ messages: MessageData[];
15
+ selected?: Set<string>;
16
+ activeMessageId?: string;
17
+ accounts?: AccountData[];
18
+ loading?: boolean;
19
+ emptyMessage?: string;
20
+ onmessageclick?: (message: MessageData) => void;
21
+ onselect?: (message: MessageData) => void;
22
+ onflag?: (message: MessageData) => void;
23
+ showType?: boolean;
24
+ showAccount?: boolean;
25
+ compact?: boolean;
26
+ card?: Snippet<[{ message: MessageData }]>;
27
+ }
28
+
29
+ const {
30
+ messages,
31
+ selected = new Set(),
32
+ activeMessageId,
33
+ accounts = [],
34
+ loading = false,
35
+ emptyMessage = 'No messages found.',
36
+ onmessageclick,
37
+ onselect,
38
+ onflag,
39
+ showType = true,
40
+ showAccount = false,
41
+ compact = false,
42
+ card,
43
+ }: Props = $props();
44
+
45
+ function getAccount(accountId: string): AccountData | undefined {
46
+ return accounts.find((a) => a.id === accountId);
47
+ }
48
+ </script>
49
+
50
+ <div class="message-list" role="grid" aria-label={t(M['messages.message_list.messages_label'])}>
51
+ {#if loading}
52
+ <div class="loading" role="status" aria-live="polite">
53
+ <p>{t(M['messages.message_list.loading'])}</p>
54
+ </div>
55
+ {:else if messages.length === 0}
56
+ <div class="empty" role="status" aria-live="polite">
57
+ <p>{emptyMessage}</p>
58
+ </div>
59
+ {:else}
60
+ {#each messages as message (message.id)}
61
+ {#if card}
62
+ {@render card({ message })}
63
+ {:else}
64
+ <MessageCard
65
+ {message}
66
+ selected={selected.has(message.id)}
67
+ {compact}
68
+ {showType}
69
+ {showAccount}
70
+ account={showAccount ? getAccount(message.accountId) : undefined}
71
+ onclick={onmessageclick}
72
+ {onselect}
73
+ {onflag}
74
+ />
75
+ {/if}
76
+ {/each}
77
+ {/if}
78
+ </div>
79
+
80
+ <style>
81
+ .message-list {
82
+ display: flex;
83
+ flex-direction: column;
84
+ border: 1px solid var(--smrt-color-outline-variant, #c4c6d0);
85
+ border-radius: var(--smrt-radius-medium, 0.5rem);
86
+ overflow: hidden;
87
+ background: var(--smrt-color-surface, #fefbff);
88
+ }
89
+
90
+ .loading,
91
+ .empty {
92
+ text-align: center;
93
+ padding: var(--smrt-spacing-3xl, 3rem);
94
+ color: var(--smrt-color-on-surface-variant, #43474e);
95
+ }
96
+
97
+ .loading p,
98
+ .empty p {
99
+ font: var(--smrt-typography-body-large-font, 1.125rem / 1.5 sans-serif);
100
+ }
101
+ </style>