@echothink-ui/inbox 0.1.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 (52) hide show
  1. package/README.md +5 -0
  2. package/dist/components/AgentDraftReviewPanel.d.ts +18 -0
  3. package/dist/components/ApprovalToSendPanel.d.ts +11 -0
  4. package/dist/components/AttachmentList.d.ts +8 -0
  5. package/dist/components/AttachmentPreview.d.ts +7 -0
  6. package/dist/components/EmailAutomationRulePanel.d.ts +8 -0
  7. package/dist/components/InboxShell.d.ts +10 -0
  8. package/dist/components/LabelManager.d.ts +9 -0
  9. package/dist/components/MailboxFolderList.d.ts +8 -0
  10. package/dist/components/MessageComposer.d.ts +13 -0
  11. package/dist/components/MessageFilterBar.d.ts +13 -0
  12. package/dist/components/MessageList.d.ts +8 -0
  13. package/dist/components/MessagePreview.d.ts +9 -0
  14. package/dist/components/MessageSearch.d.ts +11 -0
  15. package/dist/components/MessageThread.d.ts +22 -0
  16. package/dist/components/PriorityInboxView.d.ts +14 -0
  17. package/dist/components/RecipientPicker.d.ts +10 -0
  18. package/dist/components/ThreadSummaryPanel.d.ts +9 -0
  19. package/dist/index.cjs +1699 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.css +2155 -0
  22. package/dist/index.css.map +1 -0
  23. package/dist/index.d.ts +21 -0
  24. package/dist/index.js +1645 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/types.d.ts +75 -0
  27. package/dist/utils.d.ts +5 -0
  28. package/package.json +44 -0
  29. package/src/components/AgentDraftReviewPanel.tsx +128 -0
  30. package/src/components/ApprovalToSendPanel.tsx +118 -0
  31. package/src/components/AttachmentList.tsx +85 -0
  32. package/src/components/AttachmentPreview.tsx +207 -0
  33. package/src/components/EmailAutomationRulePanel.tsx +100 -0
  34. package/src/components/InboxShell.tsx +62 -0
  35. package/src/components/LabelManager.tsx +147 -0
  36. package/src/components/MailboxFolderList.tsx +66 -0
  37. package/src/components/MessageComposer.tsx +160 -0
  38. package/src/components/MessageFilterBar.test.tsx +34 -0
  39. package/src/components/MessageFilterBar.tsx +69 -0
  40. package/src/components/MessageList.tsx +101 -0
  41. package/src/components/MessagePreview.test.tsx +48 -0
  42. package/src/components/MessagePreview.tsx +84 -0
  43. package/src/components/MessageSearch.tsx +96 -0
  44. package/src/components/MessageThread.tsx +173 -0
  45. package/src/components/PriorityInboxView.tsx +74 -0
  46. package/src/components/RecipientPicker.tsx +181 -0
  47. package/src/components/ThreadSummaryPanel.tsx +107 -0
  48. package/src/index.test.tsx +276 -0
  49. package/src/index.tsx +60 -0
  50. package/src/styles.css +2523 -0
  51. package/src/types.ts +85 -0
  52. package/src/utils.ts +33 -0
@@ -0,0 +1,107 @@
1
+ import * as React from "react";
2
+ import { Badge, Panel, Tag } from "@echothink-ui/core";
3
+ import type { ThreadCitation } from "../types";
4
+
5
+ export interface ThreadSummaryPanelProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ summary?: {
7
+ text: React.ReactNode;
8
+ citations: ThreadCitation[];
9
+ };
10
+ }
11
+
12
+ export function ThreadSummaryPanel({
13
+ summary,
14
+ className,
15
+ ...props
16
+ }: ThreadSummaryPanelProps) {
17
+ const citations = summary?.citations ?? [];
18
+ const citationCount = citations.length;
19
+
20
+ return (
21
+ <Panel
22
+ {...props}
23
+ className={`eth-inbox-thread-summary ${className ?? ""}`}
24
+ title="Thread summary"
25
+ subtitle="Agent-generated recap with source references."
26
+ data-eth-component="ThreadSummaryPanel"
27
+ >
28
+ {summary ? (
29
+ <div className="eth-inbox-thread-summary__content">
30
+ <div
31
+ className="eth-inbox-thread-summary__metadata"
32
+ role="group"
33
+ aria-label="Summary metadata"
34
+ >
35
+ <Badge severity="info">Agent generated</Badge>
36
+ <span>{formatCitationCount(citationCount)}</span>
37
+ </div>
38
+
39
+ <section className="eth-inbox-thread-summary__summary" aria-label="Generated summary">
40
+ <span className="eth-inbox-thread-summary__section-label">Summary</span>
41
+ <div className="eth-inbox-thread-summary__summary-body">{summary.text}</div>
42
+ </section>
43
+
44
+ {citations.length ? (
45
+ <section className="eth-inbox-thread-summary__sources" aria-label="Source citations">
46
+ <span className="eth-inbox-thread-summary__section-label">Source citations</span>
47
+ <ul className="eth-inbox-thread-summary__source-list">
48
+ {citations.map((citation) => (
49
+ <li key={citation.id} className="eth-inbox-thread-summary__source-item">
50
+ <CitationTag citation={citation} />
51
+ </li>
52
+ ))}
53
+ </ul>
54
+ </section>
55
+ ) : (
56
+ <div className="eth-inbox-thread-summary__no-sources" role="status">
57
+ No citations attached.
58
+ </div>
59
+ )}
60
+ </div>
61
+ ) : (
62
+ <div className="eth-inbox-thread-summary__empty" role="status">
63
+ <strong>No generated summary</strong>
64
+ <p>
65
+ Summaries and source citations appear here after the agent reviews the conversation.
66
+ </p>
67
+ </div>
68
+ )}
69
+ </Panel>
70
+ );
71
+ }
72
+
73
+ function CitationTag({ citation }: { citation: ThreadCitation }) {
74
+ const referenceLabel = citation.messageRef
75
+ ? `${citation.label}, message ${citation.messageRef}`
76
+ : citation.label;
77
+ const tag = (
78
+ <Tag
79
+ className="eth-inbox-thread-summary__source-token"
80
+ aria-label={referenceLabel}
81
+ title={referenceLabel}
82
+ data-message-ref={citation.messageRef}
83
+ >
84
+ {citation.label}
85
+ </Tag>
86
+ );
87
+
88
+ if (!citation.href) {
89
+ return (
90
+ <span className="eth-inbox-thread-summary__source-reference">{tag}</span>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <a
96
+ className="eth-inbox-thread-summary__source-link"
97
+ href={citation.href}
98
+ aria-label={referenceLabel}
99
+ >
100
+ {tag}
101
+ </a>
102
+ );
103
+ }
104
+
105
+ function formatCitationCount(count: number) {
106
+ return `${count} ${count === 1 ? "citation" : "citations"}`;
107
+ }
@@ -0,0 +1,276 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import {
4
+ MessageList,
5
+ MessageThread,
6
+ PriorityInboxView,
7
+ RecipientPicker,
8
+ ThreadSummaryPanel
9
+ } from "./index";
10
+
11
+ const thread = {
12
+ id: "q3-launch",
13
+ subject: "Q3 launch - final brief",
14
+ messages: [
15
+ {
16
+ id: "m0",
17
+ from: "Launch Ops",
18
+ to: ["Jane Doe", "you@echothink.example"],
19
+ date: "2026-05-26T16:00:00Z",
20
+ body: "Initial launch packet is ready for copy review."
21
+ },
22
+ {
23
+ id: "m1",
24
+ from: "Jane Doe",
25
+ to: ["you@echothink.example"],
26
+ date: "2026-05-27T10:42:00Z",
27
+ subject: "Q3 launch - final brief",
28
+ body: "Please review the attached campaign brief and approve the messaging direction.",
29
+ attachments: [{ id: "a1", name: "brief.pdf", size: 240_000, mimeType: "application/pdf" }]
30
+ },
31
+ {
32
+ id: "m2",
33
+ from: "you@echothink.example",
34
+ to: ["Jane Doe"],
35
+ date: "2026-05-27T11:05:00Z",
36
+ body: "Looks good - one small note on the headline. Will reply with redlines."
37
+ },
38
+ {
39
+ id: "m3",
40
+ from: "Jane Doe",
41
+ to: ["you@echothink.example"],
42
+ date: "2026-05-27T11:28:00Z",
43
+ body: "Thanks. Legal can start as soon as the final headline is approved."
44
+ }
45
+ ]
46
+ };
47
+
48
+ const threads = [
49
+ {
50
+ id: "t1",
51
+ subject: "Q3 launch - final brief",
52
+ preview: "Please review the attached campaign brief and approve the messaging direction.",
53
+ from: "Jane Doe",
54
+ receivedAt: "2026-05-27T10:42:00Z",
55
+ unread: true,
56
+ priority: "high" as const,
57
+ labels: ["q3-launch"],
58
+ status: "pending-approval" as const
59
+ },
60
+ {
61
+ id: "t2",
62
+ subject: "Customer feedback digest",
63
+ preview: "Weekly digest of customer feedback themes.",
64
+ from: "Support",
65
+ receivedAt: "2026-05-26T17:20:00Z"
66
+ }
67
+ ];
68
+
69
+ describe("@echothink-ui/inbox MessageThread", () => {
70
+ it("renders summary, metadata, actions, and collapsed history without duplicating the thread subject", () => {
71
+ render(
72
+ <MessageThread
73
+ thread={thread}
74
+ summaryRef={<p>Jane needs approval on the launch brief before legal review.</p>}
75
+ status="pending-approval"
76
+ statusLabel="Awaiting approval"
77
+ actions={[{ id: "reply", label: "Reply", intent: "primary" }]}
78
+ />
79
+ );
80
+
81
+ expect(screen.getByRole("heading", { name: "Q3 launch - final brief" })).toBeTruthy();
82
+ expect(screen.getByText("Thread summary")).toBeTruthy();
83
+ expect(
84
+ screen.getByText("Jane needs approval on the launch brief before legal review.")
85
+ ).toBeTruthy();
86
+ expect(screen.getByText("4 messages")).toBeTruthy();
87
+ expect(screen.getByText("3 participants")).toBeTruthy();
88
+ expect(screen.getByText("1 attachment")).toBeTruthy();
89
+ expect(screen.getByText("Awaiting approval")).toBeTruthy();
90
+ expect(screen.getByText("1 older message")).toBeTruthy();
91
+ expect(screen.getByRole("button", { name: "Reply" })).toBeTruthy();
92
+ expect(screen.getAllByText("Q3 launch - final brief")).toHaveLength(1);
93
+ });
94
+
95
+ it("wires attachment preview actions from the thread into rendered messages", () => {
96
+ const onPreviewAttachment = vi.fn();
97
+
98
+ render(<MessageThread thread={thread} onPreviewAttachment={onPreviewAttachment} />);
99
+
100
+ fireEvent.click(screen.getByLabelText("Preview brief.pdf"));
101
+
102
+ expect(onPreviewAttachment).toHaveBeenCalledWith(thread.messages[1].attachments![0]);
103
+ });
104
+ });
105
+
106
+ describe("@echothink-ui/inbox MessageList", () => {
107
+ it("renders accessible thread options with selected, unread, and status states", () => {
108
+ render(<MessageList threads={threads} selectedThreadId="t1" />);
109
+
110
+ const selected = screen.getByRole("option", {
111
+ name: /Unread, From Jane Doe, Q3 launch - final brief/
112
+ });
113
+
114
+ expect(selected.getAttribute("aria-selected")).toBe("true");
115
+ expect(selected.getAttribute("aria-posinset")).toBe("1");
116
+ expect(selected.getAttribute("aria-setsize")).toBe("2");
117
+ expect(selected.className).toContain("eth-inbox-message-list__row--unread");
118
+ const receivedAt = selected.querySelector("time");
119
+
120
+ expect(screen.getByText("Pending Approval")).toBeTruthy();
121
+ expect(receivedAt?.textContent).toBeTruthy();
122
+ expect(receivedAt?.getAttribute("dateTime")).toBe("2026-05-27T10:42:00Z");
123
+ });
124
+
125
+ it("notifies consumers when a thread is selected", () => {
126
+ const onSelect = vi.fn();
127
+
128
+ render(<MessageList threads={threads} onSelect={onSelect} />);
129
+
130
+ fireEvent.click(screen.getByRole("option", { name: /Customer feedback digest/ }));
131
+
132
+ expect(onSelect).toHaveBeenCalledWith("t2");
133
+ });
134
+
135
+ it("shows a readable empty state when there are no threads", () => {
136
+ render(<MessageList threads={[]} />);
137
+
138
+ expect(screen.getByRole("status", { name: "No message threads" })).toBeTruthy();
139
+ });
140
+ });
141
+
142
+ describe("@echothink-ui/inbox ThreadSummaryPanel", () => {
143
+ it("renders generated summary metadata and source citations", () => {
144
+ render(
145
+ <ThreadSummaryPanel
146
+ summary={{
147
+ text: "Jane requested approval on the Q3 launch brief before legal review.",
148
+ citations: [
149
+ { id: "m1", label: "Original request", href: "#m1", messageRef: "m1" },
150
+ { id: "m2", label: "Your reply", messageRef: "m2" }
151
+ ]
152
+ }}
153
+ />
154
+ );
155
+
156
+ expect(screen.getByRole("heading", { name: "Thread summary" })).toBeTruthy();
157
+ expect(screen.getByText("Agent generated")).toBeTruthy();
158
+ expect(screen.getByText("2 citations")).toBeTruthy();
159
+ expect(screen.getByRole("region", { name: "Source citations" })).toBeTruthy();
160
+ expect(screen.getByRole("link", { name: "Original request, message m1" })).toBeTruthy();
161
+ expect(screen.getByLabelText("Your reply, message m2")).toBeTruthy();
162
+ });
163
+
164
+ it("shows an empty state when no summary has been generated", () => {
165
+ render(<ThreadSummaryPanel />);
166
+
167
+ expect(screen.getByRole("status")).toBeTruthy();
168
+ expect(screen.getByText("No generated summary")).toBeTruthy();
169
+ });
170
+ });
171
+
172
+ describe("@echothink-ui/inbox PriorityInboxView", () => {
173
+ it("renders labeled priority groups with counts, descriptions, and selected threads", () => {
174
+ render(
175
+ <PriorityInboxView
176
+ groups={[
177
+ {
178
+ id: "now",
179
+ title: "Needs response now",
180
+ description: "SLA breached or awaiting approval",
181
+ threads: threads.slice(0, 1)
182
+ },
183
+ {
184
+ id: "today",
185
+ title: "Respond today",
186
+ description: "Due before end of business",
187
+ threads: threads.slice(1)
188
+ }
189
+ ]}
190
+ selectedThreadId="t1"
191
+ />
192
+ );
193
+
194
+ expect(screen.getByRole("region", { name: "Needs response now" })).toBeTruthy();
195
+ expect(screen.getByText("SLA breached or awaiting approval")).toBeTruthy();
196
+ expect(screen.getByText("1 thread")).toBeTruthy();
197
+
198
+ const selected = screen.getByRole("option", {
199
+ name: /Unread, From Jane Doe, Q3 launch - final brief/
200
+ });
201
+
202
+ expect(selected.getAttribute("aria-selected")).toBe("true");
203
+ });
204
+
205
+ it("shows an empty state when there are no priority groups", () => {
206
+ render(<PriorityInboxView groups={[]} />);
207
+
208
+ expect(screen.getByRole("status", { name: "No priority threads" })).toBeTruthy();
209
+ });
210
+ });
211
+
212
+ describe("@echothink-ui/inbox RecipientPicker", () => {
213
+ const suggestions = [
214
+ { id: "jane", label: "Jane Doe", email: "jane@echothink.example" },
215
+ { id: "mark", label: "Mark K.", email: "mark@echothink.example" }
216
+ ];
217
+
218
+ it("renders selected recipients, manual entry, and accessible suggestions", () => {
219
+ render(
220
+ <RecipientPicker
221
+ value={["jane@echothink.example"]}
222
+ suggestions={suggestions}
223
+ defaultSuggestionsOpen
224
+ />
225
+ );
226
+
227
+ expect(screen.getByLabelText("Selected recipients")).toBeTruthy();
228
+ expect(screen.getByPlaceholderText("Search recipients")).toBeTruthy();
229
+ expect(screen.getByPlaceholderText("name@company.com")).toBeTruthy();
230
+ expect(screen.getByRole("listbox", { name: "Recipient suggestions" })).toBeTruthy();
231
+
232
+ const selected = screen.getByRole("option", {
233
+ name: /Jane Doe jane@echothink\.example/
234
+ });
235
+ const available = screen.getByRole("option", {
236
+ name: /Mark K\. mark@echothink\.example/
237
+ });
238
+
239
+ expect(selected.getAttribute("aria-selected")).toBe("true");
240
+ expect(available.getAttribute("aria-selected")).toBe("false");
241
+ });
242
+
243
+ it("adds manual and suggested recipients without duplicating selected values", () => {
244
+ const onChange = vi.fn();
245
+
246
+ render(
247
+ <RecipientPicker
248
+ value={["jane@echothink.example"]}
249
+ onChange={onChange}
250
+ suggestions={suggestions}
251
+ defaultSuggestionsOpen
252
+ />
253
+ );
254
+
255
+ fireEvent.change(screen.getByPlaceholderText("name@company.com"), {
256
+ target: { value: "ops@echothink.example" }
257
+ });
258
+ fireEvent.click(screen.getByRole("button", { name: "Add recipient" }));
259
+ fireEvent.click(
260
+ screen.getByRole("option", { name: /Mark K\. mark@echothink\.example/ })
261
+ );
262
+ fireEvent.click(
263
+ screen.getByRole("option", { name: /Jane Doe jane@echothink\.example/ })
264
+ );
265
+
266
+ expect(onChange).toHaveBeenNthCalledWith(1, [
267
+ "jane@echothink.example",
268
+ "ops@echothink.example"
269
+ ]);
270
+ expect(onChange).toHaveBeenNthCalledWith(2, [
271
+ "jane@echothink.example",
272
+ "mark@echothink.example"
273
+ ]);
274
+ expect(onChange).toHaveBeenCalledTimes(2);
275
+ });
276
+ });
package/src/index.tsx ADDED
@@ -0,0 +1,60 @@
1
+ import "./styles.css";
2
+
3
+ export * from "./types";
4
+ export { InboxShell, type InboxShellProps } from "./components/InboxShell";
5
+ export { MailboxFolderList, type MailboxFolderListProps } from "./components/MailboxFolderList";
6
+ export { MessageList, type MessageListProps } from "./components/MessageList";
7
+ export {
8
+ MessageThread,
9
+ type MessageThreadAction,
10
+ type MessageThreadProps
11
+ } from "./components/MessageThread";
12
+ export { MessageComposer, type MessageComposerProps } from "./components/MessageComposer";
13
+ export { MessagePreview, type MessagePreviewProps } from "./components/MessagePreview";
14
+ export { AttachmentList, type AttachmentListProps } from "./components/AttachmentList";
15
+ export { AttachmentPreview, type AttachmentPreviewProps } from "./components/AttachmentPreview";
16
+ export { RecipientPicker, type RecipientPickerProps } from "./components/RecipientPicker";
17
+ export { LabelManager, type LabelManagerProps } from "./components/LabelManager";
18
+ export { MessageSearch, type MessageSearchProps } from "./components/MessageSearch";
19
+ export {
20
+ MessageFilterBar,
21
+ type MessageFilter,
22
+ type MessageFilterBarProps
23
+ } from "./components/MessageFilterBar";
24
+ export {
25
+ PriorityInboxView,
26
+ type PriorityInboxGroup,
27
+ type PriorityInboxViewProps
28
+ } from "./components/PriorityInboxView";
29
+ export {
30
+ EmailAutomationRulePanel,
31
+ type EmailAutomationRulePanelProps
32
+ } from "./components/EmailAutomationRulePanel";
33
+ export {
34
+ AgentDraftReviewPanel,
35
+ type AgentDraftReviewPanelProps,
36
+ type DraftEvidence
37
+ } from "./components/AgentDraftReviewPanel";
38
+ export { ApprovalToSendPanel, type ApprovalToSendPanelProps } from "./components/ApprovalToSendPanel";
39
+ export { ThreadSummaryPanel, type ThreadSummaryPanelProps } from "./components/ThreadSummaryPanel";
40
+
41
+ export const InboxComponentNames = [
42
+ "InboxShell",
43
+ "MailboxFolderList",
44
+ "MessageList",
45
+ "MessageThread",
46
+ "MessageComposer",
47
+ "MessagePreview",
48
+ "AttachmentList",
49
+ "AttachmentPreview",
50
+ "RecipientPicker",
51
+ "LabelManager",
52
+ "MessageSearch",
53
+ "MessageFilterBar",
54
+ "PriorityInboxView",
55
+ "EmailAutomationRulePanel",
56
+ "AgentDraftReviewPanel",
57
+ "ApprovalToSendPanel",
58
+ "ThreadSummaryPanel"
59
+ ] as const;
60
+ export type InboxComponentName = (typeof InboxComponentNames)[number];