@wealthx/shadcn 1.5.0 → 1.5.2

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 +119 -119
  2. package/CHANGELOG.md +12 -0
  3. package/dist/chunk-G2EWIP2N.mjs +960 -0
  4. package/dist/{chunk-MHHA7QGO.mjs → chunk-ODO6BUOF.mjs} +1 -1
  5. package/dist/chunk-PX4M67XQ.mjs +301 -0
  6. package/dist/{chunk-FYUSF5KO.mjs → chunk-QRVEI6J3.mjs} +1 -1
  7. package/dist/{chunk-42NEC57Y.mjs → chunk-RAKBWNQH.mjs} +272 -3
  8. package/dist/components/ui/{contact-alert-dialog.js → contact-alert-dialog/index.js} +1029 -593
  9. package/dist/components/ui/contact-alert-dialog/index.mjs +31 -0
  10. package/dist/components/ui/file-preview-dialog.js +407 -100
  11. package/dist/components/ui/file-preview-dialog.mjs +3 -1
  12. package/dist/components/ui/kanban-column.js +408 -113
  13. package/dist/components/ui/kanban-column.mjs +3 -2
  14. package/dist/components/ui/opportunity-card.js +383 -88
  15. package/dist/components/ui/opportunity-card.mjs +2 -1
  16. package/dist/components/ui/pipeline-board.js +424 -129
  17. package/dist/components/ui/pipeline-board.mjs +4 -3
  18. package/dist/index.js +3081 -2282
  19. package/dist/index.mjs +39 -35
  20. package/dist/styles.css +1 -1
  21. package/package.json +6 -5
  22. package/src/components/index.tsx +3 -2
  23. package/src/components/ui/contact-alert-dialog/builder-ui.tsx +556 -0
  24. package/src/components/ui/contact-alert-dialog/config.ts +262 -0
  25. package/src/components/ui/contact-alert-dialog/contact-alert-dialog.tsx +214 -0
  26. package/src/components/ui/contact-alert-dialog/index.tsx +15 -0
  27. package/src/components/ui/contact-alert-dialog/types.ts +61 -0
  28. package/src/components/ui/contact-alert-dialog/utils.ts +93 -0
  29. package/src/components/ui/file-preview-dialog.tsx +299 -99
  30. package/src/components/ui/opportunity-card.tsx +328 -1
  31. package/src/styles/styles-css.ts +1 -1
  32. package/tsup.config.ts +1 -1
  33. package/dist/chunk-5WMFKQZ6.mjs +0 -180
  34. package/dist/chunk-Y24TXIFJ.mjs +0 -518
  35. package/dist/components/ui/contact-alert-dialog.mjs +0 -27
  36. package/src/components/ui/contact-alert-dialog.tsx +0 -710
@@ -1,5 +1,11 @@
1
1
  import * as React from "react";
2
- import { AlertCircleIcon, FileTextIcon, GripVerticalIcon } from "lucide-react";
2
+ import {
3
+ AlertCircleIcon,
4
+ CheckCircle2Icon,
5
+ CircleAlertIcon,
6
+ FileTextIcon,
7
+ Trash2Icon,
8
+ } from "lucide-react";
3
9
  import {
4
10
  Dialog,
5
11
  DialogContent,
@@ -19,6 +25,19 @@ import {
19
25
  TableHeader,
20
26
  TableRow,
21
27
  } from "@/components/ui/table";
28
+ import {
29
+ Tooltip,
30
+ TooltipContent,
31
+ TooltipProvider,
32
+ TooltipTrigger,
33
+ } from "@/components/ui/tooltip";
34
+ import {
35
+ Select,
36
+ SelectContent,
37
+ SelectItem,
38
+ SelectTrigger,
39
+ SelectValue,
40
+ } from "@/components/ui/select";
22
41
  import { cn } from "@/lib/utils";
23
42
 
24
43
  // ---------------------------------------------------------------------------
@@ -32,12 +51,25 @@ export type FilePreviewState =
32
51
  | "error"
33
52
  | "importing";
34
53
 
54
+ export type CsvRowStatus = "pending" | "success" | "failed";
55
+
35
56
  export interface CsvPreviewColumn {
36
57
  key: string;
37
58
  /** Display label shown in the column header. */
38
59
  label: string;
39
60
  }
40
61
 
62
+ export interface CsvPreviewRow extends Record<string, string | undefined> {
63
+ _status?: CsvRowStatus;
64
+ /** Shown in a tooltip when status is "failed". */
65
+ _statusMessage?: string;
66
+ }
67
+
68
+ export interface StaffOption {
69
+ id: string;
70
+ name: string;
71
+ }
72
+
41
73
  export interface FilePreviewDialogProps {
42
74
  open: boolean;
43
75
  onOpenChange: (open: boolean) => void;
@@ -48,22 +80,33 @@ export interface FilePreviewDialogProps {
48
80
  /** Ordered list of columns to display. */
49
81
  columns?: CsvPreviewColumn[];
50
82
  /** Data rows — keys match CsvPreviewColumn.key. */
51
- rows?: Record<string, string>[];
52
- /**
53
- * Called when the user reorders a column via drag-and-drop.
54
- * sourceIndex and targetIndex refer to positions in the columns array.
55
- */
56
- onColumnReorder?: (sourceIndex: number, targetIndex: number) => void;
83
+ rows?: CsvPreviewRow[];
57
84
  /** Called when the user edits a cell inline. */
58
85
  onRowChange?: (rowIndex: number, key: string, value: string) => void;
86
+ /** Called when the user clicks the delete icon on a row. */
87
+ onRowDelete?: (rowIndex: number) => void;
59
88
  /** Called when the user clicks "Import". */
60
89
  onImport?: () => void;
90
+ /** Called when the user clicks "Cancel" during import. */
91
+ onCancelImport?: () => void;
61
92
  /** 0–100 progress value shown in the importing state. */
62
93
  importProgress?: number;
63
94
  /** Total rows in the file (shown in the preview header). */
64
95
  totalRows?: number;
65
96
  /** Rows that passed validation (shown in the preview header). */
66
97
  validRows?: number;
98
+ /** Rows per page for the preview table. Defaults to 10. */
99
+ pageSize?: number;
100
+ /**
101
+ * List of staff members available for assignment.
102
+ * When provided, a staff selector is rendered above the table.
103
+ * Import is blocked until a staff member is selected.
104
+ */
105
+ staffOptions?: StaffOption[];
106
+ /** Currently selected staff ID. */
107
+ selectedStaffId?: string;
108
+ /** Called when the user picks a staff member. */
109
+ onStaffSelect?: (staffId: string) => void;
67
110
  className?: string;
68
111
  }
69
112
 
@@ -126,20 +169,64 @@ function ErrorState({ message }: { message?: string }) {
126
169
  );
127
170
  }
128
171
 
129
- function ImportingState({ progress }: { progress: number }) {
172
+ function ImportingState({
173
+ progress,
174
+ successCount,
175
+ failedCount,
176
+ }: {
177
+ progress: number;
178
+ successCount: number;
179
+ failedCount: number;
180
+ }) {
130
181
  return (
131
- <div className="flex flex-col items-center gap-4 py-12">
182
+ <div className="flex flex-col items-center gap-4 py-8">
132
183
  <Spinner className="size-6" />
133
184
  <div className="w-full max-w-sm space-y-2">
134
185
  <Progress value={progress} className="h-1.5" />
135
- <p className="text-center text-xs text-muted-foreground">
136
- Importing… {progress}%
137
- </p>
186
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
187
+ <span>Importing… {progress}%</span>
188
+ {(successCount > 0 || failedCount > 0) && (
189
+ <span>
190
+ {successCount} success · {failedCount} failed
191
+ </span>
192
+ )}
193
+ </div>
138
194
  </div>
139
195
  </div>
140
196
  );
141
197
  }
142
198
 
199
+ function RowStatusCell({ row }: { row: CsvPreviewRow }) {
200
+ const { _status, _statusMessage } = row;
201
+
202
+ if (_status === "success") {
203
+ return (
204
+ <div className="flex items-center justify-center h-full px-2">
205
+ <CheckCircle2Icon className="size-4 text-success" />
206
+ </div>
207
+ );
208
+ }
209
+
210
+ if (_status === "failed") {
211
+ return (
212
+ <TooltipProvider>
213
+ <Tooltip>
214
+ <TooltipTrigger asChild>
215
+ <div className="flex items-center justify-center h-full px-2 cursor-default">
216
+ <CircleAlertIcon className="size-4 text-destructive" />
217
+ </div>
218
+ </TooltipTrigger>
219
+ {_statusMessage && (
220
+ <TooltipContent side="top">{_statusMessage}</TooltipContent>
221
+ )}
222
+ </Tooltip>
223
+ </TooltipProvider>
224
+ );
225
+ }
226
+
227
+ return null;
228
+ }
229
+
143
230
  // ---------------------------------------------------------------------------
144
231
  // Main component
145
232
  // ---------------------------------------------------------------------------
@@ -152,43 +239,41 @@ export function FilePreviewDialog({
152
239
  errorMessage,
153
240
  columns = [],
154
241
  rows = [],
155
- onColumnReorder,
156
242
  onRowChange,
243
+ onRowDelete,
157
244
  onImport,
245
+ onCancelImport,
158
246
  importProgress = 0,
159
247
  totalRows,
160
248
  validRows,
249
+ pageSize = 10,
250
+ staffOptions,
251
+ selectedStaffId,
252
+ onStaffSelect,
161
253
  className,
162
254
  }: FilePreviewDialogProps) {
163
- // Drag-and-drop state for column reordering
164
- const dragSourceRef = React.useRef<number | null>(null);
165
- const [dragOverIndex, setDragOverIndex] = React.useState<number | null>(null);
255
+ const [page, setPage] = React.useState(0);
166
256
 
167
- function handleDragStart(index: number) {
168
- dragSourceRef.current = index;
169
- }
257
+ // Reset page when rows change
258
+ React.useEffect(() => {
259
+ setPage(0);
260
+ }, [rows.length]);
170
261
 
171
- function handleDragOver(e: React.DragEvent, index: number) {
172
- e.preventDefault();
173
- setDragOverIndex(index);
174
- }
175
-
176
- function handleDrop(targetIndex: number) {
177
- const source = dragSourceRef.current;
178
- if (source !== null && source !== targetIndex) {
179
- onColumnReorder?.(source, targetIndex);
180
- }
181
- dragSourceRef.current = null;
182
- setDragOverIndex(null);
183
- }
184
-
185
- function handleDragEnd() {
186
- dragSourceRef.current = null;
187
- setDragOverIndex(null);
188
- }
262
+ const totalPages = Math.ceil(rows.length / pageSize);
263
+ const pagedRows = rows.slice(page * pageSize, (page + 1) * pageSize);
264
+ const pageStart = page * pageSize; // used for row index display
189
265
 
190
266
  const isImporting = state === "importing";
191
- const canImport = state === "preview" && rows.length > 0;
267
+ const hasStaffSelector = !!staffOptions && staffOptions.length > 0;
268
+ // Import is blocked when staff selection is required but none is chosen yet
269
+ const canImport =
270
+ state === "preview" &&
271
+ rows.length > 0 &&
272
+ (!hasStaffSelector || !!selectedStaffId);
273
+
274
+ const successCount = rows.filter((r) => r._status === "success").length;
275
+ const failedCount = rows.filter((r) => r._status === "failed").length;
276
+ const hasStatus = rows.some((r) => r._status !== undefined);
192
277
 
193
278
  return (
194
279
  <Dialog open={open} onOpenChange={isImporting ? undefined : onOpenChange}>
@@ -208,83 +293,198 @@ export function FilePreviewDialog({
208
293
  total
209
294
  </span>
210
295
  )}
296
+ {hasStatus && (
297
+ <span>
298
+ {successCount} success · {failedCount} failed
299
+ </span>
300
+ )}
211
301
  </div>
212
302
  )}
213
303
 
214
304
  {/* Content by state */}
215
305
  {state === "loading" && <LoadingState columnCount={columns.length} />}
216
-
217
306
  {state === "empty" && <EmptyState />}
218
-
219
307
  {state === "error" && <ErrorState message={errorMessage} />}
220
-
221
- {state === "importing" && <ImportingState progress={importProgress} />}
308
+ {state === "importing" && (
309
+ <ImportingState
310
+ progress={importProgress}
311
+ successCount={successCount}
312
+ failedCount={failedCount}
313
+ />
314
+ )}
222
315
 
223
316
  {state === "preview" && (
224
- <div className="max-h-[400px] overflow-auto border border-border">
225
- <Table>
226
- <TableHeader className="sticky top-0 z-10 bg-muted/80 backdrop-blur-sm">
227
- <TableRow>
228
- {columns.map((col, colIdx) => (
229
- <TableHead
230
- key={col.key}
231
- draggable
232
- onDragStart={() => handleDragStart(colIdx)}
233
- onDragOver={(e) => handleDragOver(e, colIdx)}
234
- onDrop={() => handleDrop(colIdx)}
235
- onDragEnd={handleDragEnd}
236
- className={cn(
237
- "cursor-grab select-none transition-colors",
238
- dragOverIndex === colIdx &&
239
- dragSourceRef.current !== colIdx &&
240
- "bg-primary/10",
241
- )}
242
- >
243
- <div className="flex items-center gap-1.5">
244
- <GripVerticalIcon className="size-3.5 shrink-0 text-muted-foreground" />
245
- <span>{col.label}</span>
246
- </div>
247
- </TableHead>
248
- ))}
249
- </TableRow>
250
- </TableHeader>
317
+ <>
318
+ {/* Staff assignment — shown when caller provides staffOptions */}
319
+ {hasStaffSelector && (
320
+ <div className="flex flex-col gap-1">
321
+ <label className="text-label-medium text-foreground">
322
+ Assign staff{" "}
323
+ <span className="text-destructive" aria-hidden="true">
324
+ *
325
+ </span>
326
+ </label>
327
+ <Select
328
+ value={selectedStaffId ?? ""}
329
+ onValueChange={(id) => onStaffSelect?.(id)}
330
+ >
331
+ <SelectTrigger className="w-full">
332
+ <SelectValue placeholder="Select a staff member" />
333
+ </SelectTrigger>
334
+ <SelectContent>
335
+ {staffOptions!.map((s) => (
336
+ <SelectItem key={s.id} value={s.id}>
337
+ {s.name}
338
+ </SelectItem>
339
+ ))}
340
+ </SelectContent>
341
+ </Select>
342
+ <p className="text-xs text-muted-foreground">
343
+ All contacts in this import will be assigned to the selected
344
+ staff member.
345
+ </p>
346
+ </div>
347
+ )}
251
348
 
252
- <TableBody>
253
- {rows.map((row, rowIdx) => (
254
- <TableRow key={rowIdx}>
349
+ <div className="max-h-[360px] overflow-auto border border-border">
350
+ <Table>
351
+ <TableHeader className="sticky top-0 z-10 bg-muted/80 backdrop-blur-sm">
352
+ <TableRow>
353
+ {/* Index column */}
354
+ <TableHead className="w-10 text-center select-none">
355
+ #
356
+ </TableHead>
255
357
  {columns.map((col) => (
256
- <TableCell key={col.key} className="p-0">
257
- <input
258
- type="text"
259
- value={row[col.key] ?? ""}
260
- onChange={(e) =>
261
- onRowChange?.(rowIdx, col.key, e.target.value)
262
- }
263
- className={cn(
264
- "w-full bg-transparent px-4 py-3 text-sm text-foreground",
265
- "focus:outline-none focus:ring-1 focus:ring-inset focus:ring-primary",
266
- )}
267
- />
268
- </TableCell>
358
+ <TableHead key={col.key} className="select-none">
359
+ {col.label}
360
+ </TableHead>
269
361
  ))}
362
+ {/* Status column — only shown when any row has status */}
363
+ {hasStatus && (
364
+ <TableHead className="w-16 text-center select-none">
365
+ Status
366
+ </TableHead>
367
+ )}
368
+ {/* Delete column */}
369
+ {onRowDelete && <TableHead className="w-10 select-none" />}
270
370
  </TableRow>
271
- ))}
272
- </TableBody>
273
- </Table>
274
- </div>
371
+ </TableHeader>
372
+
373
+ <TableBody>
374
+ {pagedRows.map((row, pageRowIdx) => {
375
+ const absoluteIdx = pageStart + pageRowIdx;
376
+ return (
377
+ <TableRow
378
+ key={absoluteIdx}
379
+ className={cn(
380
+ row._status === "failed" && "bg-destructive/5",
381
+ row._status === "success" && "bg-success/5",
382
+ )}
383
+ >
384
+ {/* Index cell */}
385
+ <TableCell className="text-center text-xs text-muted-foreground select-none w-10">
386
+ {absoluteIdx + 1}
387
+ </TableCell>
388
+
389
+ {/* Data cells */}
390
+ {columns.map((col) => (
391
+ <TableCell key={col.key} className="p-0">
392
+ <input
393
+ type="text"
394
+ value={row[col.key] ?? ""}
395
+ onChange={(e) =>
396
+ onRowChange?.(
397
+ absoluteIdx,
398
+ col.key,
399
+ e.target.value,
400
+ )
401
+ }
402
+ className={cn(
403
+ "w-full bg-transparent px-4 py-3 text-sm text-foreground",
404
+ "focus:outline-none focus:ring-1 focus:ring-inset focus:ring-primary",
405
+ )}
406
+ />
407
+ </TableCell>
408
+ ))}
409
+
410
+ {/* Status cell */}
411
+ {hasStatus && (
412
+ <TableCell className="text-center w-16 p-0">
413
+ <RowStatusCell row={row} />
414
+ </TableCell>
415
+ )}
416
+
417
+ {/* Delete cell */}
418
+ {onRowDelete && (
419
+ <TableCell className="w-10 p-0 text-center">
420
+ <Button
421
+ variant="ghost"
422
+ size="icon-sm"
423
+ aria-label={`Delete row ${absoluteIdx + 1}`}
424
+ onClick={() => onRowDelete(absoluteIdx)}
425
+ className="text-muted-foreground hover:text-destructive"
426
+ >
427
+ <Trash2Icon className="size-3.5" />
428
+ </Button>
429
+ </TableCell>
430
+ )}
431
+ </TableRow>
432
+ );
433
+ })}
434
+ </TableBody>
435
+ </Table>
436
+ </div>
437
+
438
+ {/* Pagination */}
439
+ {totalPages > 1 && (
440
+ <div className="flex items-center justify-between text-xs text-muted-foreground pt-1">
441
+ <span>
442
+ Showing {pageStart + 1}–
443
+ {Math.min(pageStart + pageSize, rows.length)} of {rows.length}
444
+ </span>
445
+ <div className="flex items-center gap-1">
446
+ <Button
447
+ variant="outline"
448
+ size="sm"
449
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
450
+ disabled={page === 0}
451
+ >
452
+ Previous
453
+ </Button>
454
+ <span className="px-2">
455
+ {page + 1} / {totalPages}
456
+ </span>
457
+ <Button
458
+ variant="outline"
459
+ size="sm"
460
+ onClick={() =>
461
+ setPage((p) => Math.min(totalPages - 1, p + 1))
462
+ }
463
+ disabled={page >= totalPages - 1}
464
+ >
465
+ Next
466
+ </Button>
467
+ </div>
468
+ </div>
469
+ )}
470
+ </>
275
471
  )}
276
472
 
277
473
  <DialogFooter>
278
- <Button
279
- variant="outline"
280
- onClick={() => onOpenChange(false)}
281
- disabled={isImporting}
282
- >
283
- Cancel
284
- </Button>
285
- <Button disabled={!canImport} onClick={onImport}>
286
- Import
287
- </Button>
474
+ {isImporting ? (
475
+ <Button variant="outline" onClick={onCancelImport}>
476
+ Cancel Import
477
+ </Button>
478
+ ) : (
479
+ <>
480
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
481
+ Cancel
482
+ </Button>
483
+ <Button disabled={!canImport} onClick={onImport}>
484
+ Import
485
+ </Button>
486
+ </>
487
+ )}
288
488
  </DialogFooter>
289
489
  </DialogContent>
290
490
  </Dialog>