@wealthx/shadcn 1.5.14 → 1.5.16

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wealthx/shadcn",
3
- "version": "1.5.14",
3
+ "version": "1.5.16",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -19,6 +19,7 @@ import {
19
19
  MapPin,
20
20
  MessageSquare,
21
21
  MoreHorizontal,
22
+ Navigation,
22
23
  Paperclip,
23
24
  Phone,
24
25
  PhoneCall,
@@ -154,6 +155,8 @@ export interface AiConvDataField {
154
155
  export interface AiConvAppointmentData {
155
156
  datetime: string;
156
157
  meetingType: AiConvMeetingType;
158
+ /** Meeting link (video), phone number (phone), or address (in-person). */
159
+ meetingDetail?: string;
157
160
  status: "requested" | "confirmed" | "pending" | "cancelled";
158
161
  }
159
162
 
@@ -1298,6 +1301,12 @@ const MEETING_LABEL: Record<AiConvMeetingType, string> = {
1298
1301
  "in-person": "In Person",
1299
1302
  };
1300
1303
 
1304
+ const MEETING_DETAIL_ICON: Record<AiConvMeetingType, React.ElementType> = {
1305
+ video: Link2,
1306
+ phone: PhoneCall,
1307
+ "in-person": Navigation,
1308
+ };
1309
+
1301
1310
  interface AppointmentSectionProps {
1302
1311
  appointment: AiConvAppointmentData;
1303
1312
  contactId: string;
@@ -1307,6 +1316,36 @@ interface AppointmentSectionProps {
1307
1316
  onRescheduleAppointment?: (contactId: string) => void;
1308
1317
  }
1309
1318
 
1319
+ function MeetingDetailRow({
1320
+ meetingType,
1321
+ detail,
1322
+ }: {
1323
+ meetingType: AiConvMeetingType;
1324
+ detail: string;
1325
+ }) {
1326
+ const DetailIcon = MEETING_DETAIL_ICON[meetingType];
1327
+ const isLink = detail.startsWith("http");
1328
+ return (
1329
+ <div className="flex items-center gap-2">
1330
+ <DetailIcon className="size-4 shrink-0 text-muted-foreground" />
1331
+ {isLink ? (
1332
+ <a
1333
+ href={detail}
1334
+ target="_blank"
1335
+ rel="noopener noreferrer"
1336
+ className="text-sm text-primary underline underline-offset-2 break-all hover:text-primary/80"
1337
+ >
1338
+ {detail}
1339
+ </a>
1340
+ ) : (
1341
+ <span className="text-sm text-muted-foreground break-all">
1342
+ {detail}
1343
+ </span>
1344
+ )}
1345
+ </div>
1346
+ );
1347
+ }
1348
+
1310
1349
  function AppointmentSection({
1311
1350
  appointment,
1312
1351
  contactId,
@@ -1332,6 +1371,12 @@ function AppointmentSection({
1332
1371
  {MEETING_LABEL[appointment.meetingType]}
1333
1372
  </span>
1334
1373
  </div>
1374
+ {appointment.meetingDetail && (
1375
+ <MeetingDetailRow
1376
+ meetingType={appointment.meetingType}
1377
+ detail={appointment.meetingDetail}
1378
+ />
1379
+ )}
1335
1380
  <span
1336
1381
  className={cn("text-sm font-medium", {
1337
1382
  "text-warning-text": appointment.status === "requested",
@@ -612,7 +612,7 @@ export function AppointmentBookDialog({
612
612
 
613
613
  <Separator />
614
614
 
615
- <div className="flex flex-col gap-4">
615
+ <div className="flex flex-col gap-4 overflow-y-auto max-h-[calc(90vh-200px)]">
616
616
  {!isClientMode && (
617
617
  <div className="flex flex-col gap-1.5">
618
618
  <Label>Client</Label>
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { MoreVertical } from "lucide-react";
2
+ import { MoreVertical, Plus } from "lucide-react";
3
3
  import { cn } from "@/lib/utils";
4
4
  import { useThemeVars } from "@/lib/theme-provider";
5
5
  import { formatCurrency } from "@/lib/format-currency";
@@ -15,6 +15,12 @@ import {
15
15
  } from "@/components/ui/dropdown-menu";
16
16
  import { LeadCard, OpportunityCard } from "@/components/ui/opportunity-card";
17
17
  import type { OpportunityCardProps } from "@/components/ui/opportunity-card";
18
+ import {
19
+ Tooltip,
20
+ TooltipContent,
21
+ TooltipProvider,
22
+ TooltipTrigger,
23
+ } from "@/components/ui/tooltip";
18
24
 
19
25
  /**
20
26
  * KanbanColumn — WealthX DS (L4 Section)
@@ -52,6 +58,12 @@ export interface KanbanColumnStage {
52
58
  * Defaults to `"var(--color-border)"`.
53
59
  */
54
60
  accentColor?: string | null;
61
+ /**
62
+ * Optional color for the stage count number in the column header.
63
+ * Pass a CSS variable string: `"var(--color-destructive)"`.
64
+ * Defaults to `text-muted-foreground`.
65
+ */
66
+ countColor?: string | null;
55
67
  }
56
68
 
57
69
  export interface KanbanColumnProps {
@@ -91,6 +103,11 @@ export interface KanbanColumnProps {
91
103
  onEditColumn?: () => void;
92
104
  /** Only shown for non-default columns. */
93
105
  onDeleteColumn?: () => void;
106
+ /**
107
+ * When provided, renders a `+` icon button in the column header.
108
+ * Intended for the Leads column — opens the Add Lead flow.
109
+ */
110
+ onAddLead?: () => void;
94
111
 
95
112
  // ── Card callbacks (forwarded to each OpportunityCard) ───────
96
113
  onTaskToggle?: (opportunityId: string, taskId: string) => void;
@@ -115,10 +132,6 @@ export interface KanbanColumnProps {
115
132
  // Helpers
116
133
  // ---------------------------------------------------------------------------
117
134
 
118
- function formatTotalValue(value: number): string {
119
- return formatCurrency(value);
120
- }
121
-
122
135
  function growthColor(growth: number): string {
123
136
  // Use darkened text tokens — full-saturation tokens (#4CAF50, #F44336) fail 4.5:1 on white bg
124
137
  return growth > 0
@@ -142,6 +155,7 @@ export function KanbanColumn({
142
155
  loaderRef,
143
156
  onEditColumn,
144
157
  onDeleteColumn,
158
+ onAddLead,
145
159
  onTaskToggle,
146
160
  onMarkAsDone,
147
161
  onMoveToNextStage,
@@ -203,41 +217,68 @@ export function KanbanColumn({
203
217
  {/* Title row */}
204
218
  <div className="flex items-center justify-between gap-2">
205
219
  <h2 className="text-sm font-semibold text-foreground">
206
- <span className="text-muted-foreground">{stage.count}</span>{" "}
220
+ <span
221
+ className={cn(!stage.countColor && "text-muted-foreground")}
222
+ style={stage.countColor ? { color: stage.countColor } : undefined}
223
+ >
224
+ {stage.count}
225
+ </span>{" "}
207
226
  {stage.name}
208
227
  </h2>
209
228
 
210
- {hasMenu && (
211
- <DropdownMenu>
212
- <DropdownMenuTrigger
213
- className={cn(
214
- buttonVariants({ variant: "ghost", size: "icon" }),
215
- "-mr-1 size-7 shrink-0",
216
- )}
217
- aria-label="Column actions"
218
- >
219
- <MoreVertical className="size-4" />
220
- </DropdownMenuTrigger>
221
- <DropdownMenuContent align="end">
222
- {onEditColumn && (
223
- <DropdownMenuItem onClick={onEditColumn}>
224
- Edit column settings
225
- </DropdownMenuItem>
226
- )}
227
- {!isDefault && onDeleteColumn && (
228
- <>
229
- {onEditColumn && <DropdownMenuSeparator />}
230
- <DropdownMenuItem
231
- onClick={onDeleteColumn}
232
- className="text-destructive focus:text-destructive"
229
+ <div className="flex items-center gap-0.5">
230
+ {onAddLead && (
231
+ <TooltipProvider>
232
+ <Tooltip>
233
+ <TooltipTrigger asChild>
234
+ <button
235
+ className={cn(
236
+ buttonVariants({ variant: "ghost", size: "icon" }),
237
+ "size-7 shrink-0",
238
+ )}
239
+ onClick={onAddLead}
240
+ aria-label="Add lead"
233
241
  >
234
- Delete column
242
+ <Plus className="size-4" />
243
+ </button>
244
+ </TooltipTrigger>
245
+ <TooltipContent>Add lead</TooltipContent>
246
+ </Tooltip>
247
+ </TooltipProvider>
248
+ )}
249
+
250
+ {hasMenu && (
251
+ <DropdownMenu>
252
+ <DropdownMenuTrigger
253
+ className={cn(
254
+ buttonVariants({ variant: "ghost", size: "icon" }),
255
+ "-mr-1 size-7 shrink-0",
256
+ )}
257
+ aria-label="Column actions"
258
+ >
259
+ <MoreVertical className="size-4" />
260
+ </DropdownMenuTrigger>
261
+ <DropdownMenuContent align="end">
262
+ {onEditColumn && (
263
+ <DropdownMenuItem onClick={onEditColumn}>
264
+ Edit column settings
235
265
  </DropdownMenuItem>
236
- </>
237
- )}
238
- </DropdownMenuContent>
239
- </DropdownMenu>
240
- )}
266
+ )}
267
+ {!isDefault && onDeleteColumn && (
268
+ <>
269
+ {onEditColumn && <DropdownMenuSeparator />}
270
+ <DropdownMenuItem
271
+ onClick={onDeleteColumn}
272
+ className="text-destructive focus:text-destructive"
273
+ >
274
+ Delete column
275
+ </DropdownMenuItem>
276
+ </>
277
+ )}
278
+ </DropdownMenuContent>
279
+ </DropdownMenu>
280
+ )}
281
+ </div>
241
282
  </div>
242
283
 
243
284
  {/* Stats row */}
@@ -253,8 +294,8 @@ export function KanbanColumn({
253
294
  ) : (
254
295
  <span />
255
296
  )}
256
- <span className="text-xs font-medium tabular-nums text-muted-foreground">
257
- {formatTotalValue(stage.totalValue)}
297
+ <span className="text-sm font-bold tabular-nums text-foreground">
298
+ {formatCurrency(stage.totalValue)}
258
299
  </span>
259
300
  </div>
260
301
  </div>
@@ -5,10 +5,7 @@ import { cn } from "@/lib/utils";
5
5
  import { Input } from "@/components/ui/input";
6
6
  import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
7
7
  import { KanbanColumn } from "@/components/ui/kanban-column";
8
- import type {
9
- KanbanColumnProps,
10
- KanbanColumnStage,
11
- } from "@/components/ui/kanban-column";
8
+ import type { KanbanColumnStage } from "@/components/ui/kanban-column";
12
9
  import type { OpportunityCardProps } from "@/components/ui/opportunity-card";
13
10
 
14
11
  /**
@@ -53,10 +50,21 @@ export interface PipelineBoardColumn {
53
50
  isDropTarget?: boolean;
54
51
  /** Whether this is a fixed/system column (no Delete in menu). */
55
52
  isDefault?: boolean;
53
+ /**
54
+ * When true, this column is rendered in a fixed left section and does not
55
+ * scroll horizontally with the rest of the board. Use for the High Priority
56
+ * column, which must always be visible.
57
+ */
58
+ isPinned?: boolean;
56
59
  isLoading?: boolean;
57
60
  isLoadingMore?: boolean;
58
61
  hasMore?: boolean;
59
62
  loaderRef?: React.Ref<HTMLDivElement>;
63
+ /**
64
+ * When provided, renders a `+` icon button in the column header.
65
+ * Intended for the Leads column — opens the Add Lead flow.
66
+ */
67
+ onAddLead?: () => void;
60
68
  }
61
69
 
62
70
  export interface PipelineBoardProps {
@@ -78,8 +86,6 @@ export interface PipelineBoardProps {
78
86
  // ── Column callbacks ─────────────────────────────────────────
79
87
  onEditColumn?: (stageId: string) => void;
80
88
  onDeleteColumn?: (stageId: string) => void;
81
-
82
- // ── Toolbar ──────────────────────────────────────────────────
83
89
  onRefresh?: () => void;
84
90
 
85
91
  // ── Card callbacks ───────────────────────────────────────────
@@ -217,6 +223,48 @@ export function PipelineBoard({
217
223
  const hasToolbar =
218
224
  onSearchChange || (filterOptions.length > 0 && onFilterChange);
219
225
 
226
+ const pinnedCols = columns.filter((c) => c.isPinned);
227
+ const scrollableCols = columns.filter((c) => !c.isPinned);
228
+
229
+ const renderColumn = (col: PipelineBoardColumn) => (
230
+ <KanbanColumn
231
+ key={col.key}
232
+ stage={col.stage}
233
+ opportunities={col.opportunities}
234
+ isDragging={col.isDragging}
235
+ isDropTarget={col.isDropTarget}
236
+ isDefault={col.isDefault}
237
+ isLoading={col.isLoading}
238
+ isLoadingMore={col.isLoadingMore}
239
+ hasMore={col.hasMore}
240
+ loaderRef={col.loaderRef}
241
+ onEditColumn={
242
+ !col.isPinned && onEditColumn
243
+ ? () => onEditColumn(col.stage.id)
244
+ : undefined
245
+ }
246
+ onDeleteColumn={
247
+ onDeleteColumn && !col.isDefault && !col.isPinned
248
+ ? () => onDeleteColumn(col.stage.id)
249
+ : undefined
250
+ }
251
+ onCardDrop={
252
+ onMoveCard ? (cardId) => onMoveCard(cardId, col.key) : undefined
253
+ }
254
+ onCardClick={onCardClick}
255
+ onTaskToggle={onTaskToggle}
256
+ onMarkAsDone={onMarkAsDone}
257
+ onMoveToNextStage={onMoveToNextStage}
258
+ onSendLoanApplication={col.onSendLoanApplication}
259
+ onViewDetails={onViewDetails}
260
+ onChangePriority={onChangePriority}
261
+ onPutOnHold={onPutOnHold}
262
+ onDeleteOpportunity={onDeleteOpportunity}
263
+ onAddLead={col.onAddLead}
264
+ submittingOpportunityId={submittingOpportunityId}
265
+ />
266
+ );
267
+
220
268
  return (
221
269
  <div
222
270
  className={cn("flex h-full flex-col bg-muted/20", className)}
@@ -234,51 +282,27 @@ export function PipelineBoard({
234
282
  />
235
283
  )}
236
284
 
237
- {/* ── Board scroll area ── */}
238
- <div className="flex flex-1 gap-3 overflow-x-auto p-4">
239
- {columns.map((col) => (
240
- <KanbanColumn
241
- key={col.key}
242
- stage={col.stage}
243
- opportunities={col.opportunities}
244
- isDragging={col.isDragging}
245
- isDropTarget={col.isDropTarget}
246
- isDefault={col.isDefault}
247
- isLoading={col.isLoading}
248
- isLoadingMore={col.isLoadingMore}
249
- hasMore={col.hasMore}
250
- loaderRef={col.loaderRef}
251
- onEditColumn={
252
- onEditColumn ? () => onEditColumn(col.stage.id) : undefined
253
- }
254
- onDeleteColumn={
255
- onDeleteColumn && !col.isDefault
256
- ? () => onDeleteColumn(col.stage.id)
257
- : undefined
258
- }
259
- onCardDrop={
260
- onMoveCard ? (cardId) => onMoveCard(cardId, col.key) : undefined
261
- }
262
- onCardClick={onCardClick}
263
- onTaskToggle={onTaskToggle}
264
- onMarkAsDone={onMarkAsDone}
265
- onMoveToNextStage={onMoveToNextStage}
266
- onSendLoanApplication={col.onSendLoanApplication}
267
- onViewDetails={onViewDetails}
268
- onChangePriority={onChangePriority}
269
- onPutOnHold={onPutOnHold}
270
- onDeleteOpportunity={onDeleteOpportunity}
271
- submittingOpportunityId={submittingOpportunityId}
272
- />
273
- ))}
274
-
275
- {columns.length === 0 && (
276
- <div className="flex flex-1 items-center justify-center">
277
- <p className="text-sm text-muted-foreground">
278
- No columns to display.
279
- </p>
285
+ {/* ── Board area ── */}
286
+ <div className="flex flex-1 overflow-hidden">
287
+ {/* Pinned columns always visible, do not scroll */}
288
+ {pinnedCols.length > 0 && (
289
+ <div className="flex shrink-0 gap-3 border-r border-border p-4">
290
+ {pinnedCols.map(renderColumn)}
280
291
  </div>
281
292
  )}
293
+
294
+ {/* Scrollable columns */}
295
+ <div className="flex flex-1 gap-3 overflow-x-auto p-4">
296
+ {scrollableCols.map(renderColumn)}
297
+
298
+ {columns.length === 0 && (
299
+ <div className="flex flex-1 items-center justify-center">
300
+ <p className="text-sm text-muted-foreground">
301
+ No columns to display.
302
+ </p>
303
+ </div>
304
+ )}
305
+ </div>
282
306
  </div>
283
307
  </div>
284
308
  );