@mdxui/zero 6.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/.storybook/preview.ts +20 -0
- package/.turbo/turbo-typecheck.log +5 -0
- package/ARCHITECTURE.md +415 -0
- package/CHANGELOG.md +80 -0
- package/README.md +205 -0
- package/package.json +43 -0
- package/playwright.config.ts +55 -0
- package/src/components/index.ts +20 -0
- package/src/compose/email-composer.stories.tsx +219 -0
- package/src/compose/email-composer.tsx +619 -0
- package/src/compose/index.ts +14 -0
- package/src/dashboard/index.ts +14 -0
- package/src/dashboard/mail-shell.stories.tsx +272 -0
- package/src/dashboard/mail-shell.tsx +199 -0
- package/src/dashboard/mail-sidebar.stories.tsx +158 -0
- package/src/dashboard/mail-sidebar.tsx +388 -0
- package/src/index.ts +24 -0
- package/src/landing/index.ts +24 -0
- package/src/mail/index.ts +15 -0
- package/src/mail/mail-item.stories.tsx +422 -0
- package/src/mail/mail-item.tsx +229 -0
- package/src/mail/mail-list.stories.tsx +320 -0
- package/src/mail/mail-list.tsx +262 -0
- package/src/mail/message-view.stories.tsx +459 -0
- package/src/mail/message-view.tsx +378 -0
- package/src/mail/thread-display.stories.tsx +260 -0
- package/src/mail/thread-display.tsx +392 -0
- package/src/pages/index.ts +9 -0
- package/src/pages/mail-zero-page.stories.tsx +251 -0
- package/src/pages/mail-zero-page.tsx +334 -0
- package/tests/visual/report/index.html +85 -0
- package/tests/visual/snapshots/zero-components.spec.ts/mail-shell-default.png +0 -0
- package/tests/visual/zero-components.spec.ts +321 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@mdxui/primitives/avatar";
|
|
2
|
+
import { Button } from "@mdxui/primitives/button";
|
|
3
|
+
import {
|
|
4
|
+
Collapsible,
|
|
5
|
+
CollapsibleContent,
|
|
6
|
+
CollapsibleTrigger,
|
|
7
|
+
} from "@mdxui/primitives/collapsible";
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from "@mdxui/primitives/dropdown-menu";
|
|
14
|
+
import { cn } from "@mdxui/primitives/lib/utils";
|
|
15
|
+
import {
|
|
16
|
+
Tooltip,
|
|
17
|
+
TooltipContent,
|
|
18
|
+
TooltipProvider,
|
|
19
|
+
TooltipTrigger,
|
|
20
|
+
} from "@mdxui/primitives/tooltip";
|
|
21
|
+
import {
|
|
22
|
+
ChevronDown,
|
|
23
|
+
ChevronUp,
|
|
24
|
+
Download,
|
|
25
|
+
Forward,
|
|
26
|
+
MoreHorizontal,
|
|
27
|
+
Paperclip,
|
|
28
|
+
Reply,
|
|
29
|
+
ReplyAll,
|
|
30
|
+
Star,
|
|
31
|
+
} from "lucide-react";
|
|
32
|
+
import type { Message, MessageHeaderProps, MessageViewProps } from "mdxui";
|
|
33
|
+
import * as React from "react";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* MessageHeader - Displays message metadata (from, to, date).
|
|
37
|
+
*/
|
|
38
|
+
function MessageHeader({
|
|
39
|
+
message,
|
|
40
|
+
expanded = false,
|
|
41
|
+
onToggle,
|
|
42
|
+
className,
|
|
43
|
+
}: MessageHeaderProps) {
|
|
44
|
+
const formatDate = (dateStr: string) => {
|
|
45
|
+
const date = new Date(dateStr);
|
|
46
|
+
return date.toLocaleString([], {
|
|
47
|
+
weekday: "short",
|
|
48
|
+
month: "short",
|
|
49
|
+
day: "numeric",
|
|
50
|
+
year: "numeric",
|
|
51
|
+
hour: "numeric",
|
|
52
|
+
minute: "2-digit",
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const getInitials = (name?: string, email?: string) => {
|
|
57
|
+
if (name) {
|
|
58
|
+
return name
|
|
59
|
+
.split(" ")
|
|
60
|
+
.map((n) => n[0])
|
|
61
|
+
.join("")
|
|
62
|
+
.toUpperCase()
|
|
63
|
+
.slice(0, 2);
|
|
64
|
+
}
|
|
65
|
+
return email?.charAt(0).toUpperCase() || "?";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div
|
|
70
|
+
data-slot="message-header"
|
|
71
|
+
className={cn("flex items-start gap-3", className)}
|
|
72
|
+
>
|
|
73
|
+
<Avatar className="size-10 shrink-0">
|
|
74
|
+
<AvatarImage
|
|
75
|
+
src={undefined}
|
|
76
|
+
alt={message.from.name || message.from.address}
|
|
77
|
+
/>
|
|
78
|
+
<AvatarFallback>
|
|
79
|
+
{getInitials(message.from.name, message.from.address)}
|
|
80
|
+
</AvatarFallback>
|
|
81
|
+
</Avatar>
|
|
82
|
+
|
|
83
|
+
<div className="min-w-0 flex-1">
|
|
84
|
+
<div className="flex items-center justify-between gap-2">
|
|
85
|
+
<div className="flex items-center gap-2">
|
|
86
|
+
<span className="font-medium">
|
|
87
|
+
{message.from.name || message.from.address}
|
|
88
|
+
</span>
|
|
89
|
+
{message.isStarred && (
|
|
90
|
+
<Star className="size-4 fill-yellow-500 text-yellow-500" />
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
<span className="text-muted-foreground shrink-0 text-sm">
|
|
94
|
+
{formatDate(message.date)}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<Collapsible open={expanded} onOpenChange={onToggle}>
|
|
99
|
+
<CollapsibleTrigger asChild>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className="text-muted-foreground flex items-center gap-1 text-sm hover:underline"
|
|
103
|
+
>
|
|
104
|
+
to {message.to[0]?.name || message.to[0]?.address}
|
|
105
|
+
{message.to.length > 1 && ` and ${message.to.length - 1} others`}
|
|
106
|
+
{expanded ? (
|
|
107
|
+
<ChevronUp className="size-3" />
|
|
108
|
+
) : (
|
|
109
|
+
<ChevronDown className="size-3" />
|
|
110
|
+
)}
|
|
111
|
+
</button>
|
|
112
|
+
</CollapsibleTrigger>
|
|
113
|
+
<CollapsibleContent className="mt-2 space-y-1 text-sm">
|
|
114
|
+
<div className="text-muted-foreground">
|
|
115
|
+
<span className="font-medium">From:</span>{" "}
|
|
116
|
+
{message.from.name ? (
|
|
117
|
+
<>
|
|
118
|
+
{message.from.name} <{message.from.address}>
|
|
119
|
+
</>
|
|
120
|
+
) : (
|
|
121
|
+
message.from.address
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
<div className="text-muted-foreground">
|
|
125
|
+
<span className="font-medium">To:</span>{" "}
|
|
126
|
+
{message.to
|
|
127
|
+
.map((r) => (r.name ? `${r.name} <${r.address}>` : r.address))
|
|
128
|
+
.join(", ")}
|
|
129
|
+
</div>
|
|
130
|
+
{message.cc && message.cc.length > 0 && (
|
|
131
|
+
<div className="text-muted-foreground">
|
|
132
|
+
<span className="font-medium">Cc:</span>{" "}
|
|
133
|
+
{message.cc
|
|
134
|
+
.map((r) => (r.name ? `${r.name} <${r.address}>` : r.address))
|
|
135
|
+
.join(", ")}
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
{message.replyTo && (
|
|
139
|
+
<div className="text-muted-foreground">
|
|
140
|
+
<span className="font-medium">Reply-To:</span>{" "}
|
|
141
|
+
{message.replyTo.name
|
|
142
|
+
? `${message.replyTo.name} <${message.replyTo.address}>`
|
|
143
|
+
: message.replyTo.address}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</CollapsibleContent>
|
|
147
|
+
</Collapsible>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* AttachmentItem - Displays a single attachment.
|
|
155
|
+
*/
|
|
156
|
+
function AttachmentItem({
|
|
157
|
+
attachment,
|
|
158
|
+
onDownload,
|
|
159
|
+
onClick,
|
|
160
|
+
}: {
|
|
161
|
+
attachment: Message["attachments"][number];
|
|
162
|
+
onDownload?: () => void;
|
|
163
|
+
onClick?: () => void;
|
|
164
|
+
}) {
|
|
165
|
+
const formatSize = (bytes: number) => {
|
|
166
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
167
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
168
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const isImage = attachment.mimeType.startsWith("image/");
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
className="bg-muted/50 hover:bg-muted flex items-center gap-2 rounded-lg border p-2 transition-colors"
|
|
177
|
+
tabIndex={0}
|
|
178
|
+
onClick={onClick}
|
|
179
|
+
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => e.key === "Enter" && onClick?.()}
|
|
180
|
+
>
|
|
181
|
+
{isImage && attachment.thumbnailUrl ? (
|
|
182
|
+
<img
|
|
183
|
+
src={attachment.thumbnailUrl}
|
|
184
|
+
alt={attachment.filename}
|
|
185
|
+
className="size-10 rounded object-cover"
|
|
186
|
+
/>
|
|
187
|
+
) : (
|
|
188
|
+
<div className="bg-background flex size-10 items-center justify-center rounded">
|
|
189
|
+
<Paperclip className="text-muted-foreground size-5" />
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
<div className="min-w-0 flex-1">
|
|
193
|
+
<div className="truncate text-sm font-medium">
|
|
194
|
+
{attachment.filename}
|
|
195
|
+
</div>
|
|
196
|
+
<div className="text-muted-foreground text-xs">
|
|
197
|
+
{formatSize(attachment.size)}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<TooltipProvider>
|
|
201
|
+
<Tooltip>
|
|
202
|
+
<TooltipTrigger asChild>
|
|
203
|
+
<Button
|
|
204
|
+
variant="ghost"
|
|
205
|
+
size="icon"
|
|
206
|
+
className="size-8 shrink-0"
|
|
207
|
+
onClick={(e) => {
|
|
208
|
+
e.stopPropagation();
|
|
209
|
+
onDownload?.();
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<Download className="size-4" />
|
|
213
|
+
</Button>
|
|
214
|
+
</TooltipTrigger>
|
|
215
|
+
<TooltipContent>Download</TooltipContent>
|
|
216
|
+
</Tooltip>
|
|
217
|
+
</TooltipProvider>
|
|
218
|
+
</button>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* MessageView - Displays a single email message.
|
|
224
|
+
*/
|
|
225
|
+
function MessageView({
|
|
226
|
+
message,
|
|
227
|
+
showHeaders = false,
|
|
228
|
+
isExpanded = true,
|
|
229
|
+
onToggleExpand,
|
|
230
|
+
onReply,
|
|
231
|
+
onReplyAll,
|
|
232
|
+
onForward,
|
|
233
|
+
onStar,
|
|
234
|
+
onAttachmentClick,
|
|
235
|
+
showReplyComposer: _showReplyComposer = false,
|
|
236
|
+
className,
|
|
237
|
+
}: MessageViewProps) {
|
|
238
|
+
const [headersExpanded, setHeadersExpanded] = React.useState(showHeaders);
|
|
239
|
+
|
|
240
|
+
return (
|
|
241
|
+
<div
|
|
242
|
+
data-slot="message-view"
|
|
243
|
+
className={cn("border-b last:border-b-0", className)}
|
|
244
|
+
>
|
|
245
|
+
{/* Collapsed State */}
|
|
246
|
+
{!isExpanded ? (
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
onClick={() => onToggleExpand?.()}
|
|
250
|
+
className="hover:bg-muted/50 flex w-full items-center gap-3 px-4 py-3 text-left transition-colors"
|
|
251
|
+
>
|
|
252
|
+
<Avatar className="size-8 shrink-0">
|
|
253
|
+
<AvatarFallback className="text-xs">
|
|
254
|
+
{message.from.name?.charAt(0) || message.from.address.charAt(0)}
|
|
255
|
+
</AvatarFallback>
|
|
256
|
+
</Avatar>
|
|
257
|
+
<span className="text-muted-foreground flex-1 truncate text-sm">
|
|
258
|
+
{message.snippet || message.textBody?.slice(0, 100)}
|
|
259
|
+
</span>
|
|
260
|
+
</button>
|
|
261
|
+
) : (
|
|
262
|
+
<div className="space-y-4 px-4 py-4">
|
|
263
|
+
{/* Header */}
|
|
264
|
+
<div className="flex items-start justify-between gap-4">
|
|
265
|
+
<MessageHeader
|
|
266
|
+
message={message}
|
|
267
|
+
expanded={headersExpanded}
|
|
268
|
+
onToggle={() => setHeadersExpanded(!headersExpanded)}
|
|
269
|
+
/>
|
|
270
|
+
|
|
271
|
+
{/* Actions */}
|
|
272
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
273
|
+
<TooltipProvider>
|
|
274
|
+
<Tooltip>
|
|
275
|
+
<TooltipTrigger asChild>
|
|
276
|
+
<Button
|
|
277
|
+
variant="ghost"
|
|
278
|
+
size="icon"
|
|
279
|
+
className="size-8"
|
|
280
|
+
onClick={() => onReply?.()}
|
|
281
|
+
>
|
|
282
|
+
<Reply className="size-4" />
|
|
283
|
+
</Button>
|
|
284
|
+
</TooltipTrigger>
|
|
285
|
+
<TooltipContent>Reply</TooltipContent>
|
|
286
|
+
</Tooltip>
|
|
287
|
+
|
|
288
|
+
<Tooltip>
|
|
289
|
+
<TooltipTrigger asChild>
|
|
290
|
+
<Button
|
|
291
|
+
variant="ghost"
|
|
292
|
+
size="icon"
|
|
293
|
+
className="size-8"
|
|
294
|
+
onClick={() => onReplyAll?.()}
|
|
295
|
+
>
|
|
296
|
+
<ReplyAll className="size-4" />
|
|
297
|
+
</Button>
|
|
298
|
+
</TooltipTrigger>
|
|
299
|
+
<TooltipContent>Reply All</TooltipContent>
|
|
300
|
+
</Tooltip>
|
|
301
|
+
|
|
302
|
+
<Tooltip>
|
|
303
|
+
<TooltipTrigger asChild>
|
|
304
|
+
<Button
|
|
305
|
+
variant="ghost"
|
|
306
|
+
size="icon"
|
|
307
|
+
className="size-8"
|
|
308
|
+
onClick={() => onForward?.()}
|
|
309
|
+
>
|
|
310
|
+
<Forward className="size-4" />
|
|
311
|
+
</Button>
|
|
312
|
+
</TooltipTrigger>
|
|
313
|
+
<TooltipContent>Forward</TooltipContent>
|
|
314
|
+
</Tooltip>
|
|
315
|
+
|
|
316
|
+
<DropdownMenu>
|
|
317
|
+
<DropdownMenuTrigger asChild>
|
|
318
|
+
<Button variant="ghost" size="icon" className="size-8">
|
|
319
|
+
<MoreHorizontal className="size-4" />
|
|
320
|
+
</Button>
|
|
321
|
+
</DropdownMenuTrigger>
|
|
322
|
+
<DropdownMenuContent align="end">
|
|
323
|
+
<DropdownMenuItem onClick={() => onStar?.()}>
|
|
324
|
+
<Star className="mr-2 size-4" />
|
|
325
|
+
{message.isStarred ? "Unstar" : "Star"}
|
|
326
|
+
</DropdownMenuItem>
|
|
327
|
+
</DropdownMenuContent>
|
|
328
|
+
</DropdownMenu>
|
|
329
|
+
</TooltipProvider>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Body */}
|
|
334
|
+
<div className="prose prose-sm dark:prose-invert max-w-none">
|
|
335
|
+
{message.htmlBody ? (
|
|
336
|
+
<div
|
|
337
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: MessageView is a trusted component
|
|
338
|
+
dangerouslySetInnerHTML={{ __html: message.htmlBody }}
|
|
339
|
+
className="overflow-hidden"
|
|
340
|
+
/>
|
|
341
|
+
) : (
|
|
342
|
+
<pre className="whitespace-pre-wrap font-sans">
|
|
343
|
+
{message.textBody}
|
|
344
|
+
</pre>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
{/* Attachments */}
|
|
349
|
+
{message.attachments.length > 0 && (
|
|
350
|
+
<div className="space-y-2">
|
|
351
|
+
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
|
352
|
+
<Paperclip className="size-4" />
|
|
353
|
+
{message.attachments.length} attachment
|
|
354
|
+
{message.attachments.length > 1 ? "s" : ""}
|
|
355
|
+
</div>
|
|
356
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
357
|
+
{message.attachments.map((attachment) => (
|
|
358
|
+
<AttachmentItem
|
|
359
|
+
key={attachment.id}
|
|
360
|
+
attachment={attachment}
|
|
361
|
+
onClick={() => onAttachmentClick?.(attachment)}
|
|
362
|
+
onDownload={() => {
|
|
363
|
+
if (attachment.url) {
|
|
364
|
+
window.open(attachment.url, "_blank");
|
|
365
|
+
}
|
|
366
|
+
}}
|
|
367
|
+
/>
|
|
368
|
+
))}
|
|
369
|
+
</div>
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
)}
|
|
374
|
+
</div>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export { MessageView, MessageHeader };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import type { Message, MailThread as Thread } from "mdxui";
|
|
3
|
+
import { MessageHeader, MessageView } from "./message-view";
|
|
4
|
+
import { ThreadDisplay } from "./thread-display";
|
|
5
|
+
|
|
6
|
+
// Sample message with full content
|
|
7
|
+
const sampleMessage: Message = {
|
|
8
|
+
id: "m1",
|
|
9
|
+
threadId: "t1",
|
|
10
|
+
subject: "Re: Product Launch Strategy - Q1 2025",
|
|
11
|
+
from: { address: "alex@startup.io", name: "Alex Rivera" },
|
|
12
|
+
to: [{ address: "me@company.com", name: "Me" }],
|
|
13
|
+
cc: [
|
|
14
|
+
{ address: "marketing@startup.io", name: "Marketing Team" },
|
|
15
|
+
{ address: "sales@startup.io", name: "Sales Team" },
|
|
16
|
+
],
|
|
17
|
+
date: new Date(Date.now() - 3600000).toISOString(),
|
|
18
|
+
snippet:
|
|
19
|
+
"Great ideas! I think we should focus on the enterprise segment first...",
|
|
20
|
+
textBody: `Hi there,
|
|
21
|
+
|
|
22
|
+
Thanks for sharing the product launch strategy. I've reviewed the document and have some thoughts:
|
|
23
|
+
|
|
24
|
+
1. **Enterprise Focus**: I strongly agree we should target enterprise customers first. They have the budget and the pain points our product solves.
|
|
25
|
+
|
|
26
|
+
2. **Timeline**: Q1 might be aggressive given the holiday freeze. Consider a soft launch in late January with full GA in February.
|
|
27
|
+
|
|
28
|
+
3. **Marketing Channels**:
|
|
29
|
+
- LinkedIn ads for B2B reach
|
|
30
|
+
- Content marketing with case studies
|
|
31
|
+
- Webinar series with industry experts
|
|
32
|
+
|
|
33
|
+
Let me know what you think about these suggestions. Happy to hop on a call to discuss further.
|
|
34
|
+
|
|
35
|
+
Best,
|
|
36
|
+
Alex`,
|
|
37
|
+
htmlBody: `<div>
|
|
38
|
+
<p>Hi there,</p>
|
|
39
|
+
<p>Thanks for sharing the product launch strategy. I've reviewed the document and have some thoughts:</p>
|
|
40
|
+
<ol>
|
|
41
|
+
<li><strong>Enterprise Focus</strong>: I strongly agree we should target enterprise customers first. They have the budget and the pain points our product solves.</li>
|
|
42
|
+
<li><strong>Timeline</strong>: Q1 might be aggressive given the holiday freeze. Consider a soft launch in late January with full GA in February.</li>
|
|
43
|
+
<li><strong>Marketing Channels</strong>:
|
|
44
|
+
<ul>
|
|
45
|
+
<li>LinkedIn ads for B2B reach</li>
|
|
46
|
+
<li>Content marketing with case studies</li>
|
|
47
|
+
<li>Webinar series with industry experts</li>
|
|
48
|
+
</ul>
|
|
49
|
+
</li>
|
|
50
|
+
</ol>
|
|
51
|
+
<p>Let me know what you think about these suggestions. Happy to hop on a call to discuss further.</p>
|
|
52
|
+
<p>Best,<br/>Alex</p>
|
|
53
|
+
</div>`,
|
|
54
|
+
attachments: [
|
|
55
|
+
{
|
|
56
|
+
id: "a1",
|
|
57
|
+
filename: "launch-timeline.pdf",
|
|
58
|
+
mimeType: "application/pdf",
|
|
59
|
+
size: 245000,
|
|
60
|
+
url: "#",
|
|
61
|
+
inline: false,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "a2",
|
|
65
|
+
filename: "market-research.xlsx",
|
|
66
|
+
mimeType:
|
|
67
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
68
|
+
size: 89000,
|
|
69
|
+
url: "#",
|
|
70
|
+
inline: false,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
labels: [{ id: "l1", name: "Important", color: "#ef4444", type: "user" }],
|
|
74
|
+
isRead: true,
|
|
75
|
+
isStarred: true,
|
|
76
|
+
isImportant: true,
|
|
77
|
+
isDraft: false,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Sample thread with multiple messages
|
|
81
|
+
const sampleThread: Thread = {
|
|
82
|
+
id: "t1",
|
|
83
|
+
subject: "Product Launch Strategy - Q1 2025",
|
|
84
|
+
messages: [
|
|
85
|
+
{
|
|
86
|
+
id: "m0",
|
|
87
|
+
threadId: "t1",
|
|
88
|
+
subject: "Product Launch Strategy - Q1 2025",
|
|
89
|
+
from: { address: "me@company.com", name: "Me" },
|
|
90
|
+
to: [
|
|
91
|
+
{ address: "alex@startup.io", name: "Alex Rivera" },
|
|
92
|
+
{ address: "marketing@startup.io", name: "Marketing Team" },
|
|
93
|
+
],
|
|
94
|
+
date: new Date(Date.now() - 86400000).toISOString(),
|
|
95
|
+
snippet: "Team, I wanted to share our product launch strategy for Q1...",
|
|
96
|
+
textBody: `Team,
|
|
97
|
+
|
|
98
|
+
I wanted to share our product launch strategy for Q1 2025. Please review the attached document and share your thoughts.
|
|
99
|
+
|
|
100
|
+
Key points:
|
|
101
|
+
- Target audience: Mid-market to enterprise
|
|
102
|
+
- Launch date: February 15, 2025
|
|
103
|
+
- Key features to highlight: AI automation, integrations, analytics
|
|
104
|
+
|
|
105
|
+
Looking forward to your feedback!
|
|
106
|
+
|
|
107
|
+
Best,
|
|
108
|
+
Me`,
|
|
109
|
+
attachments: [
|
|
110
|
+
{
|
|
111
|
+
id: "a0",
|
|
112
|
+
filename: "launch-strategy-v1.docx",
|
|
113
|
+
mimeType:
|
|
114
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
115
|
+
size: 156000,
|
|
116
|
+
url: "#",
|
|
117
|
+
inline: false,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
labels: [],
|
|
121
|
+
isRead: true,
|
|
122
|
+
isStarred: false,
|
|
123
|
+
isImportant: false,
|
|
124
|
+
isDraft: false,
|
|
125
|
+
},
|
|
126
|
+
sampleMessage,
|
|
127
|
+
{
|
|
128
|
+
id: "m2",
|
|
129
|
+
threadId: "t1",
|
|
130
|
+
subject: "Re: Product Launch Strategy - Q1 2025",
|
|
131
|
+
from: { address: "marketing@startup.io", name: "Marketing Team" },
|
|
132
|
+
to: [
|
|
133
|
+
{ address: "me@company.com", name: "Me" },
|
|
134
|
+
{ address: "alex@startup.io", name: "Alex Rivera" },
|
|
135
|
+
],
|
|
136
|
+
date: new Date(Date.now() - 1800000).toISOString(),
|
|
137
|
+
snippet: "Adding to Alex's points, we should also consider...",
|
|
138
|
+
textBody: `Adding to Alex's points, we should also consider:
|
|
139
|
+
|
|
140
|
+
- Pre-launch email campaign to existing leads
|
|
141
|
+
- Partner co-marketing opportunities
|
|
142
|
+
- Press release timing
|
|
143
|
+
|
|
144
|
+
I've updated the timeline doc with these additions.`,
|
|
145
|
+
attachments: [],
|
|
146
|
+
labels: [],
|
|
147
|
+
isRead: false,
|
|
148
|
+
isStarred: false,
|
|
149
|
+
isImportant: false,
|
|
150
|
+
isDraft: false,
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
latestDate: new Date(Date.now() - 1800000).toISOString(),
|
|
154
|
+
participants: [
|
|
155
|
+
{ address: "me@company.com", name: "Me" },
|
|
156
|
+
{ address: "alex@startup.io", name: "Alex Rivera" },
|
|
157
|
+
{ address: "marketing@startup.io", name: "Marketing Team" },
|
|
158
|
+
],
|
|
159
|
+
unreadCount: 1,
|
|
160
|
+
labels: [{ id: "l1", name: "Important", color: "#ef4444", type: "user" }],
|
|
161
|
+
isStarred: true,
|
|
162
|
+
isImportant: true,
|
|
163
|
+
hasAttachments: true,
|
|
164
|
+
snippet: "Adding to Alex's points, we should also consider...",
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const meta: Meta<typeof ThreadDisplay> = {
|
|
168
|
+
title: "Zero/Mail/ThreadDisplay",
|
|
169
|
+
component: ThreadDisplay,
|
|
170
|
+
parameters: {
|
|
171
|
+
layout: "fullscreen",
|
|
172
|
+
},
|
|
173
|
+
tags: ["autodocs"],
|
|
174
|
+
decorators: [
|
|
175
|
+
(Story) => (
|
|
176
|
+
<div className="h-screen w-full bg-background">
|
|
177
|
+
<Story />
|
|
178
|
+
</div>
|
|
179
|
+
),
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export default meta;
|
|
184
|
+
type Story = StoryObj<typeof meta>;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Full thread view with navigation and actions.
|
|
188
|
+
* Matches the original Zero thread display panel.
|
|
189
|
+
*/
|
|
190
|
+
export const Default: Story = {
|
|
191
|
+
args: {
|
|
192
|
+
thread: sampleThread,
|
|
193
|
+
showNavigation: true,
|
|
194
|
+
hasPrevious: true,
|
|
195
|
+
hasNext: true,
|
|
196
|
+
enableAnimations: true,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Without navigation controls.
|
|
202
|
+
*/
|
|
203
|
+
export const NoNavigation: Story = {
|
|
204
|
+
args: {
|
|
205
|
+
thread: sampleThread,
|
|
206
|
+
showNavigation: false,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Single message thread.
|
|
212
|
+
*/
|
|
213
|
+
export const SingleMessage: Story = {
|
|
214
|
+
args: {
|
|
215
|
+
thread: {
|
|
216
|
+
...sampleThread,
|
|
217
|
+
messages: [sampleMessage],
|
|
218
|
+
},
|
|
219
|
+
showNavigation: true,
|
|
220
|
+
hasPrevious: false,
|
|
221
|
+
hasNext: true,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// MessageView story
|
|
226
|
+
export const SingleMessageView: StoryObj<typeof MessageView> = {
|
|
227
|
+
render: () => (
|
|
228
|
+
<div className="max-w-4xl p-4 bg-background">
|
|
229
|
+
<MessageView
|
|
230
|
+
message={sampleMessage}
|
|
231
|
+
isExpanded={true}
|
|
232
|
+
showHeaders={false}
|
|
233
|
+
showReplyComposer={false}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// MessageView collapsed
|
|
240
|
+
export const CollapsedMessageView: StoryObj<typeof MessageView> = {
|
|
241
|
+
render: () => (
|
|
242
|
+
<div className="max-w-4xl p-4 bg-background">
|
|
243
|
+
<MessageView
|
|
244
|
+
message={sampleMessage}
|
|
245
|
+
isExpanded={false}
|
|
246
|
+
showHeaders={false}
|
|
247
|
+
showReplyComposer={false}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// MessageHeader story
|
|
254
|
+
export const MessageHeaderExpanded: StoryObj<typeof MessageHeader> = {
|
|
255
|
+
render: () => (
|
|
256
|
+
<div className="max-w-4xl p-4 bg-background border rounded-lg">
|
|
257
|
+
<MessageHeader message={sampleMessage} expanded={true} />
|
|
258
|
+
</div>
|
|
259
|
+
),
|
|
260
|
+
};
|