@wealthx/shadcn 1.5.6 → 1.5.7

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 (91) hide show
  1. package/.turbo/turbo-build.log +141 -135
  2. package/CHANGELOG.md +6 -0
  3. package/dist/{chunk-2LLFNGJZ.mjs → chunk-3OOUI5TO.mjs} +1 -1
  4. package/dist/{chunk-AUEUTZIC.mjs → chunk-3QAQQBCM.mjs} +2 -2
  5. package/dist/chunk-3Z75IKFO.mjs +34 -0
  6. package/dist/{chunk-RAKBWNQH.mjs → chunk-5WCIGJ3E.mjs} +26 -65
  7. package/dist/chunk-65PZNG4Y.mjs +129 -0
  8. package/dist/{chunk-D447W45Z.mjs → chunk-7OLKVKJF.mjs} +41 -49
  9. package/dist/{chunk-UEREFDAE.mjs → chunk-AZGLSIHF.mjs} +1 -1
  10. package/dist/{chunk-QRVEI6J3.mjs → chunk-BQBSYM2I.mjs} +1 -3
  11. package/dist/{chunk-BFB3UH7V.mjs → chunk-C2QMHKLT.mjs} +1 -1
  12. package/dist/chunk-CEEVYRQA.mjs +61 -0
  13. package/dist/{chunk-VJ3GC7W3.mjs → chunk-CXGZSIQN.mjs} +4 -30
  14. package/dist/{chunk-VLELWBEW.mjs → chunk-E2BNCA6L.mjs} +1 -1
  15. package/dist/{chunk-46Q4335I.mjs → chunk-F73QFUZH.mjs} +10 -20
  16. package/dist/{chunk-Q35PNFJ7.mjs → chunk-FBNEIYSE.mjs} +1 -1
  17. package/dist/{chunk-HO6S3ECM.mjs → chunk-G4WLCKFV.mjs} +1 -1
  18. package/dist/{chunk-ODO6BUOF.mjs → chunk-HRHXZIX5.mjs} +1 -3
  19. package/dist/{chunk-IXR4BQSQ.mjs → chunk-I6QVWQCD.mjs} +1 -1
  20. package/dist/{chunk-Q5SGEIJV.mjs → chunk-IXKE6LHN.mjs} +1 -1
  21. package/dist/{chunk-OL65UQHQ.mjs → chunk-JCH2BG24.mjs} +26 -60
  22. package/dist/{chunk-PV3Y7QGK.mjs → chunk-LHWJQNLG.mjs} +3 -3
  23. package/dist/{chunk-JTG5R5YV.mjs → chunk-MS4PDUSU.mjs} +284 -58
  24. package/dist/{chunk-VFH632TB.mjs → chunk-SDNG4XL6.mjs} +1 -1
  25. package/dist/{chunk-DFL5CV75.mjs → chunk-T6MMCOX6.mjs} +1 -1
  26. package/dist/{chunk-6TX73WG7.mjs → chunk-TCE5L44O.mjs} +3 -2
  27. package/dist/{chunk-HROG643K.mjs → chunk-W5QJ57PU.mjs} +1 -1
  28. package/dist/{chunk-EW72FINW.mjs → chunk-XVZGXPIX.mjs} +1 -1
  29. package/dist/components/ui/about-you-form.mjs +3 -3
  30. package/dist/components/ui/add-column-modal.js +56 -227
  31. package/dist/components/ui/add-column-modal.mjs +1 -2
  32. package/dist/components/ui/ai-conversations.js +278 -58
  33. package/dist/components/ui/ai-conversations.mjs +1 -1
  34. package/dist/components/ui/appointment-action-dialogs.mjs +3 -3
  35. package/dist/components/ui/appointment-availability-settings.js +2 -28
  36. package/dist/components/ui/appointment-availability-settings.mjs +4 -4
  37. package/dist/components/ui/appointment-book-dialog.js +11 -21
  38. package/dist/components/ui/appointment-book-dialog.mjs +3 -3
  39. package/dist/components/ui/appointment-calendar-view.mjs +2 -2
  40. package/dist/components/ui/appointment-detail-sheet.mjs +4 -4
  41. package/dist/components/ui/appointment-upcoming-card.mjs +3 -3
  42. package/dist/components/ui/bank-statement-generate-dialog.mjs +4 -4
  43. package/dist/components/ui/calendar.mjs +2 -2
  44. package/dist/components/ui/dashboard-transactions-table.mjs +1 -1
  45. package/dist/components/ui/date-picker.mjs +3 -3
  46. package/dist/components/ui/financial-cards.mjs +2 -2
  47. package/dist/components/ui/financial-sections.mjs +3 -3
  48. package/dist/components/ui/integration-card.js +326 -0
  49. package/dist/components/ui/integration-card.mjs +11 -0
  50. package/dist/components/ui/kanban-column.js +151 -243
  51. package/dist/components/ui/kanban-column.mjs +3 -4
  52. package/dist/components/ui/loan-application-cards.mjs +1 -1
  53. package/dist/components/ui/onboarding-layout.js +65 -4
  54. package/dist/components/ui/onboarding-layout.mjs +6 -4
  55. package/dist/components/ui/opportunity-card.js +126 -216
  56. package/dist/components/ui/opportunity-card.mjs +2 -3
  57. package/dist/components/ui/opportunity-edit-modals.mjs +4 -4
  58. package/dist/components/ui/opportunity-summary-tab.mjs +6 -6
  59. package/dist/components/ui/pipeline-board.js +167 -261
  60. package/dist/components/ui/pipeline-board.mjs +4 -5
  61. package/dist/components/ui/pipeline-chart.js +128 -55
  62. package/dist/components/ui/pipeline-chart.mjs +2 -1
  63. package/dist/components/ui/pipeline-dialogs.mjs +4 -4
  64. package/dist/components/ui/savings-goal-modal.mjs +3 -3
  65. package/dist/components/ui/selectable-card.js +163 -0
  66. package/dist/components/ui/selectable-card.mjs +9 -0
  67. package/dist/components/ui/signup-shell.js +3 -2
  68. package/dist/components/ui/signup-shell.mjs +2 -2
  69. package/dist/components/ui/stepper.js +3 -2
  70. package/dist/components/ui/stepper.mjs +1 -1
  71. package/dist/index.js +2121 -1878
  72. package/dist/index.mjs +56 -46
  73. package/dist/styles.css +1 -1
  74. package/package.json +11 -1
  75. package/src/components/index.tsx +13 -1
  76. package/src/components/ui/add-column-modal.tsx +9 -58
  77. package/src/components/ui/ai-conversations.tsx +308 -42
  78. package/src/components/ui/appointment-availability-settings.tsx +2 -35
  79. package/src/components/ui/appointment-book-dialog.tsx +25 -48
  80. package/src/components/ui/integration-card.tsx +88 -0
  81. package/src/components/ui/kanban-column.tsx +0 -7
  82. package/src/components/ui/onboarding-layout.tsx +102 -1
  83. package/src/components/ui/opportunity-card.tsx +5 -53
  84. package/src/components/ui/pipeline-board.tsx +0 -3
  85. package/src/components/ui/pipeline-chart.tsx +47 -53
  86. package/src/components/ui/selectable-card.tsx +37 -0
  87. package/src/components/ui/stepper.tsx +3 -2
  88. package/src/lib/format-date.ts +6 -6
  89. package/src/styles/styles-css.ts +1 -1
  90. package/tsup.config.ts +2 -0
  91. package/dist/chunk-2P7HP7LR.mjs +0 -68
@@ -27,7 +27,7 @@ import { Toggle } from "./toggle";
27
27
  import { Badge } from "./badge";
28
28
  import { Avatar, AvatarFallback } from "./avatar";
29
29
  import { CalendarCheck, MapPin, Phone, Users, Video } from "lucide-react";
30
- import { formatDateLong } from "@/lib/format-date";
30
+ import { formatDateLong } from "../../lib/format-date";
31
31
  import {
32
32
  AppointmentSlotSection,
33
33
  type AppointmentMeetingFormat,
@@ -259,16 +259,9 @@ const FORMAT_OPTIONS: {
259
259
  },
260
260
  ];
261
261
 
262
- // Pre-baked references — FORMAT_OPTIONS is a module constant so these are stable
263
- const FORMAT_CALL = FORMAT_OPTIONS[0];
264
- const FORMAT_OFFLINE = FORMAT_OPTIONS[3];
265
- // Icon shared by the "Online Meeting" collapsed option in client mode
266
- const ONLINE_MEETING_ICON = <Video className="h-4 w-4" />;
267
-
268
262
  function MeetingFormatSection({
269
263
  format,
270
264
  onFormatChange,
271
- options = FORMAT_OPTIONS,
272
265
  offlineLocation,
273
266
  onOfflineLocationChange,
274
267
  customAddress,
@@ -279,7 +272,6 @@ function MeetingFormatSection({
279
272
  }: {
280
273
  format: AppointmentMeetingFormat;
281
274
  onFormatChange: (f: AppointmentMeetingFormat) => void;
282
- options?: typeof FORMAT_OPTIONS;
283
275
  offlineLocation: AppointmentOfflineLocation;
284
276
  onOfflineLocationChange: (l: AppointmentOfflineLocation) => void;
285
277
  customAddress: string;
@@ -291,7 +283,7 @@ function MeetingFormatSection({
291
283
  return (
292
284
  <div className="flex flex-col gap-2">
293
285
  <div className="flex gap-2">
294
- {options.map((opt) => (
286
+ {FORMAT_OPTIONS.map((opt) => (
295
287
  <Toggle
296
288
  key={opt.value}
297
289
  variant="outline"
@@ -463,22 +455,6 @@ export function AppointmentBookDialog({
463
455
  // Guest form is shown in client mode when at least one identity field is missing.
464
456
  const showGuestForm = isClientMode && !(guestInfo?.name && guestInfo?.email);
465
457
 
466
- // In client mode with a preset online platform, collapse Google Meet / MS Teams
467
- // into a single "Online Meeting" option so the client sees 3 choices:
468
- // Call | Online Meeting | Offline — without knowing the specific platform.
469
- const visibleFormatOptions: typeof FORMAT_OPTIONS =
470
- isClientMode && defaultMeetingFormat
471
- ? [
472
- FORMAT_CALL,
473
- {
474
- value: defaultMeetingFormat,
475
- label: "Online Meeting",
476
- icon: ONLINE_MEETING_ICON,
477
- },
478
- FORMAT_OFFLINE,
479
- ]
480
- : FORMAT_OPTIONS;
481
-
482
458
  const disabledDayOfWeek = React.useMemo(
483
459
  () =>
484
460
  schedule
@@ -578,7 +554,7 @@ export function AppointmentBookDialog({
578
554
 
579
555
  return (
580
556
  <Dialog open={open} onOpenChange={handleOpenChange}>
581
- <DialogContent size="2xl">
557
+ <DialogContent size="2xl" align="top">
582
558
  {submittedBooking ? (
583
559
  <BookingConfirmationScreen
584
560
  booking={submittedBooking}
@@ -691,26 +667,23 @@ export function AppointmentBookDialog({
691
667
  </>
692
668
  )}
693
669
 
694
- {/* Format picker
695
- - Advisor mode: all options (Call / Google Meet / MS Teams / Offline)
696
- - Client mode, no preset: all options
697
- - Client mode, preset online format: only [broker's platform, Offline]
698
- so clients can still choose in-person while the online platform is locked */}
699
- <div className="flex flex-col gap-1.5">
700
- <Label>Meeting format</Label>
701
- <MeetingFormatSection
702
- format={meetingFormat}
703
- onFormatChange={setMeetingFormat}
704
- options={visibleFormatOptions}
705
- offlineLocation={offlineLocation}
706
- onOfflineLocationChange={setOfflineLocation}
707
- customAddress={customAddress}
708
- onCustomAddressChange={setCustomAddress}
709
- advisorOfficeAddress={advisorOfficeAddress}
710
- clientHomeAddress={clientHomeAddress}
711
- isClientMode={isClientMode}
712
- />
713
- </div>
670
+ {/* Format picker — hidden in client mode when broker pre-set a platform */}
671
+ {!(isClientMode && defaultMeetingFormat) && (
672
+ <div className="flex flex-col gap-1.5">
673
+ <Label>Meeting format</Label>
674
+ <MeetingFormatSection
675
+ format={meetingFormat}
676
+ onFormatChange={setMeetingFormat}
677
+ offlineLocation={offlineLocation}
678
+ onOfflineLocationChange={setOfflineLocation}
679
+ customAddress={customAddress}
680
+ onCustomAddressChange={setCustomAddress}
681
+ advisorOfficeAddress={advisorOfficeAddress}
682
+ clientHomeAddress={clientHomeAddress}
683
+ isClientMode={isClientMode}
684
+ />
685
+ </div>
686
+ )}
714
687
 
715
688
  {/* Phone selection — advisor mode, Call format, client has phones */}
716
689
  {!isClientMode &&
@@ -774,7 +747,11 @@ export function AppointmentBookDialog({
774
747
  {date ? (
775
748
  <div className="flex flex-col gap-3">
776
749
  <p className="text-xs text-muted-foreground">
777
- {formatDateLong(date)}
750
+ {date.toLocaleDateString("en-AU", {
751
+ weekday: "long",
752
+ day: "numeric",
753
+ month: "long",
754
+ })}
778
755
  </p>
779
756
  <AppointmentSlotSection
780
757
  label="Morning"
@@ -0,0 +1,88 @@
1
+ import React from "react";
2
+ import { Check } from "lucide-react";
3
+ import { Badge } from "./badge";
4
+ import { Button } from "./button";
5
+ import { cn } from "../../lib/utils";
6
+
7
+ export interface IntegrationCardProps {
8
+ name: string;
9
+ description: string;
10
+ /** Image src for the integration logo (e.g. aggregator logo) */
11
+ logoSrc?: string;
12
+ /** Fallback icon node shown when no logoSrc is provided but a logo area is desired */
13
+ logoFallback?: React.ReactNode;
14
+ connected?: boolean;
15
+ onConnect?: () => void;
16
+ onDisconnect?: () => void;
17
+ /** Extra content rendered between description and action button (e.g. inbox assignment Select) */
18
+ children?: React.ReactNode;
19
+ className?: string;
20
+ }
21
+
22
+ export function IntegrationCard({
23
+ name,
24
+ description,
25
+ logoSrc,
26
+ logoFallback,
27
+ connected = false,
28
+ onConnect,
29
+ onDisconnect,
30
+ children,
31
+ className,
32
+ }: IntegrationCardProps) {
33
+ const hasLogo = Boolean(logoSrc || logoFallback);
34
+
35
+ return (
36
+ <div
37
+ className={cn(
38
+ "flex flex-col gap-3 border border-border p-4",
39
+ connected && "bg-muted/20",
40
+ className,
41
+ )}
42
+ >
43
+ <div className="flex items-center gap-2">
44
+ {hasLogo && (
45
+ <div className="flex shrink-0 items-center">
46
+ {logoSrc ? (
47
+ <img
48
+ src={logoSrc}
49
+ alt={`${name} logo`}
50
+ className="h-8 w-16 object-contain object-left"
51
+ />
52
+ ) : (
53
+ <div className="flex h-9 w-9 items-center justify-center">
54
+ {logoFallback}
55
+ </div>
56
+ )}
57
+ </div>
58
+ )}
59
+ <span className="text-label-medium">{name}</span>
60
+ {connected && (
61
+ <Badge variant="success" className="gap-1">
62
+ <Check className="h-3 w-3" />
63
+ Connected
64
+ </Badge>
65
+ )}
66
+ </div>
67
+
68
+ <p className="text-body-small text-muted-foreground">{description}</p>
69
+
70
+ {children}
71
+
72
+ {!connected && onConnect && (
73
+ <div className="flex justify-end">
74
+ <Button variant="outline" size="sm" onClick={onConnect}>
75
+ Connect
76
+ </Button>
77
+ </div>
78
+ )}
79
+ {connected && onDisconnect && (
80
+ <div className="flex justify-end">
81
+ <Button variant="outline" size="sm" onClick={onDisconnect}>
82
+ Disconnect
83
+ </Button>
84
+ </div>
85
+ )}
86
+ </div>
87
+ );
88
+ }
@@ -101,7 +101,6 @@ export interface KanbanColumnProps {
101
101
  onCardClick?: (opportunityId: string) => void;
102
102
  onViewDetails?: (opportunityId: string) => void;
103
103
  onChangePriority?: (opportunityId: string) => void;
104
- onLaunchAssistant?: (opportunityId: string) => void;
105
104
  onPutOnHold?: (opportunityId: string) => void;
106
105
  onDeleteOpportunity?: (opportunityId: string) => void;
107
106
  /** Fires when a card is dropped onto this column (HTML5 DnD). */
@@ -150,7 +149,6 @@ export function KanbanColumn({
150
149
  onCardClick,
151
150
  onViewDetails,
152
151
  onChangePriority,
153
- onLaunchAssistant,
154
152
  onPutOnHold,
155
153
  onDeleteOpportunity,
156
154
  onCardDrop,
@@ -344,11 +342,6 @@ export function KanbanColumn({
344
342
  ? () => onChangePriority(opp.id)
345
343
  : undefined
346
344
  }
347
- onLaunchAssistant={
348
- onLaunchAssistant
349
- ? () => onLaunchAssistant(opp.id)
350
- : undefined
351
- }
352
345
  onPutOnHold={
353
346
  onPutOnHold ? () => onPutOnHold(opp.id) : undefined
354
347
  }
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Button } from "./button";
3
- import { Stepper, Step, StepItem } from "./stepper";
3
+ import { Stepper, Step, StepItem, StepIndicator, StepLabel } from "./stepper";
4
4
  import { cn } from "../../lib/utils";
5
5
 
6
6
  // ─── Types ────────────────────────────────────────────────────────────────────
@@ -8,6 +8,7 @@ import { cn } from "../../lib/utils";
8
8
  export type OnboardingStep = {
9
9
  id: string;
10
10
  title: string;
11
+ description?: string;
11
12
  isOptional?: boolean;
12
13
  isLocked?: boolean;
13
14
  };
@@ -31,6 +32,106 @@ export type OnboardingLayoutProps = {
31
32
 
32
33
  // ─── OnboardingLayout ─────────────────────────────────────────────────────────
33
34
 
35
+ // ─── OnboardingPanelLayoutProps ───────────────────────────────────────────────
36
+
37
+ export type OnboardingPanelLayoutProps = {
38
+ title: string;
39
+ subtitle?: string;
40
+ steps: OnboardingStep[];
41
+ currentStepIndex: number;
42
+ canProceed?: boolean;
43
+ onBack: () => void;
44
+ onNext: () => void;
45
+ onSkip?: () => void;
46
+ className?: string;
47
+ children: React.ReactNode;
48
+ };
49
+
50
+ // ─── OnboardingPanelLayout ────────────────────────────────────────────────────
51
+
52
+ export function OnboardingPanelLayout({
53
+ title,
54
+ subtitle,
55
+ steps,
56
+ currentStepIndex,
57
+ canProceed = true,
58
+ onBack,
59
+ onNext,
60
+ onSkip,
61
+ className,
62
+ children,
63
+ }: OnboardingPanelLayoutProps) {
64
+ const isFirstStep = currentStepIndex === 0;
65
+ const isLastStep = currentStepIndex === steps.length - 1;
66
+
67
+ return (
68
+ <div className={cn("flex h-screen bg-background font-sans", className)}>
69
+ {/* ── Left panel — vertical stepper ── */}
70
+ <div className="flex w-72 shrink-0 flex-col border-r border-border bg-muted/20 p-8">
71
+ <div className="flex flex-col gap-1 mb-8">
72
+ <span className="text-h1">{title}</span>
73
+ {subtitle && (
74
+ <span className="text-body-medium text-muted-foreground">
75
+ {subtitle}
76
+ </span>
77
+ )}
78
+ </div>
79
+ {steps.length > 1 && (
80
+ <Stepper
81
+ activeStep={currentStepIndex}
82
+ orientation="vertical"
83
+ className="flex-1"
84
+ >
85
+ {steps.map((step) => (
86
+ <Step key={step.id} className="flex-1">
87
+ <StepIndicator />
88
+ <div className="flex flex-1 flex-col">
89
+ <StepLabel
90
+ description={
91
+ step.isLocked ? "Upgrade to unlock" : step.description
92
+ }
93
+ >
94
+ {step.title}
95
+ </StepLabel>
96
+ </div>
97
+ </Step>
98
+ ))}
99
+ </Stepper>
100
+ )}
101
+ </div>
102
+
103
+ {/* ── Right panel — content + footer ── */}
104
+ <div className="flex min-h-0 flex-1 flex-col">
105
+ <div className="flex-1 overflow-y-auto px-10 py-8">
106
+ <div className="max-w-2xl">{children}</div>
107
+ </div>
108
+ <div className="flex items-center gap-2 border-t border-border px-10 py-4">
109
+ <Button
110
+ variant="outline-primary"
111
+ size="sm"
112
+ onClick={onNext}
113
+ disabled={!canProceed}
114
+ >
115
+ {isLastStep ? "Finish Setup" : "Next"}
116
+ </Button>
117
+ {!isFirstStep && (
118
+ <Button variant="outline-secondary" size="sm" onClick={onBack}>
119
+ Back
120
+ </Button>
121
+ )}
122
+ {onSkip && (
123
+ <Button variant="ghost" size="sm" onClick={onSkip}>
124
+ Skip for now
125
+ </Button>
126
+ )}
127
+ </div>
128
+ </div>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ // ─── OnboardingLayout ─────────────────────────────────────────────────────────
134
+
34
135
  export function OnboardingLayout({
35
136
  steps,
36
137
  currentStepIndex,
@@ -9,7 +9,6 @@ import {
9
9
  MoreVertical,
10
10
  Clock,
11
11
  ArrowRight,
12
- Bot,
13
12
  ChevronDown,
14
13
  ChevronRight,
15
14
  } from "lucide-react";
@@ -34,12 +33,6 @@ import {
34
33
  } from "@/components/ui/accordion";
35
34
  import { TaskCheckItem } from "@/components/ui/pipeline-primitives";
36
35
  import { Progress } from "@/components/ui/progress";
37
- import {
38
- Tooltip,
39
- TooltipContent,
40
- TooltipProvider,
41
- TooltipTrigger,
42
- } from "@/components/ui/tooltip";
43
36
 
44
37
  /**
45
38
  * OpportunityCard — WealthX DS (L3 Card)
@@ -63,8 +56,6 @@ export interface OpportunityTask {
63
56
  id: string;
64
57
  title: string;
65
58
  completed: boolean;
66
- aiAgentId?: string | null;
67
- aiAgentName?: string | null;
68
59
  }
69
60
 
70
61
  export interface OpportunityCardProps {
@@ -121,7 +112,6 @@ export interface OpportunityCardProps {
121
112
  onTaskToggle?: (taskId: string) => void;
122
113
  onMarkAsDone?: () => void;
123
114
  onMoveToNextStage?: () => void;
124
- onLaunchAssistant?: () => void;
125
115
  onChangePriority?: () => void;
126
116
  onDelete?: () => void;
127
117
  onPutOnHold?: () => void;
@@ -226,10 +216,6 @@ function TaskViewCard({
226
216
  const hasTasks = totalCount > 0;
227
217
  const allDone = hasTasks && completedCount === totalCount;
228
218
 
229
- // Find the next pending task to surface AI agent info
230
- const nextPendingTask = tasks.find((t) => !t.completed);
231
- const agentName = nextPendingTask?.aiAgentName ?? null;
232
-
233
219
  const [subtasksExpanded, setSubtasksExpanded] = useState(false);
234
220
 
235
221
  const hasMenu = onViewDetails || onChangePriority || onPutOnHold || onDelete;
@@ -262,22 +248,7 @@ function TaskViewCard({
262
248
  );
263
249
  } else if (nextTask) {
264
250
  taskHeader = (
265
- <>
266
- <div className="flex items-center gap-1.5">
267
- {agentName && (
268
- <Bot
269
- className="size-3.5 shrink-0 text-primary"
270
- aria-hidden="true"
271
- />
272
- )}
273
- <p className="truncate text-sm font-semibold leading-snug">
274
- {nextTask}
275
- </p>
276
- </div>
277
- {agentName && (
278
- <p className="mt-0.5 text-xs text-muted-foreground">{agentName}</p>
279
- )}
280
- </>
251
+ <p className="truncate text-sm font-semibold leading-snug">{nextTask}</p>
281
252
  );
282
253
  } else {
283
254
  taskHeader = (
@@ -360,7 +331,6 @@ function TaskViewCard({
360
331
  key={task.id}
361
332
  title={task.title}
362
333
  completed={task.completed}
363
- aiAgentName={task.aiAgentName}
364
334
  onToggle={() => onTaskToggle?.(task.id)}
365
335
  size="xs"
366
336
  />
@@ -486,7 +456,6 @@ export function OpportunityCard({
486
456
  onTaskToggle,
487
457
  onMarkAsDone,
488
458
  onMoveToNextStage,
489
- onLaunchAssistant,
490
459
  onChangePriority,
491
460
  onDelete,
492
461
  onPutOnHold,
@@ -596,25 +565,6 @@ export function OpportunityCard({
596
565
  </div>
597
566
 
598
567
  <div className="flex items-center gap-1 -mr-1 -mt-1" onClick={stopProp}>
599
- {onLaunchAssistant && (
600
- <TooltipProvider delay={0}>
601
- <Tooltip>
602
- <TooltipTrigger asChild>
603
- <Button
604
- type="button"
605
- variant="ghost"
606
- size="icon"
607
- className="size-7 shrink-0"
608
- onClick={onLaunchAssistant}
609
- aria-label="Launch AI"
610
- >
611
- <Bot className="size-4" />
612
- </Button>
613
- </TooltipTrigger>
614
- <TooltipContent>Launch AI</TooltipContent>
615
- </Tooltip>
616
- </TooltipProvider>
617
- )}
618
568
  {hasMenu && (
619
569
  <DropdownMenu>
620
570
  <DropdownMenuTrigger
@@ -762,7 +712,6 @@ export function OpportunityCard({
762
712
  key={task.id}
763
713
  title={task.title}
764
714
  completed={task.completed}
765
- aiAgentName={task.aiAgentName}
766
715
  onToggle={
767
716
  onTaskToggle ? () => onTaskToggle(task.id) : undefined
768
717
  }
@@ -926,7 +875,10 @@ export function LeadCard({
926
875
  Or the link below to fill out the loan application directly.
927
876
  <br />
928
877
  <a
929
- href={`https://${loanApplicationUrl.replace(/^https?:\/\//, "")}`}
878
+ href={`https://${loanApplicationUrl.replace(
879
+ /^https?:\/\//,
880
+ "",
881
+ )}`}
930
882
  target="_blank"
931
883
  rel="noreferrer"
932
884
  className="text-primary underline-offset-2 hover:underline"
@@ -95,7 +95,6 @@ export interface PipelineBoardProps {
95
95
  onMoveToNextStage?: (opportunityId: string) => void;
96
96
  onViewDetails?: (opportunityId: string) => void;
97
97
  onChangePriority?: (opportunityId: string) => void;
98
- onLaunchAssistant?: (opportunityId: string) => void;
99
98
  onPutOnHold?: (opportunityId: string) => void;
100
99
  onDeleteOpportunity?: (opportunityId: string) => void;
101
100
  submittingOpportunityId?: string | null;
@@ -210,7 +209,6 @@ export function PipelineBoard({
210
209
  onMoveToNextStage,
211
210
  onViewDetails,
212
211
  onChangePriority,
213
- onLaunchAssistant,
214
212
  onPutOnHold,
215
213
  onDeleteOpportunity,
216
214
  submittingOpportunityId,
@@ -268,7 +266,6 @@ export function PipelineBoard({
268
266
  onSendLoanApplication={col.onSendLoanApplication}
269
267
  onViewDetails={onViewDetails}
270
268
  onChangePriority={onChangePriority}
271
- onLaunchAssistant={onLaunchAssistant}
272
269
  onPutOnHold={onPutOnHold}
273
270
  onDeleteOpportunity={onDeleteOpportunity}
274
271
  submittingOpportunityId={submittingOpportunityId}
@@ -2,6 +2,12 @@ import * as React from "react";
2
2
  import { cn } from "@/lib/utils";
3
3
  import { useThemeVars } from "@/lib/theme-provider";
4
4
  import { formatCurrencyAbbrev } from "@/lib/format-currency";
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipProvider,
9
+ TooltipTrigger,
10
+ } from "./tooltip";
5
11
 
6
12
  /**
7
13
  * PipelineChart — WealthX DS (L4 Section)
@@ -65,8 +71,6 @@ export function PipelineChart({
65
71
  }: PipelineChartProps) {
66
72
  const themeVars = useThemeVars();
67
73
  const [activeId, setActiveId] = React.useState<string | null>(null);
68
- const [tooltipX, setTooltipX] = React.useState(0);
69
- const barRef = React.useRef<HTMLDivElement>(null);
70
74
 
71
75
  const nonEmpty = stages.filter((s) => s.value > 0);
72
76
  const total = nonEmpty.reduce((sum, s) => sum + s.value, 0);
@@ -86,63 +90,53 @@ export function PipelineChart({
86
90
  );
87
91
  }
88
92
 
89
- const activeStage = activeId ? stages.find((s) => s.id === activeId) : null;
90
-
91
93
  return (
92
94
  <div
93
95
  className={cn("flex flex-col gap-3", className)}
94
96
  data-slot="pipeline-chart"
95
97
  style={themeVars as React.CSSProperties}
96
98
  >
97
- {/* Stacked bar */}
98
- <div
99
- ref={barRef}
100
- className="relative flex w-full overflow-hidden"
101
- style={{ height: barHeight }}
102
- >
103
- {nonEmpty.map((stage, i) => {
104
- const pct = (stage.value / total) * 100;
105
- const color =
106
- stage.color ?? FALLBACK_COLORS[i % FALLBACK_COLORS.length];
107
- const isActive = activeId === stage.id;
108
-
109
- return (
110
- <div
111
- key={stage.id}
112
- role="img"
113
- className="relative h-full cursor-pointer transition-opacity"
114
- style={{
115
- width: `${pct}%`,
116
- backgroundColor: color,
117
- opacity: activeId && !isActive ? 0.5 : 1,
118
- minWidth: 2,
119
- }}
120
- onMouseEnter={(e) => {
121
- setActiveId(stage.id);
122
- const bar = barRef.current;
123
- if (!bar) return;
124
- const rect = e.currentTarget.getBoundingClientRect();
125
- const barRect = bar.getBoundingClientRect();
126
- setTooltipX(rect.left + rect.width / 2 - barRect.left);
127
- }}
128
- onMouseLeave={() => setActiveId(null)}
129
- aria-label={`${stage.name}: ${formatValue(stage.value)}`}
130
- />
131
- );
132
- })}
133
-
134
- {/* Tooltip */}
135
- {activeStage && (
136
- <div
137
- className="pointer-events-none absolute -top-8 z-10 whitespace-nowrap border border-border bg-popover px-2 py-1 text-xs text-popover-foreground shadow-sm"
138
- style={{ left: tooltipX, transform: "translateX(-50%)" }}
139
- >
140
- <span className="font-medium">{activeStage.name}</span>
141
- {" — "}
142
- {formatValue(activeStage.value)}
143
- </div>
144
- )}
145
- </div>
99
+ {/* Stacked bar — TooltipContent uses a portal so it is never clipped */}
100
+ <TooltipProvider>
101
+ <div
102
+ className="relative flex w-full overflow-hidden"
103
+ style={{ height: barHeight }}
104
+ >
105
+ {nonEmpty.map((stage, i) => {
106
+ const pct = (stage.value / total) * 100;
107
+ const color =
108
+ stage.color ?? FALLBACK_COLORS[i % FALLBACK_COLORS.length];
109
+ const isActive = activeId === stage.id;
110
+
111
+ return (
112
+ <Tooltip key={stage.id}>
113
+ <TooltipTrigger
114
+ render={
115
+ <div
116
+ role="img"
117
+ className="relative h-full cursor-pointer transition-opacity"
118
+ style={{
119
+ width: `${pct}%`,
120
+ backgroundColor: color,
121
+ opacity: activeId && !isActive ? 0.5 : 1,
122
+ minWidth: 2,
123
+ }}
124
+ onMouseEnter={() => setActiveId(stage.id)}
125
+ onMouseLeave={() => setActiveId(null)}
126
+ aria-label={`${stage.name}: ${formatValue(stage.value)}`}
127
+ />
128
+ }
129
+ />
130
+ <TooltipContent side="top">
131
+ <span className="font-medium">{stage.name}</span>
132
+ {" — "}
133
+ {formatValue(stage.value)}
134
+ </TooltipContent>
135
+ </Tooltip>
136
+ );
137
+ })}
138
+ </div>
139
+ </TooltipProvider>
146
140
 
147
141
  {/* Legend */}
148
142
  <div className="flex flex-wrap gap-x-4 gap-y-1.5">