@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,118 @@
1
+ import * as React from "react";
2
+ import { Badge, Button, Panel, Textarea } from "@echothink-ui/core";
3
+ import type { MessageDraft } from "../types";
4
+
5
+ export interface ApprovalToSendPanelProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ draft?: MessageDraft;
7
+ recipients?: string[];
8
+ riskLevel?: "low" | "medium" | "high" | "critical";
9
+ policyRef?: React.ReactNode;
10
+ onApprove?: (reason?: string) => void;
11
+ onReject?: (reason?: string) => void;
12
+ }
13
+
14
+ export function ApprovalToSendPanel({
15
+ draft,
16
+ recipients = [],
17
+ riskLevel = "low",
18
+ policyRef,
19
+ onApprove,
20
+ onReject,
21
+ className,
22
+ ...props
23
+ }: ApprovalToSendPanelProps) {
24
+ const [reason, setReason] = React.useState("");
25
+ const toRecipients = recipients.length ? recipients : (draft?.to ?? []);
26
+ const ccRecipients = draft?.cc ?? [];
27
+ const bccRecipients = draft?.bcc ?? [];
28
+ const subject = draft?.subject?.trim() || "No subject";
29
+ const body = draft?.body?.trim() || "No message body provided.";
30
+ const riskSeverity =
31
+ riskLevel === "critical" || riskLevel === "high"
32
+ ? "danger"
33
+ : riskLevel === "medium"
34
+ ? "warning"
35
+ : "info";
36
+ const riskLabel = `${riskLevel.charAt(0).toUpperCase()}${riskLevel.slice(1)} risk`;
37
+ const recipientCount =
38
+ recipients.length || toRecipients.length + ccRecipients.length + bccRecipients.length;
39
+ const recipientKind = recipients.length ? "external recipient" : "recipient";
40
+ const recipientSummary =
41
+ recipientCount === 1 ? `1 ${recipientKind}` : `${recipientCount || "No"} ${recipientKind}s`;
42
+
43
+ const formatRecipients = (values: string[]) =>
44
+ values.length ? values.join(", ") : "Not specified";
45
+
46
+ return (
47
+ <Panel
48
+ {...props}
49
+ className={`eth-inbox-send-approval eth-inbox-send-approval--${riskLevel} ${className ?? ""}`}
50
+ title="Approval required"
51
+ subtitle="Review this outbound message before it is sent."
52
+ data-eth-component="ApprovalToSendPanel"
53
+ >
54
+ <div className="eth-inbox-send-approval__status">
55
+ <div className="eth-inbox-send-approval__summary">
56
+ <Badge severity={riskSeverity}>{riskLabel}</Badge>
57
+ {policyRef ? <span className="eth-inbox-send-approval__policy">{policyRef}</span> : null}
58
+ </div>
59
+ <span className="eth-inbox-send-approval__state">Awaiting approval</span>
60
+ </div>
61
+
62
+ <section
63
+ className="eth-inbox-send-approval__message"
64
+ aria-label="Outbound message for approval"
65
+ >
66
+ <header className="eth-inbox-send-approval__message-header">
67
+ <div>
68
+ <h4>Outbound message</h4>
69
+ <p>{recipientSummary}</p>
70
+ </div>
71
+ </header>
72
+ <dl className="eth-inbox-send-approval__meta">
73
+ <div>
74
+ <dt>To</dt>
75
+ <dd>{formatRecipients(toRecipients)}</dd>
76
+ </div>
77
+ {ccRecipients.length ? (
78
+ <div>
79
+ <dt>Cc</dt>
80
+ <dd>{formatRecipients(ccRecipients)}</dd>
81
+ </div>
82
+ ) : null}
83
+ {bccRecipients.length ? (
84
+ <div>
85
+ <dt>Bcc</dt>
86
+ <dd>{formatRecipients(bccRecipients)}</dd>
87
+ </div>
88
+ ) : null}
89
+ <div>
90
+ <dt>Subject</dt>
91
+ <dd>{subject}</dd>
92
+ </div>
93
+ </dl>
94
+ <div className="eth-inbox-send-approval__message-body">
95
+ <span>Message body</span>
96
+ <p>{body}</p>
97
+ </div>
98
+ </section>
99
+
100
+ <div className="eth-inbox-send-approval__decision">
101
+ <Textarea
102
+ value={reason}
103
+ labelText="Decision note"
104
+ helperText="Optional for approval. Include the policy reason when rejecting."
105
+ placeholder="Add an approval or rejection reason"
106
+ onChange={(event) => setReason(event.currentTarget.value)}
107
+ />
108
+ </div>
109
+
110
+ <div className="eth-inbox-send-approval__actions">
111
+ <Button intent="danger" onClick={() => onReject?.(reason || undefined)}>
112
+ Reject
113
+ </Button>
114
+ <Button onClick={() => onApprove?.(reason || undefined)}>Approve send</Button>
115
+ </div>
116
+ </Panel>
117
+ );
118
+ }
@@ -0,0 +1,85 @@
1
+ import * as React from "react";
2
+ import { IconButton } from "@echothink-ui/core";
3
+ import { DocumentIcon, DownloadIcon, ViewIcon } from "@echothink-ui/icons";
4
+ import type { Attachment } from "../types";
5
+ import { formatBytes } from "../utils";
6
+
7
+ export interface AttachmentListProps extends React.HTMLAttributes<HTMLDivElement> {
8
+ attachments?: Attachment[];
9
+ onPreview?: (attachment: Attachment) => void;
10
+ onDownload?: (attachment: Attachment) => void;
11
+ }
12
+
13
+ export function AttachmentList({
14
+ attachments = [],
15
+ onPreview,
16
+ onDownload,
17
+ className,
18
+ role,
19
+ "aria-label": ariaLabel,
20
+ ...props
21
+ }: AttachmentListProps) {
22
+ if (!attachments.length) return null;
23
+ return (
24
+ <div
25
+ {...props}
26
+ className={["eth-inbox-attachments", className].filter(Boolean).join(" ")}
27
+ data-eth-component="AttachmentList"
28
+ role={role ?? "list"}
29
+ aria-label={ariaLabel ?? "Message attachments"}
30
+ >
31
+ {attachments.map((attachment) => {
32
+ const typeLabel = attachmentTypeLabel(attachment);
33
+ const hasActions = Boolean(onPreview || onDownload);
34
+
35
+ return (
36
+ <div key={attachment.id} className="eth-inbox-attachments__tile" role="listitem">
37
+ <span className="eth-inbox-attachments__icon" aria-hidden="true">
38
+ <DocumentIcon size={18} />
39
+ </span>
40
+ <div className="eth-inbox-attachments__content">
41
+ <strong title={attachment.name}>{attachment.name}</strong>
42
+ <span className="eth-inbox-attachments__meta">
43
+ <span className="eth-inbox-attachments__type">{typeLabel}</span>
44
+ <span>{attachment.mimeType}</span>
45
+ <span>{formatBytes(attachment.size)}</span>
46
+ </span>
47
+ </div>
48
+ {hasActions ? (
49
+ <div
50
+ className="eth-inbox-attachments__actions"
51
+ aria-label={`Actions for ${attachment.name}`}
52
+ >
53
+ {onPreview ? (
54
+ <IconButton
55
+ intent="ghost"
56
+ density="compact"
57
+ label={`Preview ${attachment.name}`}
58
+ icon={<ViewIcon size={16} />}
59
+ onClick={() => onPreview(attachment)}
60
+ />
61
+ ) : null}
62
+ {onDownload ? (
63
+ <IconButton
64
+ intent="ghost"
65
+ density="compact"
66
+ label={`Download ${attachment.name}`}
67
+ icon={<DownloadIcon size={16} />}
68
+ onClick={() => onDownload(attachment)}
69
+ />
70
+ ) : null}
71
+ </div>
72
+ ) : null}
73
+ </div>
74
+ );
75
+ })}
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function attachmentTypeLabel(attachment: Attachment) {
81
+ const extension = attachment.name.split(".").pop();
82
+ if (extension && extension !== attachment.name) return extension.slice(0, 4).toUpperCase();
83
+ const subtype = attachment.mimeType.split("/").pop();
84
+ return subtype ? subtype.slice(0, 4).toUpperCase() : "FILE";
85
+ }
@@ -0,0 +1,207 @@
1
+ import * as React from "react";
2
+ import type { Attachment } from "../types";
3
+ import { formatBytes } from "../utils";
4
+
5
+ export interface AttachmentPreviewProps extends React.HTMLAttributes<HTMLElement> {
6
+ attachment?: Attachment;
7
+ children?: React.ReactNode;
8
+ }
9
+
10
+ export function AttachmentPreview({
11
+ attachment,
12
+ children,
13
+ className,
14
+ ...props
15
+ }: AttachmentPreviewProps) {
16
+ const kind = getPreviewKind(attachment);
17
+ const previewSource = attachment?.previewUrl ?? attachment?.src;
18
+ const hasCustomPreview = children !== undefined && children !== null;
19
+ const label = attachment ? `Attachment preview for ${attachment.name}` : "Attachment preview";
20
+
21
+ return (
22
+ <section
23
+ {...props}
24
+ aria-label={props["aria-label"] ?? label}
25
+ className={`eth-inbox-attachment-preview ${className ?? ""}`}
26
+ data-eth-component="AttachmentPreview"
27
+ >
28
+ {attachment ? (
29
+ <header className="eth-inbox-attachment-preview__header">
30
+ <div className="eth-inbox-attachment-preview__file">
31
+ <span
32
+ className="eth-inbox-attachment-preview__glyph"
33
+ data-kind={kind}
34
+ aria-hidden="true"
35
+ >
36
+ {kindToken(kind)}
37
+ </span>
38
+ <div className="eth-inbox-attachment-preview__summary">
39
+ <strong title={attachment.name}>{attachment.name}</strong>
40
+ <span>
41
+ {attachment.mimeType} - {formatBytes(attachment.size)}
42
+ </span>
43
+ </div>
44
+ </div>
45
+ <span className="eth-inbox-attachment-preview__state">{previewStateLabel(kind)}</span>
46
+ </header>
47
+ ) : null}
48
+ <div
49
+ className={`eth-inbox-attachment-preview__body eth-inbox-attachment-preview__body--${kind}`}
50
+ >
51
+ {hasCustomPreview ? children : renderDefaultPreview(attachment, kind, previewSource)}
52
+ </div>
53
+ </section>
54
+ );
55
+ }
56
+
57
+ type AttachmentPreviewKind =
58
+ | "empty"
59
+ | "pdf"
60
+ | "image"
61
+ | "text"
62
+ | "spreadsheet"
63
+ | "archive"
64
+ | "file";
65
+
66
+ function getPreviewKind(attachment?: Attachment): AttachmentPreviewKind {
67
+ if (!attachment) return "empty";
68
+ const mimeType = attachment.mimeType.toLowerCase();
69
+ const name = attachment.name.toLowerCase();
70
+
71
+ if (mimeType === "application/pdf" || name.endsWith(".pdf")) return "pdf";
72
+ if (mimeType.startsWith("image/")) return "image";
73
+ if (
74
+ mimeType.includes("spreadsheet") ||
75
+ mimeType.includes("csv") ||
76
+ name.endsWith(".csv") ||
77
+ name.endsWith(".xls") ||
78
+ name.endsWith(".xlsx")
79
+ ) {
80
+ return "spreadsheet";
81
+ }
82
+ if (
83
+ mimeType.startsWith("text/") ||
84
+ ["application/json", "application/xml", "application/yaml"].includes(mimeType) ||
85
+ name.endsWith(".md")
86
+ ) {
87
+ return "text";
88
+ }
89
+ if (
90
+ mimeType.includes("zip") ||
91
+ mimeType.includes("compressed") ||
92
+ name.endsWith(".zip") ||
93
+ name.endsWith(".gz")
94
+ ) {
95
+ return "archive";
96
+ }
97
+
98
+ return "file";
99
+ }
100
+
101
+ function kindToken(kind: AttachmentPreviewKind) {
102
+ if (kind === "pdf") return "PDF";
103
+ if (kind === "image") return "IMG";
104
+ if (kind === "text") return "TXT";
105
+ if (kind === "spreadsheet") return "CSV";
106
+ if (kind === "archive") return "ZIP";
107
+ return "FILE";
108
+ }
109
+
110
+ function previewStateLabel(kind: AttachmentPreviewKind) {
111
+ if (kind === "archive" || kind === "file") return "Preview unavailable";
112
+ return "Inline preview";
113
+ }
114
+
115
+ function renderDefaultPreview(
116
+ attachment: Attachment | undefined,
117
+ kind: AttachmentPreviewKind,
118
+ previewSource?: string
119
+ ) {
120
+ if (!attachment) {
121
+ return (
122
+ <div className="eth-inbox-attachment-preview__empty">
123
+ <strong>No attachment selected</strong>
124
+ <span>PDF, image, text, and spreadsheet files can be previewed inline.</span>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ if (kind === "image") {
130
+ return previewSource ? (
131
+ <img
132
+ className="eth-inbox-attachment-preview__image"
133
+ src={previewSource}
134
+ alt={`${attachment.name} preview`}
135
+ />
136
+ ) : (
137
+ <div
138
+ className="eth-inbox-attachment-preview__image-frame"
139
+ role="img"
140
+ aria-label={`${attachment.name} image preview`}
141
+ >
142
+ <span />
143
+ <span />
144
+ </div>
145
+ );
146
+ }
147
+
148
+ if (kind === "text") {
149
+ return (
150
+ <div
151
+ className="eth-inbox-attachment-preview__text"
152
+ role="img"
153
+ aria-label={`${attachment.name} text preview`}
154
+ >
155
+ {Array.from({ length: 9 }, (_, index) => (
156
+ <span key={index} />
157
+ ))}
158
+ </div>
159
+ );
160
+ }
161
+
162
+ if (kind === "spreadsheet") {
163
+ return (
164
+ <div
165
+ className="eth-inbox-attachment-preview__sheet"
166
+ role="img"
167
+ aria-label={`${attachment.name} spreadsheet preview`}
168
+ >
169
+ {Array.from({ length: 20 }, (_, index) => (
170
+ <span key={index} />
171
+ ))}
172
+ </div>
173
+ );
174
+ }
175
+
176
+ if (kind === "pdf") {
177
+ return (
178
+ <div
179
+ className="eth-inbox-attachment-preview__document"
180
+ role="img"
181
+ aria-label={`${attachment.name} PDF preview`}
182
+ >
183
+ <div className="eth-inbox-attachment-preview__document-bar">
184
+ <span>{attachment.mimeType}</span>
185
+ <span>Page 1</span>
186
+ </div>
187
+ <div className="eth-inbox-attachment-preview__page">
188
+ <strong>{attachment.name.replace(/\.[^.]+$/, "") || attachment.name}</strong>
189
+ <span />
190
+ <span />
191
+ <span />
192
+ <span />
193
+ <span />
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ return (
200
+ <div className="eth-inbox-attachment-preview__empty">
201
+ <strong>Preview unavailable</strong>
202
+ <span>
203
+ {attachment.mimeType} files are kept as attachments for download or external review.
204
+ </span>
205
+ </div>
206
+ );
207
+ }
@@ -0,0 +1,100 @@
1
+ import * as React from "react";
2
+ import { Badge, Button, Panel, Toggle } from "@echothink-ui/core";
3
+ import { EditIcon } from "@echothink-ui/icons";
4
+ import type { AutomationRule } from "../types";
5
+
6
+ export interface EmailAutomationRulePanelProps extends Omit<
7
+ React.HTMLAttributes<HTMLDivElement>,
8
+ "onToggle"
9
+ > {
10
+ rules?: AutomationRule[];
11
+ onToggle?: (id: string, enabled: boolean) => void;
12
+ onEdit?: (id: string) => void;
13
+ }
14
+
15
+ export function EmailAutomationRulePanel({
16
+ rules = [],
17
+ onToggle,
18
+ onEdit,
19
+ className,
20
+ ...props
21
+ }: EmailAutomationRulePanelProps) {
22
+ const enabledCount = rules.reduce((count, rule) => count + (rule.enabled ? 1 : 0), 0);
23
+ const ruleCountLabel = `${rules.length} automation ${rules.length === 1 ? "rule" : "rules"}`;
24
+ const subtitle = rules.length
25
+ ? `${enabledCount} of ${ruleCountLabel} enabled`
26
+ : "Create routing, drafting, and mailbox automation rules.";
27
+
28
+ return (
29
+ <Panel
30
+ {...props}
31
+ className={`eth-inbox-rules ${className ?? ""}`}
32
+ title="Automation rules"
33
+ subtitle={subtitle}
34
+ data-eth-component="EmailAutomationRulePanel"
35
+ >
36
+ {rules.length ? (
37
+ <div className="eth-inbox-rules__list" role="list">
38
+ {rules.map((rule) => {
39
+ const headingId = `eth-inbox-rule-${rule.id.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
40
+
41
+ return (
42
+ <article
43
+ key={rule.id}
44
+ aria-labelledby={headingId}
45
+ className={`eth-inbox-rules__item ${
46
+ rule.enabled ? "eth-inbox-rules__item--enabled" : "eth-inbox-rules__item--paused"
47
+ }`}
48
+ role="listitem"
49
+ >
50
+ <div className="eth-inbox-rules__content">
51
+ <header className="eth-inbox-rules__item-header">
52
+ <h4 id={headingId}>{rule.name}</h4>
53
+ <Badge severity={rule.enabled ? "success" : "neutral"}>
54
+ {rule.enabled ? "Enabled" : "Paused"}
55
+ </Badge>
56
+ </header>
57
+ <dl className="eth-inbox-rules__definition">
58
+ <div>
59
+ <dt>Trigger</dt>
60
+ <dd>{rule.conditions}</dd>
61
+ </div>
62
+ <div>
63
+ <dt>Action</dt>
64
+ <dd>{rule.actions}</dd>
65
+ </div>
66
+ </dl>
67
+ </div>
68
+
69
+ <div className="eth-inbox-rules__controls">
70
+ <Toggle
71
+ checked={rule.enabled}
72
+ className="eth-inbox-rules__toggle"
73
+ disabled={!onToggle}
74
+ hideLabel
75
+ label={`Automation status for ${rule.name}`}
76
+ offLabel="Paused"
77
+ onChange={(event) => onToggle?.(rule.id, event.currentTarget.checked)}
78
+ onLabel="Enabled"
79
+ />
80
+ <Button
81
+ aria-label={`Edit ${rule.name}`}
82
+ density="compact"
83
+ disabled={!onEdit}
84
+ icon={<EditIcon size={16} />}
85
+ intent="secondary"
86
+ onClick={() => onEdit?.(rule.id)}
87
+ >
88
+ Edit
89
+ </Button>
90
+ </div>
91
+ </article>
92
+ );
93
+ })}
94
+ </div>
95
+ ) : (
96
+ <p className="eth-inbox-rules__empty">No automation rules configured.</p>
97
+ )}
98
+ </Panel>
99
+ );
100
+ }
@@ -0,0 +1,62 @@
1
+ import * as React from "react";
2
+ import type { SurfaceComponentProps } from "@echothink-ui/core";
3
+
4
+ export interface InboxShellProps extends SurfaceComponentProps {
5
+ folders?: React.ReactNode;
6
+ list?: React.ReactNode;
7
+ thread?: React.ReactNode;
8
+ inspector?: React.ReactNode;
9
+ toolbar?: React.ReactNode;
10
+ }
11
+
12
+ export function InboxShell({
13
+ folders,
14
+ list,
15
+ thread,
16
+ inspector,
17
+ toolbar,
18
+ className,
19
+ title,
20
+ description,
21
+ ...props
22
+ }: InboxShellProps) {
23
+ const hasInspector = Boolean(inspector);
24
+
25
+ return (
26
+ <section
27
+ {...props}
28
+ className={`eth-inbox-shell ${className ?? ""}`}
29
+ data-eth-component="InboxShell"
30
+ >
31
+ {(title || description || toolbar) && (
32
+ <header className="eth-inbox-shell__header">
33
+ <div>
34
+ {title ? <h2>{title}</h2> : null}
35
+ {description ? <p>{description}</p> : null}
36
+ </div>
37
+ {toolbar ? <div className="eth-inbox-shell__toolbar">{toolbar}</div> : null}
38
+ </header>
39
+ )}
40
+ <div
41
+ className={`eth-inbox-shell__layout ${
42
+ hasInspector ? "eth-inbox-shell__layout--has-inspector" : ""
43
+ }`}
44
+ >
45
+ <aside className="eth-inbox-shell__folders" aria-label="Mailbox folders">
46
+ {folders}
47
+ </aside>
48
+ <aside className="eth-inbox-shell__list" aria-label="Message list">
49
+ {list}
50
+ </aside>
51
+ <main className="eth-inbox-shell__thread" aria-label="Selected conversation">
52
+ {thread}
53
+ </main>
54
+ {inspector ? (
55
+ <aside className="eth-inbox-shell__inspector" aria-label="Conversation inspector">
56
+ {inspector}
57
+ </aside>
58
+ ) : null}
59
+ </div>
60
+ </section>
61
+ );
62
+ }