@wealthx/shadcn 1.5.11 → 1.5.13

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 (36) hide show
  1. package/.turbo/turbo-build.log +88 -88
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-O5CP6VP6.mjs → chunk-BF5FKUF6.mjs} +104 -63
  4. package/dist/{chunk-ZMTCMP2G.mjs → chunk-EB626HVW.mjs} +70 -3
  5. package/dist/chunk-KICT4VQL.mjs +508 -0
  6. package/dist/chunk-V23CBULF.mjs +432 -0
  7. package/dist/components/ui/ai-conversations.js +70 -3
  8. package/dist/components/ui/ai-conversations.mjs +1 -1
  9. package/dist/components/ui/appointment-calendar-view.js +177 -176
  10. package/dist/components/ui/appointment-calendar-view.mjs +1 -1
  11. package/dist/components/ui/bank-statement-generate-dialog.js +209 -107
  12. package/dist/components/ui/bank-statement-generate-dialog.mjs +2 -1
  13. package/dist/components/ui/resource-center/index.js +1030 -0
  14. package/dist/components/ui/resource-center/index.mjs +29 -0
  15. package/dist/index.js +661 -403
  16. package/dist/index.mjs +16 -14
  17. package/dist/styles.css +1 -1
  18. package/package.json +4 -4
  19. package/src/components/index.tsx +2 -0
  20. package/src/components/ui/ai-conversations.tsx +157 -23
  21. package/src/components/ui/appointment-calendar-view.tsx +211 -199
  22. package/src/components/ui/bank-statement-generate-dialog.tsx +147 -96
  23. package/src/components/ui/resource-center/index.tsx +35 -0
  24. package/src/components/ui/resource-center/resource-cards.tsx +218 -0
  25. package/src/components/ui/resource-center/resource-carousel.tsx +122 -0
  26. package/src/components/ui/resource-center/resource-center-header.tsx +95 -0
  27. package/src/components/ui/resource-center/resource-email-editor-dialog.tsx +131 -0
  28. package/src/components/ui/resource-center/resource-modal.tsx +76 -0
  29. package/src/components/ui/resource-center/types.ts +81 -0
  30. package/src/styles/styles-css.ts +1 -1
  31. package/tsup.config.ts +1 -1
  32. package/dist/chunk-IODGRCQG.mjs +0 -438
  33. package/dist/chunk-XYWEGBAA.mjs +0 -348
  34. package/dist/components/ui/resource-center.js +0 -748
  35. package/dist/components/ui/resource-center.mjs +0 -24
  36. package/src/components/ui/resource-center.tsx +0 -539
@@ -1,6 +1,5 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
2
  import { format, parseISO, subDays } from "date-fns";
3
- import { cn } from "@/lib/utils";
4
3
  import { Button } from "@/components/ui/button";
5
4
  import { Checkbox } from "@/components/ui/checkbox";
6
5
  import { DatePicker } from "@/components/ui/date-picker";
@@ -13,6 +12,7 @@ import {
13
12
  } from "@/components/ui/dialog";
14
13
  import { Field, FieldLabel } from "@/components/ui/field";
15
14
  import { Input } from "@/components/ui/input";
15
+ import { PaginationNavButtons } from "@/components/ui/pagination";
16
16
  import {
17
17
  Select,
18
18
  SelectContent,
@@ -42,7 +42,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
42
42
  * - Statement name (Input)
43
43
  * - Range preset: 90 / 180 / 365 days or Custom (ToggleGroup + DatePickers)
44
44
  * - Applicant type: primary / secondary (Select)
45
- * - Selected bank account IDs (Table with Checkboxes)
45
+ * - Selected bank account IDs (Table with Checkboxes, paginated at 5/page)
46
46
  *
47
47
  * Data handed in via props (consumer owns fetching):
48
48
  * - `bankAccounts` — pre-filtered for the current `applicantType`
@@ -111,6 +111,17 @@ export interface BankStatementGenerateDialogProps {
111
111
  className?: string;
112
112
  }
113
113
 
114
+ // ---------------------------------------------------------------------------
115
+ // Constants
116
+ // ---------------------------------------------------------------------------
117
+
118
+ const APPLICANT_TYPE_LABELS: Record<"primary" | "secondary", string> = {
119
+ primary: "Main applicant",
120
+ secondary: "Co-applicant",
121
+ };
122
+
123
+ const ACCOUNTS_PAGE_SIZE = 5;
124
+
114
125
  // ---------------------------------------------------------------------------
115
126
  // Helpers
116
127
  // ---------------------------------------------------------------------------
@@ -144,24 +155,29 @@ export function BankStatementGenerateDialog({
144
155
  }: BankStatementGenerateDialogProps) {
145
156
  const [statementName, setStatementName] = useState("Bank Statement 1");
146
157
  const [rangePreset, setRangePreset] = useState<BankStatementRangePreset>(90);
147
- const [fromDate, setFromDate] = useState<string>("");
148
- const [toDate, setToDate] = useState<string>("");
158
+ // Initialise immediately so the Period column never shows "" on first open.
159
+ const [fromDate, setFromDate] = useState<string>(
160
+ () => presetToDateRange(90).from,
161
+ );
162
+ const [toDate, setToDate] = useState<string>(() => presetToDateRange(90).to);
149
163
  const [applicantType, setApplicantType] = useState<
150
164
  "primary" | "secondary" | ""
151
165
  >("");
152
166
  const [selectedAccountIds, setSelectedAccountIds] = useState<string[]>([]);
167
+ const [accountPage, setAccountPage] = useState(1);
153
168
 
154
169
  // Reset form whenever the dialog opens.
155
170
  useEffect(() => {
156
171
  if (!open) return;
157
172
  const timer = setTimeout(() => {
158
- setStatementName("Bank Statement 1");
159
- setApplicantType("");
160
- setSelectedAccountIds([]);
161
173
  const { from, to } = presetToDateRange(90);
174
+ setStatementName("Bank Statement 1");
162
175
  setRangePreset(90);
163
176
  setFromDate(from);
164
177
  setToDate(to);
178
+ setApplicantType("");
179
+ setSelectedAccountIds([]);
180
+ setAccountPage(1);
165
181
  }, 0);
166
182
  return () => clearTimeout(timer);
167
183
  }, [open]);
@@ -171,12 +187,17 @@ export function BankStatementGenerateDialog({
171
187
  const handleApplicantTypeChange = (type: "primary" | "secondary") => {
172
188
  setApplicantType(type);
173
189
  setSelectedAccountIds([]);
190
+ setAccountPage(1);
174
191
  onApplicantTypeChange?.(type);
175
192
  };
176
193
 
177
194
  // ── Date range preset ────────────────────────────────────────────────────
178
195
 
179
- 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);
180
201
  setRangePreset(preset);
181
202
  if (preset !== "custom") {
182
203
  const { from, to } = presetToDateRange(preset);
@@ -199,13 +220,17 @@ export function BankStatementGenerateDialog({
199
220
  };
200
221
 
201
222
  const handleToggleAll = () => {
202
- if (areAllSelected) {
203
- setSelectedAccountIds([]);
204
- } else {
205
- setSelectedAccountIds(bankAccounts.map((a) => a.id));
206
- }
223
+ setSelectedAccountIds(areAllSelected ? [] : bankAccounts.map((a) => a.id));
207
224
  };
208
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
+
209
234
  // ── Period label for the accounts table ─────────────────────────────────
210
235
 
211
236
  const periodLabel = useMemo(() => {
@@ -240,7 +265,8 @@ export function BankStatementGenerateDialog({
240
265
  return (
241
266
  <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
242
267
  <DialogContent
243
- className={cn("max-w-[700px]", className)}
268
+ size="3xl"
269
+ className={className}
244
270
  data-slot="bank-statement-generate-dialog"
245
271
  >
246
272
  <DialogHeader>
@@ -267,15 +293,7 @@ export function BankStatementGenerateDialog({
267
293
  type="single"
268
294
  variant="outline"
269
295
  value={[String(rangePreset)]}
270
- onValueChange={(val) => {
271
- if (val.length === 0) return;
272
- const raw = val[0];
273
- if (raw === "custom") {
274
- applyPreset("custom");
275
- } else {
276
- applyPreset(Number(raw) as 90 | 180 | 365);
277
- }
278
- }}
296
+ onValueChange={handlePresetChange}
279
297
  className="w-full"
280
298
  >
281
299
  <ToggleGroupItem value="90" className="flex-1">
@@ -317,14 +335,19 @@ export function BankStatementGenerateDialog({
317
335
  onValueChange={(v) =>
318
336
  handleApplicantTypeChange(v as "primary" | "secondary")
319
337
  }
338
+ items={APPLICANT_TYPE_LABELS}
320
339
  >
321
340
  <SelectTrigger className="w-full">
322
341
  <SelectValue placeholder="Applicant Type" />
323
342
  </SelectTrigger>
324
343
  <SelectContent>
325
- <SelectItem value="primary">Main applicant</SelectItem>
344
+ <SelectItem value="primary">
345
+ {APPLICANT_TYPE_LABELS.primary}
346
+ </SelectItem>
326
347
  {hasCoApplicant && (
327
- <SelectItem value="secondary">Co-applicant</SelectItem>
348
+ <SelectItem value="secondary">
349
+ {APPLICANT_TYPE_LABELS.secondary}
350
+ </SelectItem>
328
351
  )}
329
352
  </SelectContent>
330
353
  </Select>
@@ -332,86 +355,114 @@ export function BankStatementGenerateDialog({
332
355
  {/* Bank accounts table */}
333
356
  {applicantType && (
334
357
  <div className="mt-1">
335
- {isLoadingAccounts && (
358
+ {isLoadingAccounts ? (
336
359
  <div className="flex items-center justify-center p-6">
337
360
  <Spinner size="lg" />
338
361
  </div>
339
- )}
340
-
341
- {!isLoadingAccounts && bankAccounts.length === 0 && (
362
+ ) : bankAccounts.length === 0 ? (
342
363
  <p className="py-4 text-center text-body-medium text-muted-foreground">
343
364
  No bank accounts found for the selected applicant
344
365
  </p>
345
- )}
346
-
347
- {!isLoadingAccounts && bankAccounts.length > 0 && (
348
- <Table>
349
- <TableHeader>
350
- <TableRow>
351
- <TableHead className="w-10">
352
- <Checkbox
353
- checked={areAllSelected}
354
- indeterminate={isSomeSelected}
355
- onCheckedChange={handleToggleAll}
356
- aria-label="Select all accounts"
357
- />
358
- </TableHead>
359
- <TableHead>Account Name</TableHead>
360
- <TableHead>Account Number</TableHead>
361
- <TableHead>Period</TableHead>
362
- <TableHead>Last Updated</TableHead>
363
- </TableRow>
364
- </TableHeader>
365
- <TableBody>
366
- {bankAccounts.map((account) => (
367
- <TableRow key={account.id}>
368
- <TableCell>
366
+ ) : (
367
+ <>
368
+ <Table className="table-fixed">
369
+ <TableHeader>
370
+ <TableRow>
371
+ <TableHead className="w-10">
369
372
  <Checkbox
370
- checked={selectedAccountIds.includes(account.id)}
371
- onCheckedChange={() =>
372
- handleToggleAccount(account.id)
373
- }
374
- aria-label={`Select ${account.name}`}
373
+ checked={areAllSelected}
374
+ indeterminate={isSomeSelected}
375
+ onCheckedChange={handleToggleAll}
376
+ aria-label="Select all accounts"
375
377
  />
376
- </TableCell>
377
- <TableCell>
378
- <div className="flex items-center gap-2">
379
- {account.institutionLogo && (
380
- <img
381
- src={account.institutionLogo}
382
- alt={account.institutionName ?? ""}
383
- className="size-8 rounded object-cover"
384
- />
385
- )}
386
- <span className="text-body-medium font-semibold">
387
- {account.name || account.institutionName || "—"}
388
- </span>
389
- </div>
390
- </TableCell>
391
- <TableCell>
392
- <span className="text-body-medium">
393
- {account.accountNo ?? "—"}
394
- </span>
395
- </TableCell>
396
- <TableCell>
397
- <span className="text-body-medium">
398
- {periodLabel}
399
- </span>
400
- </TableCell>
401
- <TableCell>
402
- <span className="text-body-medium">
403
- {account.lastUpdated
404
- ? format(
405
- parseISO(account.lastUpdated),
406
- "dd MMM yyyy",
407
- )
408
- : "—"}
409
- </span>
410
- </TableCell>
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>
411
383
  </TableRow>
412
- ))}
413
- </TableBody>
414
- </Table>
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 items-center gap-2">
406
+ {account.institutionLogo && (
407
+ <img
408
+ src={account.institutionLogo}
409
+ alt={account.institutionName ?? ""}
410
+ className="size-8 object-cover"
411
+ />
412
+ )}
413
+ <span className="text-body-medium font-semibold">
414
+ {account.name ||
415
+ account.institutionName ||
416
+ "—"}
417
+ </span>
418
+ </div>
419
+ </TableCell>
420
+ <TableCell>
421
+ <span className="text-body-medium">
422
+ {account.accountNo ?? "—"}
423
+ </span>
424
+ </TableCell>
425
+ <TableCell>
426
+ <span className="text-body-medium">
427
+ {periodLabel}
428
+ </span>
429
+ </TableCell>
430
+ <TableCell>
431
+ <span className="text-body-medium">
432
+ {account.lastUpdated
433
+ ? format(
434
+ parseISO(account.lastUpdated),
435
+ "dd MMM yyyy",
436
+ )
437
+ : "—"}
438
+ </span>
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
+ </>
415
466
  )}
416
467
  </div>
417
468
  )}
@@ -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
+ }