@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,388 @@
|
|
|
1
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@mdxui/primitives/avatar";
|
|
2
|
+
import { Badge } from "@mdxui/primitives/badge";
|
|
3
|
+
import { Button } from "@mdxui/primitives/button";
|
|
4
|
+
import {
|
|
5
|
+
Collapsible,
|
|
6
|
+
CollapsibleContent,
|
|
7
|
+
CollapsibleTrigger,
|
|
8
|
+
} from "@mdxui/primitives/collapsible";
|
|
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
|
+
Calendar,
|
|
22
|
+
ChevronRight,
|
|
23
|
+
Clock,
|
|
24
|
+
File,
|
|
25
|
+
Inbox,
|
|
26
|
+
PenSquare,
|
|
27
|
+
Plus,
|
|
28
|
+
Send,
|
|
29
|
+
Settings,
|
|
30
|
+
Sparkles,
|
|
31
|
+
Star,
|
|
32
|
+
Tag,
|
|
33
|
+
Trash2,
|
|
34
|
+
} from "lucide-react";
|
|
35
|
+
import type { Folder, MailLabel as Label, MailSidebarProps } from "mdxui";
|
|
36
|
+
import * as React from "react";
|
|
37
|
+
|
|
38
|
+
// Default folder icons
|
|
39
|
+
const FOLDER_ICONS: Record<
|
|
40
|
+
string,
|
|
41
|
+
React.ComponentType<{ className?: string }>
|
|
42
|
+
> = {
|
|
43
|
+
inbox: Inbox,
|
|
44
|
+
sent: Send,
|
|
45
|
+
drafts: File,
|
|
46
|
+
spam: AlertOctagon,
|
|
47
|
+
trash: Trash2,
|
|
48
|
+
archive: Archive,
|
|
49
|
+
starred: Star,
|
|
50
|
+
important: Star,
|
|
51
|
+
snoozed: Clock,
|
|
52
|
+
scheduled: Calendar,
|
|
53
|
+
all: Inbox,
|
|
54
|
+
custom: Tag,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* FolderItem - Individual folder in the sidebar.
|
|
59
|
+
*/
|
|
60
|
+
function FolderItem({
|
|
61
|
+
folder,
|
|
62
|
+
isActive,
|
|
63
|
+
isCollapsed,
|
|
64
|
+
onClick,
|
|
65
|
+
}: {
|
|
66
|
+
folder: Folder;
|
|
67
|
+
isActive: boolean;
|
|
68
|
+
isCollapsed: boolean;
|
|
69
|
+
onClick?: () => void;
|
|
70
|
+
}) {
|
|
71
|
+
const Icon = FOLDER_ICONS[folder.type] || FOLDER_ICONS.custom;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<TooltipProvider>
|
|
75
|
+
<Tooltip>
|
|
76
|
+
<TooltipTrigger asChild>
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={onClick}
|
|
80
|
+
className={cn(
|
|
81
|
+
"flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors",
|
|
82
|
+
isActive
|
|
83
|
+
? "bg-accent text-accent-foreground"
|
|
84
|
+
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<Icon className="size-4 shrink-0" />
|
|
88
|
+
{!isCollapsed && (
|
|
89
|
+
<>
|
|
90
|
+
<span className="flex-1 truncate text-left">{folder.name}</span>
|
|
91
|
+
{folder.unreadCount > 0 && (
|
|
92
|
+
<Badge
|
|
93
|
+
variant="secondary"
|
|
94
|
+
className="ml-auto shrink-0 text-xs"
|
|
95
|
+
>
|
|
96
|
+
{folder.unreadCount > 99 ? "99+" : folder.unreadCount}
|
|
97
|
+
</Badge>
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
</button>
|
|
102
|
+
</TooltipTrigger>
|
|
103
|
+
{isCollapsed && (
|
|
104
|
+
<TooltipContent side="right">
|
|
105
|
+
{folder.name}
|
|
106
|
+
{folder.unreadCount > 0 && ` (${folder.unreadCount})`}
|
|
107
|
+
</TooltipContent>
|
|
108
|
+
)}
|
|
109
|
+
</Tooltip>
|
|
110
|
+
</TooltipProvider>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* LabelItem - Individual label in the sidebar.
|
|
116
|
+
*/
|
|
117
|
+
function LabelItem({
|
|
118
|
+
label,
|
|
119
|
+
isCollapsed,
|
|
120
|
+
onClick,
|
|
121
|
+
}: {
|
|
122
|
+
label: Label;
|
|
123
|
+
isCollapsed: boolean;
|
|
124
|
+
onClick?: () => void;
|
|
125
|
+
}) {
|
|
126
|
+
return (
|
|
127
|
+
<TooltipProvider>
|
|
128
|
+
<Tooltip>
|
|
129
|
+
<TooltipTrigger asChild>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={onClick}
|
|
133
|
+
className="text-muted-foreground hover:bg-accent/50 hover:text-foreground flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors"
|
|
134
|
+
>
|
|
135
|
+
<div
|
|
136
|
+
className="size-3 shrink-0 rounded-full"
|
|
137
|
+
style={{ backgroundColor: label.color || "#6b7280" }}
|
|
138
|
+
/>
|
|
139
|
+
{!isCollapsed && (
|
|
140
|
+
<span className="flex-1 truncate text-left">{label.name}</span>
|
|
141
|
+
)}
|
|
142
|
+
</button>
|
|
143
|
+
</TooltipTrigger>
|
|
144
|
+
{isCollapsed && (
|
|
145
|
+
<TooltipContent side="right">{label.name}</TooltipContent>
|
|
146
|
+
)}
|
|
147
|
+
</Tooltip>
|
|
148
|
+
</TooltipProvider>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* MailSidebar - Email app sidebar with folder navigation.
|
|
154
|
+
*
|
|
155
|
+
* Features:
|
|
156
|
+
* - Folder navigation with unread counts
|
|
157
|
+
* - Compose button
|
|
158
|
+
* - Labels section
|
|
159
|
+
* - User info
|
|
160
|
+
* - Collapsible state
|
|
161
|
+
* - Upgrade CTA
|
|
162
|
+
*/
|
|
163
|
+
function MailSidebar({
|
|
164
|
+
folders,
|
|
165
|
+
activeFolderId,
|
|
166
|
+
onFolderClick,
|
|
167
|
+
onCompose,
|
|
168
|
+
labels,
|
|
169
|
+
onLabelClick,
|
|
170
|
+
onCreateLabel,
|
|
171
|
+
isCollapsed = false,
|
|
172
|
+
onToggleCollapse,
|
|
173
|
+
user,
|
|
174
|
+
showUpgrade = false,
|
|
175
|
+
onUpgradeClick,
|
|
176
|
+
className,
|
|
177
|
+
}: MailSidebarProps) {
|
|
178
|
+
const [labelsExpanded, setLabelsExpanded] = React.useState(true);
|
|
179
|
+
|
|
180
|
+
// Group folders by type
|
|
181
|
+
const primaryFolders = folders.filter((f) =>
|
|
182
|
+
["inbox", "starred", "snoozed", "sent", "drafts"].includes(f.type),
|
|
183
|
+
);
|
|
184
|
+
const secondaryFolders = folders.filter((f) =>
|
|
185
|
+
["spam", "trash", "archive", "all"].includes(f.type),
|
|
186
|
+
);
|
|
187
|
+
const customFolders = folders.filter((f) => f.type === "custom");
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div
|
|
191
|
+
data-slot="mail-sidebar"
|
|
192
|
+
className={cn(
|
|
193
|
+
"bg-background flex h-full flex-col border-r transition-all duration-200",
|
|
194
|
+
isCollapsed ? "w-16" : "w-64",
|
|
195
|
+
className,
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{/* Header */}
|
|
199
|
+
<div className="flex items-center justify-between p-4">
|
|
200
|
+
{!isCollapsed && (
|
|
201
|
+
<div className="flex items-center gap-2">
|
|
202
|
+
<Sparkles className="text-primary size-5" />
|
|
203
|
+
<span className="font-semibold">Zero Mail</span>
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
{onToggleCollapse && (
|
|
207
|
+
<Button
|
|
208
|
+
variant="ghost"
|
|
209
|
+
size="icon"
|
|
210
|
+
className="size-8 shrink-0"
|
|
211
|
+
onClick={onToggleCollapse}
|
|
212
|
+
>
|
|
213
|
+
<ChevronRight
|
|
214
|
+
className={cn(
|
|
215
|
+
"size-4 transition-transform",
|
|
216
|
+
!isCollapsed && "rotate-180",
|
|
217
|
+
)}
|
|
218
|
+
/>
|
|
219
|
+
</Button>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Compose Button */}
|
|
224
|
+
<div className="px-3 pb-4">
|
|
225
|
+
<TooltipProvider>
|
|
226
|
+
<Tooltip>
|
|
227
|
+
<TooltipTrigger asChild>
|
|
228
|
+
<Button
|
|
229
|
+
onClick={onCompose}
|
|
230
|
+
className={cn("w-full", isCollapsed && "px-0")}
|
|
231
|
+
>
|
|
232
|
+
<PenSquare className="size-4" />
|
|
233
|
+
{!isCollapsed && <span className="ml-2">Compose</span>}
|
|
234
|
+
</Button>
|
|
235
|
+
</TooltipTrigger>
|
|
236
|
+
{isCollapsed && (
|
|
237
|
+
<TooltipContent side="right">Compose</TooltipContent>
|
|
238
|
+
)}
|
|
239
|
+
</Tooltip>
|
|
240
|
+
</TooltipProvider>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{/* Folders */}
|
|
244
|
+
<ScrollArea className="flex-1">
|
|
245
|
+
<div className="space-y-1 px-3">
|
|
246
|
+
{/* Primary Folders */}
|
|
247
|
+
{primaryFolders.map((folder) => (
|
|
248
|
+
<FolderItem
|
|
249
|
+
key={folder.id}
|
|
250
|
+
folder={folder}
|
|
251
|
+
isActive={activeFolderId === folder.id}
|
|
252
|
+
isCollapsed={isCollapsed}
|
|
253
|
+
onClick={() => onFolderClick?.(folder.id)}
|
|
254
|
+
/>
|
|
255
|
+
))}
|
|
256
|
+
|
|
257
|
+
{secondaryFolders.length > 0 && (
|
|
258
|
+
<>
|
|
259
|
+
<Separator className="my-3" />
|
|
260
|
+
{secondaryFolders.map((folder) => (
|
|
261
|
+
<FolderItem
|
|
262
|
+
key={folder.id}
|
|
263
|
+
folder={folder}
|
|
264
|
+
isActive={activeFolderId === folder.id}
|
|
265
|
+
isCollapsed={isCollapsed}
|
|
266
|
+
onClick={() => onFolderClick?.(folder.id)}
|
|
267
|
+
/>
|
|
268
|
+
))}
|
|
269
|
+
</>
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
{customFolders.length > 0 && (
|
|
273
|
+
<>
|
|
274
|
+
<Separator className="my-3" />
|
|
275
|
+
{customFolders.map((folder) => (
|
|
276
|
+
<FolderItem
|
|
277
|
+
key={folder.id}
|
|
278
|
+
folder={folder}
|
|
279
|
+
isActive={activeFolderId === folder.id}
|
|
280
|
+
isCollapsed={isCollapsed}
|
|
281
|
+
onClick={() => onFolderClick?.(folder.id)}
|
|
282
|
+
/>
|
|
283
|
+
))}
|
|
284
|
+
</>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{/* Labels Section */}
|
|
288
|
+
{labels && labels.length > 0 && (
|
|
289
|
+
<>
|
|
290
|
+
<Separator className="my-3" />
|
|
291
|
+
<Collapsible
|
|
292
|
+
open={labelsExpanded}
|
|
293
|
+
onOpenChange={setLabelsExpanded}
|
|
294
|
+
>
|
|
295
|
+
<CollapsibleTrigger asChild>
|
|
296
|
+
<button
|
|
297
|
+
type="button"
|
|
298
|
+
className="text-muted-foreground hover:text-foreground flex w-full items-center justify-between px-3 py-2 text-xs font-medium uppercase tracking-wider"
|
|
299
|
+
>
|
|
300
|
+
{!isCollapsed && "Labels"}
|
|
301
|
+
<ChevronRight
|
|
302
|
+
className={cn(
|
|
303
|
+
"size-4 transition-transform",
|
|
304
|
+
labelsExpanded && "rotate-90",
|
|
305
|
+
)}
|
|
306
|
+
/>
|
|
307
|
+
</button>
|
|
308
|
+
</CollapsibleTrigger>
|
|
309
|
+
<CollapsibleContent className="space-y-1">
|
|
310
|
+
{labels.map((label) => (
|
|
311
|
+
<LabelItem
|
|
312
|
+
key={label.id}
|
|
313
|
+
label={label}
|
|
314
|
+
isCollapsed={isCollapsed}
|
|
315
|
+
onClick={() => onLabelClick?.(label.id)}
|
|
316
|
+
/>
|
|
317
|
+
))}
|
|
318
|
+
{onCreateLabel && !isCollapsed && (
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={onCreateLabel}
|
|
322
|
+
className="text-muted-foreground hover:text-foreground flex w-full items-center gap-3 px-3 py-2 text-sm"
|
|
323
|
+
>
|
|
324
|
+
<Plus className="size-4" />
|
|
325
|
+
Create label
|
|
326
|
+
</button>
|
|
327
|
+
)}
|
|
328
|
+
</CollapsibleContent>
|
|
329
|
+
</Collapsible>
|
|
330
|
+
</>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
</ScrollArea>
|
|
334
|
+
|
|
335
|
+
{/* Footer */}
|
|
336
|
+
<div className="border-t p-3">
|
|
337
|
+
{/* Upgrade Card */}
|
|
338
|
+
{showUpgrade && !isCollapsed && (
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
onClick={onUpgradeClick}
|
|
342
|
+
className="bg-gradient-to-r from-primary/10 to-primary/5 hover:from-primary/20 hover:to-primary/10 mb-3 w-full rounded-lg p-3 text-left transition-colors"
|
|
343
|
+
>
|
|
344
|
+
<div className="flex items-center gap-2">
|
|
345
|
+
<Sparkles className="text-primary size-4" />
|
|
346
|
+
<span className="text-sm font-medium">Upgrade to Pro</span>
|
|
347
|
+
</div>
|
|
348
|
+
<p className="text-muted-foreground mt-1 text-xs">
|
|
349
|
+
Get AI features and more storage
|
|
350
|
+
</p>
|
|
351
|
+
</button>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{/* User Info */}
|
|
355
|
+
{user && (
|
|
356
|
+
<div className="flex items-center gap-3">
|
|
357
|
+
<Avatar className="size-8">
|
|
358
|
+
<AvatarImage src={user.avatar} alt={user.name} />
|
|
359
|
+
<AvatarFallback>
|
|
360
|
+
{user.name
|
|
361
|
+
.split(" ")
|
|
362
|
+
.map((n) => n[0])
|
|
363
|
+
.join("")
|
|
364
|
+
.toUpperCase()
|
|
365
|
+
.slice(0, 2)}
|
|
366
|
+
</AvatarFallback>
|
|
367
|
+
</Avatar>
|
|
368
|
+
{!isCollapsed && (
|
|
369
|
+
<div className="min-w-0 flex-1">
|
|
370
|
+
<div className="truncate text-sm font-medium">{user.name}</div>
|
|
371
|
+
<div className="text-muted-foreground truncate text-xs">
|
|
372
|
+
{user.email}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
{!isCollapsed && (
|
|
377
|
+
<Button variant="ghost" size="icon" className="size-8 shrink-0">
|
|
378
|
+
<Settings className="size-4" />
|
|
379
|
+
</Button>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export { MailSidebar };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/zero - Zero Email Components
|
|
3
|
+
*
|
|
4
|
+
* AI-powered email client components for mdxui, ported from github.com/Mail-0/Zero.
|
|
5
|
+
* Provides a complete email client UI including:
|
|
6
|
+
* - Mail list and thread display
|
|
7
|
+
* - Email composition with AI assistance
|
|
8
|
+
* - Landing page components
|
|
9
|
+
* - Dashboard/app shell
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Shared UI components
|
|
13
|
+
export * from "./components";
|
|
14
|
+
|
|
15
|
+
// Compose components
|
|
16
|
+
export * from "./compose";
|
|
17
|
+
// Dashboard/shell components
|
|
18
|
+
export * from "./dashboard";
|
|
19
|
+
// Landing page components
|
|
20
|
+
export * from "./landing";
|
|
21
|
+
// Mail components
|
|
22
|
+
export * from "./mail";
|
|
23
|
+
// Page compositions (complete examples with state management)
|
|
24
|
+
export * from "./pages";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landing Page Components
|
|
3
|
+
*
|
|
4
|
+
* Marketing and landing page components from 0.email:
|
|
5
|
+
* - ZeroHero: AI-powered email hero section
|
|
6
|
+
* - FeatureTabs: Tab-based feature showcase
|
|
7
|
+
* - SpeedShowcase: Speed/efficiency demo
|
|
8
|
+
* - FeatureCards: Three-column feature grid
|
|
9
|
+
* - ChatDemo: AI chat interface demo
|
|
10
|
+
* - ZeroNavigation: Header navigation
|
|
11
|
+
* - ZeroFooter: Footer component
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Components will be added as they are ported from Zero
|
|
15
|
+
// export { ZeroHero } from './zero-hero'
|
|
16
|
+
// export { FeatureTabs } from './feature-tabs'
|
|
17
|
+
// export { SpeedShowcase } from './speed-showcase'
|
|
18
|
+
// export { FeatureCards } from './feature-cards'
|
|
19
|
+
// export { ChatDemo } from './chat-demo'
|
|
20
|
+
// export { ZeroNavigation } from './zero-navigation'
|
|
21
|
+
// export { ZeroFooter } from './zero-footer'
|
|
22
|
+
|
|
23
|
+
// Placeholder export to make this a valid module
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail Components
|
|
3
|
+
*
|
|
4
|
+
* Core email viewing and management components:
|
|
5
|
+
* - MailList: Virtualized thread list with selection
|
|
6
|
+
* - MailItem: Individual thread item in list
|
|
7
|
+
* - ThreadDisplay: Email thread viewer with actions
|
|
8
|
+
* - MessageView: Individual message rendering
|
|
9
|
+
* - MailSkeleton: Loading states
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { MailItem } from "./mail-item";
|
|
13
|
+
export { MailList, MailSkeleton } from "./mail-list";
|
|
14
|
+
export { MessageHeader, MessageView } from "./message-view";
|
|
15
|
+
export { ThreadDisplay } from "./thread-display";
|