@braedonsaunders/appkit-mailbox 1.0.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.
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAC7C,YAAY,EACV,UAAU,EACV,cAAc,EACd,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,UAAU,GACX,MAAM,SAAS,CAAA"}
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { syncMailbox, verifyImap } from './sync.js';
2
+ export { sendMail, verifySmtp } from './send.js';
3
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAChD,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA","sourcesContent":["export { syncMailbox, verifyImap } from './sync'\nexport { sendMail, verifySmtp } from './send'\nexport type {\n ImapCursor,\n InboundMessage,\n MailAddress,\n MailboxConnection,\n MailboxStore,\n OutboundAttachment,\n SendMailArgs,\n SendMailResult,\n SyncResult,\n} from './types'\n"]}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@braedonsaunders/appkit-mailbox",
3
+ "version": "1.0.0",
4
+ "description": "Mail engine: incremental IMAP sync + SMTP send with RFC-5322 threading, persistence-injected so the application owns the ledger, plus the three-pane operator inbox.",
5
+ "license": "AGPL-3.0-or-later",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "import": "./index.js",
11
+ "default": "./index.js"
12
+ },
13
+ "./react": {
14
+ "types": "./react.d.ts",
15
+ "import": "./react.js",
16
+ "default": "./react.js"
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "main": "./index.js",
21
+ "types": "./index.d.ts",
22
+ "dependencies": {
23
+ "imapflow": "^1.0.191",
24
+ "mailparser": "^3.7.5",
25
+ "nodemailer": "^9.0.1"
26
+ },
27
+ "peerDependencies": {
28
+ "@braedonsaunders/appkit-ui": "^0.2.0",
29
+ "lucide-react": "^1.24.0",
30
+ "react": "^19.0.0",
31
+ "react-dom": "^19.0.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "@braedonsaunders/appkit-ui": {
35
+ "optional": true
36
+ },
37
+ "lucide-react": {
38
+ "optional": true
39
+ },
40
+ "react": {
41
+ "optional": true
42
+ },
43
+ "react-dom": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "author": "Braedon Saunders",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/braedonsaunders/appkit.git",
51
+ "directory": "packages/mailbox"
52
+ },
53
+ "homepage": "https://github.com/braedonsaunders/appkit/tree/main/packages/mailbox#readme",
54
+ "bugs": {
55
+ "url": "https://github.com/braedonsaunders/appkit/issues"
56
+ },
57
+ "engines": {
58
+ "node": ">=22"
59
+ },
60
+ "keywords": [
61
+ "appkit",
62
+ "application-framework",
63
+ "email",
64
+ "imap",
65
+ "smtp",
66
+ "typescript"
67
+ ]
68
+ }
package/react.d.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * MailboxInbox — the operator surface for the mail engine.
3
+ *
4
+ * The engine in `./index` speaks IMAP/SMTP and owns no state; this component
5
+ * owns no data. Everything it renders arrives as props and every mutation
6
+ * leaves through a callback, so the application keeps the ledger, the folder
7
+ * counts, and the send/reply transport. Only the reply and compose drafts
8
+ * (with their busy/error state) live here, because a draft has no meaning
9
+ * outside the pane that is typing it.
10
+ *
11
+ * The three-pane layout, folder rail, row anatomy, reading pane, empty states,
12
+ * and skeleton follow `@braedonsaunders/appkit-notifications`' `NotificationInbox` so a mail
13
+ * screen and a notification screen read as the same product.
14
+ */
15
+ import * as React from 'react';
16
+ export type MailboxOption = {
17
+ id: string;
18
+ ownerName: string;
19
+ address: string;
20
+ };
21
+ export type MailFolderKey = 'inbox' | 'sent' | 'all';
22
+ export type MailThreadListItem = {
23
+ id: string;
24
+ /** The other side of the conversation — one name, or a joined list. */
25
+ counterparty: string;
26
+ subject: string;
27
+ /** ISO 8601 timestamp of the most recent message in the thread. */
28
+ at: string;
29
+ /** The thread is still awaiting a resolution. */
30
+ open: boolean;
31
+ unread?: boolean;
32
+ };
33
+ export type MailAttachmentView = {
34
+ filename: string;
35
+ /** Pre-formatted by the application ("184 KB") — this component never guesses units. */
36
+ sizeLabel: string;
37
+ href: string;
38
+ };
39
+ export type MailMessageView = {
40
+ id: string;
41
+ direction: 'inbound' | 'outbound';
42
+ fromLabel: string;
43
+ toLabel: string;
44
+ /** ISO 8601 timestamp. */
45
+ at: string;
46
+ bodyText: string;
47
+ attachments: MailAttachmentView[];
48
+ };
49
+ export type MailboxInboxCopy = {
50
+ title: string;
51
+ mailbox: string;
52
+ searchMailboxes: string;
53
+ folders: string;
54
+ inbox: string;
55
+ sent: string;
56
+ all: string;
57
+ refresh: string;
58
+ newMessage: string;
59
+ back: string;
60
+ cancel: string;
61
+ open: string;
62
+ unread: string;
63
+ conversation: string;
64
+ conversations: string;
65
+ messageNoun: string;
66
+ messagesNoun: string;
67
+ emptyList: string;
68
+ emptyListDescription: string;
69
+ emptyPane: string;
70
+ emptyPaneDescription: string;
71
+ noBody: string;
72
+ attachments: string;
73
+ download: string;
74
+ reply: string;
75
+ replyPlaceholder: string;
76
+ send: string;
77
+ sending: string;
78
+ to: string;
79
+ toPlaceholder: string;
80
+ subject: string;
81
+ subjectPlaceholder: string;
82
+ message: string;
83
+ messagePlaceholder: string;
84
+ };
85
+ export type MailboxInboxProps = {
86
+ mailboxes: MailboxOption[];
87
+ activeMailboxId: string;
88
+ /**
89
+ * Where switching mailbox goes. Omit it — or pass a single mailbox — on a
90
+ * surface that is about one mailbox, and the switcher is not rendered.
91
+ */
92
+ onSwitchMailbox?: ((id: string) => void) | undefined;
93
+ folder: MailFolderKey;
94
+ folderCounts?: Partial<Record<MailFolderKey, number>>;
95
+ onSwitchFolder: (key: MailFolderKey) => void;
96
+ threads: MailThreadListItem[];
97
+ activeThreadId: string | null;
98
+ onSelectThread: (id: string | null) => void;
99
+ conversation: {
100
+ subject: string;
101
+ messages: MailMessageView[];
102
+ } | null;
103
+ onReply: (text: string) => Promise<void>;
104
+ onCompose: (draft: {
105
+ to: string;
106
+ subject: string;
107
+ text: string;
108
+ }) => Promise<void>;
109
+ onRefresh?: () => void;
110
+ /** Whose voice the reply is sent in — e.g. "Reply as Dana". */
111
+ replyLabel?: string;
112
+ className?: string;
113
+ copy?: Partial<MailboxInboxCopy>;
114
+ };
115
+ export declare function MailboxInbox({ mailboxes, activeMailboxId, onSwitchMailbox, folder, folderCounts, onSwitchFolder, threads, activeThreadId, onSelectThread, conversation, onReply, onCompose, onRefresh, replyLabel, className, copy: copyOverrides, }: MailboxInboxProps): React.JSX.Element;
116
+ export declare function MailboxInboxSkeleton({ className }?: {
117
+ className?: string;
118
+ }): React.JSX.Element;
119
+ //# sourceMappingURL=react.d.ts.map
package/react.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
package/react.js ADDED
@@ -0,0 +1,271 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * MailboxInbox — the operator surface for the mail engine.
5
+ *
6
+ * The engine in `./index` speaks IMAP/SMTP and owns no state; this component
7
+ * owns no data. Everything it renders arrives as props and every mutation
8
+ * leaves through a callback, so the application keeps the ledger, the folder
9
+ * counts, and the send/reply transport. Only the reply and compose drafts
10
+ * (with their busy/error state) live here, because a draft has no meaning
11
+ * outside the pane that is typing it.
12
+ *
13
+ * The three-pane layout, folder rail, row anatomy, reading pane, empty states,
14
+ * and skeleton follow `@braedonsaunders/appkit-notifications`' `NotificationInbox` so a mail
15
+ * screen and a notification screen read as the same product.
16
+ */
17
+ import * as React from 'react';
18
+ import { ArrowRight, ChevronLeft, Download, Inbox, Loader2, Mail, Mails, Menu, Paperclip, RotateCw, Send, SquarePen, TriangleAlert, X, } from 'lucide-react';
19
+ import { Badge, Button, Drawer, Input, Select, Skeleton, Textarea, cn } from '@braedonsaunders/appkit-ui';
20
+ const DEFAULT_COPY = {
21
+ title: 'Mail',
22
+ mailbox: 'Mailbox',
23
+ searchMailboxes: 'Search mailboxes',
24
+ folders: 'Folders',
25
+ inbox: 'Inbox',
26
+ sent: 'Sent',
27
+ all: 'All mail',
28
+ refresh: 'Refresh',
29
+ newMessage: 'New message',
30
+ back: 'Back to conversations',
31
+ cancel: 'Cancel',
32
+ open: 'Open thread',
33
+ unread: 'unread',
34
+ conversation: 'conversation',
35
+ conversations: 'conversations',
36
+ messageNoun: 'message',
37
+ messagesNoun: 'messages',
38
+ emptyList: 'No conversations',
39
+ emptyListDescription: 'Messages in this folder will appear here.',
40
+ emptyPane: 'Select a conversation',
41
+ emptyPaneDescription: 'Choose a conversation from the list to read it here.',
42
+ noBody: 'This message has no text body.',
43
+ attachments: 'Attachments',
44
+ download: 'Download',
45
+ reply: 'Reply',
46
+ replyPlaceholder: 'Write a reply…',
47
+ send: 'Send',
48
+ sending: 'Sending…',
49
+ to: 'To',
50
+ toPlaceholder: 'name@example.com',
51
+ subject: 'Subject',
52
+ subjectPlaceholder: 'What is this about?',
53
+ message: 'Message',
54
+ messagePlaceholder: 'Write your message…',
55
+ };
56
+ const FOLDER_ORDER = ['inbox', 'sent', 'all'];
57
+ function useIsDesktop() {
58
+ const [isDesktop, setIsDesktop] = React.useState(true);
59
+ React.useEffect(() => {
60
+ const media = window.matchMedia('(min-width: 1024px)');
61
+ const sync = () => setIsDesktop(media.matches);
62
+ sync();
63
+ media.addEventListener('change', sync);
64
+ return () => media.removeEventListener('change', sync);
65
+ }, []);
66
+ return isDesktop;
67
+ }
68
+ function relativeTime(iso) {
69
+ const minutes = Math.floor((Date.now() - new Date(iso).getTime()) / 60_000);
70
+ if (minutes < 1)
71
+ return 'now';
72
+ if (minutes < 60)
73
+ return `${minutes}m`;
74
+ const hours = Math.floor(minutes / 60);
75
+ if (hours < 24)
76
+ return `${hours}h`;
77
+ const days = Math.floor(hours / 24);
78
+ if (days < 7)
79
+ return `${days}d`;
80
+ return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
81
+ }
82
+ function fullDate(iso) {
83
+ return new Date(iso).toLocaleString(undefined, {
84
+ weekday: 'short',
85
+ month: 'short',
86
+ day: 'numeric',
87
+ hour: 'numeric',
88
+ minute: '2-digit',
89
+ });
90
+ }
91
+ function initials(name) {
92
+ const parts = name
93
+ .replace(/<[^>]*>/g, ' ')
94
+ .split(/[\s,]+/)
95
+ .map((part) => part.trim())
96
+ .filter(Boolean);
97
+ const first = parts[0];
98
+ if (!first)
99
+ return '?';
100
+ const last = parts.length > 1 ? parts[parts.length - 1] : undefined;
101
+ const letters = `${first[0] ?? ''}${last?.[0] ?? ''}`;
102
+ return letters.toUpperCase() || '?';
103
+ }
104
+ function errorMessage(cause) {
105
+ if (cause instanceof Error && cause.message)
106
+ return cause.message;
107
+ if (typeof cause === 'string' && cause)
108
+ return cause;
109
+ return 'The message could not be sent.';
110
+ }
111
+ function CountPill({ value, active }) {
112
+ if (value <= 0)
113
+ return null;
114
+ return (_jsx("span", { className: cn('ml-auto inline-flex min-w-5 items-center justify-center rounded-full px-1.5 text-[11px] font-semibold tabular-nums', active ? 'bg-primary text-primary-fg' : 'bg-bg-subtle text-fg-muted'), children: value > 99 ? '99+' : value }));
115
+ }
116
+ function FolderButton({ icon, label, count, active, onClick, }) {
117
+ return (_jsxs("button", { type: "button", onClick: onClick, "aria-current": active ? 'true' : undefined, className: cn('flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm transition-colors', active
118
+ ? 'bg-primary-subtle font-medium text-fg'
119
+ : 'text-fg-muted hover:bg-surface-hover hover:text-fg'), children: [_jsx("span", { className: cn('shrink-0', active ? 'text-primary' : 'text-fg-subtle'), children: icon }), _jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: label }), _jsx(CountPill, { value: count, active: active })] }));
120
+ }
121
+ function FolderIcon({ folder }) {
122
+ if (folder === 'inbox')
123
+ return _jsx(Inbox, { size: 16 });
124
+ if (folder === 'sent')
125
+ return _jsx(Send, { size: 16 });
126
+ return _jsx(Mails, { size: 16 });
127
+ }
128
+ function MailboxSwitcher({ mailboxes, activeMailboxId, onSwitchMailbox, copy, className, }) {
129
+ return (_jsxs("div", { className: cn('shrink-0 space-y-1.5', className), children: [_jsx("p", { className: "px-0.5 text-[11px] font-semibold uppercase tracking-wide text-fg-subtle", children: copy.mailbox }), _jsx(Select, { value: activeMailboxId, onChange: (event) => onSwitchMailbox(event.currentTarget.value), "aria-label": copy.mailbox, sheetTitle: copy.mailbox, searchPlaceholder: copy.searchMailboxes, triggerClassName: "h-9 text-sm", children: mailboxes.map((mailbox) => (_jsx("option", { value: mailbox.id, children: `${mailbox.ownerName} · ${mailbox.address}` }, mailbox.id))) })] }));
130
+ }
131
+ function FolderRail({ mailboxes, activeMailboxId, onSwitchMailbox, folder, folderCounts, onSwitchFolder, copy, variant, className, }) {
132
+ const nav = (_jsx("div", { className: "space-y-0.5", children: FOLDER_ORDER.map((key) => (_jsx(FolderButton, { icon: _jsx(FolderIcon, { folder: key }), label: copy[key], count: folderCounts?.[key] ?? 0, active: folder === key, onClick: () => onSwitchFolder(key) }, key))) }));
133
+ // A control that changes nothing is worse than no control: on a surface that
134
+ // is one mailbox — an inbox opened on the person it belongs to — a picker
135
+ // offering that same mailbox reads as an invitation to go somewhere else.
136
+ // So the switcher appears only where there is somewhere to switch to, which
137
+ // is both a second mailbox and a handler willing to go there.
138
+ const switcher = onSwitchMailbox && mailboxes.length > 1 ? (_jsx(MailboxSwitcher, { mailboxes: mailboxes, activeMailboxId: activeMailboxId, onSwitchMailbox: onSwitchMailbox, copy: copy, className: variant === 'flyout' ? 'border-b border-border pb-3' : 'border-b border-border px-3 pb-3' })) : null;
139
+ if (variant === 'flyout') {
140
+ return (_jsxs("div", { className: "flex h-full flex-col", children: [switcher, _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto pt-2", children: nav })] }));
141
+ }
142
+ return (_jsxs("aside", { className: cn('flex-col border-r border-border bg-surface', className), children: [_jsxs("div", { className: "flex h-14 shrink-0 items-center gap-2 px-4", children: [_jsx(Mail, { size: 18, className: "text-primary" }), _jsx("span", { className: "text-base font-semibold text-fg", children: copy.title })] }), switcher, _jsx("nav", { className: "app-scroll min-h-0 flex-1 overflow-y-auto px-2 py-2", children: nav })] }));
143
+ }
144
+ function ThreadRow({ thread, copy, selected, onOpen, }) {
145
+ const unread = thread.unread === true;
146
+ return (_jsxs("div", { role: "button", tabIndex: 0, onClick: onOpen, onKeyDown: (event) => {
147
+ if (event.key === 'Enter' || event.key === ' ') {
148
+ event.preventDefault();
149
+ onOpen();
150
+ }
151
+ }, className: cn('group relative flex cursor-pointer gap-3 border-b border-border-subtle px-3 py-2.5 transition-colors sm:px-4', selected ? 'bg-primary-subtle/70' : 'hover:bg-surface-hover'), children: [unread ? _jsx("span", { className: "absolute inset-y-0 left-0 w-[3px] bg-primary", "aria-hidden": true }) : null, _jsx("span", { "aria-hidden": true, className: cn('flex size-9 shrink-0 items-center justify-center rounded-full text-xs font-semibold', unread ? 'bg-primary-subtle text-primary' : 'bg-bg-subtle text-fg-muted'), children: initials(thread.counterparty) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: cn('min-w-0 truncate text-sm', unread ? 'font-semibold text-fg' : 'text-fg'), children: thread.counterparty }), thread.open ? (_jsx("span", { title: copy.open, "aria-label": copy.open, role: "img", className: "size-1.5 shrink-0 rounded-full bg-success" })) : null, _jsx("time", { suppressHydrationWarning: true, dateTime: thread.at, className: "ml-auto shrink-0 text-[11px] whitespace-nowrap text-fg-subtle", children: relativeTime(thread.at) })] }), _jsx("p", { className: cn('truncate text-sm', unread ? 'font-medium text-fg' : 'text-fg-muted'), children: thread.subject })] })] }));
152
+ }
153
+ function AttachmentChip({ attachment, copy }) {
154
+ return (_jsxs("a", { href: attachment.href, download: attachment.filename, title: `${copy.download}: ${attachment.filename}`, className: "group/chip inline-flex max-w-full items-center gap-2 rounded-lg border border-border bg-surface px-2.5 py-1.5 text-xs transition-colors hover:border-primary/40 hover:bg-surface-hover", children: [_jsx(Paperclip, { size: 13, className: "shrink-0 text-fg-subtle", "aria-hidden": true }), _jsx("span", { className: "min-w-0 truncate font-medium text-fg", children: attachment.filename }), _jsx("span", { className: "shrink-0 tabular-nums text-fg-subtle", children: attachment.sizeLabel }), _jsx(Download, { size: 13, "aria-hidden": true, className: "shrink-0 text-fg-subtle transition-colors group-hover/chip:text-primary" })] }));
155
+ }
156
+ function MessageCard({ message, copy }) {
157
+ const outbound = message.direction === 'outbound';
158
+ return (_jsxs("article", { className: cn('max-w-[52rem] rounded-xl border px-4 py-3 shadow-sm', outbound ? 'ml-auto border-primary/25 bg-primary-subtle/50' : 'mr-auto border-border bg-bg-subtle'), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-xs", children: [_jsx("span", { className: "min-w-0 truncate font-semibold text-fg", children: message.fromLabel }), _jsx(ArrowRight, { size: 12, className: "shrink-0 text-fg-subtle", "aria-hidden": true }), _jsx("span", { className: "min-w-0 truncate text-fg-muted", children: message.toLabel }), _jsx("time", { suppressHydrationWarning: true, dateTime: message.at, className: "ml-auto shrink-0 text-[11px] whitespace-nowrap text-fg-subtle", children: fullDate(message.at) })] }), message.bodyText.trim() ? (_jsx("p", { className: "mt-2 text-sm leading-relaxed whitespace-pre-wrap text-fg", children: message.bodyText })) : (_jsx("p", { className: "mt-2 text-sm text-fg-subtle", children: copy.noBody })), message.attachments.length ? (_jsxs("div", { className: "mt-3 border-t border-border-subtle pt-2.5", children: [_jsx("p", { className: "mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-fg-subtle", children: copy.attachments }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: message.attachments.map((attachment) => (_jsx(AttachmentChip, { attachment: attachment, copy: copy }, `${attachment.filename}:${attachment.href}`))) })] })) : null] }));
159
+ }
160
+ function InlineError({ message }) {
161
+ return (_jsxs("p", { role: "alert", className: "flex items-start gap-1.5 text-xs text-danger", children: [_jsx(TriangleAlert, { size: 13, className: "mt-0.5 shrink-0", "aria-hidden": true }), _jsx("span", { className: "min-w-0", children: message })] }));
162
+ }
163
+ function ReplyComposer({ copy, replyLabel, onReply, }) {
164
+ const fieldId = React.useId();
165
+ const [text, setText] = React.useState('');
166
+ const [busy, setBusy] = React.useState(false);
167
+ const [error, setError] = React.useState(null);
168
+ const submit = async (event) => {
169
+ event.preventDefault();
170
+ const value = text.trim();
171
+ if (!value || busy)
172
+ return;
173
+ setBusy(true);
174
+ setError(null);
175
+ try {
176
+ await onReply(value);
177
+ setText('');
178
+ }
179
+ catch (cause) {
180
+ setError(errorMessage(cause));
181
+ }
182
+ finally {
183
+ setBusy(false);
184
+ }
185
+ };
186
+ return (_jsxs("form", { onSubmit: (event) => void submit(event), className: "shrink-0 space-y-2 border-t border-border bg-surface px-4 py-3 sm:px-6 sm:py-4", children: [_jsx("label", { htmlFor: fieldId, className: "sr-only", children: replyLabel ?? copy.reply }), _jsx(Textarea, { id: fieldId, value: text, onChange: (event) => setText(event.currentTarget.value), placeholder: copy.replyPlaceholder, rows: 3, disabled: busy, className: "min-h-20 resize-y" }), error ? _jsx(InlineError, { message: error }) : null, _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "min-w-0 flex-1 truncate text-xs text-fg-subtle", children: replyLabel ?? copy.reply }), _jsxs(Button, { type: "submit", size: "sm", disabled: busy || !text.trim(), children: [busy ? _jsx(Loader2, { size: 14, className: "animate-spin" }) : _jsx(Send, { size: 14 }), busy ? copy.sending : copy.send] })] })] }));
187
+ }
188
+ function ConversationPane({ conversation, copy, replyLabel, threadKey, onReply, onClose, }) {
189
+ if (!conversation) {
190
+ return (_jsxs("div", { className: "flex h-full flex-col items-center justify-center gap-3 px-6 text-center", children: [_jsx("div", { className: "flex size-16 items-center justify-center rounded-full bg-bg-subtle", children: _jsx(Mail, { size: 28, className: "text-fg-subtle" }) }), _jsx("p", { className: "text-sm font-medium text-fg-muted", children: copy.emptyPane }), _jsx("p", { className: "max-w-xs text-xs text-fg-subtle", children: copy.emptyPaneDescription })] }));
191
+ }
192
+ const count = conversation.messages.length;
193
+ const latest = conversation.messages[count - 1];
194
+ const header = (_jsxs("div", { className: "flex items-start gap-3", children: [onClose ? (_jsx("button", { type: "button", onClick: onClose, "aria-label": copy.back, className: "-ml-1 rounded-md p-1.5 text-fg-muted hover:bg-surface-hover hover:text-fg lg:hidden", children: _jsx(ChevronLeft, { size: 20 }) })) : (_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-primary-subtle", children: _jsx(Mail, { size: 18, className: "text-primary" }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h2", { className: "text-lg font-semibold leading-snug text-fg", children: conversation.subject }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2 text-xs text-fg-muted", children: [_jsxs(Badge, { variant: "secondary", className: "font-normal", children: [count, " ", count === 1 ? copy.messageNoun : copy.messagesNoun] }), latest ? _jsx("span", { suppressHydrationWarning: true, children: fullDate(latest.at) }) : null] })] })] }));
195
+ const body = (_jsx("div", { className: "space-y-4", children: conversation.messages.map((message) => (_jsx(MessageCard, { message: message, copy: copy }, message.id))) }));
196
+ const composer = (_jsx(ReplyComposer, { copy: copy, replyLabel: replyLabel, onReply: onReply }, threadKey ?? 'thread'));
197
+ if (onClose) {
198
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border pb-4", children: header }), _jsx("div", { className: "app-scroll min-h-0 flex-1 overflow-y-auto py-4", children: body }), composer] }));
199
+ }
200
+ return (_jsxs("div", { className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border px-6 py-4", children: header }), _jsx("div", { className: "app-scroll min-h-0 flex-1 overflow-y-auto px-6 py-5", children: body }), composer] }));
201
+ }
202
+ function ComposePanel({ copy, variant, onCompose, onClose, }) {
203
+ const toId = React.useId();
204
+ const subjectId = React.useId();
205
+ const messageId = React.useId();
206
+ const [to, setTo] = React.useState('');
207
+ const [subject, setSubject] = React.useState('');
208
+ const [text, setText] = React.useState('');
209
+ const [busy, setBusy] = React.useState(false);
210
+ const [error, setError] = React.useState(null);
211
+ const submit = async (event) => {
212
+ event.preventDefault();
213
+ if (busy)
214
+ return;
215
+ setBusy(true);
216
+ setError(null);
217
+ try {
218
+ await onCompose({ to: to.trim(), subject: subject.trim(), text });
219
+ onClose();
220
+ }
221
+ catch (cause) {
222
+ setError(errorMessage(cause));
223
+ }
224
+ finally {
225
+ setBusy(false);
226
+ }
227
+ };
228
+ const header = (_jsxs("div", { className: "flex items-start gap-3", children: [_jsx("span", { className: "flex size-9 shrink-0 items-center justify-center rounded-full bg-primary-subtle", children: _jsx(SquarePen, { size: 18, className: "text-primary" }) }), _jsx("div", { className: "min-w-0 flex-1", children: _jsx("h2", { className: "text-lg font-semibold leading-snug text-fg", children: copy.newMessage }) }), _jsx("button", { type: "button", onClick: onClose, "aria-label": copy.cancel, className: "-mr-1 rounded-md p-1.5 text-fg-muted hover:bg-surface-hover hover:text-fg", children: _jsx(X, { size: 18 }) })] }));
229
+ const fields = (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { htmlFor: toId, className: "block text-xs font-medium text-fg-muted", children: copy.to }), _jsx(Input, { id: toId, type: "email", required: true, value: to, onChange: (event) => setTo(event.currentTarget.value), placeholder: copy.toPlaceholder, disabled: busy, autoComplete: "email" })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { htmlFor: subjectId, className: "block text-xs font-medium text-fg-muted", children: copy.subject }), _jsx(Input, { id: subjectId, required: true, value: subject, onChange: (event) => setSubject(event.currentTarget.value), placeholder: copy.subjectPlaceholder, disabled: busy })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { htmlFor: messageId, className: "block text-xs font-medium text-fg-muted", children: copy.message }), _jsx(Textarea, { id: messageId, required: true, value: text, onChange: (event) => setText(event.currentTarget.value), placeholder: copy.messagePlaceholder, disabled: busy, className: "min-h-40 resize-y" })] })] }));
230
+ const actions = (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [error ? (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(InlineError, { message: error }) })) : (_jsx("span", { className: "flex-1" })), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: onClose, disabled: busy, children: copy.cancel }), _jsxs(Button, { type: "submit", size: "sm", disabled: busy, children: [busy ? _jsx(Loader2, { size: 14, className: "animate-spin" }) : _jsx(Send, { size: 14 }), busy ? copy.sending : copy.send] })] }));
231
+ if (variant === 'drawer') {
232
+ return (_jsxs("form", { onSubmit: (event) => void submit(event), className: "space-y-4", children: [fields, actions] }));
233
+ }
234
+ return (_jsxs("form", { onSubmit: (event) => void submit(event), className: "flex h-full min-h-0 flex-col", children: [_jsx("div", { className: "shrink-0 border-b border-border px-6 py-4", children: header }), _jsx("div", { className: "app-scroll min-h-0 flex-1 overflow-y-auto px-6 py-5", children: fields }), _jsx("div", { className: "shrink-0 border-t border-border bg-surface px-6 py-4", children: actions })] }));
235
+ }
236
+ export function MailboxInbox({ mailboxes, activeMailboxId, onSwitchMailbox, folder, folderCounts, onSwitchFolder, threads, activeThreadId, onSelectThread, conversation, onReply, onCompose, onRefresh, replyLabel, className, copy: copyOverrides, }) {
237
+ const copy = React.useMemo(() => ({ ...DEFAULT_COPY, ...copyOverrides }), [copyOverrides]);
238
+ const isDesktop = useIsDesktop();
239
+ const [foldersOpen, setFoldersOpen] = React.useState(false);
240
+ const [composeOpen, setComposeOpen] = React.useState(false);
241
+ const total = folderCounts?.[folder] ?? threads.length;
242
+ const unread = threads.reduce((count, thread) => (thread.unread ? count + 1 : count), 0);
243
+ const openCompose = () => {
244
+ setFoldersOpen(false);
245
+ setComposeOpen(true);
246
+ };
247
+ const closeCompose = () => setComposeOpen(false);
248
+ const selectFolder = (key) => {
249
+ setFoldersOpen(false);
250
+ onSwitchFolder(key);
251
+ };
252
+ const selectThread = (id) => {
253
+ setComposeOpen(false);
254
+ onSelectThread(id);
255
+ };
256
+ const railProps = {
257
+ mailboxes,
258
+ activeMailboxId,
259
+ onSwitchMailbox,
260
+ folder,
261
+ folderCounts,
262
+ onSwitchFolder: selectFolder,
263
+ copy,
264
+ };
265
+ const listBody = threads.length ? (threads.map((thread) => (_jsx(ThreadRow, { thread: thread, copy: copy, selected: thread.id === activeThreadId, onOpen: () => selectThread(thread.id) }, thread.id)))) : (_jsxs("div", { className: "flex flex-col items-center justify-center px-6 py-20 text-center", children: [_jsx("div", { className: "flex size-14 items-center justify-center rounded-full bg-bg-subtle text-fg-subtle", children: _jsx(Inbox, { size: 26 }) }), _jsx("p", { className: "mt-3 text-sm font-medium text-fg-muted", children: copy.emptyList }), _jsx("p", { className: "mt-1 text-xs text-fg-subtle", children: copy.emptyListDescription })] }));
266
+ return (_jsxs("div", { className: cn('flex h-full min-h-0 bg-bg-subtle', className), children: [_jsx(FolderRail, { ...railProps, variant: "rail", className: "hidden w-64 shrink-0 lg:flex" }), _jsxs("section", { className: "flex min-w-0 flex-1 flex-col border-r border-border bg-surface lg:w-96 lg:flex-none xl:w-[28rem]", children: [_jsx("header", { className: "shrink-0 border-b border-border", children: _jsxs("div", { className: "flex h-14 items-center gap-2 px-3 sm:px-4", children: [_jsx("button", { type: "button", onClick: () => setFoldersOpen(true), "aria-label": copy.folders, className: "-ml-1 rounded-md p-1.5 text-fg-muted hover:bg-surface-hover hover:text-fg lg:hidden", children: _jsx(Menu, { size: 20 }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("h1", { className: "truncate text-sm font-semibold text-fg", children: copy[folder] }), _jsxs("p", { className: "text-[11px] text-fg-subtle", children: [total, " ", total === 1 ? copy.conversation : copy.conversations, unread ? ` · ${unread} ${copy.unread}` : ''] })] }), onRefresh ? (_jsx("button", { type: "button", onClick: onRefresh, title: copy.refresh, "aria-label": copy.refresh, className: "rounded-md p-1.5 text-fg-muted hover:bg-surface-hover hover:text-fg", children: _jsx(RotateCw, { size: 18 }) })) : null, _jsxs(Button, { size: "sm", onClick: openCompose, children: [_jsx(SquarePen, { size: 14 }), copy.newMessage] })] }) }), _jsx("div", { className: "app-scroll min-h-0 flex-1 overflow-y-auto", children: listBody })] }), _jsx("section", { className: "hidden min-w-0 flex-1 bg-surface lg:flex", children: _jsx("div", { className: "w-full", children: composeOpen ? (_jsx(ComposePanel, { copy: copy, variant: "pane", onCompose: onCompose, onClose: closeCompose })) : (_jsx(ConversationPane, { conversation: conversation, copy: copy, replyLabel: replyLabel, threadKey: activeThreadId, onReply: onReply })) }) }), !isDesktop ? (_jsxs(_Fragment, { children: [_jsx(Drawer, { open: foldersOpen, onClose: () => setFoldersOpen(false), side: "left", size: "sm", title: copy.folders, disableFullscreen: true, bodyClassName: "min-h-0 flex-1 overflow-hidden px-4 py-5", children: _jsx(FolderRail, { ...railProps, variant: "flyout" }) }), _jsx(Drawer, { open: Boolean(activeThreadId) && !composeOpen, onClose: () => onSelectThread(null), size: "lg", title: conversation?.subject, disableFullscreen: true, children: activeThreadId && !composeOpen ? (_jsx(ConversationPane, { conversation: conversation, copy: copy, replyLabel: replyLabel, threadKey: activeThreadId, onReply: onReply, onClose: () => onSelectThread(null) })) : null }), _jsx(Drawer, { open: composeOpen, onClose: closeCompose, size: "lg", title: copy.newMessage, disableFullscreen: true, children: composeOpen ? (_jsx(ComposePanel, { copy: copy, variant: "drawer", onCompose: onCompose, onClose: closeCompose })) : null })] })) : null] }));
267
+ }
268
+ export function MailboxInboxSkeleton({ className } = {}) {
269
+ return (_jsxs("div", { role: "status", "aria-label": "Loading mailbox", "aria-busy": "true", className: cn('flex h-full min-h-0 bg-bg-subtle', className), children: [_jsxs("div", { className: "hidden w-64 shrink-0 flex-col gap-2 border-r border-border p-3 lg:flex", children: [_jsx(Skeleton, { className: "h-9 w-full rounded-lg" }), _jsx(Skeleton, { className: "mt-2 h-9 w-full rounded-lg" }), Array.from({ length: 3 }).map((_, index) => (_jsx(Skeleton, { className: "h-9 w-full rounded-lg" }, index)))] }), _jsxs("section", { className: "flex min-w-0 flex-1 flex-col border-r border-border bg-surface lg:w-96 lg:flex-none xl:w-[28rem]", children: [_jsxs("div", { className: "flex h-14 shrink-0 items-center gap-2 border-b border-border px-3 sm:px-4", children: [_jsx(Skeleton, { className: "h-5 w-28" }), _jsx(Skeleton, { className: "ml-auto h-8 w-28 rounded-md" })] }), _jsx("div", { className: "flex-1 overflow-hidden", children: Array.from({ length: 8 }).map((_, index) => (_jsxs("div", { className: "flex items-center gap-3 border-b border-border-subtle px-3 py-3 sm:px-4", children: [_jsx(Skeleton, { className: "size-9 shrink-0 rounded-full" }), _jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [_jsx(Skeleton, { className: "h-3.5 w-3/4" }), _jsx(Skeleton, { className: "h-3 w-1/2" })] })] }, index))) })] }), _jsx("div", { className: "hidden min-w-0 flex-1 items-center justify-center lg:flex", children: _jsx(Skeleton, { className: "size-10 rounded-full" }) })] }));
270
+ }
271
+ //# sourceMappingURL=react.js.map