@nocobase/portal-template-default 1.0.16 → 1.0.17

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 (150) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.MD +4 -0
  3. package/dist/assets/{LineDraw-vPRTWiQ5.js → LineDraw-BBB5hwuJ.js} +1 -1
  4. package/dist/assets/{SymbolDraw-DOl_Tss3.js → SymbolDraw-CKS3uUb_.js} +1 -1
  5. package/dist/assets/{VisualMapping-j08W3KBm.js → VisualMapping-BCwaW3TD.js} +1 -1
  6. package/dist/assets/auth-method-demo-BdCCvEEz.js +1 -0
  7. package/dist/assets/{auto-login-provider-Bukw7zEe.js → auto-login-provider-DO5Il6Dp.js} +2 -2
  8. package/dist/assets/auto-login-provider-TQ08B6-i.js +1 -0
  9. package/dist/assets/auto-redirect-provider-ButXp0A3.js +1 -0
  10. package/dist/assets/auto-redirect-provider-DnGGP2Vn.js +1 -0
  11. package/dist/assets/auto-redirect-provider-ja-vFPaF.js +1 -0
  12. package/dist/assets/calendar-C7xnRgOT.js +1 -0
  13. package/dist/assets/{cas-sign-in-button-BQn3Mttx.js → cas-sign-in-button-BZCa4lR6.js} +1 -1
  14. package/dist/assets/{components-BT5mL9gW.js → components-_g-GjQeV.js} +3 -3
  15. package/dist/assets/{configuration-gate-BJoadoA6.js → configuration-gate-B4XVz7tS.js} +1 -1
  16. package/dist/assets/{createClipPathFromCoordSys-m8IbnJ1z.js → createClipPathFromCoordSys-z9Kmh-tu.js} +1 -1
  17. package/dist/assets/createSeriesDataSimply-CPqysF0K.js +1 -0
  18. package/dist/assets/{customGraphicKeyframeAnimation-CYLPurwK.js → customGraphicKeyframeAnimation-DMU5crAu.js} +1 -1
  19. package/dist/assets/demo-BPlae2bp.js +1 -0
  20. package/dist/assets/demo-Bj_Y0RrW.js +1 -0
  21. package/dist/assets/demo-CKPAQOib.js +1 -0
  22. package/dist/assets/demo-CXRTPvIx.js +1 -0
  23. package/dist/assets/demo-DJuVDu1n.js +1 -0
  24. package/dist/assets/demo-DUmWU7ik.js +1 -0
  25. package/dist/assets/demo-NhIzFRE1.js +1 -0
  26. package/dist/assets/{dingtalk-sign-in-button-DSluFaP9.js → dingtalk-sign-in-button-DMPgE5kT.js} +1 -1
  27. package/dist/assets/{echarts-preview-mRXoEciY.js → echarts-preview-R96Pbqmz.js} +1 -1
  28. package/dist/assets/{echarts-runtime-CvLr4Slh.js → echarts-runtime-Ce_z2eX_.js} +3 -3
  29. package/dist/assets/{echarts-runtime-advanced-BC1tvfQ0.js → echarts-runtime-advanced-Dr80KHJR.js} +1 -1
  30. package/dist/assets/{echarts-runtime-common-DhNaV5B8.js → echarts-runtime-common-DB95Yfc1.js} +1 -1
  31. package/dist/assets/{echarts-runtime-components-BiioS8t1.js → echarts-runtime-components-DCJEaTJk.js} +1 -1
  32. package/dist/assets/{echarts-runtime-hierarchy-CAWqYvFh.js → echarts-runtime-hierarchy-CezgZs9O.js} +1 -1
  33. package/dist/assets/{floating-RVbHvIiD.js → floating-O9WmiDXk.js} +2 -2
  34. package/dist/assets/index-BBKV6O5d.css +1 -0
  35. package/dist/assets/{index-Dw20Lzpp.js → index-CPzehPGe.js} +3 -3
  36. package/dist/assets/index-Cc6awvdg.js +1064 -0
  37. package/dist/assets/index-ChA0z9ke.js +38 -0
  38. package/dist/assets/{index-BysibBZu.js → index-DgB7uTMs.js} +2 -2
  39. package/dist/assets/{index-C-tdHirk.js → index-DroUAK80.js} +1 -1
  40. package/dist/assets/index-WKvygTBp.js +59 -0
  41. package/dist/assets/{labelHelper-DbITmOUW.js → labelHelper-Dk5jkiD-.js} +1 -1
  42. package/dist/assets/ldap-sign-in-form-CKN4l7qM.js +1 -0
  43. package/dist/assets/{oidc-sign-in-button-Cgpa8Qzj.js → oidc-sign-in-button-BIRonrpC.js} +1 -1
  44. package/dist/assets/{page-context-AU6RU0Qi.js → page-context-DAiW_dId.js} +4 -4
  45. package/dist/assets/prompt-card-rGZqpqkF.js +1 -0
  46. package/dist/assets/prompt-output-B4h3uLWf.js +1 -0
  47. package/dist/assets/saml-sign-in-button-B4ugaLIb.js +1 -0
  48. package/dist/assets/{sectorHelper-C6L782Ue.js → sectorHelper-BuxrrJrA.js} +1 -1
  49. package/dist/assets/shortcut-BhKXxJLs.js +47 -0
  50. package/dist/assets/{sms-sign-in-form-Bo2WLZuP.js → sms-sign-in-form-DKpxRDdZ.js} +1 -1
  51. package/dist/assets/{static.browser-CoPxsICt.js → static.browser-kzzpH0Xt.js} +1 -1
  52. package/dist/assets/switch-BdVxPIVU.js +1 -0
  53. package/dist/assets/{tool-cards-ipgJ_3Ec.js → tool-cards-CpczIke-.js} +3 -3
  54. package/dist/assets/{use-authenticator-sign-in-BzFeh7qe.js → use-authenticator-sign-in-CIj210ry.js} +1 -1
  55. package/dist/assets/use-dingtalk-sign-in-BW4kkXD3.js +1 -0
  56. package/dist/assets/{use-wecom-sign-in-PPIMLvGa.js → use-wecom-sign-in-dIqMtcN9.js} +1 -1
  57. package/dist/assets/{wecom-sign-in-button-rUq9RW8R.js → wecom-sign-in-button-Dn_2zDab.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/dist/index.raw.html +2 -2
  60. package/package.json +9 -1
  61. package/src/App.tsx +5 -2
  62. package/src/app/extension.ts +2 -0
  63. package/src/app/extensions.tsx +18 -6
  64. package/src/app/route-access-guard.tsx +22 -0
  65. package/src/app/route-runtime.ts +173 -0
  66. package/src/app/routes.tsx +2 -2
  67. package/src/components/app-shell/header.tsx +56 -1
  68. package/src/components/app-shell/sidebar.tsx +14 -2
  69. package/src/extensions/nocobase-acl/README.md +28 -14
  70. package/src/extensions/nocobase-acl/demo/index.tsx +87 -15
  71. package/src/extensions/nocobase-acl/demo/prompt-generator.tsx +1 -0
  72. package/src/extensions/nocobase-mail/README.md +74 -0
  73. package/src/extensions/nocobase-mail/components/index.ts +79 -0
  74. package/src/extensions/nocobase-mail/components/mail-api.ts +423 -0
  75. package/src/extensions/nocobase-mail/components/mail-attachment-list.tsx +67 -0
  76. package/src/extensions/nocobase-mail/components/mail-compose-attachments.tsx +166 -0
  77. package/src/extensions/nocobase-mail/components/mail-compose.tsx +1071 -0
  78. package/src/extensions/nocobase-mail/components/mail-detail.tsx +653 -0
  79. package/src/extensions/nocobase-mail/components/mail-draft-autosave.ts +26 -0
  80. package/src/extensions/nocobase-mail/components/mail-empty.tsx +48 -0
  81. package/src/extensions/nocobase-mail/components/mail-filters.tsx +277 -0
  82. package/src/extensions/nocobase-mail/components/mail-inbox.tsx +544 -0
  83. package/src/extensions/nocobase-mail/components/mail-inline-images.ts +69 -0
  84. package/src/extensions/nocobase-mail/components/mail-label-badge.tsx +25 -0
  85. package/src/extensions/nocobase-mail/components/mail-label-manager.tsx +271 -0
  86. package/src/extensions/nocobase-mail/components/mail-labels-editor.tsx +267 -0
  87. package/src/extensions/nocobase-mail/components/mail-mass-tracking.tsx +271 -0
  88. package/src/extensions/nocobase-mail/components/mail-note-editor.tsx +99 -0
  89. package/src/extensions/nocobase-mail/components/mail-recipient-input.tsx +260 -0
  90. package/src/extensions/nocobase-mail/components/mail-reply-quote.ts +28 -0
  91. package/src/extensions/nocobase-mail/components/mail-rich-editor.tsx +287 -0
  92. package/src/extensions/nocobase-mail/components/mail-send-actions.tsx +238 -0
  93. package/src/extensions/nocobase-mail/components/mail-senders.ts +61 -0
  94. package/src/extensions/nocobase-mail/components/mail-settings-drawer.tsx +708 -0
  95. package/src/extensions/nocobase-mail/components/mail-showcase-layout.tsx +64 -0
  96. package/src/extensions/nocobase-mail/components/mail-signature-manager.tsx +258 -0
  97. package/src/extensions/nocobase-mail/components/mail-signatures.ts +25 -0
  98. package/src/extensions/nocobase-mail/components/mail-table.tsx +476 -0
  99. package/src/extensions/nocobase-mail/components/mail-template-manager.tsx +204 -0
  100. package/src/extensions/nocobase-mail/components/mail-toolbar.tsx +155 -0
  101. package/src/extensions/nocobase-mail/components/mail-unread.tsx +111 -0
  102. package/src/extensions/nocobase-mail/components/types.ts +297 -0
  103. package/src/extensions/nocobase-mail/components/use-mail-compose.tsx +262 -0
  104. package/src/extensions/nocobase-mail/components/use-mail-mass-messages.ts +54 -0
  105. package/src/extensions/nocobase-mail/components/use-mail-messages.ts +113 -0
  106. package/src/extensions/nocobase-mail/components/use-mail-signatures.ts +122 -0
  107. package/src/extensions/nocobase-mail/components/use-mail-templates.ts +54 -0
  108. package/src/extensions/nocobase-mail/extension.tsx +105 -0
  109. package/src/extensions/nocobase-mail/index.ts +1 -0
  110. package/src/extensions/nocobase-mail/mail-demo-pages.tsx +503 -0
  111. package/src/extensions/nocobase-mail/mail-pages.tsx +201 -0
  112. package/src/extensions/nocobase-mail/tests/mail-quote-html-regression.mjs +51 -0
  113. package/src/extensions/nocobase-mail/tests/mail-regression.mjs +649 -0
  114. package/src/extensions/nocobase-users-example/app-routes.tsx +121 -0
  115. package/src/extensions/nocobase-users-example/extension.tsx +2 -100
  116. package/src/extensions/nocobase-users-example/index.ts +1 -0
  117. package/src/lib/nocobase/acl/menu.ts +17 -3
  118. package/src/lib/nocobase/acl/types.ts +4 -0
  119. package/src/locales/en-US.ts +2 -1
  120. package/src/locales/zh-CN.ts +2 -1
  121. package/src/providers/runtime-config.ts +18 -0
  122. package/src/routes.tsx +10 -0
  123. package/src/vite-env.d.ts +3 -0
  124. package/vite.config.ts +8 -0
  125. package/dist/assets/auth-method-demo-B2qcpY8z.js +0 -1
  126. package/dist/assets/auto-login-provider-BMtzvdxG.js +0 -1
  127. package/dist/assets/auto-redirect-provider-C6GNPg5r.js +0 -1
  128. package/dist/assets/auto-redirect-provider-D5c6kN2R.js +0 -1
  129. package/dist/assets/auto-redirect-provider-iTzGEDpU.js +0 -1
  130. package/dist/assets/calendar-Dv2RNnsr.js +0 -1
  131. package/dist/assets/card-C9Hpeq1c.js +0 -1
  132. package/dist/assets/createSeriesDataSimply-DKUV5b5e.js +0 -1
  133. package/dist/assets/demo-BWGOwqtJ.js +0 -1
  134. package/dist/assets/demo-BWbcvkya.js +0 -1
  135. package/dist/assets/demo-DIASSPgn.js +0 -1
  136. package/dist/assets/demo-DuZ6asUU.js +0 -1
  137. package/dist/assets/demo-Dxcy4bDE.js +0 -1
  138. package/dist/assets/demo-YpEC76uw.js +0 -1
  139. package/dist/assets/demo-wzkoR8T2.js +0 -1
  140. package/dist/assets/index-CBZbj45X.js +0 -58
  141. package/dist/assets/index-CgkdZ6Oc.js +0 -686
  142. package/dist/assets/index-NH3aqqQz.css +0 -1
  143. package/dist/assets/index-hSBzW-VA.js +0 -43
  144. package/dist/assets/ldap-sign-in-form-BOMNDTkK.js +0 -1
  145. package/dist/assets/prompt-card-FWugRgi-.js +0 -1
  146. package/dist/assets/prompt-output-DMTuTNab.js +0 -1
  147. package/dist/assets/saml-sign-in-button-qt8bMeoQ.js +0 -1
  148. package/dist/assets/shortcut-Bu7jT_-w.js +0 -47
  149. package/dist/assets/switch-DQkkIE7G.js +0 -1
  150. package/dist/assets/use-dingtalk-sign-in-mrq6K5Yx.js +0 -1
@@ -0,0 +1 @@
1
+ export * from "./components";
@@ -0,0 +1,503 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ ListFilter,
4
+ MessagesSquare,
5
+ PanelsLeftRight,
6
+ PenLine,
7
+ RefreshCw,
8
+ Send,
9
+ Users,
10
+ } from "lucide-react";
11
+ import { Link } from "react-router";
12
+ import {
13
+ MailInbox,
14
+ MailUnreadIndicator,
15
+ mailApi,
16
+ useMailUnread,
17
+ useMailCompose,
18
+ type MailColumnId,
19
+ type MailScope,
20
+ type MailUserRecord,
21
+ } from "./components";
22
+ import {
23
+ MailShowcasePage,
24
+ MailShowcaseSection,
25
+ } from "./components/mail-showcase-layout";
26
+ import { Button } from "@/components/ui/button";
27
+ import { Checkbox } from "@/components/ui/checkbox";
28
+ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
29
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
30
+ import {
31
+ Sheet,
32
+ SheetContent,
33
+ SheetDescription,
34
+ SheetHeader,
35
+ SheetTitle,
36
+ } from "@/components/ui/sheet";
37
+ import { Skeleton } from "@/components/ui/skeleton";
38
+ import {
39
+ Table,
40
+ TableBody,
41
+ TableCell,
42
+ TableHead,
43
+ TableHeader,
44
+ TableRow,
45
+ } from "@/components/ui/table";
46
+
47
+ const PERSONAL_COLUMNS: MailColumnId[] = [
48
+ "from",
49
+ "to",
50
+ "subject",
51
+ "boxType",
52
+ "date",
53
+ "isRead",
54
+ "labels",
55
+ ];
56
+ const ALL_COLUMNS: MailColumnId[] = [
57
+ "from",
58
+ "user",
59
+ "email",
60
+ "subject",
61
+ "boxType",
62
+ "date",
63
+ "isRead",
64
+ ];
65
+
66
+ const scenarios = [
67
+ {
68
+ title: "My mailbox",
69
+ description:
70
+ "A personal mailbox with folders, filters, and message actions.",
71
+ path: "workspace",
72
+ icon: PanelsLeftRight,
73
+ },
74
+ {
75
+ title: "Mailbox views",
76
+ description:
77
+ "Switch between the current user's inbox and all connected mailboxes.",
78
+ path: "personal",
79
+ icon: Users,
80
+ },
81
+ {
82
+ title: "Unread indicator",
83
+ description:
84
+ "Show a live unread count in navigation, buttons, or mailbox summaries.",
85
+ path: "unread",
86
+ icon: MessagesSquare,
87
+ },
88
+ {
89
+ title: "Compose page",
90
+ description: "A standalone compose route with query-string prefilling.",
91
+ path: "compose",
92
+ icon: PenLine,
93
+ },
94
+ {
95
+ title: "Correspondence per user",
96
+ description:
97
+ "Select a user and inspect all messages exchanged with that address.",
98
+ path: "filtered",
99
+ icon: ListFilter,
100
+ },
101
+ ];
102
+
103
+ function useMailUsers() {
104
+ const [users, setUsers] = useState<MailUserRecord[]>([]);
105
+ const [loading, setLoading] = useState(true);
106
+
107
+ useEffect(() => {
108
+ let active = true;
109
+ mailApi
110
+ .getUsers()
111
+ .then((next) => active && setUsers(next))
112
+ .catch(() => active && setUsers([]))
113
+ .finally(() => active && setLoading(false));
114
+ return () => {
115
+ active = false;
116
+ };
117
+ }, []);
118
+
119
+ return { users, loading };
120
+ }
121
+
122
+ function userName(user: MailUserRecord) {
123
+ return user.nickname || user.username || user.email || `User #${user.id}`;
124
+ }
125
+
126
+ function userInitials(user: MailUserRecord) {
127
+ return userName(user)
128
+ .split(/[\s@._-]+/)
129
+ .filter(Boolean)
130
+ .slice(0, 2)
131
+ .map((part) => part[0]?.toUpperCase())
132
+ .join("");
133
+ }
134
+
135
+ export function MailScenarioOverview() {
136
+ return (
137
+ <MailShowcasePage
138
+ title="Mail integration scenarios"
139
+ description="Compose and record-driven sending share one page, while the other examples cover personal mail, audience, and correspondence patterns."
140
+ badge="Overview"
141
+ >
142
+ <MailShowcaseSection
143
+ eyebrow="Component patterns"
144
+ title="Choose a mail workflow to explore"
145
+ description="Each example uses the same public mail components with a different page-level composition."
146
+ >
147
+ <div className="grid gap-4 md:grid-cols-2">
148
+ {scenarios.map((scenario, index) => {
149
+ const Icon = scenario.icon;
150
+ return (
151
+ <Link
152
+ key={scenario.path}
153
+ to={scenario.path}
154
+ className="group flex gap-4 rounded-xl border bg-card p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)] transition-colors hover:border-primary/35 hover:bg-muted/30"
155
+ >
156
+ <span className="font-mono text-2xl font-semibold text-muted-foreground/40">
157
+ {String(index + 1).padStart(2, "0")}
158
+ </span>
159
+ <Icon className="mt-1 size-5 text-muted-foreground group-hover:text-primary" />
160
+ <span>
161
+ <span className="block font-semibold">{scenario.title}</span>
162
+ <span className="mt-1 block text-sm leading-5 text-muted-foreground">
163
+ {scenario.description}
164
+ </span>
165
+ </span>
166
+ </Link>
167
+ );
168
+ })}
169
+ </div>
170
+ </MailShowcaseSection>
171
+ </MailShowcasePage>
172
+ );
173
+ }
174
+
175
+ export function MailAudienceScenario() {
176
+ const [scope, setScope] = useState<MailScope>("personal");
177
+ return (
178
+ <MailShowcasePage
179
+ title="Mailbox views"
180
+ description="Switch between the signed-in user's inbox and all connected mailboxes."
181
+ badge="Mailbox scope"
182
+ >
183
+ <MailShowcaseSection
184
+ eyebrow="Interactive preview"
185
+ title="Personal and administrative mailbox views"
186
+ description="The audience switch stays in the inbox action row while the table columns adapt to the selected scope."
187
+ >
188
+ <MailInbox
189
+ scope={scope}
190
+ columns={scope === "all" ? ALL_COLUMNS : PERSONAL_COLUMNS}
191
+ toolbarActions={
192
+ <Tabs
193
+ value={scope}
194
+ onValueChange={(value) => setScope(value as MailScope)}
195
+ >
196
+ <TabsList>
197
+ <TabsTrigger value="personal">My inbox</TabsTrigger>
198
+ <TabsTrigger value="all">All users</TabsTrigger>
199
+ </TabsList>
200
+ </Tabs>
201
+ }
202
+ />
203
+ </MailShowcaseSection>
204
+ </MailShowcasePage>
205
+ );
206
+ }
207
+
208
+ export function MailUnreadScenario() {
209
+ const { count, refresh } = useMailUnread();
210
+
211
+ return (
212
+ <MailShowcasePage
213
+ title="Unread indicator"
214
+ description="A reusable unread-count indicator that refreshes automatically with the mailbox."
215
+ badge="Live status"
216
+ >
217
+ <MailShowcaseSection
218
+ eyebrow="Interactive preview"
219
+ title="Unread mail at a glance"
220
+ description="The count refreshes every 60 seconds and whenever the browser window becomes active."
221
+ >
222
+ <div className="grid gap-4 md:grid-cols-2">
223
+ <div className="flex min-h-40 flex-col justify-between rounded-xl border bg-card p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
224
+ <div>
225
+ <p className="text-sm font-semibold">Icon indicator</p>
226
+ <p className="mt-1 text-sm text-muted-foreground">
227
+ Use the compact form in navigation and action areas.
228
+ </p>
229
+ </div>
230
+ <div className="flex items-end justify-between">
231
+ <div className="rounded-lg border bg-background p-3">
232
+ <MailUnreadIndicator showZero />
233
+ </div>
234
+ <span className="text-3xl font-semibold tabular-nums">{count}</span>
235
+ </div>
236
+ </div>
237
+
238
+ <div className="flex min-h-40 flex-col justify-between rounded-xl border bg-card p-5 shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
239
+ <div>
240
+ <p className="text-sm font-semibold">Labeled indicator</p>
241
+ <p className="mt-1 text-sm text-muted-foreground">
242
+ Add a label when the component needs more context.
243
+ </p>
244
+ </div>
245
+ <div className="flex items-center justify-between">
246
+ <MailUnreadIndicator label="Unread mail" showZero />
247
+ <Button variant="outline" size="sm" onClick={refresh}>
248
+ <RefreshCw />
249
+ Refresh
250
+ </Button>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </MailShowcaseSection>
255
+ </MailShowcasePage>
256
+ );
257
+ }
258
+
259
+ export function MailCorrespondenceScenario() {
260
+ const { users, loading } = useMailUsers();
261
+ const [selectedUser, setSelectedUser] = useState<MailUserRecord>();
262
+ return (
263
+ <MailShowcasePage
264
+ title="Correspondence per user"
265
+ description="Open a user from the directory to inspect the messages owned by that user's connected mailbox."
266
+ badge="Record action"
267
+ >
268
+ <MailShowcaseSection
269
+ eyebrow="User directory"
270
+ title="Open mailbox correspondence from a business record"
271
+ description="The user table owns the action entry; the reusable inbox opens in a side sheet scoped to that user."
272
+ >
273
+ <div className="overflow-hidden rounded-xl border bg-card shadow-[0_1px_2px_rgba(0,0,0,0.04)]">
274
+ <div className="flex items-center gap-2 border-b border-border/60 px-5 py-4">
275
+ <Users className="size-4 text-muted-foreground" />
276
+ <div>
277
+ <h3 className="text-sm font-semibold">Users</h3>
278
+ <p className="mt-0.5 text-xs text-muted-foreground">
279
+ {loading ? "Loading…" : `${users.length} people`}
280
+ </p>
281
+ </div>
282
+ </div>
283
+ <Table>
284
+ <TableHeader className="bg-muted/45">
285
+ <TableRow>
286
+ <TableHead className="w-[45%]">Name</TableHead>
287
+ <TableHead>Email</TableHead>
288
+ <TableHead className="text-right">Actions</TableHead>
289
+ </TableRow>
290
+ </TableHeader>
291
+ <TableBody>
292
+ {loading
293
+ ? Array.from({ length: 4 }).map((_, index) => (
294
+ <TableRow key={index}>
295
+ <TableCell>
296
+ <Skeleton className="h-9 w-44" />
297
+ </TableCell>
298
+ <TableCell>
299
+ <Skeleton className="h-4 w-48" />
300
+ </TableCell>
301
+ <TableCell>
302
+ <Skeleton className="ml-auto h-8 w-32" />
303
+ </TableCell>
304
+ </TableRow>
305
+ ))
306
+ : users.map((user) => (
307
+ <TableRow key={user.id}>
308
+ <TableCell>
309
+ <div className="flex items-center gap-3">
310
+ <Avatar>
311
+ <AvatarFallback className="bg-primary/10 text-xs font-semibold text-primary">
312
+ {userInitials(user)}
313
+ </AvatarFallback>
314
+ </Avatar>
315
+ <div className="min-w-0">
316
+ <div className="truncate text-sm font-medium">
317
+ {userName(user)}
318
+ </div>
319
+ {user.username && (
320
+ <div className="truncate text-xs text-muted-foreground">
321
+ @{user.username}
322
+ </div>
323
+ )}
324
+ </div>
325
+ </div>
326
+ </TableCell>
327
+ <TableCell className="text-sm text-muted-foreground">
328
+ {user.email || "—"}
329
+ </TableCell>
330
+ <TableCell className="text-right">
331
+ <Button
332
+ variant="outline"
333
+ size="sm"
334
+ disabled={!user.email}
335
+ onClick={() => setSelectedUser(user)}
336
+ >
337
+ <MessagesSquare /> Correspondence
338
+ </Button>
339
+ </TableCell>
340
+ </TableRow>
341
+ ))}
342
+ </TableBody>
343
+ </Table>
344
+ </div>
345
+ </MailShowcaseSection>
346
+
347
+ <Sheet
348
+ open={Boolean(selectedUser)}
349
+ onOpenChange={(open) => !open && setSelectedUser(undefined)}
350
+ >
351
+ <SheetContent
352
+ side="right"
353
+ className="flex w-full flex-col gap-0 p-0 data-[side=right]:sm:max-w-4xl"
354
+ >
355
+ {selectedUser?.email && (
356
+ <>
357
+ <SheetHeader className="flex-row items-center gap-3 border-b border-border/60 px-6 py-4">
358
+ <Avatar>
359
+ <AvatarFallback className="bg-primary/10 text-xs font-semibold text-primary">
360
+ {userInitials(selectedUser)}
361
+ </AvatarFallback>
362
+ </Avatar>
363
+ <div className="min-w-0">
364
+ <SheetTitle className="truncate">
365
+ {userName(selectedUser)}
366
+ </SheetTitle>
367
+ <SheetDescription className="truncate">
368
+ {selectedUser.email}
369
+ </SheetDescription>
370
+ </div>
371
+ </SheetHeader>
372
+ <div className="flex-1 overflow-y-auto p-4">
373
+ <MailInbox
374
+ key={selectedUser.id}
375
+ scope="all"
376
+ userId={selectedUser.id}
377
+ columns={PERSONAL_COLUMNS}
378
+ />
379
+ </div>
380
+ </>
381
+ )}
382
+ </SheetContent>
383
+ </Sheet>
384
+ </MailShowcasePage>
385
+ );
386
+ }
387
+
388
+ export function MailRecipientSelectionDemo() {
389
+ const { users, loading } = useMailUsers();
390
+ const { openCompose, composeDialog } = useMailCompose();
391
+ const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
392
+ const eligibleUsers = users.filter((user) => Boolean(user.email));
393
+ const selectedUsers = eligibleUsers.filter((user) =>
394
+ selectedIds.has(user.id)
395
+ );
396
+ const allSelected =
397
+ eligibleUsers.length > 0 && selectedUsers.length === eligibleUsers.length;
398
+
399
+ const selectUser = (id: number, selected: boolean) => {
400
+ setSelectedIds((current) => {
401
+ const next = new Set(current);
402
+ if (selected) next.add(id);
403
+ else next.delete(id);
404
+ return next;
405
+ });
406
+ };
407
+
408
+ return (
409
+ <>
410
+ <div className="overflow-hidden rounded-xl border bg-card">
411
+ <div className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
412
+ <span className="text-sm text-muted-foreground">
413
+ {selectedUsers.length
414
+ ? `${selectedUsers.length} selected`
415
+ : "Select users to prefill recipients"}
416
+ </span>
417
+ <Button
418
+ size="sm"
419
+ disabled={!selectedUsers.length}
420
+ onClick={() =>
421
+ openCompose({
422
+ to: selectedUsers.map((user) => user.email).join(", "),
423
+ subject:
424
+ selectedUsers.length === 1
425
+ ? `Following up, ${userName(selectedUsers[0])}`
426
+ : undefined,
427
+ })
428
+ }
429
+ >
430
+ <Send /> Email selected
431
+ </Button>
432
+ </div>
433
+ {loading ? (
434
+ <p className="p-8 text-center text-sm text-muted-foreground">
435
+ Loading users…
436
+ </p>
437
+ ) : (
438
+ <Table>
439
+ <TableHeader>
440
+ <TableRow>
441
+ <TableHead className="w-12">
442
+ <Checkbox
443
+ checked={allSelected}
444
+ onCheckedChange={(checked) =>
445
+ setSelectedIds(
446
+ checked
447
+ ? new Set(eligibleUsers.map((user) => user.id))
448
+ : new Set()
449
+ )
450
+ }
451
+ aria-label="Select all users with email addresses"
452
+ />
453
+ </TableHead>
454
+ <TableHead>User</TableHead>
455
+ <TableHead>Email</TableHead>
456
+ <TableHead className="w-36 text-right">Actions</TableHead>
457
+ </TableRow>
458
+ </TableHeader>
459
+ <TableBody>
460
+ {users.map((user) => (
461
+ <TableRow key={user.id}>
462
+ <TableCell>
463
+ <Checkbox
464
+ checked={selectedIds.has(user.id)}
465
+ disabled={!user.email}
466
+ onCheckedChange={(checked) =>
467
+ selectUser(user.id, Boolean(checked))
468
+ }
469
+ aria-label={`Select ${userName(user)}`}
470
+ />
471
+ </TableCell>
472
+ <TableCell className="font-medium">
473
+ {userName(user)}
474
+ </TableCell>
475
+ <TableCell className="text-muted-foreground">
476
+ {user.email || "No email address"}
477
+ </TableCell>
478
+ <TableCell className="text-right">
479
+ <Button
480
+ variant="outline"
481
+ size="sm"
482
+ disabled={!user.email}
483
+ onClick={() =>
484
+ openCompose({
485
+ to: user.email,
486
+ subject: `Following up, ${userName(user)}`,
487
+ body: `Hi ${userName(user)},<p></p>`,
488
+ })
489
+ }
490
+ >
491
+ <Send /> Send email
492
+ </Button>
493
+ </TableCell>
494
+ </TableRow>
495
+ ))}
496
+ </TableBody>
497
+ </Table>
498
+ )}
499
+ </div>
500
+ {composeDialog}
501
+ </>
502
+ );
503
+ }