@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,392 @@
|
|
|
1
|
+
import { Button } from "@mdxui/primitives/button";
|
|
2
|
+
import {
|
|
3
|
+
DropdownMenu,
|
|
4
|
+
DropdownMenuContent,
|
|
5
|
+
DropdownMenuItem,
|
|
6
|
+
DropdownMenuSeparator,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from "@mdxui/primitives/dropdown-menu";
|
|
9
|
+
import { cn } from "@mdxui/primitives/lib/utils";
|
|
10
|
+
import { ScrollArea } from "@mdxui/primitives/scroll-area";
|
|
11
|
+
import { Separator } from "@mdxui/primitives/separator";
|
|
12
|
+
import {
|
|
13
|
+
Tooltip,
|
|
14
|
+
TooltipContent,
|
|
15
|
+
TooltipProvider,
|
|
16
|
+
TooltipTrigger,
|
|
17
|
+
} from "@mdxui/primitives/tooltip";
|
|
18
|
+
import {
|
|
19
|
+
AlertOctagon,
|
|
20
|
+
Archive,
|
|
21
|
+
ArrowLeft,
|
|
22
|
+
ChevronLeft,
|
|
23
|
+
ChevronRight,
|
|
24
|
+
Clock,
|
|
25
|
+
MoreHorizontal,
|
|
26
|
+
Printer,
|
|
27
|
+
Reply,
|
|
28
|
+
Star,
|
|
29
|
+
Tag,
|
|
30
|
+
Trash2,
|
|
31
|
+
} from "lucide-react";
|
|
32
|
+
import type { ThreadDisplayProps } from "mdxui";
|
|
33
|
+
import * as React from "react";
|
|
34
|
+
import { MessageView } from "./message-view";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ThreadDisplay - Displays an email thread with all messages.
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Thread navigation (prev/next)
|
|
41
|
+
* - Message list with expand/collapse
|
|
42
|
+
* - Actions (reply, archive, delete, star, etc.)
|
|
43
|
+
* - Print functionality
|
|
44
|
+
* - Animations
|
|
45
|
+
*/
|
|
46
|
+
function ThreadDisplay({
|
|
47
|
+
thread,
|
|
48
|
+
activeMessageId: _activeMessageId,
|
|
49
|
+
showNavigation = true,
|
|
50
|
+
onPrevious,
|
|
51
|
+
onNext,
|
|
52
|
+
onClose,
|
|
53
|
+
onReply,
|
|
54
|
+
onReplyAll,
|
|
55
|
+
onForward,
|
|
56
|
+
onStar,
|
|
57
|
+
onArchive,
|
|
58
|
+
onDelete,
|
|
59
|
+
onSpam,
|
|
60
|
+
onSnooze,
|
|
61
|
+
onMove,
|
|
62
|
+
onLabel,
|
|
63
|
+
onPrint,
|
|
64
|
+
enableAnimations = true,
|
|
65
|
+
hasPrevious = false,
|
|
66
|
+
hasNext = false,
|
|
67
|
+
className,
|
|
68
|
+
}: ThreadDisplayProps) {
|
|
69
|
+
const [expandedMessages, setExpandedMessages] = React.useState<Set<string>>(
|
|
70
|
+
() => {
|
|
71
|
+
// By default, expand the last message
|
|
72
|
+
if (thread.messages.length > 0) {
|
|
73
|
+
return new Set([thread.messages[thread.messages.length - 1].id]);
|
|
74
|
+
}
|
|
75
|
+
return new Set();
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Toggle message expansion
|
|
80
|
+
const toggleMessage = (messageId: string) => {
|
|
81
|
+
setExpandedMessages((prev) => {
|
|
82
|
+
const next = new Set(prev);
|
|
83
|
+
if (next.has(messageId)) {
|
|
84
|
+
next.delete(messageId);
|
|
85
|
+
} else {
|
|
86
|
+
next.add(messageId);
|
|
87
|
+
}
|
|
88
|
+
return next;
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Expand all messages
|
|
93
|
+
const expandAll = () => {
|
|
94
|
+
setExpandedMessages(new Set(thread.messages.map((m) => m.id)));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Collapse all except latest
|
|
98
|
+
const collapseAll = () => {
|
|
99
|
+
if (thread.messages.length > 0) {
|
|
100
|
+
setExpandedMessages(
|
|
101
|
+
new Set([thread.messages[thread.messages.length - 1].id]),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Handle print
|
|
107
|
+
const handlePrint = () => {
|
|
108
|
+
if (onPrint) {
|
|
109
|
+
onPrint();
|
|
110
|
+
} else {
|
|
111
|
+
window.print();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
data-slot="thread-display"
|
|
118
|
+
className={cn("flex h-full flex-col", className)}
|
|
119
|
+
>
|
|
120
|
+
{/* Header */}
|
|
121
|
+
<div className="flex items-center justify-between gap-4 border-b px-4 py-2">
|
|
122
|
+
<div className="flex items-center gap-2">
|
|
123
|
+
{/* Back Button */}
|
|
124
|
+
<TooltipProvider>
|
|
125
|
+
<Tooltip>
|
|
126
|
+
<TooltipTrigger asChild>
|
|
127
|
+
<Button
|
|
128
|
+
variant="ghost"
|
|
129
|
+
size="icon"
|
|
130
|
+
className="size-8"
|
|
131
|
+
onClick={() => onClose?.()}
|
|
132
|
+
>
|
|
133
|
+
<ArrowLeft className="size-4" />
|
|
134
|
+
</Button>
|
|
135
|
+
</TooltipTrigger>
|
|
136
|
+
<TooltipContent>Back to inbox</TooltipContent>
|
|
137
|
+
</Tooltip>
|
|
138
|
+
</TooltipProvider>
|
|
139
|
+
|
|
140
|
+
{/* Thread Navigation */}
|
|
141
|
+
{showNavigation && (
|
|
142
|
+
<>
|
|
143
|
+
<Separator orientation="vertical" className="h-6" />
|
|
144
|
+
<div className="flex items-center gap-1">
|
|
145
|
+
<TooltipProvider>
|
|
146
|
+
<Tooltip>
|
|
147
|
+
<TooltipTrigger asChild>
|
|
148
|
+
<Button
|
|
149
|
+
variant="ghost"
|
|
150
|
+
size="icon"
|
|
151
|
+
className="size-8"
|
|
152
|
+
disabled={!hasPrevious}
|
|
153
|
+
onClick={() => onPrevious?.()}
|
|
154
|
+
>
|
|
155
|
+
<ChevronLeft className="size-4" />
|
|
156
|
+
</Button>
|
|
157
|
+
</TooltipTrigger>
|
|
158
|
+
<TooltipContent>Newer</TooltipContent>
|
|
159
|
+
</Tooltip>
|
|
160
|
+
|
|
161
|
+
<Tooltip>
|
|
162
|
+
<TooltipTrigger asChild>
|
|
163
|
+
<Button
|
|
164
|
+
variant="ghost"
|
|
165
|
+
size="icon"
|
|
166
|
+
className="size-8"
|
|
167
|
+
disabled={!hasNext}
|
|
168
|
+
onClick={() => onNext?.()}
|
|
169
|
+
>
|
|
170
|
+
<ChevronRight className="size-4" />
|
|
171
|
+
</Button>
|
|
172
|
+
</TooltipTrigger>
|
|
173
|
+
<TooltipContent>Older</TooltipContent>
|
|
174
|
+
</Tooltip>
|
|
175
|
+
</TooltipProvider>
|
|
176
|
+
</div>
|
|
177
|
+
</>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Actions */}
|
|
182
|
+
<div className="flex items-center gap-1">
|
|
183
|
+
<TooltipProvider>
|
|
184
|
+
<Tooltip>
|
|
185
|
+
<TooltipTrigger asChild>
|
|
186
|
+
<Button
|
|
187
|
+
variant="ghost"
|
|
188
|
+
size="icon"
|
|
189
|
+
className="size-8"
|
|
190
|
+
onClick={() => onArchive?.()}
|
|
191
|
+
>
|
|
192
|
+
<Archive className="size-4" />
|
|
193
|
+
</Button>
|
|
194
|
+
</TooltipTrigger>
|
|
195
|
+
<TooltipContent>Archive</TooltipContent>
|
|
196
|
+
</Tooltip>
|
|
197
|
+
|
|
198
|
+
<Tooltip>
|
|
199
|
+
<TooltipTrigger asChild>
|
|
200
|
+
<Button
|
|
201
|
+
variant="ghost"
|
|
202
|
+
size="icon"
|
|
203
|
+
className="size-8"
|
|
204
|
+
onClick={() => onDelete?.()}
|
|
205
|
+
>
|
|
206
|
+
<Trash2 className="size-4" />
|
|
207
|
+
</Button>
|
|
208
|
+
</TooltipTrigger>
|
|
209
|
+
<TooltipContent>Delete</TooltipContent>
|
|
210
|
+
</Tooltip>
|
|
211
|
+
|
|
212
|
+
<Tooltip>
|
|
213
|
+
<TooltipTrigger asChild>
|
|
214
|
+
<Button
|
|
215
|
+
variant="ghost"
|
|
216
|
+
size="icon"
|
|
217
|
+
className={cn(
|
|
218
|
+
"size-8",
|
|
219
|
+
thread.isStarred && "text-yellow-500",
|
|
220
|
+
)}
|
|
221
|
+
onClick={() => onStar?.()}
|
|
222
|
+
>
|
|
223
|
+
<Star
|
|
224
|
+
className="size-4"
|
|
225
|
+
fill={thread.isStarred ? "currentColor" : "none"}
|
|
226
|
+
/>
|
|
227
|
+
</Button>
|
|
228
|
+
</TooltipTrigger>
|
|
229
|
+
<TooltipContent>
|
|
230
|
+
{thread.isStarred ? "Unstar" : "Star"}
|
|
231
|
+
</TooltipContent>
|
|
232
|
+
</Tooltip>
|
|
233
|
+
|
|
234
|
+
{/* Snooze dropdown with preset options */}
|
|
235
|
+
<DropdownMenu>
|
|
236
|
+
<Tooltip>
|
|
237
|
+
<TooltipTrigger asChild>
|
|
238
|
+
<DropdownMenuTrigger asChild>
|
|
239
|
+
<Button variant="ghost" size="icon" className="size-8">
|
|
240
|
+
<Clock className="size-4" />
|
|
241
|
+
</Button>
|
|
242
|
+
</DropdownMenuTrigger>
|
|
243
|
+
</TooltipTrigger>
|
|
244
|
+
<TooltipContent>Snooze</TooltipContent>
|
|
245
|
+
</Tooltip>
|
|
246
|
+
<DropdownMenuContent align="end">
|
|
247
|
+
<DropdownMenuItem
|
|
248
|
+
onClick={() => {
|
|
249
|
+
const tomorrow = new Date();
|
|
250
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
251
|
+
tomorrow.setHours(9, 0, 0, 0);
|
|
252
|
+
onSnooze?.(tomorrow.toISOString());
|
|
253
|
+
}}
|
|
254
|
+
>
|
|
255
|
+
Tomorrow morning
|
|
256
|
+
</DropdownMenuItem>
|
|
257
|
+
<DropdownMenuItem
|
|
258
|
+
onClick={() => {
|
|
259
|
+
const nextWeek = new Date();
|
|
260
|
+
nextWeek.setDate(nextWeek.getDate() + 7);
|
|
261
|
+
nextWeek.setHours(9, 0, 0, 0);
|
|
262
|
+
onSnooze?.(nextWeek.toISOString());
|
|
263
|
+
}}
|
|
264
|
+
>
|
|
265
|
+
Next week
|
|
266
|
+
</DropdownMenuItem>
|
|
267
|
+
<DropdownMenuItem
|
|
268
|
+
onClick={() => {
|
|
269
|
+
const nextMonth = new Date();
|
|
270
|
+
nextMonth.setMonth(nextMonth.getMonth() + 1);
|
|
271
|
+
nextMonth.setHours(9, 0, 0, 0);
|
|
272
|
+
onSnooze?.(nextMonth.toISOString());
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
Next month
|
|
276
|
+
</DropdownMenuItem>
|
|
277
|
+
</DropdownMenuContent>
|
|
278
|
+
</DropdownMenu>
|
|
279
|
+
|
|
280
|
+
<DropdownMenu>
|
|
281
|
+
<DropdownMenuTrigger asChild>
|
|
282
|
+
<Button variant="ghost" size="icon" className="size-8">
|
|
283
|
+
<MoreHorizontal className="size-4" />
|
|
284
|
+
</Button>
|
|
285
|
+
</DropdownMenuTrigger>
|
|
286
|
+
<DropdownMenuContent align="end">
|
|
287
|
+
{/* TODO: Label selection requires available labels prop */}
|
|
288
|
+
<DropdownMenuItem
|
|
289
|
+
disabled={!onLabel}
|
|
290
|
+
onClick={() => onLabel?.([])}
|
|
291
|
+
>
|
|
292
|
+
<Tag className="mr-2 size-4" />
|
|
293
|
+
Add label
|
|
294
|
+
</DropdownMenuItem>
|
|
295
|
+
{/* TODO: Move requires available folders prop */}
|
|
296
|
+
<DropdownMenuItem
|
|
297
|
+
disabled={!onMove}
|
|
298
|
+
onClick={() => onMove?.("inbox")}
|
|
299
|
+
>
|
|
300
|
+
<Archive className="mr-2 size-4" />
|
|
301
|
+
Move to folder
|
|
302
|
+
</DropdownMenuItem>
|
|
303
|
+
<DropdownMenuSeparator />
|
|
304
|
+
<DropdownMenuItem onClick={() => onSpam?.()}>
|
|
305
|
+
<AlertOctagon className="mr-2 size-4" />
|
|
306
|
+
Report spam
|
|
307
|
+
</DropdownMenuItem>
|
|
308
|
+
<DropdownMenuSeparator />
|
|
309
|
+
<DropdownMenuItem onClick={handlePrint}>
|
|
310
|
+
<Printer className="mr-2 size-4" />
|
|
311
|
+
Print
|
|
312
|
+
</DropdownMenuItem>
|
|
313
|
+
</DropdownMenuContent>
|
|
314
|
+
</DropdownMenu>
|
|
315
|
+
</TooltipProvider>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
{/* Subject */}
|
|
320
|
+
<div className="border-b px-4 py-3">
|
|
321
|
+
<h1 className="text-lg font-semibold">{thread.subject}</h1>
|
|
322
|
+
<div className="text-muted-foreground mt-1 flex items-center gap-2 text-sm">
|
|
323
|
+
<span>
|
|
324
|
+
{thread.messages.length} message
|
|
325
|
+
{thread.messages.length > 1 ? "s" : ""}
|
|
326
|
+
</span>
|
|
327
|
+
{thread.messages.length > 1 && (
|
|
328
|
+
<>
|
|
329
|
+
<span>·</span>
|
|
330
|
+
<button
|
|
331
|
+
type="button"
|
|
332
|
+
onClick={expandAll}
|
|
333
|
+
className="hover:underline"
|
|
334
|
+
>
|
|
335
|
+
Expand all
|
|
336
|
+
</button>
|
|
337
|
+
<span>·</span>
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
onClick={collapseAll}
|
|
341
|
+
className="hover:underline"
|
|
342
|
+
>
|
|
343
|
+
Collapse
|
|
344
|
+
</button>
|
|
345
|
+
</>
|
|
346
|
+
)}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{/* Messages */}
|
|
351
|
+
<ScrollArea className="flex-1">
|
|
352
|
+
<div
|
|
353
|
+
className={cn(
|
|
354
|
+
"divide-y",
|
|
355
|
+
enableAnimations && "transition-all duration-200",
|
|
356
|
+
)}
|
|
357
|
+
>
|
|
358
|
+
{thread.messages.map((message) => (
|
|
359
|
+
<MessageView
|
|
360
|
+
key={message.id}
|
|
361
|
+
message={message}
|
|
362
|
+
isExpanded={expandedMessages.has(message.id)}
|
|
363
|
+
showHeaders={false}
|
|
364
|
+
showReplyComposer={false}
|
|
365
|
+
onToggleExpand={() => toggleMessage(message.id)}
|
|
366
|
+
onReply={() => onReply?.(message.id)}
|
|
367
|
+
onReplyAll={() => onReplyAll?.(message.id)}
|
|
368
|
+
onForward={() => onForward?.(message.id)}
|
|
369
|
+
onStar={() => onStar?.()}
|
|
370
|
+
/>
|
|
371
|
+
))}
|
|
372
|
+
</div>
|
|
373
|
+
</ScrollArea>
|
|
374
|
+
|
|
375
|
+
{/* Reply Footer */}
|
|
376
|
+
<div className="border-t p-4">
|
|
377
|
+
<Button
|
|
378
|
+
onClick={() => {
|
|
379
|
+
const lastMessage = thread.messages[thread.messages.length - 1];
|
|
380
|
+
if (lastMessage) onReply?.(lastMessage.id);
|
|
381
|
+
}}
|
|
382
|
+
className="w-full sm:w-auto"
|
|
383
|
+
>
|
|
384
|
+
<Reply className="mr-2 size-4" />
|
|
385
|
+
Reply
|
|
386
|
+
</Button>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export { ThreadDisplay };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Components - Complete page compositions with state management.
|
|
3
|
+
*
|
|
4
|
+
* These are reference implementations showing how to wire up
|
|
5
|
+
* shell components with proper state management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { MailZeroPage } from "./mail-zero-page";
|
|
9
|
+
export type { MailZeroPageProps } from "./mail-zero-page";
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import type { Folder, MailLabel as Label, MailThread as Thread } from "mdxui";
|
|
3
|
+
import { MailZeroPage } from "./mail-zero-page";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof MailZeroPage> = {
|
|
6
|
+
title: "Zero/Pages/MailZeroPage",
|
|
7
|
+
component: MailZeroPage,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "fullscreen",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof MailZeroPage>;
|
|
15
|
+
|
|
16
|
+
// Sample data
|
|
17
|
+
const sampleFolders: Folder[] = [
|
|
18
|
+
{ id: "inbox", name: "Inbox", type: "inbox", unreadCount: 12 },
|
|
19
|
+
{ id: "starred", name: "Starred", type: "starred", unreadCount: 0 },
|
|
20
|
+
{ id: "snoozed", name: "Snoozed", type: "snoozed", unreadCount: 2 },
|
|
21
|
+
{ id: "sent", name: "Sent", type: "sent", unreadCount: 0 },
|
|
22
|
+
{ id: "drafts", name: "Drafts", type: "drafts", unreadCount: 3 },
|
|
23
|
+
{ id: "spam", name: "Spam", type: "spam", unreadCount: 5 },
|
|
24
|
+
{ id: "trash", name: "Trash", type: "trash", unreadCount: 0 },
|
|
25
|
+
{ id: "archive", name: "Archive", type: "archive", unreadCount: 0 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const sampleLabels: Label[] = [
|
|
29
|
+
{ id: "work", name: "Work", color: "#3b82f6", type: "user" },
|
|
30
|
+
{ id: "personal", name: "Personal", color: "#10b981", type: "user" },
|
|
31
|
+
{ id: "urgent", name: "Urgent", color: "#ef4444", type: "user" },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const sampleThreads: Thread[] = [
|
|
35
|
+
{
|
|
36
|
+
id: "thread-1",
|
|
37
|
+
subject: "Q4 Planning Meeting Notes",
|
|
38
|
+
messages: [
|
|
39
|
+
{
|
|
40
|
+
id: "msg-1a",
|
|
41
|
+
threadId: "thread-1",
|
|
42
|
+
subject: "Q4 Planning Meeting Notes",
|
|
43
|
+
from: { address: "sarah@company.com", name: "Sarah Chen" },
|
|
44
|
+
to: [{ address: "team@company.com", name: "Team" }],
|
|
45
|
+
date: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
|
|
46
|
+
snippet:
|
|
47
|
+
"Hi team, here are the notes from today's Q4 planning session...",
|
|
48
|
+
textBody:
|
|
49
|
+
"Hi team,\n\nHere are the notes from today's Q4 planning session.\n\nKey decisions:\n1. Launch new product line in October\n2. Increase marketing budget by 20%\n3. Hire 5 new engineers\n\nAction items attached.\n\nBest,\nSarah",
|
|
50
|
+
isRead: false,
|
|
51
|
+
isStarred: false,
|
|
52
|
+
isImportant: false,
|
|
53
|
+
isDraft: false,
|
|
54
|
+
attachments: [],
|
|
55
|
+
labels: [],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "msg-1b",
|
|
59
|
+
threadId: "thread-1",
|
|
60
|
+
subject: "Re: Q4 Planning Meeting Notes",
|
|
61
|
+
from: { address: "mike@company.com", name: "Mike Johnson" },
|
|
62
|
+
to: [{ address: "team@company.com", name: "Team" }],
|
|
63
|
+
date: new Date(Date.now() - 1000 * 60 * 15).toISOString(),
|
|
64
|
+
snippet: "Thanks Sarah! I have a question about the timeline...",
|
|
65
|
+
textBody:
|
|
66
|
+
"Thanks Sarah!\n\nI have a question about the timeline for the product launch. Can we discuss tomorrow?\n\nMike",
|
|
67
|
+
isRead: false,
|
|
68
|
+
isStarred: false,
|
|
69
|
+
isImportant: false,
|
|
70
|
+
isDraft: false,
|
|
71
|
+
attachments: [],
|
|
72
|
+
labels: [],
|
|
73
|
+
inReplyTo: "msg-1a",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
latestDate: new Date(Date.now() - 1000 * 60 * 15).toISOString(),
|
|
77
|
+
participants: [
|
|
78
|
+
{ address: "sarah@company.com", name: "Sarah Chen" },
|
|
79
|
+
{ address: "mike@company.com", name: "Mike Johnson" },
|
|
80
|
+
],
|
|
81
|
+
unreadCount: 2,
|
|
82
|
+
labels: [],
|
|
83
|
+
isStarred: false,
|
|
84
|
+
isImportant: false,
|
|
85
|
+
hasAttachments: false,
|
|
86
|
+
snippet: "Thanks Sarah! I have a question about the timeline...",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "thread-2",
|
|
90
|
+
subject: "Invoice #2024-156 - Payment Confirmation",
|
|
91
|
+
messages: [
|
|
92
|
+
{
|
|
93
|
+
id: "msg-2a",
|
|
94
|
+
threadId: "thread-2",
|
|
95
|
+
subject: "Invoice #2024-156 - Payment Confirmation",
|
|
96
|
+
from: { address: "billing@vendor.com", name: "Vendor Billing" },
|
|
97
|
+
to: [{ address: "you@company.com", name: "You" }],
|
|
98
|
+
date: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(),
|
|
99
|
+
snippet:
|
|
100
|
+
"Your payment of $4,500.00 has been received and processed...",
|
|
101
|
+
textBody:
|
|
102
|
+
"Dear Customer,\n\nYour payment of $4,500.00 has been received and processed.\n\nInvoice: #2024-156\nAmount: $4,500.00\nStatus: Paid\n\nThank you for your business.\n\nVendor Billing Team",
|
|
103
|
+
isRead: true,
|
|
104
|
+
isStarred: true,
|
|
105
|
+
isImportant: false,
|
|
106
|
+
isDraft: false,
|
|
107
|
+
attachments: [
|
|
108
|
+
{
|
|
109
|
+
id: "att-1",
|
|
110
|
+
filename: "invoice-2024-156.pdf",
|
|
111
|
+
mimeType: "application/pdf",
|
|
112
|
+
size: 245000,
|
|
113
|
+
inline: false,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
labels: [],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
latestDate: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(),
|
|
120
|
+
participants: [{ address: "billing@vendor.com", name: "Vendor Billing" }],
|
|
121
|
+
unreadCount: 0,
|
|
122
|
+
labels: [],
|
|
123
|
+
isStarred: true,
|
|
124
|
+
isImportant: false,
|
|
125
|
+
hasAttachments: true,
|
|
126
|
+
snippet: "Your payment of $4,500.00 has been received and processed...",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "thread-3",
|
|
130
|
+
subject: "Weekend hiking trip?",
|
|
131
|
+
messages: [
|
|
132
|
+
{
|
|
133
|
+
id: "msg-3a",
|
|
134
|
+
threadId: "thread-3",
|
|
135
|
+
subject: "Weekend hiking trip?",
|
|
136
|
+
from: { address: "alex@personal.com", name: "Alex Rivera" },
|
|
137
|
+
to: [{ address: "you@personal.com", name: "You" }],
|
|
138
|
+
date: new Date(Date.now() - 1000 * 60 * 60 * 5).toISOString(),
|
|
139
|
+
snippet:
|
|
140
|
+
"Hey! Want to go hiking this Saturday? I found this great trail...",
|
|
141
|
+
textBody:
|
|
142
|
+
"Hey!\n\nWant to go hiking this Saturday? I found this great trail in the mountains - about 8 miles round trip with amazing views.\n\nLet me know if you're interested!\n\nAlex",
|
|
143
|
+
isRead: true,
|
|
144
|
+
isStarred: false,
|
|
145
|
+
isImportant: false,
|
|
146
|
+
isDraft: false,
|
|
147
|
+
attachments: [],
|
|
148
|
+
labels: [],
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
latestDate: new Date(Date.now() - 1000 * 60 * 60 * 5).toISOString(),
|
|
152
|
+
participants: [{ address: "alex@personal.com", name: "Alex Rivera" }],
|
|
153
|
+
unreadCount: 0,
|
|
154
|
+
labels: [],
|
|
155
|
+
isStarred: false,
|
|
156
|
+
isImportant: false,
|
|
157
|
+
hasAttachments: false,
|
|
158
|
+
snippet:
|
|
159
|
+
"Hey! Want to go hiking this Saturday? I found this great trail...",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "thread-4",
|
|
163
|
+
subject: "Your weekly digest",
|
|
164
|
+
messages: [
|
|
165
|
+
{
|
|
166
|
+
id: "msg-4a",
|
|
167
|
+
threadId: "thread-4",
|
|
168
|
+
subject: "Your weekly digest",
|
|
169
|
+
from: { address: "digest@news.com", name: "Weekly News" },
|
|
170
|
+
to: [{ address: "you@company.com", name: "You" }],
|
|
171
|
+
date: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
|
|
172
|
+
snippet: "Here's what happened this week in tech...",
|
|
173
|
+
textBody:
|
|
174
|
+
"Your Weekly Tech Digest\n\n1. AI breakthrough in natural language processing\n2. New smartphone launches next month\n3. Startup raises $50M in Series B\n\nRead more at our website.",
|
|
175
|
+
isRead: true,
|
|
176
|
+
isStarred: false,
|
|
177
|
+
isImportant: false,
|
|
178
|
+
isDraft: false,
|
|
179
|
+
attachments: [],
|
|
180
|
+
labels: [],
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
latestDate: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
|
|
184
|
+
participants: [{ address: "digest@news.com", name: "Weekly News" }],
|
|
185
|
+
unreadCount: 0,
|
|
186
|
+
labels: [],
|
|
187
|
+
isStarred: false,
|
|
188
|
+
isImportant: false,
|
|
189
|
+
hasAttachments: false,
|
|
190
|
+
snippet: "Here's what happened this week in tech...",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "thread-5",
|
|
194
|
+
subject: "Project deadline reminder",
|
|
195
|
+
messages: [
|
|
196
|
+
{
|
|
197
|
+
id: "msg-5a",
|
|
198
|
+
threadId: "thread-5",
|
|
199
|
+
subject: "Project deadline reminder",
|
|
200
|
+
from: { address: "pm@company.com", name: "Project Manager" },
|
|
201
|
+
to: [{ address: "team@company.com", name: "Team" }],
|
|
202
|
+
date: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
|
203
|
+
snippet:
|
|
204
|
+
"Reminder: The project milestone is due this Friday. Please ensure...",
|
|
205
|
+
textBody:
|
|
206
|
+
"Team,\n\nReminder: The project milestone is due this Friday.\n\nPlease ensure all deliverables are ready for review by Thursday EOD.\n\nThanks,\nPM",
|
|
207
|
+
isRead: false,
|
|
208
|
+
isStarred: false,
|
|
209
|
+
isImportant: true,
|
|
210
|
+
isDraft: false,
|
|
211
|
+
attachments: [],
|
|
212
|
+
labels: [],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
latestDate: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(),
|
|
216
|
+
participants: [{ address: "pm@company.com", name: "Project Manager" }],
|
|
217
|
+
unreadCount: 1,
|
|
218
|
+
labels: [],
|
|
219
|
+
isStarred: false,
|
|
220
|
+
isImportant: true,
|
|
221
|
+
hasAttachments: false,
|
|
222
|
+
snippet:
|
|
223
|
+
"Reminder: The project milestone is due this Friday. Please ensure...",
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const sampleUser = {
|
|
228
|
+
name: "John Doe",
|
|
229
|
+
email: "john@company.com",
|
|
230
|
+
avatar: "https://i.pravatar.cc/150?u=john",
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Complete email page with internal state management.
|
|
235
|
+
* Click threads to view them, use navigation buttons, try actions.
|
|
236
|
+
* All actions are handled internally - check console for logs.
|
|
237
|
+
*/
|
|
238
|
+
export const Default: Story = {
|
|
239
|
+
args: {
|
|
240
|
+
initialThreads: sampleThreads,
|
|
241
|
+
initialFolders: sampleFolders,
|
|
242
|
+
initialLabels: sampleLabels,
|
|
243
|
+
initialFolderId: "inbox",
|
|
244
|
+
user: sampleUser,
|
|
245
|
+
},
|
|
246
|
+
render: (args) => (
|
|
247
|
+
<div className="h-screen">
|
|
248
|
+
<MailZeroPage {...args} />
|
|
249
|
+
</div>
|
|
250
|
+
),
|
|
251
|
+
};
|