@wealthx/shadcn 1.5.12 → 1.5.14

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 (40) hide show
  1. package/.turbo/turbo-build.log +79 -79
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-TC43SMIN.mjs → chunk-7N6O3VPJ.mjs} +6 -6
  4. package/dist/{chunk-CPM6P63C.mjs → chunk-GMF7INNS.mjs} +59 -30
  5. package/dist/chunk-KICT4VQL.mjs +508 -0
  6. package/dist/{chunk-BXL74CM2.mjs → chunk-UXWNUMQA.mjs} +4 -4
  7. package/dist/chunk-V23CBULF.mjs +432 -0
  8. package/dist/components/ui/appointment-calendar-view.js +177 -176
  9. package/dist/components/ui/appointment-calendar-view.mjs +1 -1
  10. package/dist/components/ui/bank-statement-document-table.js +6 -6
  11. package/dist/components/ui/bank-statement-document-table.mjs +1 -1
  12. package/dist/components/ui/bank-statement-generate-dialog.js +164 -77
  13. package/dist/components/ui/bank-statement-generate-dialog.mjs +2 -1
  14. package/dist/components/ui/bank-statement-pdf-viewer.js +4 -4
  15. package/dist/components/ui/bank-statement-pdf-viewer.mjs +1 -1
  16. package/dist/components/ui/resource-center/index.js +1030 -0
  17. package/dist/components/ui/resource-center/index.mjs +29 -0
  18. package/dist/index.js +556 -380
  19. package/dist/index.mjs +17 -15
  20. package/dist/styles.css +1 -1
  21. package/package.json +4 -4
  22. package/src/components/index.tsx +2 -0
  23. package/src/components/ui/appointment-calendar-view.tsx +211 -199
  24. package/src/components/ui/bank-statement-document-table.tsx +12 -6
  25. package/src/components/ui/bank-statement-generate-dialog.tsx +125 -97
  26. package/src/components/ui/bank-statement-pdf-viewer.tsx +4 -4
  27. package/src/components/ui/resource-center/index.tsx +35 -0
  28. package/src/components/ui/resource-center/resource-cards.tsx +218 -0
  29. package/src/components/ui/resource-center/resource-carousel.tsx +122 -0
  30. package/src/components/ui/resource-center/resource-center-header.tsx +95 -0
  31. package/src/components/ui/resource-center/resource-email-editor-dialog.tsx +131 -0
  32. package/src/components/ui/resource-center/resource-modal.tsx +76 -0
  33. package/src/components/ui/resource-center/types.ts +81 -0
  34. package/src/styles/styles-css.ts +1 -1
  35. package/tsup.config.ts +1 -1
  36. package/dist/chunk-IODGRCQG.mjs +0 -438
  37. package/dist/chunk-XYWEGBAA.mjs +0 -348
  38. package/dist/components/ui/resource-center.js +0 -748
  39. package/dist/components/ui/resource-center.mjs +0 -24
  40. package/src/components/ui/resource-center.tsx +0 -539
@@ -12,6 +12,7 @@ import {
12
12
  } from "@/components/ui/dialog";
13
13
  import { Field, FieldLabel } from "@/components/ui/field";
14
14
  import { Input } from "@/components/ui/input";
15
+ import { PaginationNavButtons } from "@/components/ui/pagination";
15
16
  import {
16
17
  Select,
17
18
  SelectContent,
@@ -41,7 +42,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
41
42
  * - Statement name (Input)
42
43
  * - Range preset: 90 / 180 / 365 days or Custom (ToggleGroup + DatePickers)
43
44
  * - Applicant type: primary / secondary (Select)
44
- * - Selected bank account IDs (Table with Checkboxes)
45
+ * - Selected bank account IDs (Table with Checkboxes, paginated at 5/page)
45
46
  *
46
47
  * Data handed in via props (consumer owns fetching):
47
48
  * - `bankAccounts` — pre-filtered for the current `applicantType`
@@ -119,6 +120,8 @@ const APPLICANT_TYPE_LABELS: Record<"primary" | "secondary", string> = {
119
120
  secondary: "Co-applicant",
120
121
  };
121
122
 
123
+ const ACCOUNTS_PAGE_SIZE = 5;
124
+
122
125
  // ---------------------------------------------------------------------------
123
126
  // Helpers
124
127
  // ---------------------------------------------------------------------------
@@ -161,18 +164,20 @@ export function BankStatementGenerateDialog({
161
164
  "primary" | "secondary" | ""
162
165
  >("");
163
166
  const [selectedAccountIds, setSelectedAccountIds] = useState<string[]>([]);
167
+ const [accountPage, setAccountPage] = useState(1);
164
168
 
165
169
  // Reset form whenever the dialog opens.
166
170
  useEffect(() => {
167
171
  if (!open) return;
168
172
  const timer = setTimeout(() => {
169
- setStatementName("Bank Statement 1");
170
- setApplicantType("");
171
- setSelectedAccountIds([]);
172
173
  const { from, to } = presetToDateRange(90);
174
+ setStatementName("Bank Statement 1");
173
175
  setRangePreset(90);
174
176
  setFromDate(from);
175
177
  setToDate(to);
178
+ setApplicantType("");
179
+ setSelectedAccountIds([]);
180
+ setAccountPage(1);
176
181
  }, 0);
177
182
  return () => clearTimeout(timer);
178
183
  }, [open]);
@@ -182,12 +187,17 @@ export function BankStatementGenerateDialog({
182
187
  const handleApplicantTypeChange = (type: "primary" | "secondary") => {
183
188
  setApplicantType(type);
184
189
  setSelectedAccountIds([]);
190
+ setAccountPage(1);
185
191
  onApplicantTypeChange?.(type);
186
192
  };
187
193
 
188
194
  // ── Date range preset ────────────────────────────────────────────────────
189
195
 
190
- const applyPreset = (preset: BankStatementRangePreset) => {
196
+ const handlePresetChange = (val: string[]) => {
197
+ if (val.length === 0) return;
198
+ const raw = val[0];
199
+ const preset: BankStatementRangePreset =
200
+ raw === "custom" ? "custom" : (Number(raw) as 90 | 180 | 365);
191
201
  setRangePreset(preset);
192
202
  if (preset !== "custom") {
193
203
  const { from, to } = presetToDateRange(preset);
@@ -196,12 +206,6 @@ export function BankStatementGenerateDialog({
196
206
  }
197
207
  };
198
208
 
199
- const handlePresetChange = (val: string[]) => {
200
- if (val.length === 0) return;
201
- const raw = val[0];
202
- applyPreset(raw === "custom" ? "custom" : (Number(raw) as 90 | 180 | 365));
203
- };
204
-
205
209
  // ── Account selection ────────────────────────────────────────────────────
206
210
 
207
211
  const areAllSelected =
@@ -216,13 +220,17 @@ export function BankStatementGenerateDialog({
216
220
  };
217
221
 
218
222
  const handleToggleAll = () => {
219
- if (areAllSelected) {
220
- setSelectedAccountIds([]);
221
- } else {
222
- setSelectedAccountIds(bankAccounts.map((a) => a.id));
223
- }
223
+ setSelectedAccountIds(areAllSelected ? [] : bankAccounts.map((a) => a.id));
224
224
  };
225
225
 
226
+ // ── Accounts pagination ──────────────────────────────────────────────────
227
+
228
+ const accountPageCount = Math.ceil(bankAccounts.length / ACCOUNTS_PAGE_SIZE);
229
+ const pagedAccounts = bankAccounts.slice(
230
+ (accountPage - 1) * ACCOUNTS_PAGE_SIZE,
231
+ accountPage * ACCOUNTS_PAGE_SIZE,
232
+ );
233
+
226
234
  // ── Period label for the accounts table ─────────────────────────────────
227
235
 
228
236
  const periodLabel = useMemo(() => {
@@ -347,94 +355,114 @@ export function BankStatementGenerateDialog({
347
355
  {/* Bank accounts table */}
348
356
  {applicantType && (
349
357
  <div className="mt-1">
350
- {isLoadingAccounts && (
358
+ {isLoadingAccounts ? (
351
359
  <div className="flex items-center justify-center p-6">
352
360
  <Spinner size="lg" />
353
361
  </div>
354
- )}
355
-
356
- {!isLoadingAccounts && bankAccounts.length === 0 && (
362
+ ) : bankAccounts.length === 0 ? (
357
363
  <p className="py-4 text-center text-body-medium text-muted-foreground">
358
364
  No bank accounts found for the selected applicant
359
365
  </p>
360
- )}
361
-
362
- {!isLoadingAccounts && bankAccounts.length > 0 && (
363
- <Table>
364
- <TableHeader>
365
- <TableRow>
366
- <TableHead className="w-10">
367
- <Checkbox
368
- checked={areAllSelected}
369
- indeterminate={isSomeSelected}
370
- onCheckedChange={handleToggleAll}
371
- aria-label="Select all accounts"
372
- />
373
- </TableHead>
374
- <TableHead>Account Name</TableHead>
375
- <TableHead>Account Number</TableHead>
376
- <TableHead>Period</TableHead>
377
- <TableHead>Last Updated</TableHead>
378
- </TableRow>
379
- </TableHeader>
380
- <TableBody>
381
- {bankAccounts.map((account) => {
382
- const isSelected = selectedAccountIds.includes(
383
- account.id,
384
- );
385
- return (
386
- <TableRow
387
- key={account.id}
388
- data-state={isSelected ? "selected" : undefined}
389
- >
390
- <TableCell>
391
- <Checkbox
392
- checked={isSelected}
393
- onCheckedChange={() =>
394
- handleToggleAccount(account.id)
395
- }
396
- aria-label={`Select ${account.name}`}
397
- />
398
- </TableCell>
399
- <TableCell>
400
- <div className="flex items-center gap-2">
401
- {account.institutionLogo && (
402
- <img
403
- src={account.institutionLogo}
404
- alt={account.institutionName ?? ""}
405
- className="size-8 object-cover"
406
- />
407
- )}
408
- <span className="text-body-medium font-semibold">
409
- {account.name || account.institutionName || "—"}
366
+ ) : (
367
+ <>
368
+ <Table className="table-fixed">
369
+ <TableHeader>
370
+ <TableRow>
371
+ <TableHead className="w-10">
372
+ <Checkbox
373
+ checked={areAllSelected}
374
+ indeterminate={isSomeSelected}
375
+ onCheckedChange={handleToggleAll}
376
+ aria-label="Select all accounts"
377
+ />
378
+ </TableHead>
379
+ <TableHead>Account Name</TableHead>
380
+ <TableHead className="w-40">Account Number</TableHead>
381
+ <TableHead className="w-52">Period</TableHead>
382
+ <TableHead className="w-28">Last Updated</TableHead>
383
+ </TableRow>
384
+ </TableHeader>
385
+ <TableBody>
386
+ {pagedAccounts.map((account) => {
387
+ const isSelected = selectedAccountIds.includes(
388
+ account.id,
389
+ );
390
+ return (
391
+ <TableRow
392
+ key={account.id}
393
+ data-state={isSelected ? "selected" : undefined}
394
+ >
395
+ <TableCell>
396
+ <Checkbox
397
+ checked={isSelected}
398
+ onCheckedChange={() =>
399
+ handleToggleAccount(account.id)
400
+ }
401
+ aria-label={`Select ${account.name}`}
402
+ />
403
+ </TableCell>
404
+ <TableCell>
405
+ <div className="flex min-w-0 items-center gap-2">
406
+ {account.institutionLogo && (
407
+ <img
408
+ src={account.institutionLogo}
409
+ alt={account.institutionName ?? ""}
410
+ className="size-8 shrink-0 object-cover"
411
+ />
412
+ )}
413
+ <span className="truncate text-body-medium font-semibold">
414
+ {account.name ||
415
+ account.institutionName ||
416
+ "—"}
417
+ </span>
418
+ </div>
419
+ </TableCell>
420
+ <TableCell>
421
+ <span className="truncate text-body-medium">
422
+ {account.accountNo ?? "—"}
423
+ </span>
424
+ </TableCell>
425
+ <TableCell>
426
+ <span className="truncate text-body-medium">
427
+ {periodLabel}
428
+ </span>
429
+ </TableCell>
430
+ <TableCell>
431
+ <span className="truncate text-body-medium">
432
+ {account.lastUpdated
433
+ ? format(
434
+ parseISO(account.lastUpdated),
435
+ "dd MMM yyyy",
436
+ )
437
+ : "—"}
410
438
  </span>
411
- </div>
412
- </TableCell>
413
- <TableCell>
414
- <span className="text-body-medium">
415
- {account.accountNo ?? "—"}
416
- </span>
417
- </TableCell>
418
- <TableCell>
419
- <span className="text-body-medium">
420
- {periodLabel}
421
- </span>
422
- </TableCell>
423
- <TableCell>
424
- <span className="text-body-medium">
425
- {account.lastUpdated
426
- ? format(
427
- parseISO(account.lastUpdated),
428
- "dd MMM yyyy",
429
- )
430
- : "—"}
431
- </span>
432
- </TableCell>
433
- </TableRow>
434
- );
435
- })}
436
- </TableBody>
437
- </Table>
439
+ </TableCell>
440
+ </TableRow>
441
+ );
442
+ })}
443
+ </TableBody>
444
+ </Table>
445
+ {accountPageCount > 1 && (
446
+ <div className="flex items-center justify-between pt-2">
447
+ <span className="text-body-small text-muted-foreground">
448
+ {(accountPage - 1) * ACCOUNTS_PAGE_SIZE + 1}
449
+ {Math.min(
450
+ accountPage * ACCOUNTS_PAGE_SIZE,
451
+ bankAccounts.length,
452
+ )}{" "}
453
+ of {bankAccounts.length} accounts
454
+ </span>
455
+ <PaginationNavButtons
456
+ hasPrev={accountPage > 1}
457
+ hasNext={accountPage < accountPageCount}
458
+ onFirst={() => setAccountPage(1)}
459
+ onPrev={() => setAccountPage((p) => p - 1)}
460
+ onNext={() => setAccountPage((p) => p + 1)}
461
+ onLast={() => setAccountPage(accountPageCount)}
462
+ />
463
+ </div>
464
+ )}
465
+ </>
438
466
  )}
439
467
  </div>
440
468
  )}
@@ -143,8 +143,8 @@ export function BankStatementPDFViewer({
143
143
  data-slot="bank-statement-pdf-viewer"
144
144
  >
145
145
  {/* Navigation bar */}
146
- <div className="flex shrink-0 items-center justify-between border-b border-border px-3 py-2">
147
- <div className="flex items-center gap-2">
146
+ <div className="flex min-w-0 shrink-0 items-center justify-between border-b border-border px-3 py-2">
147
+ <div className="flex min-w-0 items-center gap-2">
148
148
  <Button
149
149
  variant="ghost"
150
150
  size="icon-sm"
@@ -155,7 +155,7 @@ export function BankStatementPDFViewer({
155
155
  <ChevronLeft className="size-4" />
156
156
  </Button>
157
157
 
158
- <span className="text-body-medium tabular-nums">
158
+ <span className="shrink-0 text-body-medium tabular-nums">
159
159
  Account {currentAccountIndex + 1} of {accounts.length}
160
160
  </span>
161
161
 
@@ -169,7 +169,7 @@ export function BankStatementPDFViewer({
169
169
  <ChevronRight className="size-4" />
170
170
  </Button>
171
171
 
172
- <span className="ml-2 text-body-medium text-muted-foreground">
172
+ <span className="ml-2 min-w-0 truncate text-body-medium text-muted-foreground">
173
173
  {accountLabel}
174
174
  </span>
175
175
  </div>
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Resource Center — WealthX Backoffice
3
+ *
4
+ * Full-page resource hub with video cards, email template cards, document cards,
5
+ * a hero header, and a generic carousel container.
6
+ *
7
+ * Component hierarchy:
8
+ * Molecule → ResourceVideoCard, ResourceEmailTemplateCard, ResourceDocumentCard
9
+ * Organism → ResourceModal, ResourceEmailEditorDialog, ResourceCarousel, ResourceCenterHeader
10
+ * Template → (composed by consumers)
11
+ */
12
+
13
+ export type {
14
+ ResourceCarouselProps,
15
+ ResourceCenterHeaderProps,
16
+ ResourceDocumentCardProps,
17
+ ResourceDocumentItem,
18
+ ResourceEmailEditorDialogProps,
19
+ ResourceEmailTemplateCardProps,
20
+ ResourceEmailTemplateItem,
21
+ ResourceModalAttachment,
22
+ ResourceModalProps,
23
+ ResourceVideoCardProps,
24
+ ResourceVideoItem,
25
+ } from "./types";
26
+
27
+ export { ResourceCarousel } from "./resource-carousel";
28
+ export { ResourceCenterHeader } from "./resource-center-header";
29
+ export {
30
+ ResourceDocumentCard,
31
+ ResourceEmailTemplateCard,
32
+ ResourceVideoCard,
33
+ } from "./resource-cards";
34
+ export { ResourceEmailEditorDialog } from "./resource-email-editor-dialog";
35
+ export { ResourceModal } from "./resource-modal";
@@ -0,0 +1,218 @@
1
+ import React, { useState } from "react";
2
+ import { Download, Play } from "lucide-react";
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { Button } from "@/components/ui/button";
5
+ import { cn } from "@/lib/utils";
6
+ import { ResourceModal } from "./resource-modal";
7
+ import type {
8
+ ResourceVideoCardProps,
9
+ ResourceEmailTemplateCardProps,
10
+ ResourceDocumentCardProps,
11
+ } from "./types";
12
+
13
+ export function ResourceVideoCard({ video }: ResourceVideoCardProps) {
14
+ const [modalOpen, setModalOpen] = useState(false);
15
+
16
+ return (
17
+ <>
18
+ <button
19
+ type="button"
20
+ onClick={() => setModalOpen(true)}
21
+ className="group relative flex w-full flex-col gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
22
+ >
23
+ {/* Thumbnail */}
24
+ <div className="relative w-full overflow-hidden bg-muted aspect-video">
25
+ {video.thumbnailUrl ? (
26
+ <img
27
+ src={video.thumbnailUrl}
28
+ alt={video.title}
29
+ className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-105"
30
+ />
31
+ ) : (
32
+ <div className="flex h-full w-full items-center justify-center">
33
+ <Play className="size-10 text-muted-foreground opacity-40" />
34
+ </div>
35
+ )}
36
+
37
+ {/* Play overlay on hover */}
38
+ <div className="absolute inset-0 flex items-center justify-center bg-black/30 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
39
+ <div className="flex size-12 items-center justify-center bg-background/90">
40
+ <Play className="size-5 text-foreground" />
41
+ </div>
42
+ </div>
43
+
44
+ {/* Duration badge */}
45
+ {video.duration && (
46
+ <span className="absolute bottom-2 right-2 bg-black/70 px-1.5 py-0.5 text-caption text-white">
47
+ {video.duration}
48
+ </span>
49
+ )}
50
+ </div>
51
+
52
+ {/* Title */}
53
+ <p className="text-body-medium font-semibold text-foreground leading-snug line-clamp-2">
54
+ {video.title}
55
+ </p>
56
+ </button>
57
+
58
+ <ResourceModal
59
+ open={modalOpen}
60
+ onClose={() => setModalOpen(false)}
61
+ title={video.title}
62
+ videoUrl={video.videoUrl}
63
+ tags={video.tags}
64
+ attachments={video.attachments}
65
+ />
66
+ </>
67
+ );
68
+ }
69
+
70
+ export function ResourceEmailTemplateCard({
71
+ template,
72
+ onClick,
73
+ }: ResourceEmailTemplateCardProps) {
74
+ if (template.isAddTemplate) {
75
+ return (
76
+ <button
77
+ type="button"
78
+ onClick={onClick}
79
+ className={cn(
80
+ "flex w-full flex-col items-center justify-center gap-3",
81
+ "border-2 border-dashed border-border bg-muted/40",
82
+ "aspect-video hover:border-primary hover:bg-muted/70 transition-colors",
83
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
84
+ )}
85
+ >
86
+ <div className="flex size-10 items-center justify-center border-2 border-dashed border-muted-foreground/40">
87
+ <span className="text-2xl text-muted-foreground/60">+</span>
88
+ </div>
89
+ <p className="text-body-small font-medium text-muted-foreground">
90
+ Add Template
91
+ </p>
92
+ </button>
93
+ );
94
+ }
95
+
96
+ return (
97
+ <button
98
+ type="button"
99
+ onClick={onClick}
100
+ className="group relative flex w-full flex-col gap-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
101
+ >
102
+ {/* Preview area */}
103
+ <div className="relative w-full overflow-hidden border border-border bg-background aspect-video">
104
+ {template.htmlContent ? (
105
+ <iframe
106
+ srcDoc={template.htmlContent}
107
+ title={template.title}
108
+ className="h-full w-full pointer-events-none"
109
+ style={{
110
+ transform: "scale(0.5)",
111
+ transformOrigin: "top left",
112
+ width: "200%",
113
+ height: "200%",
114
+ }}
115
+ sandbox="allow-same-origin"
116
+ />
117
+ ) : (
118
+ <div className="flex h-full w-full items-center justify-center bg-muted">
119
+ <p className="text-caption text-muted-foreground">
120
+ No preview available
121
+ </p>
122
+ </div>
123
+ )}
124
+
125
+ {/* Hover overlay */}
126
+ <div className="absolute inset-0 flex items-center justify-center bg-foreground/60 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
127
+ <span className="text-body-small font-medium text-white">
128
+ Use Template
129
+ </span>
130
+ </div>
131
+ </div>
132
+
133
+ {/* Meta */}
134
+ <div className="flex items-center justify-between gap-2">
135
+ <p className="text-body-small font-medium text-foreground leading-snug line-clamp-1">
136
+ {template.title}
137
+ </p>
138
+ {template.type && (
139
+ <Badge variant="secondary" className="shrink-0">
140
+ {template.type === "system" ? "System" : "Company"}
141
+ </Badge>
142
+ )}
143
+ </div>
144
+ </button>
145
+ );
146
+ }
147
+
148
+ export function ResourceDocumentCard({
149
+ document,
150
+ onClick,
151
+ }: ResourceDocumentCardProps) {
152
+ return (
153
+ <div className="flex w-full flex-col gap-2">
154
+ {/* PDF preview */}
155
+ <button
156
+ type="button"
157
+ onClick={onClick}
158
+ className="group relative w-full overflow-hidden border border-border bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aspect-video"
159
+ >
160
+ {document.pdfUrl ? (
161
+ <iframe
162
+ src={document.pdfUrl}
163
+ title={document.title}
164
+ className="h-full w-full pointer-events-none"
165
+ />
166
+ ) : document.thumbnailUrl ? (
167
+ <img
168
+ src={document.thumbnailUrl}
169
+ alt={document.title}
170
+ className="h-full w-full object-cover"
171
+ />
172
+ ) : (
173
+ <div className="flex h-full w-full flex-col items-center justify-center gap-2">
174
+ <div className="flex items-center justify-center bg-muted-foreground/10 p-4">
175
+ <Download className="size-8 text-muted-foreground opacity-50" />
176
+ </div>
177
+ <span className="text-caption text-muted-foreground">
178
+ {document.title}
179
+ </span>
180
+ </div>
181
+ )}
182
+
183
+ <div className="absolute inset-0 bg-foreground/40 opacity-0 transition-opacity duration-200 group-hover:opacity-100" />
184
+ </button>
185
+
186
+ {/* Footer: tags + download */}
187
+ <div className="flex items-center justify-between gap-2">
188
+ <div className="flex flex-col gap-1 min-w-0">
189
+ <p className="text-body-small font-medium text-foreground leading-snug line-clamp-1">
190
+ {document.title}
191
+ </p>
192
+ {document.tags && document.tags.length > 0 && (
193
+ <div className="flex flex-wrap gap-1">
194
+ {document.tags.map((tag) => (
195
+ <Badge key={tag} variant="secondary">
196
+ {tag}
197
+ </Badge>
198
+ ))}
199
+ </div>
200
+ )}
201
+ </div>
202
+ {document.downloadUrl && (
203
+ <a
204
+ href={document.downloadUrl}
205
+ download
206
+ onClick={(e) => e.stopPropagation()}
207
+ className="shrink-0"
208
+ aria-label={`Download ${document.title}`}
209
+ >
210
+ <Button variant="outline" size="icon" className="size-8">
211
+ <Download className="size-4" />
212
+ </Button>
213
+ </a>
214
+ )}
215
+ </div>
216
+ </div>
217
+ );
218
+ }