@1kbirds/chidori-mock-gmail 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 (49) hide show
  1. package/README.md +85 -0
  2. package/dist/api.d.ts +14 -0
  3. package/dist/api.d.ts.map +1 -0
  4. package/dist/api.js +139 -0
  5. package/dist/client.d.ts +228 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +50 -0
  8. package/dist/errors.d.ts +19 -0
  9. package/dist/errors.d.ts.map +1 -0
  10. package/dist/errors.js +25 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +9 -0
  14. package/dist/seed.d.ts +6 -0
  15. package/dist/seed.d.ts.map +1 -0
  16. package/dist/seed.js +127 -0
  17. package/dist/service.d.ts +41 -0
  18. package/dist/service.d.ts.map +1 -0
  19. package/dist/service.js +372 -0
  20. package/dist/state.d.ts +22 -0
  21. package/dist/state.d.ts.map +1 -0
  22. package/dist/state.js +139 -0
  23. package/dist/types.d.ts +123 -0
  24. package/dist/types.d.ts.map +1 -0
  25. package/dist/types.js +1 -0
  26. package/dist/ui/GmailMockApp.d.ts +7 -0
  27. package/dist/ui/GmailMockApp.d.ts.map +1 -0
  28. package/dist/ui/GmailMockApp.js +93 -0
  29. package/dist/ui/dev.d.ts +2 -0
  30. package/dist/ui/dev.d.ts.map +1 -0
  31. package/dist/ui/dev.js +11 -0
  32. package/dist/ui/index.d.ts +3 -0
  33. package/dist/ui/index.d.ts.map +1 -0
  34. package/dist/ui/index.js +1 -0
  35. package/dist/ui/styles.css +340 -0
  36. package/package.json +56 -0
  37. package/src/__tests__/service.test.ts +120 -0
  38. package/src/api.ts +157 -0
  39. package/src/client.ts +54 -0
  40. package/src/errors.ts +29 -0
  41. package/src/index.ts +12 -0
  42. package/src/seed.ts +143 -0
  43. package/src/service.ts +405 -0
  44. package/src/state.ts +159 -0
  45. package/src/types.ts +149 -0
  46. package/src/ui/GmailMockApp.tsx +236 -0
  47. package/src/ui/dev.tsx +16 -0
  48. package/src/ui/index.ts +2 -0
  49. package/src/ui/styles.css +340 -0
package/src/state.ts ADDED
@@ -0,0 +1,159 @@
1
+ import type { GmailDraft, GmailLabel, GmailMessage, GmailMockOptions, GmailSeed, GmailSnapshot, GmailUser, SystemLabelId } from "./types";
2
+
3
+ export interface GmailMockStore {
4
+ users: Map<string, GmailUser>;
5
+ currentUserId: string;
6
+ labels: Map<string, GmailLabel>;
7
+ messages: Map<string, GmailMessage>;
8
+ drafts: Map<string, GmailDraft>;
9
+ historyId: number;
10
+ now(): Date;
11
+ nextId(prefix: string): string;
12
+ listeners: Set<() => void>;
13
+ }
14
+
15
+ const defaultUser: GmailUser = {
16
+ id: "me",
17
+ emailAddress: "user@example.com",
18
+ displayName: "Mock User",
19
+ };
20
+
21
+ const systemLabels: Array<{ id: SystemLabelId; name: string }> = [
22
+ { id: "INBOX", name: "Inbox" },
23
+ { id: "SENT", name: "Sent" },
24
+ { id: "DRAFT", name: "Drafts" },
25
+ { id: "TRASH", name: "Trash" },
26
+ { id: "SPAM", name: "Spam" },
27
+ { id: "STARRED", name: "Starred" },
28
+ { id: "UNREAD", name: "Unread" },
29
+ { id: "IMPORTANT", name: "Important" },
30
+ { id: "CATEGORY_PRIMARY", name: "Primary" },
31
+ ];
32
+
33
+ export function createStore(options: GmailMockOptions = {}): GmailMockStore {
34
+ const fixedNow = options.now ? new Date(options.now) : undefined;
35
+ const user = { ...defaultUser, ...options.currentUser };
36
+ const counters = new Map<string, number>();
37
+ const store: GmailMockStore = {
38
+ users: new Map([[user.id, user]]),
39
+ currentUserId: user.id,
40
+ labels: new Map(defaultLabels().map((label) => [label.id, label])),
41
+ messages: new Map(),
42
+ drafts: new Map(),
43
+ historyId: 1,
44
+ now: () => new Date(fixedNow ?? new Date()),
45
+ nextId: (prefix) => {
46
+ const next = (counters.get(prefix) ?? 0) + 1;
47
+ counters.set(prefix, next);
48
+ return `${prefix}_${next.toString(36)}`;
49
+ },
50
+ listeners: new Set(),
51
+ };
52
+
53
+ if (options.seed) {
54
+ applySeed(store, options.seed);
55
+ } else {
56
+ recomputeLabelStats(store);
57
+ }
58
+
59
+ return store;
60
+ }
61
+
62
+ export function defaultLabels(): GmailLabel[] {
63
+ return systemLabels.map((label) => ({
64
+ id: label.id,
65
+ name: label.name,
66
+ type: "system",
67
+ messageListVisibility: "show",
68
+ labelListVisibility: "labelShow",
69
+ messagesTotal: 0,
70
+ messagesUnread: 0,
71
+ threadsTotal: 0,
72
+ threadsUnread: 0,
73
+ }));
74
+ }
75
+
76
+ export function snapshot(store: GmailMockStore): GmailSnapshot {
77
+ recomputeLabelStats(store);
78
+ return {
79
+ users: Array.from(store.users.values()).map(clone),
80
+ currentUserId: store.currentUserId,
81
+ labels: Array.from(store.labels.values()).map(clone),
82
+ messages: Array.from(store.messages.values()).map(clone),
83
+ drafts: Array.from(store.drafts.values()).map(clone),
84
+ historyId: String(store.historyId),
85
+ };
86
+ }
87
+
88
+ export function applySnapshot(store: GmailMockStore, next: GmailSnapshot): void {
89
+ store.users = new Map(next.users.map((user) => [user.id, clone(user)]));
90
+ store.currentUserId = next.currentUserId;
91
+ store.labels = new Map(next.labels.map((label) => [label.id, clone(label)]));
92
+ store.messages = new Map(next.messages.map((message) => [message.id, clone(message)]));
93
+ store.drafts = new Map(next.drafts.map((draft) => [draft.id, clone(draft)]));
94
+ store.historyId = Number(next.historyId);
95
+ recomputeLabelStats(store);
96
+ emit(store);
97
+ }
98
+
99
+ export function applySeed(store: GmailMockStore, seed: GmailSeed): void {
100
+ applySnapshot(store, {
101
+ users: seed.users ?? Array.from(store.users.values()),
102
+ currentUserId: seed.currentUserId ?? seed.users?.[0]?.id ?? store.currentUserId,
103
+ labels: seed.labels ?? defaultLabels(),
104
+ messages: seed.messages ?? [],
105
+ drafts: seed.drafts ?? [],
106
+ historyId: "1",
107
+ });
108
+ }
109
+
110
+ export function bumpHistory(store: GmailMockStore): string {
111
+ store.historyId += 1;
112
+ return String(store.historyId);
113
+ }
114
+
115
+ export function emit(store: GmailMockStore): void {
116
+ recomputeLabelStats(store);
117
+ for (const listener of store.listeners) {
118
+ listener();
119
+ }
120
+ }
121
+
122
+ export function recomputeLabelStats(store: GmailMockStore): void {
123
+ for (const label of store.labels.values()) {
124
+ label.messagesTotal = 0;
125
+ label.messagesUnread = 0;
126
+ label.threadsTotal = 0;
127
+ label.threadsUnread = 0;
128
+ }
129
+ const threadIdsByLabel = new Map<string, Set<string>>();
130
+ const unreadThreadIdsByLabel = new Map<string, Set<string>>();
131
+ for (const message of store.messages.values()) {
132
+ for (const labelId of message.labelIds) {
133
+ const label = store.labels.get(labelId);
134
+ if (!label) continue;
135
+ label.messagesTotal += 1;
136
+ if (message.labelIds.includes("UNREAD")) {
137
+ label.messagesUnread += 1;
138
+ }
139
+ if (!threadIdsByLabel.has(labelId)) threadIdsByLabel.set(labelId, new Set());
140
+ threadIdsByLabel.get(labelId)!.add(message.threadId);
141
+ if (message.labelIds.includes("UNREAD")) {
142
+ if (!unreadThreadIdsByLabel.has(labelId)) unreadThreadIdsByLabel.set(labelId, new Set());
143
+ unreadThreadIdsByLabel.get(labelId)!.add(message.threadId);
144
+ }
145
+ }
146
+ }
147
+ for (const [labelId, threadIds] of threadIdsByLabel) {
148
+ const label = store.labels.get(labelId);
149
+ if (label) label.threadsTotal = threadIds.size;
150
+ }
151
+ for (const [labelId, threadIds] of unreadThreadIdsByLabel) {
152
+ const label = store.labels.get(labelId);
153
+ if (label) label.threadsUnread = threadIds.size;
154
+ }
155
+ }
156
+
157
+ export function clone<T>(value: T): T {
158
+ return structuredClone(value);
159
+ }
package/src/types.ts ADDED
@@ -0,0 +1,149 @@
1
+ export type SystemLabelId =
2
+ | "INBOX"
3
+ | "SENT"
4
+ | "DRAFT"
5
+ | "TRASH"
6
+ | "SPAM"
7
+ | "STARRED"
8
+ | "UNREAD"
9
+ | "IMPORTANT"
10
+ | "CATEGORY_PRIMARY";
11
+
12
+ export type LabelType = "system" | "user";
13
+
14
+ export interface GmailUser {
15
+ id: string;
16
+ emailAddress: string;
17
+ displayName: string;
18
+ }
19
+
20
+ export interface GmailLabel {
21
+ id: string;
22
+ name: string;
23
+ type: LabelType;
24
+ messageListVisibility?: "show" | "hide";
25
+ labelListVisibility?: "labelShow" | "labelHide";
26
+ color?: {
27
+ textColor: string;
28
+ backgroundColor: string;
29
+ };
30
+ messagesTotal: number;
31
+ messagesUnread: number;
32
+ threadsTotal: number;
33
+ threadsUnread: number;
34
+ }
35
+
36
+ export interface GmailHeader {
37
+ name: string;
38
+ value: string;
39
+ }
40
+
41
+ export interface GmailMessagePartBody {
42
+ attachmentId?: string;
43
+ size: number;
44
+ data?: string;
45
+ }
46
+
47
+ export interface GmailMessagePart {
48
+ partId: string;
49
+ mimeType: string;
50
+ filename?: string;
51
+ headers: GmailHeader[];
52
+ body: GmailMessagePartBody;
53
+ parts?: GmailMessagePart[];
54
+ }
55
+
56
+ export interface GmailAttachment {
57
+ id: string;
58
+ filename: string;
59
+ mimeType: string;
60
+ size: number;
61
+ data?: string;
62
+ }
63
+
64
+ export interface GmailMessage {
65
+ id: string;
66
+ threadId: string;
67
+ labelIds: string[];
68
+ snippet: string;
69
+ historyId: string;
70
+ internalDate: string;
71
+ payload: GmailMessagePart;
72
+ sizeEstimate: number;
73
+ raw?: string;
74
+ attachments?: GmailAttachment[];
75
+ }
76
+
77
+ export interface GmailThread {
78
+ id: string;
79
+ historyId: string;
80
+ snippet: string;
81
+ messages: GmailMessage[];
82
+ }
83
+
84
+ export interface GmailDraft {
85
+ id: string;
86
+ message: GmailMessage;
87
+ }
88
+
89
+ export interface GmailProfile {
90
+ emailAddress: string;
91
+ messagesTotal: number;
92
+ threadsTotal: number;
93
+ historyId: string;
94
+ }
95
+
96
+ export interface GmailMockOptions {
97
+ currentUser?: Partial<GmailUser>;
98
+ now?: string | Date;
99
+ seed?: GmailSeed;
100
+ }
101
+
102
+ export interface GmailSeed {
103
+ users?: GmailUser[];
104
+ currentUserId?: string;
105
+ labels?: GmailLabel[];
106
+ messages?: GmailMessage[];
107
+ drafts?: GmailDraft[];
108
+ }
109
+
110
+ export interface GmailSnapshot {
111
+ users: GmailUser[];
112
+ currentUserId: string;
113
+ labels: GmailLabel[];
114
+ messages: GmailMessage[];
115
+ drafts: GmailDraft[];
116
+ historyId: string;
117
+ }
118
+
119
+ export interface GmailListResponse<T> {
120
+ resultSizeEstimate: number;
121
+ nextPageToken?: string;
122
+ messages?: T[];
123
+ threads?: T[];
124
+ labels?: T[];
125
+ drafts?: T[];
126
+ }
127
+
128
+ export interface GmailModifyRequest {
129
+ addLabelIds?: string[];
130
+ removeLabelIds?: string[];
131
+ }
132
+
133
+ export interface GmailSendRequest {
134
+ raw?: string;
135
+ message?: Partial<GmailMessage>;
136
+ to?: string[];
137
+ cc?: string[];
138
+ bcc?: string[];
139
+ subject?: string;
140
+ body?: string;
141
+ attachments?: GmailAttachment[];
142
+ }
143
+
144
+ export interface GmailSearchOptions {
145
+ q?: string;
146
+ labelIds?: string[];
147
+ includeSpamTrash?: boolean;
148
+ maxResults?: number;
149
+ }
@@ -0,0 +1,236 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { demoSeed } from "../seed";
3
+ import { decodeBase64Url } from "../service";
4
+ import type { GmailMessage } from "../types";
5
+ import type { GmailMock } from "../service";
6
+ import "./styles.css";
7
+
8
+ export interface GmailMockAppProps {
9
+ mock: GmailMock;
10
+ }
11
+
12
+ interface ComposeDraft {
13
+ id?: string;
14
+ to: string;
15
+ subject: string;
16
+ body: string;
17
+ }
18
+
19
+ const primaryLabels = ["INBOX", "STARRED", "SENT", "DRAFT", "TRASH", "SPAM"];
20
+
21
+ export function GmailMockApp({ mock }: GmailMockAppProps) {
22
+ const [state, setState] = useState(() => mock.snapshot());
23
+ useEffect(() => mock.subscribe(() => setState(mock.snapshot())), [mock]);
24
+ const [activeLabel, setActiveLabel] = useState("INBOX");
25
+ const [query, setQuery] = useState("");
26
+ const [selectedMessageId, setSelectedMessageId] = useState<string | null>(null);
27
+ const [compose, setCompose] = useState<ComposeDraft | null>(null);
28
+
29
+ const label = state.labels.find((item) => item.id === activeLabel);
30
+ const messages = useMemo(
31
+ () =>
32
+ state.messages
33
+ .filter((message) => activeLabel === "ALL" || message.labelIds.includes(activeLabel))
34
+ .filter((message) => activeLabel === "TRASH" || activeLabel === "SPAM" || (!message.labelIds.includes("TRASH") && !message.labelIds.includes("SPAM")))
35
+ .filter((message) => !query || matchesUiQuery(message, query))
36
+ .sort((a, b) => Number(b.internalDate) - Number(a.internalDate)),
37
+ [activeLabel, query, state.messages],
38
+ );
39
+ const selected = messages.find((message) => message.id === selectedMessageId) ?? messages[0] ?? null;
40
+ const selectedThread = selected ? state.messages.filter((message) => message.threadId === selected.threadId).sort((a, b) => Number(a.internalDate) - Number(b.internalDate)) : [];
41
+
42
+ function resetDemo() {
43
+ mock.reset({
44
+ users: demoSeed.users!,
45
+ currentUserId: demoSeed.currentUserId!,
46
+ labels: demoSeed.labels!,
47
+ messages: demoSeed.messages!,
48
+ drafts: demoSeed.drafts!,
49
+ historyId: "1",
50
+ });
51
+ setActiveLabel("INBOX");
52
+ setSelectedMessageId(null);
53
+ setCompose(null);
54
+ }
55
+
56
+ function openDraft(message: GmailMessage) {
57
+ const draft = state.drafts.find((item) => item.message.id === message.id);
58
+ setCompose({
59
+ id: draft?.id,
60
+ to: header(message, "To"),
61
+ subject: header(message, "Subject"),
62
+ body: bodyText(message),
63
+ });
64
+ }
65
+
66
+ function saveDraft() {
67
+ if (!compose) return;
68
+ const request = { to: compose.to.split(",").map((value) => value.trim()).filter(Boolean), subject: compose.subject, body: compose.body };
69
+ if (compose.id) {
70
+ mock.updateDraft(compose.id, request);
71
+ } else {
72
+ mock.createDraft(request);
73
+ }
74
+ setCompose(null);
75
+ setActiveLabel("DRAFT");
76
+ }
77
+
78
+ function sendCompose() {
79
+ if (!compose) return;
80
+ if (compose.id) {
81
+ mock.updateDraft(compose.id, { to: compose.to.split(",").map((value) => value.trim()).filter(Boolean), subject: compose.subject, body: compose.body });
82
+ mock.sendDraft(compose.id);
83
+ } else {
84
+ mock.send({ to: compose.to.split(",").map((value) => value.trim()).filter(Boolean), subject: compose.subject, body: compose.body });
85
+ }
86
+ setCompose(null);
87
+ setActiveLabel("SENT");
88
+ }
89
+
90
+ return (
91
+ <div className="gmail-shell">
92
+ <aside className="gmail-sidebar">
93
+ <div className="gmail-brand">Gmail</div>
94
+ <button className="gmail-compose" type="button" onClick={() => setCompose({ to: "", subject: "", body: "" })}>
95
+ Compose
96
+ </button>
97
+ <nav>
98
+ {[...primaryLabels, "ALL"].map((labelId) => {
99
+ const item = state.labels.find((entry) => entry.id === labelId);
100
+ return (
101
+ <button className={labelId === activeLabel ? "active" : ""} type="button" key={labelId} onClick={() => { setActiveLabel(labelId); setSelectedMessageId(null); }}>
102
+ <span>{item?.name ?? "All Mail"}</span>
103
+ {item?.messagesUnread ? <strong>{item.messagesUnread}</strong> : null}
104
+ </button>
105
+ );
106
+ })}
107
+ </nav>
108
+ <section className="gmail-labels">
109
+ <h2>Labels</h2>
110
+ {state.labels
111
+ .filter((item) => item.type === "user")
112
+ .map((item) => (
113
+ <button className={item.id === activeLabel ? "active" : ""} type="button" key={item.id} onClick={() => setActiveLabel(item.id)}>
114
+ {item.name}
115
+ </button>
116
+ ))}
117
+ </section>
118
+ <section className="gmail-account">
119
+ <strong>{state.users.find((user) => user.id === state.currentUserId)?.displayName}</strong>
120
+ <span>{state.users.find((user) => user.id === state.currentUserId)?.emailAddress}</span>
121
+ <button type="button" onClick={resetDemo}>
122
+ Reset demo seed
123
+ </button>
124
+ </section>
125
+ </aside>
126
+
127
+ <main className="gmail-main">
128
+ <header className="gmail-toolbar">
129
+ <input value={query} placeholder="Search mail" onChange={(event) => setQuery(event.target.value)} />
130
+ <div>
131
+ <strong>{label?.name ?? "All Mail"}</strong>
132
+ <span>{messages.length} messages</span>
133
+ </div>
134
+ </header>
135
+
136
+ <section className="gmail-workspace">
137
+ <div className="gmail-list">
138
+ {messages.map((message) => (
139
+ <button
140
+ className={`${message.id === selected?.id ? "selected" : ""} ${message.labelIds.includes("UNREAD") ? "unread" : ""}`}
141
+ type="button"
142
+ key={message.id}
143
+ onClick={() => {
144
+ setSelectedMessageId(message.id);
145
+ if (message.labelIds.includes("UNREAD")) mock.modifyMessage(message.id, { removeLabelIds: ["UNREAD"] });
146
+ }}
147
+ >
148
+ <span>{header(message, activeLabel === "SENT" || activeLabel === "DRAFT" ? "To" : "From")}</span>
149
+ <strong>{header(message, "Subject")}</strong>
150
+ <small>{message.snippet}</small>
151
+ <time>{formatDate(message.internalDate)}</time>
152
+ </button>
153
+ ))}
154
+ </div>
155
+
156
+ <article className="gmail-reader">
157
+ {selected ? (
158
+ <>
159
+ <div className="gmail-reader-head">
160
+ <div>
161
+ <h1>{header(selected, "Subject")}</h1>
162
+ <p>
163
+ {selectedThread.length} message{selectedThread.length === 1 ? "" : "s"} in thread
164
+ </p>
165
+ </div>
166
+ <div className="gmail-actions">
167
+ {selected.labelIds.includes("DRAFT") ? <button type="button" onClick={() => openDraft(selected)}>Edit draft</button> : null}
168
+ <button type="button" onClick={() => mock.modifyThread(selected.threadId, { removeLabelIds: ["INBOX"] })}>Archive</button>
169
+ <button type="button" onClick={() => mock.trashThread(selected.threadId)}>Trash</button>
170
+ <button type="button" onClick={() => mock.modifyThread(selected.threadId, { addLabelIds: ["STARRED"] })}>Star</button>
171
+ </div>
172
+ </div>
173
+ <div className="gmail-thread">
174
+ {selectedThread.map((message) => (
175
+ <section className="gmail-thread-message" key={message.id}>
176
+ <header>
177
+ <div>
178
+ <strong>{header(message, "From")}</strong>
179
+ <span>to {header(message, "To")}</span>
180
+ </div>
181
+ <time>{formatDate(message.internalDate)}</time>
182
+ </header>
183
+ <p>{bodyText(message)}</p>
184
+ {message.attachments?.length ? (
185
+ <div className="gmail-attachments">
186
+ {message.attachments.map((attachment) => (
187
+ <span key={attachment.id}>{attachment.filename} · {attachment.size} bytes</span>
188
+ ))}
189
+ </div>
190
+ ) : null}
191
+ </section>
192
+ ))}
193
+ </div>
194
+ </>
195
+ ) : (
196
+ <div className="gmail-empty">No messages.</div>
197
+ )}
198
+ </article>
199
+ </section>
200
+ </main>
201
+
202
+ {compose ? (
203
+ <form className="gmail-composer" onSubmit={(event) => { event.preventDefault(); sendCompose(); }}>
204
+ <header>
205
+ <strong>{compose.id ? "Edit draft" : "New Message"}</strong>
206
+ <button type="button" onClick={() => setCompose(null)}>×</button>
207
+ </header>
208
+ <input value={compose.to} placeholder="Recipients" onChange={(event) => setCompose({ ...compose, to: event.target.value })} />
209
+ <input value={compose.subject} placeholder="Subject" onChange={(event) => setCompose({ ...compose, subject: event.target.value })} />
210
+ <textarea value={compose.body} onChange={(event) => setCompose({ ...compose, body: event.target.value })} />
211
+ <footer>
212
+ <button type="button" onClick={saveDraft}>Save draft</button>
213
+ <button className="gmail-send" type="submit">Send</button>
214
+ </footer>
215
+ </form>
216
+ ) : null}
217
+ </div>
218
+ );
219
+ }
220
+
221
+ function header(message: GmailMessage, name: string): string {
222
+ return message.payload.headers.find((item) => item.name.toLowerCase() === name.toLowerCase())?.value ?? "";
223
+ }
224
+
225
+ function bodyText(message: GmailMessage): string {
226
+ return message.payload.body.data ? decodeBase64Url(message.payload.body.data) : message.snippet;
227
+ }
228
+
229
+ function matchesUiQuery(message: GmailMessage, query: string): boolean {
230
+ const target = [message.snippet, bodyText(message), ...message.payload.headers.map((item) => item.value)].join(" ").toLowerCase();
231
+ return target.includes(query.toLowerCase());
232
+ }
233
+
234
+ function formatDate(internalDate: string): string {
235
+ return new Date(Number(internalDate)).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
236
+ }
package/src/ui/dev.tsx ADDED
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { createGmailMock, demoSeed } from "../index";
4
+ import { GmailMockApp } from "./GmailMockApp";
5
+ import "./styles.css";
6
+
7
+ const mock = createGmailMock({
8
+ now: "2026-06-01T16:30:00.000Z",
9
+ seed: demoSeed,
10
+ });
11
+
12
+ createRoot(document.getElementById("root")!).render(
13
+ <React.StrictMode>
14
+ <GmailMockApp mock={mock} />
15
+ </React.StrictMode>,
16
+ );
@@ -0,0 +1,2 @@
1
+ export { GmailMockApp } from "./GmailMockApp";
2
+ export type { GmailMockAppProps } from "./GmailMockApp";