@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,334 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Folder,
|
|
3
|
+
MailLabel as Label,
|
|
4
|
+
MailThread as Thread,
|
|
5
|
+
} from "mdxui";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { MailShell } from "../dashboard/mail-shell";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MailZeroPage - Complete email page with internal state management.
|
|
11
|
+
*
|
|
12
|
+
* This is a self-contained page component that manages all email interactions
|
|
13
|
+
* internally. In a real app, this would integrate with your email API/service.
|
|
14
|
+
*
|
|
15
|
+
* Features:
|
|
16
|
+
* - Thread navigation (previous/next)
|
|
17
|
+
* - Thread selection and active state
|
|
18
|
+
* - Folder navigation
|
|
19
|
+
* - Star, archive, delete, spam actions
|
|
20
|
+
* - Snooze with preset times
|
|
21
|
+
* - Internal state management
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <MailZeroPage
|
|
26
|
+
* initialThreads={threads}
|
|
27
|
+
* initialFolders={folders}
|
|
28
|
+
* user={{ name: 'John', email: 'john@example.com.ai' }}
|
|
29
|
+
* />
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export interface MailZeroPageProps {
|
|
34
|
+
/** Initial threads to display */
|
|
35
|
+
initialThreads: Thread[];
|
|
36
|
+
/** Folders for sidebar */
|
|
37
|
+
initialFolders: Folder[];
|
|
38
|
+
/** Optional labels */
|
|
39
|
+
initialLabels?: Label[];
|
|
40
|
+
/** Initial active folder */
|
|
41
|
+
initialFolderId?: string;
|
|
42
|
+
/** User info for sidebar */
|
|
43
|
+
user?: {
|
|
44
|
+
name: string;
|
|
45
|
+
email: string;
|
|
46
|
+
avatar?: string;
|
|
47
|
+
};
|
|
48
|
+
/** Additional className */
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function MailZeroPage({
|
|
53
|
+
initialThreads,
|
|
54
|
+
initialFolders,
|
|
55
|
+
initialLabels = [],
|
|
56
|
+
initialFolderId,
|
|
57
|
+
user,
|
|
58
|
+
className,
|
|
59
|
+
}: MailZeroPageProps) {
|
|
60
|
+
// Thread state
|
|
61
|
+
const [threads, setThreads] = React.useState<Thread[]>(initialThreads);
|
|
62
|
+
const [activeThread, setActiveThread] = React.useState<Thread | undefined>();
|
|
63
|
+
const [activeIndex, setActiveIndex] = React.useState<number>(-1);
|
|
64
|
+
const [selectedThreadIds, setSelectedThreadIds] = React.useState<string[]>(
|
|
65
|
+
[],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Folder state
|
|
69
|
+
const [activeFolderId, setActiveFolderId] = React.useState<
|
|
70
|
+
string | undefined
|
|
71
|
+
>(initialFolderId ?? initialFolders[0]?.id);
|
|
72
|
+
|
|
73
|
+
// Sidebar state
|
|
74
|
+
const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false);
|
|
75
|
+
|
|
76
|
+
// Update threads when initialThreads changes
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
setThreads(initialThreads);
|
|
79
|
+
}, [initialThreads]);
|
|
80
|
+
|
|
81
|
+
// Navigation helpers
|
|
82
|
+
const hasPreviousThread = activeIndex > 0;
|
|
83
|
+
const hasNextThread = activeIndex >= 0 && activeIndex < threads.length - 1;
|
|
84
|
+
|
|
85
|
+
// =========================================================================
|
|
86
|
+
// Thread Navigation
|
|
87
|
+
// =========================================================================
|
|
88
|
+
|
|
89
|
+
const handleThreadClick = (thread: Thread) => {
|
|
90
|
+
const index = threads.findIndex((t) => t.id === thread.id);
|
|
91
|
+
setActiveThread(thread);
|
|
92
|
+
setActiveIndex(index);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handlePrevious = () => {
|
|
96
|
+
if (hasPreviousThread) {
|
|
97
|
+
const newIndex = activeIndex - 1;
|
|
98
|
+
setActiveIndex(newIndex);
|
|
99
|
+
setActiveThread(threads[newIndex]);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleNext = () => {
|
|
104
|
+
if (hasNextThread) {
|
|
105
|
+
const newIndex = activeIndex + 1;
|
|
106
|
+
setActiveIndex(newIndex);
|
|
107
|
+
setActiveThread(threads[newIndex]);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handleClose = () => {
|
|
112
|
+
setActiveThread(undefined);
|
|
113
|
+
setActiveIndex(-1);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// =========================================================================
|
|
117
|
+
// Folder Navigation
|
|
118
|
+
// =========================================================================
|
|
119
|
+
|
|
120
|
+
const handleFolderClick = (folderId: string) => {
|
|
121
|
+
setActiveFolderId(folderId);
|
|
122
|
+
setActiveThread(undefined);
|
|
123
|
+
setActiveIndex(-1);
|
|
124
|
+
// In a real app: fetch threads for this folder
|
|
125
|
+
console.log("📂 Folder changed to:", folderId);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleSelectionChange = (ids: string[]) => {
|
|
129
|
+
setSelectedThreadIds(ids);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// =========================================================================
|
|
133
|
+
// Compose Actions
|
|
134
|
+
// =========================================================================
|
|
135
|
+
|
|
136
|
+
const handleCompose = () => {
|
|
137
|
+
// In a real app: open compose modal/page
|
|
138
|
+
console.log("📝 Compose new email");
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleReply = (messageId: string) => {
|
|
142
|
+
// In a real app: open reply composer with messageId context
|
|
143
|
+
console.log("↩️ Reply to message:", messageId);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleReplyAll = (messageId: string) => {
|
|
147
|
+
// In a real app: open reply-all composer
|
|
148
|
+
console.log("↩️↩️ Reply-all to message:", messageId);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleForward = (messageId: string) => {
|
|
152
|
+
// In a real app: open forward composer
|
|
153
|
+
console.log("➡️ Forward message:", messageId);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// =========================================================================
|
|
157
|
+
// Thread Actions
|
|
158
|
+
// =========================================================================
|
|
159
|
+
|
|
160
|
+
const handleStar = () => {
|
|
161
|
+
if (activeThread) {
|
|
162
|
+
const updated = { ...activeThread, isStarred: !activeThread.isStarred };
|
|
163
|
+
setThreads((prev) =>
|
|
164
|
+
prev.map((t) => (t.id === activeThread.id ? updated : t)),
|
|
165
|
+
);
|
|
166
|
+
setActiveThread(updated);
|
|
167
|
+
console.log("⭐ Toggle star:", activeThread.id);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const handleStarThread = (threadId: string) => {
|
|
172
|
+
setThreads((prev) =>
|
|
173
|
+
prev.map((t) =>
|
|
174
|
+
t.id === threadId ? { ...t, isStarred: !t.isStarred } : t,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
console.log("⭐ Toggle star:", threadId);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const handleArchive = () => {
|
|
181
|
+
if (activeThread) {
|
|
182
|
+
const threadId = activeThread.id;
|
|
183
|
+
setThreads((prev) => prev.filter((t) => t.id !== threadId));
|
|
184
|
+
// Navigate to next or close
|
|
185
|
+
if (hasNextThread) {
|
|
186
|
+
const newIndex = activeIndex;
|
|
187
|
+
const remainingThreads = threads.filter((t) => t.id !== threadId);
|
|
188
|
+
setActiveIndex(Math.min(newIndex, remainingThreads.length - 1));
|
|
189
|
+
setActiveThread(remainingThreads[Math.min(newIndex, remainingThreads.length - 1)]);
|
|
190
|
+
} else if (hasPreviousThread) {
|
|
191
|
+
handlePrevious();
|
|
192
|
+
} else {
|
|
193
|
+
handleClose();
|
|
194
|
+
}
|
|
195
|
+
console.log("📦 Archive thread:", threadId);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const handleDelete = () => {
|
|
200
|
+
if (activeThread) {
|
|
201
|
+
const threadId = activeThread.id;
|
|
202
|
+
setThreads((prev) => prev.filter((t) => t.id !== threadId));
|
|
203
|
+
if (hasNextThread) {
|
|
204
|
+
const newIndex = activeIndex;
|
|
205
|
+
const remainingThreads = threads.filter((t) => t.id !== threadId);
|
|
206
|
+
setActiveIndex(Math.min(newIndex, remainingThreads.length - 1));
|
|
207
|
+
setActiveThread(remainingThreads[Math.min(newIndex, remainingThreads.length - 1)]);
|
|
208
|
+
} else if (hasPreviousThread) {
|
|
209
|
+
handlePrevious();
|
|
210
|
+
} else {
|
|
211
|
+
handleClose();
|
|
212
|
+
}
|
|
213
|
+
console.log("🗑️ Delete thread:", threadId);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const handleSpam = () => {
|
|
218
|
+
if (activeThread) {
|
|
219
|
+
const threadId = activeThread.id;
|
|
220
|
+
setThreads((prev) => prev.filter((t) => t.id !== threadId));
|
|
221
|
+
handleClose();
|
|
222
|
+
console.log("⚠️ Mark spam:", threadId);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const handleSnooze = (until: string) => {
|
|
227
|
+
if (activeThread) {
|
|
228
|
+
const threadId = activeThread.id;
|
|
229
|
+
setThreads((prev) => prev.filter((t) => t.id !== threadId));
|
|
230
|
+
handleClose();
|
|
231
|
+
console.log("⏰ Snooze thread:", threadId, "until:", until);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const handleMove = (folderId: string) => {
|
|
236
|
+
if (activeThread) {
|
|
237
|
+
const threadId = activeThread.id;
|
|
238
|
+
setThreads((prev) => prev.filter((t) => t.id !== threadId));
|
|
239
|
+
handleClose();
|
|
240
|
+
console.log("📁 Move thread:", threadId, "to:", folderId);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const handleLabel = (labelIds: string[]) => {
|
|
245
|
+
if (activeThread) {
|
|
246
|
+
console.log("🏷️ Label thread:", activeThread.id, "with:", labelIds);
|
|
247
|
+
// In a real app: update thread labels
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const handlePrint = () => {
|
|
252
|
+
window.print();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// =========================================================================
|
|
256
|
+
// Label Actions
|
|
257
|
+
// =========================================================================
|
|
258
|
+
|
|
259
|
+
const handleLabelClick = (labelId: string) => {
|
|
260
|
+
console.log("🏷️ Filter by label:", labelId);
|
|
261
|
+
// In a real app: filter threads by label
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const handleCreateLabel = () => {
|
|
265
|
+
console.log("➕ Create new label");
|
|
266
|
+
// In a real app: open create label dialog
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// =========================================================================
|
|
270
|
+
// Render
|
|
271
|
+
// =========================================================================
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<MailShell
|
|
275
|
+
// Sidebar props
|
|
276
|
+
folders={initialFolders}
|
|
277
|
+
activeFolderId={activeFolderId}
|
|
278
|
+
onFolderClick={handleFolderClick}
|
|
279
|
+
onCompose={handleCompose}
|
|
280
|
+
labels={initialLabels}
|
|
281
|
+
onLabelClick={handleLabelClick}
|
|
282
|
+
onCreateLabel={handleCreateLabel}
|
|
283
|
+
sidebarCollapsed={sidebarCollapsed}
|
|
284
|
+
onToggleSidebar={() => setSidebarCollapsed((prev) => !prev)}
|
|
285
|
+
user={user}
|
|
286
|
+
showUpgrade={false}
|
|
287
|
+
// Mail list props
|
|
288
|
+
threads={threads}
|
|
289
|
+
selectedThreadIds={selectedThreadIds}
|
|
290
|
+
activeThreadId={activeThread?.id}
|
|
291
|
+
selectionMode="single"
|
|
292
|
+
displayMode="comfortable"
|
|
293
|
+
isLoadingThreads={false}
|
|
294
|
+
hasMoreThreads={false}
|
|
295
|
+
onThreadClick={handleThreadClick}
|
|
296
|
+
onThreadSelectionChange={handleSelectionChange}
|
|
297
|
+
onStarThread={handleStarThread}
|
|
298
|
+
enableKeyboardNav={true}
|
|
299
|
+
showAvatars={true}
|
|
300
|
+
showSnippets={true}
|
|
301
|
+
emptyMessage="No messages in this folder"
|
|
302
|
+
// Thread display props
|
|
303
|
+
activeThread={activeThread}
|
|
304
|
+
showNavigation={true}
|
|
305
|
+
showThreadPanel={true}
|
|
306
|
+
hasPreviousThread={hasPreviousThread}
|
|
307
|
+
hasNextThread={hasNextThread}
|
|
308
|
+
onPrevious={handlePrevious}
|
|
309
|
+
onNext={handleNext}
|
|
310
|
+
onClose={handleClose}
|
|
311
|
+
onReply={handleReply}
|
|
312
|
+
onReplyAll={handleReplyAll}
|
|
313
|
+
onForward={handleForward}
|
|
314
|
+
onStar={handleStar}
|
|
315
|
+
onArchive={handleArchive}
|
|
316
|
+
onDelete={handleDelete}
|
|
317
|
+
onSpam={handleSpam}
|
|
318
|
+
onSnooze={handleSnooze}
|
|
319
|
+
onMove={handleMove}
|
|
320
|
+
onLabel={handleLabel}
|
|
321
|
+
onPrint={handlePrint}
|
|
322
|
+
// Layout props
|
|
323
|
+
defaultSidebarWidth={20}
|
|
324
|
+
defaultListWidth={35}
|
|
325
|
+
minSidebarWidth={12}
|
|
326
|
+
maxSidebarWidth={25}
|
|
327
|
+
minListWidth={20}
|
|
328
|
+
maxListWidth={50}
|
|
329
|
+
className={className}
|
|
330
|
+
/>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export { MailZeroPage };
|