@wealthx/shadcn 1.5.10 → 1.5.11

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 (111) hide show
  1. package/.turbo/turbo-build.log +126 -126
  2. package/CHANGELOG.md +6 -0
  3. package/dist/{chunk-AANINK2B.mjs → chunk-2KNQZG5S.mjs} +1 -1
  4. package/dist/chunk-3KLJ4XRE.mjs +375 -0
  5. package/dist/{chunk-6U4NQGVM.mjs → chunk-4X4MGYHE.mjs} +2 -2
  6. package/dist/{chunk-CEEVYRQA.mjs → chunk-67DGIPQ4.mjs} +1 -1
  7. package/dist/{chunk-7UIL5UN3.mjs → chunk-7II6QRCZ.mjs} +1 -1
  8. package/dist/{chunk-W5QJ57PU.mjs → chunk-7LN5OGC2.mjs} +1 -1
  9. package/dist/{chunk-ZXEUBBHJ.mjs → chunk-7TMPOZDE.mjs} +1 -1
  10. package/dist/{chunk-AHSCWXYJ.mjs → chunk-AJUAJC5O.mjs} +1 -1
  11. package/dist/{chunk-3CGM3QXQ.mjs → chunk-AKWN5ZQG.mjs} +2 -2
  12. package/dist/{chunk-FRT3S72S.mjs → chunk-CQ7HKBEX.mjs} +1 -1
  13. package/dist/{chunk-E2BNCA6L.mjs → chunk-EHQL64B7.mjs} +1 -1
  14. package/dist/{chunk-2WCIORP7.mjs → chunk-EXI64H46.mjs} +1 -1
  15. package/dist/{chunk-BBXSNDS3.mjs → chunk-FQYFPHDO.mjs} +1 -1
  16. package/dist/{chunk-3VZ6CYY2.mjs → chunk-GAXNO4JB.mjs} +1 -1
  17. package/dist/{chunk-3WGFIFP6.mjs → chunk-I4P7RXAE.mjs} +1 -1
  18. package/dist/{chunk-Z2BW5T7P.mjs → chunk-IODGRCQG.mjs} +1 -1
  19. package/dist/{chunk-GS47ZSSA.mjs → chunk-J7KQON2N.mjs} +20 -5
  20. package/dist/{chunk-IQGKOT7A.mjs → chunk-K35TFQUB.mjs} +1 -1
  21. package/dist/{chunk-4DO3WM7V.mjs → chunk-K4VWSDJJ.mjs} +1 -1
  22. package/dist/{chunk-KWD6GANL.mjs → chunk-MPA2HV5U.mjs} +1 -1
  23. package/dist/{chunk-5LZZYODG.mjs → chunk-QHAMVWDG.mjs} +19 -1
  24. package/dist/{chunk-XUCDPAVI.mjs → chunk-R6U246E4.mjs} +2 -2
  25. package/dist/{chunk-VCDGLN25.mjs → chunk-S6AYZJYO.mjs} +47 -21
  26. package/dist/{chunk-WL6WVV47.mjs → chunk-X6RC5UWB.mjs} +1 -1
  27. package/dist/{chunk-4BHDDLWK.mjs → chunk-XAS6KBIG.mjs} +2 -2
  28. package/dist/{chunk-VWZS32ZQ.mjs → chunk-XYWEGBAA.mjs} +1 -1
  29. package/dist/{chunk-54MTIKNC.mjs → chunk-YV7XF32X.mjs} +49 -24
  30. package/dist/{chunk-E5EDZQ5J.mjs → chunk-ZA44WICP.mjs} +1 -1
  31. package/dist/{chunk-54TRNCID.mjs → chunk-ZMTCMP2G.mjs} +8 -8
  32. package/dist/components/ui/advisor-card.js +144 -55
  33. package/dist/components/ui/advisor-card.mjs +5 -2
  34. package/dist/components/ui/agent-evaluation-toast.js +1 -1
  35. package/dist/components/ui/agent-evaluation-toast.mjs +2 -2
  36. package/dist/components/ui/ai-assistant-drawer.js +1 -1
  37. package/dist/components/ui/ai-assistant-drawer.mjs +2 -2
  38. package/dist/components/ui/ai-builder.js +1 -1
  39. package/dist/components/ui/ai-builder.mjs +2 -2
  40. package/dist/components/ui/ai-conversations.js +1 -1
  41. package/dist/components/ui/ai-conversations.mjs +3 -3
  42. package/dist/components/ui/appointment-action-dialogs.js +1 -1
  43. package/dist/components/ui/appointment-action-dialogs.mjs +3 -3
  44. package/dist/components/ui/appointment-book-dialog.js +19 -4
  45. package/dist/components/ui/appointment-book-dialog.mjs +3 -3
  46. package/dist/components/ui/appointment-calendar-view.js +1 -1
  47. package/dist/components/ui/appointment-calendar-view.mjs +2 -2
  48. package/dist/components/ui/appointment-detail-sheet.js +1 -1
  49. package/dist/components/ui/appointment-detail-sheet.mjs +4 -4
  50. package/dist/components/ui/appointment-gmail-connect.js +1 -1
  51. package/dist/components/ui/appointment-gmail-connect.mjs +2 -2
  52. package/dist/components/ui/appointment-time-slot-picker.js +1 -1
  53. package/dist/components/ui/appointment-time-slot-picker.mjs +2 -2
  54. package/dist/components/ui/appointment-upcoming-card.js +1 -1
  55. package/dist/components/ui/appointment-upcoming-card.mjs +3 -3
  56. package/dist/components/ui/badge.js +1 -1
  57. package/dist/components/ui/badge.mjs +1 -1
  58. package/dist/components/ui/chat-widget-primitives.js +1 -1
  59. package/dist/components/ui/chat-widget-primitives.mjs +2 -2
  60. package/dist/components/ui/chat-widget.js +1 -1
  61. package/dist/components/ui/chat-widget.mjs +3 -3
  62. package/dist/components/ui/chip.js +1 -1
  63. package/dist/components/ui/chip.mjs +2 -2
  64. package/dist/components/ui/contact-alert-dialog/index.js +19 -1
  65. package/dist/components/ui/contact-alert-dialog/index.mjs +1 -1
  66. package/dist/components/ui/dashboard-transactions-table.js +1 -1
  67. package/dist/components/ui/dashboard-transactions-table.mjs +2 -2
  68. package/dist/components/ui/financial-cards.js +1 -1
  69. package/dist/components/ui/financial-cards.mjs +2 -2
  70. package/dist/components/ui/financial-sections.js +1 -1
  71. package/dist/components/ui/financial-sections.mjs +3 -3
  72. package/dist/components/ui/income-summary-component.js +1 -1
  73. package/dist/components/ui/income-summary-component.mjs +1 -1
  74. package/dist/components/ui/integration-card.js +1 -1
  75. package/dist/components/ui/integration-card.mjs +2 -2
  76. package/dist/components/ui/kanban-column.js +46 -23
  77. package/dist/components/ui/kanban-column.mjs +4 -4
  78. package/dist/components/ui/loan-applicant-information.js +1 -1
  79. package/dist/components/ui/loan-applicant-information.mjs +1 -1
  80. package/dist/components/ui/loan-application-badge.js +1 -1
  81. package/dist/components/ui/loan-application-badge.mjs +2 -2
  82. package/dist/components/ui/opportunity-card.js +46 -23
  83. package/dist/components/ui/opportunity-card.mjs +3 -3
  84. package/dist/components/ui/opportunity-summary-tab.js +1 -1
  85. package/dist/components/ui/opportunity-summary-tab.mjs +3 -3
  86. package/dist/components/ui/pipeline-board.js +46 -23
  87. package/dist/components/ui/pipeline-board.mjs +5 -5
  88. package/dist/components/ui/pipeline-primitives.js +1 -1
  89. package/dist/components/ui/pipeline-primitives.mjs +2 -2
  90. package/dist/components/ui/property-asset-card.js +1 -1
  91. package/dist/components/ui/property-asset-card.mjs +1 -1
  92. package/dist/components/ui/resource-center.js +1 -1
  93. package/dist/components/ui/resource-center.mjs +2 -2
  94. package/dist/components/ui/share-details-dialog.js +326 -30
  95. package/dist/components/ui/share-details-dialog.mjs +4 -1
  96. package/dist/components/ui/stage-timeline.js +1 -1
  97. package/dist/components/ui/stage-timeline.mjs +3 -3
  98. package/dist/index.js +457 -188
  99. package/dist/index.mjs +44 -42
  100. package/dist/styles.css +1 -1
  101. package/package.json +1 -1
  102. package/src/components/index.tsx +4 -0
  103. package/src/components/ui/advisor-card.tsx +75 -25
  104. package/src/components/ui/appointment-book-dialog.tsx +26 -3
  105. package/src/components/ui/appointment-time-slot-picker.tsx +1 -0
  106. package/src/components/ui/badge.tsx +1 -1
  107. package/src/components/ui/contact-alert-dialog/contact-alert-dialog.tsx +19 -1
  108. package/src/components/ui/opportunity-card.tsx +56 -20
  109. package/src/components/ui/share-details-dialog.tsx +251 -0
  110. package/src/styles/styles-css.ts +1 -1
  111. package/dist/chunk-OZ2R6ERP.mjs +0 -174
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wealthx/shadcn",
3
- "version": "1.5.10",
3
+ "version": "1.5.11",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -73,6 +73,7 @@ export type {
73
73
  AdvisorCardProps,
74
74
  AdvisorInviteCardProps,
75
75
  AdvisorAppointmentStrip,
76
+ AdvisorCardMenuItem,
76
77
  } from "./ui/advisor-card";
77
78
 
78
79
  export { AppointmentUpcomingCard } from "./ui/appointment-upcoming-card";
@@ -492,10 +493,13 @@ export type {
492
493
  export {
493
494
  ShareDetailsDialog,
494
495
  EmailTemplateDialog,
496
+ ShareContactDialog,
495
497
  } from "./ui/share-details-dialog";
496
498
  export type {
497
499
  ShareDetailsDialogProps,
498
500
  EmailTemplateDialogProps,
501
+ ShareContactDialogProps,
502
+ InternalAdvisor,
499
503
  } from "./ui/share-details-dialog";
500
504
 
501
505
  export { FilePreviewDialog } from "./ui/file-preview-dialog";
@@ -3,6 +3,12 @@ import { Badge } from "./badge";
3
3
  import { Button } from "./button";
4
4
  import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
5
5
  import { Separator } from "./separator";
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuTrigger,
11
+ } from "./dropdown-menu";
6
12
  import {
7
13
  Calendar,
8
14
  CalendarCheck,
@@ -18,6 +24,18 @@ import type { AppointmentStatus } from "./appointment-time-slot-picker";
18
24
  // Types
19
25
  // ---------------------------------------------------------------------------
20
26
 
27
+ /** A single item in the ⋮ overflow DropdownMenu */
28
+ export interface AdvisorCardMenuItem {
29
+ /** Display label */
30
+ label: string;
31
+ /** Called when the item is clicked */
32
+ onClick: () => void;
33
+ /** Visual variant — use "destructive" for delete/remove actions */
34
+ variant?: "default" | "destructive";
35
+ /** Disables the item without removing it */
36
+ disabled?: boolean;
37
+ }
38
+
21
39
  /** Appointment data shown in the strip inside the advisor card */
22
40
  export interface AdvisorAppointmentStrip {
23
41
  status: AppointmentStatus;
@@ -57,7 +75,12 @@ export interface AdvisorCardProps {
57
75
  appointments?: AdvisorAppointmentStrip[] | null;
58
76
  /** Called when "Refer [name] to Others" is clicked */
59
77
  onRefer?: () => void;
60
- /** Called when the ⋮ overflow menu is clicked */
78
+ /**
79
+ * Items to render in the ⋮ overflow DropdownMenu.
80
+ * When provided, the button opens an inline dropdown — `onMoreOptions` is ignored.
81
+ */
82
+ menuItems?: AdvisorCardMenuItem[];
83
+ /** @deprecated Pass `menuItems` for an inline DropdownMenu instead */
61
84
  onMoreOptions?: () => void;
62
85
  /** Called when "Book Appointment" is clicked */
63
86
  onBookAppointment?: () => void;
@@ -113,6 +136,7 @@ export function AdvisorCard({
113
136
  isPrimary = false,
114
137
  appointments,
115
138
  onRefer,
139
+ menuItems,
116
140
  onMoreOptions,
117
141
  onBookAppointment,
118
142
  onViewAppointment,
@@ -132,7 +156,7 @@ export function AdvisorCard({
132
156
  {companyLogoUrl && (
133
157
  <AvatarImage src={companyLogoUrl} alt={`${name} company logo`} />
134
158
  )}
135
- <AvatarFallback className="text-sm">
159
+ <AvatarFallback className="text-caption">
136
160
  {avatarInitials ??
137
161
  (companyName
138
162
  ? companyName
@@ -146,14 +170,14 @@ export function AdvisorCard({
146
170
  </Avatar>
147
171
 
148
172
  <div className="flex min-w-0 flex-1 flex-col gap-0.5">
149
- <p className="text-sm font-semibold leading-tight">{name}</p>
150
- <p className="text-sm text-muted-foreground">{role}</p>
173
+ <p className="text-label-large leading-tight">{name}</p>
174
+ <p className="text-body-small text-muted-foreground">{role}</p>
151
175
  <div className="mt-2 flex flex-col gap-1">
152
- <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
176
+ <div className="flex items-center gap-1.5 text-body-small text-muted-foreground">
153
177
  <Phone className="h-4 w-4 shrink-0" />
154
178
  <span>{phone}</span>
155
179
  </div>
156
- <div className="flex items-center gap-1.5 text-sm text-muted-foreground">
180
+ <div className="flex items-center gap-1.5 text-body-small text-muted-foreground">
157
181
  <Mail className="h-4 w-4 shrink-0" />
158
182
  <span>{email}</span>
159
183
  </div>
@@ -163,15 +187,44 @@ export function AdvisorCard({
163
187
  {/* Badge + overflow menu */}
164
188
  <div className="flex shrink-0 items-center gap-1.5">
165
189
  {isPrimary && <Badge variant="success">Primary</Badge>}
166
- <Button
167
- variant="ghost"
168
- size="icon"
169
- className="h-7 w-7"
170
- onClick={onMoreOptions}
171
- aria-label="More options"
172
- >
173
- <MoreVertical className="h-4 w-4" />
174
- </Button>
190
+ {menuItems && menuItems.length > 0 ? (
191
+ <DropdownMenu>
192
+ <DropdownMenuTrigger
193
+ render={
194
+ <Button
195
+ variant="ghost"
196
+ size="icon"
197
+ className="h-7 w-7"
198
+ aria-label="More options"
199
+ />
200
+ }
201
+ >
202
+ <MoreVertical className="h-4 w-4" />
203
+ </DropdownMenuTrigger>
204
+ <DropdownMenuContent align="end">
205
+ {menuItems.map((item, i) => (
206
+ <DropdownMenuItem
207
+ key={i}
208
+ variant={item.variant}
209
+ disabled={item.disabled}
210
+ onClick={item.onClick}
211
+ >
212
+ {item.label}
213
+ </DropdownMenuItem>
214
+ ))}
215
+ </DropdownMenuContent>
216
+ </DropdownMenu>
217
+ ) : (
218
+ <Button
219
+ variant="ghost"
220
+ size="icon"
221
+ className="h-7 w-7"
222
+ onClick={onMoreOptions}
223
+ aria-label="More options"
224
+ >
225
+ <MoreVertical className="h-4 w-4" />
226
+ </Button>
227
+ )}
175
228
  </div>
176
229
  </div>
177
230
 
@@ -189,19 +242,16 @@ export function AdvisorCard({
189
242
  <CalendarCheck className="h-4 w-4 shrink-0 text-muted-foreground" />
190
243
  <div className="flex min-w-0 flex-1 flex-col gap-0.5">
191
244
  <div className="flex items-center gap-2">
192
- <Badge
193
- variant={STATUS_VARIANT[appt.status]}
194
- className="text-[10px]"
195
- >
245
+ <Badge variant={STATUS_VARIANT[appt.status]}>
196
246
  {STATUS_LABEL[appt.status]}
197
247
  </Badge>
198
248
  {appt.appointmentType && (
199
- <span className="truncate text-sm font-semibold">
249
+ <span className="truncate text-h6">
200
250
  {appt.appointmentType}
201
251
  </span>
202
252
  )}
203
253
  </div>
204
- <p className="whitespace-nowrap text-sm text-muted-foreground">
254
+ <p className="whitespace-nowrap text-body-small text-muted-foreground">
205
255
  {appt.date} · {appt.timeStart}–{appt.timeEnd}
206
256
  </p>
207
257
  </div>
@@ -223,7 +273,7 @@ export function AdvisorCard({
223
273
  /* Empty state */
224
274
  <div className="flex items-center gap-3 px-4 py-3">
225
275
  <Calendar className="h-4 w-4 shrink-0 text-muted-foreground" />
226
- <p className="flex-1 text-xs text-muted-foreground">
276
+ <p className="flex-1 text-caption text-muted-foreground">
227
277
  No upcoming appointments
228
278
  </p>
229
279
  </div>
@@ -246,7 +296,7 @@ export function AdvisorCard({
246
296
  )}
247
297
  {onRefer && (
248
298
  <Button
249
- variant="outline"
299
+ variant="default"
250
300
  size="sm"
251
301
  className="w-full"
252
302
  onClick={onRefer}
@@ -274,8 +324,8 @@ export function AdvisorInviteCard({ onInvite }: AdvisorInviteCardProps) {
274
324
  <Plus className="h-5 w-5 text-muted-foreground" />
275
325
  </div>
276
326
  <div className="flex flex-col gap-1">
277
- <p className="text-sm font-medium">Add Another Advisor</p>
278
- <p className="text-xs text-muted-foreground">
327
+ <p className="text-label-large">Add Another Advisor</p>
328
+ <p className="text-caption text-muted-foreground">
279
329
  Connect more advisors to your account
280
330
  </p>
281
331
  </div>
@@ -236,11 +236,14 @@ function ClientSearch({
236
236
  // Meeting format sub-component
237
237
  // ---------------------------------------------------------------------------
238
238
 
239
- const FORMAT_OPTIONS: {
239
+ interface FormatOption {
240
240
  value: AppointmentMeetingFormat;
241
241
  label: string;
242
242
  icon: React.ReactNode;
243
- }[] = [
243
+ }
244
+
245
+ /** All four formats — shown in advisor mode */
246
+ const FORMAT_OPTIONS: FormatOption[] = [
244
247
  { value: "call", label: "Call", icon: <Phone className="h-4 w-4" /> },
245
248
  {
246
249
  value: "google-meet",
@@ -259,6 +262,21 @@ const FORMAT_OPTIONS: {
259
262
  },
260
263
  ];
261
264
 
265
+ /** Simplified three-option set shown in client mode */
266
+ const CLIENT_FORMAT_OPTIONS: FormatOption[] = [
267
+ { value: "call", label: "Call", icon: <Phone className="h-4 w-4" /> },
268
+ {
269
+ value: "online",
270
+ label: "Online Meeting",
271
+ icon: <Video className="h-4 w-4" />,
272
+ },
273
+ {
274
+ value: "offline",
275
+ label: "Offline Meeting",
276
+ icon: <MapPin className="h-4 w-4" />,
277
+ },
278
+ ];
279
+
262
280
  function MeetingFormatSection({
263
281
  format,
264
282
  onFormatChange,
@@ -269,6 +287,7 @@ function MeetingFormatSection({
269
287
  advisorOfficeAddress,
270
288
  clientHomeAddress,
271
289
  isClientMode,
290
+ formatOptions,
272
291
  }: {
273
292
  format: AppointmentMeetingFormat;
274
293
  onFormatChange: (f: AppointmentMeetingFormat) => void;
@@ -279,11 +298,12 @@ function MeetingFormatSection({
279
298
  advisorOfficeAddress?: string;
280
299
  clientHomeAddress?: string;
281
300
  isClientMode?: boolean;
301
+ formatOptions: FormatOption[];
282
302
  }) {
283
303
  return (
284
304
  <div className="flex flex-col gap-2">
285
305
  <div className="flex gap-2">
286
- {FORMAT_OPTIONS.map((opt) => (
306
+ {formatOptions.map((opt) => (
287
307
  <Toggle
288
308
  key={opt.value}
289
309
  variant="outline"
@@ -681,6 +701,9 @@ export function AppointmentBookDialog({
681
701
  advisorOfficeAddress={advisorOfficeAddress}
682
702
  clientHomeAddress={clientHomeAddress}
683
703
  isClientMode={isClientMode}
704
+ formatOptions={
705
+ isClientMode ? CLIENT_FORMAT_OPTIONS : FORMAT_OPTIONS
706
+ }
684
707
  />
685
708
  </div>
686
709
  )}
@@ -18,6 +18,7 @@ export type AppointmentMeetingFormat =
18
18
  | "call"
19
19
  | "google-meet"
20
20
  | "microsoft-teams"
21
+ | "online"
21
22
  | "offline";
22
23
 
23
24
  // ---------------------------------------------------------------------------
@@ -14,7 +14,7 @@ import { Slot } from "@/lib/slot";
14
14
  * This gives softer, more readable badges and works correctly in light and dark mode.
15
15
  */
16
16
  const badgeVariants = cva(
17
- "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2.5 py-0.5 text-xs font-medium font-sans whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
17
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2.5 py-0.5 text-caption whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
18
18
  {
19
19
  variants: {
20
20
  variant: {
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { AlertCircleIcon, EyeIcon, InfoIcon } from "lucide-react";
2
3
  import {
3
4
  Query,
4
5
  Utils as QbUtils,
@@ -17,6 +18,7 @@ import { Checkbox } from "@/components/ui/checkbox";
17
18
  import { Label } from "@/components/ui/label";
18
19
  import { Field, FieldLabel } from "@/components/ui/field";
19
20
  import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
21
+ import { cn } from "@/lib/utils";
20
22
  import type {
21
23
  ContactAlertDialogProps,
22
24
  ContactAlertQueryBuilderProps,
@@ -161,7 +163,23 @@ export function ContactAlertDialog({
161
163
  {(
162
164
  ["NEED_ACTION", "WATCH", "HEALTHY"] as ContactAlertSeverity[]
163
165
  ).map((s) => (
164
- <ToggleGroupItem key={s} value={s}>
166
+ <ToggleGroupItem
167
+ key={s}
168
+ value={s}
169
+ className={cn(
170
+ s === "NEED_ACTION" &&
171
+ "data-pressed:bg-destructive/10 data-pressed:inset-ring-destructive data-pressed:text-destructive-text data-pressed:hover:bg-destructive/10",
172
+ s === "WATCH" &&
173
+ "data-pressed:bg-warning/10 data-pressed:inset-ring-warning data-pressed:text-warning-text data-pressed:hover:bg-warning/10",
174
+ s === "HEALTHY" &&
175
+ "data-pressed:bg-info/10 data-pressed:inset-ring-info data-pressed:text-info-text data-pressed:hover:bg-info/10",
176
+ )}
177
+ >
178
+ {s === "NEED_ACTION" && (
179
+ <AlertCircleIcon className="size-3.5" />
180
+ )}
181
+ {s === "WATCH" && <EyeIcon className="size-3.5" />}
182
+ {s === "HEALTHY" && <InfoIcon className="size-3.5" />}
165
183
  {SEVERITY_LABELS[s]}
166
184
  </ToggleGroupItem>
167
185
  ))}
@@ -6,6 +6,8 @@ import {
6
6
  Users,
7
7
  Calendar,
8
8
  Check,
9
+ Copy,
10
+ Link2,
9
11
  MoreVertical,
10
12
  Clock,
11
13
  ArrowRight,
@@ -782,6 +784,54 @@ export function OpportunityCard({
782
784
  // Primary action: send them the loan application link.
783
785
  // ---------------------------------------------------------------------------
784
786
 
787
+ function normalizeUrl(url: string): string {
788
+ return `https://${url.replace(/^https?:\/\//, "")}`;
789
+ }
790
+
791
+ /** Shortened + copyable loan application URL row. */
792
+ function LoanApplicationLink({ url }: { url: string }) {
793
+ const [copied, setCopied] = useState(false);
794
+ const href = normalizeUrl(url);
795
+
796
+ function handleCopy() {
797
+ navigator.clipboard.writeText(href).then(() => {
798
+ setCopied(true);
799
+ setTimeout(() => setCopied(false), 2000);
800
+ });
801
+ }
802
+
803
+ return (
804
+ <div className="flex flex-col gap-1">
805
+ <p className="text-caption text-muted-foreground">
806
+ Or the link below to fill out the loan application directly.
807
+ </p>
808
+ <div className="flex items-center gap-1.5">
809
+ <Link2 className="size-4 shrink-0 text-muted-foreground" />
810
+ <a
811
+ href={href}
812
+ target="_blank"
813
+ rel="noreferrer"
814
+ className="min-w-0 flex-1 truncate text-body-small text-primary underline-offset-2 hover:underline"
815
+ >
816
+ {url}
817
+ </a>
818
+ <button
819
+ type="button"
820
+ onClick={handleCopy}
821
+ className="shrink-0 rounded p-1 text-muted-foreground transition-colors hover:text-foreground"
822
+ aria-label="Copy link"
823
+ >
824
+ {copied ? (
825
+ <Check className="size-4 text-success-text" />
826
+ ) : (
827
+ <Copy className="size-4" />
828
+ )}
829
+ </button>
830
+ </div>
831
+ </div>
832
+ );
833
+ }
834
+
785
835
  export interface LeadCardProps {
786
836
  id: string;
787
837
  customerName: string;
@@ -816,18 +866,18 @@ export function LeadCard({
816
866
  {/* ── Customer info + delete menu ── */}
817
867
  <div className="flex items-start justify-between gap-2">
818
868
  <div className="flex min-w-0 flex-1 flex-col gap-1">
819
- <span className="text-sm font-semibold text-foreground">
869
+ <span className="text-label-medium text-foreground">
820
870
  {customerName}
821
871
  </span>
822
872
  {customerPhone && (
823
- <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
824
- <Phone className="size-3 shrink-0" aria-hidden="true" />
873
+ <span className="flex items-center gap-1.5 text-caption text-muted-foreground">
874
+ <Phone className="size-3.5 shrink-0" aria-hidden="true" />
825
875
  {customerPhone}
826
876
  </span>
827
877
  )}
828
878
  {customerEmail && (
829
- <span className="flex items-center gap-1.5 text-xs text-muted-foreground">
830
- <Mail className="size-3 shrink-0" aria-hidden="true" />
879
+ <span className="flex items-center gap-1.5 text-caption text-muted-foreground">
880
+ <Mail className="size-3.5 shrink-0" aria-hidden="true" />
831
881
  <span className="truncate">{customerEmail}</span>
832
882
  </span>
833
883
  )}
@@ -871,21 +921,7 @@ export function LeadCard({
871
921
  Send Loan Application Request
872
922
  </Button>
873
923
  {loanApplicationUrl && (
874
- <p className="text-xs text-muted-foreground">
875
- Or the link below to fill out the loan application directly.
876
- <br />
877
- <a
878
- href={`https://${loanApplicationUrl.replace(
879
- /^https?:\/\//,
880
- "",
881
- )}`}
882
- target="_blank"
883
- rel="noreferrer"
884
- className="text-primary underline-offset-2 hover:underline"
885
- >
886
- {loanApplicationUrl}
887
- </a>
888
- </p>
924
+ <LoanApplicationLink url={loanApplicationUrl} />
889
925
  )}
890
926
  </div>
891
927
  )}