@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/dist/state.js ADDED
@@ -0,0 +1,139 @@
1
+ const defaultUser = {
2
+ id: "me",
3
+ emailAddress: "user@example.com",
4
+ displayName: "Mock User",
5
+ };
6
+ const systemLabels = [
7
+ { id: "INBOX", name: "Inbox" },
8
+ { id: "SENT", name: "Sent" },
9
+ { id: "DRAFT", name: "Drafts" },
10
+ { id: "TRASH", name: "Trash" },
11
+ { id: "SPAM", name: "Spam" },
12
+ { id: "STARRED", name: "Starred" },
13
+ { id: "UNREAD", name: "Unread" },
14
+ { id: "IMPORTANT", name: "Important" },
15
+ { id: "CATEGORY_PRIMARY", name: "Primary" },
16
+ ];
17
+ export function createStore(options = {}) {
18
+ const fixedNow = options.now ? new Date(options.now) : undefined;
19
+ const user = { ...defaultUser, ...options.currentUser };
20
+ const counters = new Map();
21
+ const store = {
22
+ users: new Map([[user.id, user]]),
23
+ currentUserId: user.id,
24
+ labels: new Map(defaultLabels().map((label) => [label.id, label])),
25
+ messages: new Map(),
26
+ drafts: new Map(),
27
+ historyId: 1,
28
+ now: () => new Date(fixedNow ?? new Date()),
29
+ nextId: (prefix) => {
30
+ const next = (counters.get(prefix) ?? 0) + 1;
31
+ counters.set(prefix, next);
32
+ return `${prefix}_${next.toString(36)}`;
33
+ },
34
+ listeners: new Set(),
35
+ };
36
+ if (options.seed) {
37
+ applySeed(store, options.seed);
38
+ }
39
+ else {
40
+ recomputeLabelStats(store);
41
+ }
42
+ return store;
43
+ }
44
+ export function defaultLabels() {
45
+ return systemLabels.map((label) => ({
46
+ id: label.id,
47
+ name: label.name,
48
+ type: "system",
49
+ messageListVisibility: "show",
50
+ labelListVisibility: "labelShow",
51
+ messagesTotal: 0,
52
+ messagesUnread: 0,
53
+ threadsTotal: 0,
54
+ threadsUnread: 0,
55
+ }));
56
+ }
57
+ export function snapshot(store) {
58
+ recomputeLabelStats(store);
59
+ return {
60
+ users: Array.from(store.users.values()).map(clone),
61
+ currentUserId: store.currentUserId,
62
+ labels: Array.from(store.labels.values()).map(clone),
63
+ messages: Array.from(store.messages.values()).map(clone),
64
+ drafts: Array.from(store.drafts.values()).map(clone),
65
+ historyId: String(store.historyId),
66
+ };
67
+ }
68
+ export function applySnapshot(store, next) {
69
+ store.users = new Map(next.users.map((user) => [user.id, clone(user)]));
70
+ store.currentUserId = next.currentUserId;
71
+ store.labels = new Map(next.labels.map((label) => [label.id, clone(label)]));
72
+ store.messages = new Map(next.messages.map((message) => [message.id, clone(message)]));
73
+ store.drafts = new Map(next.drafts.map((draft) => [draft.id, clone(draft)]));
74
+ store.historyId = Number(next.historyId);
75
+ recomputeLabelStats(store);
76
+ emit(store);
77
+ }
78
+ export function applySeed(store, seed) {
79
+ applySnapshot(store, {
80
+ users: seed.users ?? Array.from(store.users.values()),
81
+ currentUserId: seed.currentUserId ?? seed.users?.[0]?.id ?? store.currentUserId,
82
+ labels: seed.labels ?? defaultLabels(),
83
+ messages: seed.messages ?? [],
84
+ drafts: seed.drafts ?? [],
85
+ historyId: "1",
86
+ });
87
+ }
88
+ export function bumpHistory(store) {
89
+ store.historyId += 1;
90
+ return String(store.historyId);
91
+ }
92
+ export function emit(store) {
93
+ recomputeLabelStats(store);
94
+ for (const listener of store.listeners) {
95
+ listener();
96
+ }
97
+ }
98
+ export function recomputeLabelStats(store) {
99
+ for (const label of store.labels.values()) {
100
+ label.messagesTotal = 0;
101
+ label.messagesUnread = 0;
102
+ label.threadsTotal = 0;
103
+ label.threadsUnread = 0;
104
+ }
105
+ const threadIdsByLabel = new Map();
106
+ const unreadThreadIdsByLabel = new Map();
107
+ for (const message of store.messages.values()) {
108
+ for (const labelId of message.labelIds) {
109
+ const label = store.labels.get(labelId);
110
+ if (!label)
111
+ continue;
112
+ label.messagesTotal += 1;
113
+ if (message.labelIds.includes("UNREAD")) {
114
+ label.messagesUnread += 1;
115
+ }
116
+ if (!threadIdsByLabel.has(labelId))
117
+ threadIdsByLabel.set(labelId, new Set());
118
+ threadIdsByLabel.get(labelId).add(message.threadId);
119
+ if (message.labelIds.includes("UNREAD")) {
120
+ if (!unreadThreadIdsByLabel.has(labelId))
121
+ unreadThreadIdsByLabel.set(labelId, new Set());
122
+ unreadThreadIdsByLabel.get(labelId).add(message.threadId);
123
+ }
124
+ }
125
+ }
126
+ for (const [labelId, threadIds] of threadIdsByLabel) {
127
+ const label = store.labels.get(labelId);
128
+ if (label)
129
+ label.threadsTotal = threadIds.size;
130
+ }
131
+ for (const [labelId, threadIds] of unreadThreadIdsByLabel) {
132
+ const label = store.labels.get(labelId);
133
+ if (label)
134
+ label.threadsUnread = threadIds.size;
135
+ }
136
+ }
137
+ export function clone(value) {
138
+ return structuredClone(value);
139
+ }
@@ -0,0 +1,123 @@
1
+ export type SystemLabelId = "INBOX" | "SENT" | "DRAFT" | "TRASH" | "SPAM" | "STARRED" | "UNREAD" | "IMPORTANT" | "CATEGORY_PRIMARY";
2
+ export type LabelType = "system" | "user";
3
+ export interface GmailUser {
4
+ id: string;
5
+ emailAddress: string;
6
+ displayName: string;
7
+ }
8
+ export interface GmailLabel {
9
+ id: string;
10
+ name: string;
11
+ type: LabelType;
12
+ messageListVisibility?: "show" | "hide";
13
+ labelListVisibility?: "labelShow" | "labelHide";
14
+ color?: {
15
+ textColor: string;
16
+ backgroundColor: string;
17
+ };
18
+ messagesTotal: number;
19
+ messagesUnread: number;
20
+ threadsTotal: number;
21
+ threadsUnread: number;
22
+ }
23
+ export interface GmailHeader {
24
+ name: string;
25
+ value: string;
26
+ }
27
+ export interface GmailMessagePartBody {
28
+ attachmentId?: string;
29
+ size: number;
30
+ data?: string;
31
+ }
32
+ export interface GmailMessagePart {
33
+ partId: string;
34
+ mimeType: string;
35
+ filename?: string;
36
+ headers: GmailHeader[];
37
+ body: GmailMessagePartBody;
38
+ parts?: GmailMessagePart[];
39
+ }
40
+ export interface GmailAttachment {
41
+ id: string;
42
+ filename: string;
43
+ mimeType: string;
44
+ size: number;
45
+ data?: string;
46
+ }
47
+ export interface GmailMessage {
48
+ id: string;
49
+ threadId: string;
50
+ labelIds: string[];
51
+ snippet: string;
52
+ historyId: string;
53
+ internalDate: string;
54
+ payload: GmailMessagePart;
55
+ sizeEstimate: number;
56
+ raw?: string;
57
+ attachments?: GmailAttachment[];
58
+ }
59
+ export interface GmailThread {
60
+ id: string;
61
+ historyId: string;
62
+ snippet: string;
63
+ messages: GmailMessage[];
64
+ }
65
+ export interface GmailDraft {
66
+ id: string;
67
+ message: GmailMessage;
68
+ }
69
+ export interface GmailProfile {
70
+ emailAddress: string;
71
+ messagesTotal: number;
72
+ threadsTotal: number;
73
+ historyId: string;
74
+ }
75
+ export interface GmailMockOptions {
76
+ currentUser?: Partial<GmailUser>;
77
+ now?: string | Date;
78
+ seed?: GmailSeed;
79
+ }
80
+ export interface GmailSeed {
81
+ users?: GmailUser[];
82
+ currentUserId?: string;
83
+ labels?: GmailLabel[];
84
+ messages?: GmailMessage[];
85
+ drafts?: GmailDraft[];
86
+ }
87
+ export interface GmailSnapshot {
88
+ users: GmailUser[];
89
+ currentUserId: string;
90
+ labels: GmailLabel[];
91
+ messages: GmailMessage[];
92
+ drafts: GmailDraft[];
93
+ historyId: string;
94
+ }
95
+ export interface GmailListResponse<T> {
96
+ resultSizeEstimate: number;
97
+ nextPageToken?: string;
98
+ messages?: T[];
99
+ threads?: T[];
100
+ labels?: T[];
101
+ drafts?: T[];
102
+ }
103
+ export interface GmailModifyRequest {
104
+ addLabelIds?: string[];
105
+ removeLabelIds?: string[];
106
+ }
107
+ export interface GmailSendRequest {
108
+ raw?: string;
109
+ message?: Partial<GmailMessage>;
110
+ to?: string[];
111
+ cc?: string[];
112
+ bcc?: string[];
113
+ subject?: string;
114
+ body?: string;
115
+ attachments?: GmailAttachment[];
116
+ }
117
+ export interface GmailSearchOptions {
118
+ q?: string;
119
+ labelIds?: string[];
120
+ includeSpamTrash?: boolean;
121
+ maxResults?: number;
122
+ }
123
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GACrB,OAAO,GACP,MAAM,GACN,OAAO,GACP,OAAO,GACP,MAAM,GACN,SAAS,GACT,QAAQ,GACR,WAAW,GACX,kBAAkB,CAAC;AAEvB,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE1C,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,qBAAqB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxC,mBAAmB,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;IAChD,KAAK,CAAC,EAAE;QACN,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,IAAI,EAAE,oBAAoB,CAAC;IAC3B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;IACd,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;IACb,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IAChC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,kBAAkB;IACjC,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { GmailMock } from "../service";
2
+ import "./styles.css";
3
+ export interface GmailMockAppProps {
4
+ mock: GmailMock;
5
+ }
6
+ export declare function GmailMockApp({ mock }: GmailMockAppProps): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=GmailMockApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GmailMockApp.d.ts","sourceRoot":"","sources":["../../src/ui/GmailMockApp.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,cAAc,CAAC;AAEtB,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;CACjB;AAWD,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE,iBAAiB,2CAsMvD"}
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useState } from "react";
3
+ import { demoSeed } from "../seed";
4
+ import { decodeBase64Url } from "../service";
5
+ import "./styles.css";
6
+ const primaryLabels = ["INBOX", "STARRED", "SENT", "DRAFT", "TRASH", "SPAM"];
7
+ export function GmailMockApp({ mock }) {
8
+ const [state, setState] = useState(() => mock.snapshot());
9
+ useEffect(() => mock.subscribe(() => setState(mock.snapshot())), [mock]);
10
+ const [activeLabel, setActiveLabel] = useState("INBOX");
11
+ const [query, setQuery] = useState("");
12
+ const [selectedMessageId, setSelectedMessageId] = useState(null);
13
+ const [compose, setCompose] = useState(null);
14
+ const label = state.labels.find((item) => item.id === activeLabel);
15
+ const messages = useMemo(() => state.messages
16
+ .filter((message) => activeLabel === "ALL" || message.labelIds.includes(activeLabel))
17
+ .filter((message) => activeLabel === "TRASH" || activeLabel === "SPAM" || (!message.labelIds.includes("TRASH") && !message.labelIds.includes("SPAM")))
18
+ .filter((message) => !query || matchesUiQuery(message, query))
19
+ .sort((a, b) => Number(b.internalDate) - Number(a.internalDate)), [activeLabel, query, state.messages]);
20
+ const selected = messages.find((message) => message.id === selectedMessageId) ?? messages[0] ?? null;
21
+ const selectedThread = selected ? state.messages.filter((message) => message.threadId === selected.threadId).sort((a, b) => Number(a.internalDate) - Number(b.internalDate)) : [];
22
+ function resetDemo() {
23
+ mock.reset({
24
+ users: demoSeed.users,
25
+ currentUserId: demoSeed.currentUserId,
26
+ labels: demoSeed.labels,
27
+ messages: demoSeed.messages,
28
+ drafts: demoSeed.drafts,
29
+ historyId: "1",
30
+ });
31
+ setActiveLabel("INBOX");
32
+ setSelectedMessageId(null);
33
+ setCompose(null);
34
+ }
35
+ function openDraft(message) {
36
+ const draft = state.drafts.find((item) => item.message.id === message.id);
37
+ setCompose({
38
+ id: draft?.id,
39
+ to: header(message, "To"),
40
+ subject: header(message, "Subject"),
41
+ body: bodyText(message),
42
+ });
43
+ }
44
+ function saveDraft() {
45
+ if (!compose)
46
+ return;
47
+ const request = { to: compose.to.split(",").map((value) => value.trim()).filter(Boolean), subject: compose.subject, body: compose.body };
48
+ if (compose.id) {
49
+ mock.updateDraft(compose.id, request);
50
+ }
51
+ else {
52
+ mock.createDraft(request);
53
+ }
54
+ setCompose(null);
55
+ setActiveLabel("DRAFT");
56
+ }
57
+ function sendCompose() {
58
+ if (!compose)
59
+ return;
60
+ if (compose.id) {
61
+ mock.updateDraft(compose.id, { to: compose.to.split(",").map((value) => value.trim()).filter(Boolean), subject: compose.subject, body: compose.body });
62
+ mock.sendDraft(compose.id);
63
+ }
64
+ else {
65
+ mock.send({ to: compose.to.split(",").map((value) => value.trim()).filter(Boolean), subject: compose.subject, body: compose.body });
66
+ }
67
+ setCompose(null);
68
+ setActiveLabel("SENT");
69
+ }
70
+ return (_jsxs("div", { className: "gmail-shell", children: [_jsxs("aside", { className: "gmail-sidebar", children: [_jsx("div", { className: "gmail-brand", children: "Gmail" }), _jsx("button", { className: "gmail-compose", type: "button", onClick: () => setCompose({ to: "", subject: "", body: "" }), children: "Compose" }), _jsx("nav", { children: [...primaryLabels, "ALL"].map((labelId) => {
71
+ const item = state.labels.find((entry) => entry.id === labelId);
72
+ return (_jsxs("button", { className: labelId === activeLabel ? "active" : "", type: "button", onClick: () => { setActiveLabel(labelId); setSelectedMessageId(null); }, children: [_jsx("span", { children: item?.name ?? "All Mail" }), item?.messagesUnread ? _jsx("strong", { children: item.messagesUnread }) : null] }, labelId));
73
+ }) }), _jsxs("section", { className: "gmail-labels", children: [_jsx("h2", { children: "Labels" }), state.labels
74
+ .filter((item) => item.type === "user")
75
+ .map((item) => (_jsx("button", { className: item.id === activeLabel ? "active" : "", type: "button", onClick: () => setActiveLabel(item.id), children: item.name }, item.id)))] }), _jsxs("section", { className: "gmail-account", children: [_jsx("strong", { children: state.users.find((user) => user.id === state.currentUserId)?.displayName }), _jsx("span", { children: state.users.find((user) => user.id === state.currentUserId)?.emailAddress }), _jsx("button", { type: "button", onClick: resetDemo, children: "Reset demo seed" })] })] }), _jsxs("main", { className: "gmail-main", children: [_jsxs("header", { className: "gmail-toolbar", children: [_jsx("input", { value: query, placeholder: "Search mail", onChange: (event) => setQuery(event.target.value) }), _jsxs("div", { children: [_jsx("strong", { children: label?.name ?? "All Mail" }), _jsxs("span", { children: [messages.length, " messages"] })] })] }), _jsxs("section", { className: "gmail-workspace", children: [_jsx("div", { className: "gmail-list", children: messages.map((message) => (_jsxs("button", { className: `${message.id === selected?.id ? "selected" : ""} ${message.labelIds.includes("UNREAD") ? "unread" : ""}`, type: "button", onClick: () => {
76
+ setSelectedMessageId(message.id);
77
+ if (message.labelIds.includes("UNREAD"))
78
+ mock.modifyMessage(message.id, { removeLabelIds: ["UNREAD"] });
79
+ }, children: [_jsx("span", { children: header(message, activeLabel === "SENT" || activeLabel === "DRAFT" ? "To" : "From") }), _jsx("strong", { children: header(message, "Subject") }), _jsx("small", { children: message.snippet }), _jsx("time", { children: formatDate(message.internalDate) })] }, message.id))) }), _jsx("article", { className: "gmail-reader", children: selected ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "gmail-reader-head", children: [_jsxs("div", { children: [_jsx("h1", { children: header(selected, "Subject") }), _jsxs("p", { children: [selectedThread.length, " message", selectedThread.length === 1 ? "" : "s", " in thread"] })] }), _jsxs("div", { className: "gmail-actions", children: [selected.labelIds.includes("DRAFT") ? _jsx("button", { type: "button", onClick: () => openDraft(selected), children: "Edit draft" }) : null, _jsx("button", { type: "button", onClick: () => mock.modifyThread(selected.threadId, { removeLabelIds: ["INBOX"] }), children: "Archive" }), _jsx("button", { type: "button", onClick: () => mock.trashThread(selected.threadId), children: "Trash" }), _jsx("button", { type: "button", onClick: () => mock.modifyThread(selected.threadId, { addLabelIds: ["STARRED"] }), children: "Star" })] })] }), _jsx("div", { className: "gmail-thread", children: selectedThread.map((message) => (_jsxs("section", { className: "gmail-thread-message", children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("strong", { children: header(message, "From") }), _jsxs("span", { children: ["to ", header(message, "To")] })] }), _jsx("time", { children: formatDate(message.internalDate) })] }), _jsx("p", { children: bodyText(message) }), message.attachments?.length ? (_jsx("div", { className: "gmail-attachments", children: message.attachments.map((attachment) => (_jsxs("span", { children: [attachment.filename, " \u00B7 ", attachment.size, " bytes"] }, attachment.id))) })) : null] }, message.id))) })] })) : (_jsx("div", { className: "gmail-empty", children: "No messages." })) })] })] }), compose ? (_jsxs("form", { className: "gmail-composer", onSubmit: (event) => { event.preventDefault(); sendCompose(); }, children: [_jsxs("header", { children: [_jsx("strong", { children: compose.id ? "Edit draft" : "New Message" }), _jsx("button", { type: "button", onClick: () => setCompose(null), children: "\u00D7" })] }), _jsx("input", { value: compose.to, placeholder: "Recipients", onChange: (event) => setCompose({ ...compose, to: event.target.value }) }), _jsx("input", { value: compose.subject, placeholder: "Subject", onChange: (event) => setCompose({ ...compose, subject: event.target.value }) }), _jsx("textarea", { value: compose.body, onChange: (event) => setCompose({ ...compose, body: event.target.value }) }), _jsxs("footer", { children: [_jsx("button", { type: "button", onClick: saveDraft, children: "Save draft" }), _jsx("button", { className: "gmail-send", type: "submit", children: "Send" })] })] })) : null] }));
80
+ }
81
+ function header(message, name) {
82
+ return message.payload.headers.find((item) => item.name.toLowerCase() === name.toLowerCase())?.value ?? "";
83
+ }
84
+ function bodyText(message) {
85
+ return message.payload.body.data ? decodeBase64Url(message.payload.body.data) : message.snippet;
86
+ }
87
+ function matchesUiQuery(message, query) {
88
+ const target = [message.snippet, bodyText(message), ...message.payload.headers.map((item) => item.value)].join(" ").toLowerCase();
89
+ return target.includes(query.toLowerCase());
90
+ }
91
+ function formatDate(internalDate) {
92
+ return new Date(Number(internalDate)).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
93
+ }
@@ -0,0 +1,2 @@
1
+ import "./styles.css";
2
+ //# sourceMappingURL=dev.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/ui/dev.tsx"],"names":[],"mappings":"AAIA,OAAO,cAAc,CAAC"}
package/dist/ui/dev.js ADDED
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ import { createGmailMock, demoSeed } from "../index";
5
+ import { GmailMockApp } from "./GmailMockApp";
6
+ import "./styles.css";
7
+ const mock = createGmailMock({
8
+ now: "2026-06-01T16:30:00.000Z",
9
+ seed: demoSeed,
10
+ });
11
+ createRoot(document.getElementById("root")).render(_jsx(React.StrictMode, { children: _jsx(GmailMockApp, { mock: mock }) }));
@@ -0,0 +1,3 @@
1
+ export { GmailMockApp } from "./GmailMockApp";
2
+ export type { GmailMockAppProps } from "./GmailMockApp";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1 @@
1
+ export { GmailMockApp } from "./GmailMockApp";