@handled-ai/design-system 0.18.54 → 0.18.55

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.
@@ -19,15 +19,15 @@ function EmailPreviewCard({
19
19
  signatureHtml,
20
20
  className
21
21
  }) {
22
- const recipientLabel = to ? `${to}'s` : "the recipient's";
22
+ const recipientLabel = to || "the recipient";
23
23
  const bodyHtml = htmlBody != null ? htmlBody : textBody ? escapeHtml(textBody) : "";
24
24
  return /* @__PURE__ */ jsxs("div", { className: cn("p-4 bg-muted/30 min-h-full", className), children: [
25
25
  /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 mb-3 py-2.5 px-3 border rounded-lg bg-background text-[11.5px] text-muted-foreground", children: [
26
26
  /* @__PURE__ */ jsx(Eye, { className: "size-4 shrink-0 mt-0.5" }),
27
27
  /* @__PURE__ */ jsxs("span", { children: [
28
- "This is how your email lands in ",
28
+ "This is a preview for ",
29
29
  recipientLabel,
30
- " inbox. Nothing has been sent yet."
30
+ ". Nothing has been sent yet."
31
31
  ] })
32
32
  ] }),
33
33
  /* @__PURE__ */ jsxs("div", { className: "bg-background border rounded-xl shadow-sm overflow-hidden", children: [
@@ -48,20 +48,27 @@ function EmailPreviewCard({
48
48
  ] }),
49
49
  /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground shrink-0", children: "just now" })
50
50
  ] }),
51
- /* @__PURE__ */ jsx(
52
- "div",
53
- {
54
- className: "px-[18px] py-2 ml-[47px] text-[13.5px] leading-relaxed whitespace-pre-wrap",
55
- dangerouslySetInnerHTML: { __html: bodyHtml }
56
- }
57
- ),
58
- signatureHtml ? /* @__PURE__ */ jsx(
59
- "div",
60
- {
61
- className: "ml-[47px] px-[18px] pt-3 mt-3 border-t border-border/50 pb-4 text-xs leading-relaxed text-muted-foreground",
62
- dangerouslySetInnerHTML: { __html: signatureHtml }
63
- }
64
- ) : null
51
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-3 px-[18px] pt-3 pb-4", children: [
52
+ /* @__PURE__ */ jsx("div", { className: "size-9 shrink-0", "aria-hidden": "true" }),
53
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-3", children: [
54
+ /* @__PURE__ */ jsx(
55
+ "div",
56
+ {
57
+ className: "text-[13.5px] leading-relaxed whitespace-pre-wrap",
58
+ "data-testid": "email-preview-body",
59
+ dangerouslySetInnerHTML: { __html: bodyHtml }
60
+ }
61
+ ),
62
+ signatureHtml ? /* @__PURE__ */ jsx(
63
+ "div",
64
+ {
65
+ className: "border-t border-border/50 pt-3 text-xs leading-relaxed text-muted-foreground",
66
+ "data-testid": "email-preview-signature",
67
+ dangerouslySetInnerHTML: { __html: signatureHtml }
68
+ }
69
+ ) : null
70
+ ] })
71
+ ] })
65
72
  ] })
66
73
  ] });
67
74
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/email-preview-card.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Eye } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\n\nexport interface EmailPreviewCardProps {\n from: { name: string; email: string }\n to?: string\n subject?: string\n htmlBody?: string\n textBody?: string\n signatureHtml?: string | null\n className?: string\n}\n\nfunction getInitials(name: string): string {\n return name\n .split(\" \")\n .map((part) => part[0])\n .filter(Boolean)\n .slice(0, 2)\n .join(\"\")\n .toUpperCase()\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\")\n}\n\nexport function EmailPreviewCard({\n from,\n to,\n subject,\n htmlBody,\n textBody,\n signatureHtml,\n className,\n}: EmailPreviewCardProps) {\n const recipientLabel = to ? `${to}'s` : \"the recipient's\"\n const bodyHtml = htmlBody ?? (textBody ? escapeHtml(textBody) : \"\")\n\n return (\n <div className={cn(\"p-4 bg-muted/30 min-h-full\", className)}>\n <div className=\"flex items-start gap-2 mb-3 py-2.5 px-3 border rounded-lg bg-background text-[11.5px] text-muted-foreground\">\n <Eye className=\"size-4 shrink-0 mt-0.5\" />\n <span>\n This is how your email lands in {recipientLabel} inbox. Nothing has\n been sent yet.\n </span>\n </div>\n\n <div className=\"bg-background border rounded-xl shadow-sm overflow-hidden\">\n <div className=\"px-[18px] pt-4 pb-3 text-base font-semibold border-b border-border/50\">\n {subject || \"(no subject)\"}\n </div>\n\n <div className=\"flex items-center gap-3 px-[18px] pt-3\">\n <div className=\"flex size-9 shrink-0 items-center justify-center rounded-full bg-foreground text-background text-xs font-semibold\">\n {getInitials(from.name)}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"text-sm\">\n <span className=\"font-semibold text-foreground\">{from.name}</span>{\" \"}\n <span className=\"text-muted-foreground\">&lt;{from.email}&gt;</span>\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {to ? `to ${to}` : \"to no recipient yet\"}\n </div>\n </div>\n <div className=\"text-xs text-muted-foreground shrink-0\">just now</div>\n </div>\n\n <div\n className=\"px-[18px] py-2 ml-[47px] text-[13.5px] leading-relaxed whitespace-pre-wrap\"\n dangerouslySetInnerHTML={{ __html: bodyHtml }}\n />\n\n {signatureHtml ? (\n <div\n className=\"ml-[47px] px-[18px] pt-3 mt-3 border-t border-border/50 pb-4 text-xs leading-relaxed text-muted-foreground\"\n dangerouslySetInnerHTML={{ __html: signatureHtml }}\n />\n ) : null}\n </div>\n </div>\n )\n}\n"],"mappings":";AAmDQ,cACA,YADA;AAhDR,SAAS,WAAW;AAEpB,SAAS,UAAU;AAYnB,SAAS,YAAY,MAAsB;AACzC,SAAO,KACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,CAAC,CAAC,EACrB,OAAO,OAAO,EACd,MAAM,GAAG,CAAC,EACV,KAAK,EAAE,EACP,YAAY;AACjB;AAEA,SAAS,WAAW,MAAsB;AACxC,SAAO,KACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAEO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA0B;AACxB,QAAM,iBAAiB,KAAK,GAAG,EAAE,OAAO;AACxC,QAAM,WAAW,8BAAa,WAAW,WAAW,QAAQ,IAAI;AAEhE,SACE,qBAAC,SAAI,WAAW,GAAG,8BAA8B,SAAS,GACxD;AAAA,yBAAC,SAAI,WAAU,+GACb;AAAA,0BAAC,OAAI,WAAU,0BAAyB;AAAA,MACxC,qBAAC,UAAK;AAAA;AAAA,QAC6B;AAAA,QAAe;AAAA,SAElD;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,6DACb;AAAA,0BAAC,SAAI,WAAU,yEACZ,qBAAW,gBACd;AAAA,MAEA,qBAAC,SAAI,WAAU,0CACb;AAAA,4BAAC,SAAI,WAAU,qHACZ,sBAAY,KAAK,IAAI,GACxB;AAAA,QACA,qBAAC,SAAI,WAAU,kBACb;AAAA,+BAAC,SAAI,WAAU,WACb;AAAA,gCAAC,UAAK,WAAU,iCAAiC,eAAK,MAAK;AAAA,YAAQ;AAAA,YACnE,qBAAC,UAAK,WAAU,yBAAwB;AAAA;AAAA,cAAK,KAAK;AAAA,cAAM;AAAA,eAAI;AAAA,aAC9D;AAAA,UACA,oBAAC,SAAI,WAAU,iCACZ,eAAK,MAAM,EAAE,KAAK,uBACrB;AAAA,WACF;AAAA,QACA,oBAAC,SAAI,WAAU,0CAAyC,sBAAQ;AAAA,SAClE;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,yBAAyB,EAAE,QAAQ,SAAS;AAAA;AAAA,MAC9C;AAAA,MAEC,gBACC;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,yBAAyB,EAAE,QAAQ,cAAc;AAAA;AAAA,MACnD,IACE;AAAA,OACN;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/email-preview-card.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { Eye } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\n\nexport interface EmailPreviewCardProps {\n from: { name: string; email: string }\n to?: string\n subject?: string\n htmlBody?: string\n textBody?: string\n signatureHtml?: string | null\n className?: string\n}\n\nfunction getInitials(name: string): string {\n return name\n .split(\" \")\n .map((part) => part[0])\n .filter(Boolean)\n .slice(0, 2)\n .join(\"\")\n .toUpperCase()\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\")\n}\n\nexport function EmailPreviewCard({\n from,\n to,\n subject,\n htmlBody,\n textBody,\n signatureHtml,\n className,\n}: EmailPreviewCardProps) {\n const recipientLabel = to || \"the recipient\"\n const bodyHtml = htmlBody ?? (textBody ? escapeHtml(textBody) : \"\")\n\n return (\n <div className={cn(\"p-4 bg-muted/30 min-h-full\", className)}>\n <div className=\"flex items-start gap-2 mb-3 py-2.5 px-3 border rounded-lg bg-background text-[11.5px] text-muted-foreground\">\n <Eye className=\"size-4 shrink-0 mt-0.5\" />\n <span>\n This is a preview for {recipientLabel}. Nothing has been sent yet.\n </span>\n </div>\n\n <div className=\"bg-background border rounded-xl shadow-sm overflow-hidden\">\n <div className=\"px-[18px] pt-4 pb-3 text-base font-semibold border-b border-border/50\">\n {subject || \"(no subject)\"}\n </div>\n\n <div className=\"flex items-center gap-3 px-[18px] pt-3\">\n <div className=\"flex size-9 shrink-0 items-center justify-center rounded-full bg-foreground text-background text-xs font-semibold\">\n {getInitials(from.name)}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"text-sm\">\n <span className=\"font-semibold text-foreground\">{from.name}</span>{\" \"}\n <span className=\"text-muted-foreground\">&lt;{from.email}&gt;</span>\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {to ? `to ${to}` : \"to no recipient yet\"}\n </div>\n </div>\n <div className=\"text-xs text-muted-foreground shrink-0\">just now</div>\n </div>\n\n <div className=\"flex gap-3 px-[18px] pt-3 pb-4\">\n <div className=\"size-9 shrink-0\" aria-hidden=\"true\" />\n <div className=\"min-w-0 flex-1 space-y-3\">\n <div\n className=\"text-[13.5px] leading-relaxed whitespace-pre-wrap\"\n data-testid=\"email-preview-body\"\n dangerouslySetInnerHTML={{ __html: bodyHtml }}\n />\n\n {signatureHtml ? (\n <div\n className=\"border-t border-border/50 pt-3 text-xs leading-relaxed text-muted-foreground\"\n data-testid=\"email-preview-signature\"\n dangerouslySetInnerHTML={{ __html: signatureHtml }}\n />\n ) : null}\n </div>\n </div>\n </div>\n </div>\n )\n}\n"],"mappings":";AAmDQ,cACA,YADA;AAhDR,SAAS,WAAW;AAEpB,SAAS,UAAU;AAYnB,SAAS,YAAY,MAAsB;AACzC,SAAO,KACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,CAAC,CAAC,EACrB,OAAO,OAAO,EACd,MAAM,GAAG,CAAC,EACV,KAAK,EAAE,EACP,YAAY;AACjB;AAEA,SAAS,WAAW,MAAsB;AACxC,SAAO,KACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;AAEO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA0B;AACxB,QAAM,iBAAiB,MAAM;AAC7B,QAAM,WAAW,8BAAa,WAAW,WAAW,QAAQ,IAAI;AAEhE,SACE,qBAAC,SAAI,WAAW,GAAG,8BAA8B,SAAS,GACxD;AAAA,yBAAC,SAAI,WAAU,+GACb;AAAA,0BAAC,OAAI,WAAU,0BAAyB;AAAA,MACxC,qBAAC,UAAK;AAAA;AAAA,QACmB;AAAA,QAAe;AAAA,SACxC;AAAA,OACF;AAAA,IAEA,qBAAC,SAAI,WAAU,6DACb;AAAA,0BAAC,SAAI,WAAU,yEACZ,qBAAW,gBACd;AAAA,MAEA,qBAAC,SAAI,WAAU,0CACb;AAAA,4BAAC,SAAI,WAAU,qHACZ,sBAAY,KAAK,IAAI,GACxB;AAAA,QACA,qBAAC,SAAI,WAAU,kBACb;AAAA,+BAAC,SAAI,WAAU,WACb;AAAA,gCAAC,UAAK,WAAU,iCAAiC,eAAK,MAAK;AAAA,YAAQ;AAAA,YACnE,qBAAC,UAAK,WAAU,yBAAwB;AAAA;AAAA,cAAK,KAAK;AAAA,cAAM;AAAA,eAAI;AAAA,aAC9D;AAAA,UACA,oBAAC,SAAI,WAAU,iCACZ,eAAK,MAAM,EAAE,KAAK,uBACrB;AAAA,WACF;AAAA,QACA,oBAAC,SAAI,WAAU,0CAAyC,sBAAQ;AAAA,SAClE;AAAA,MAEA,qBAAC,SAAI,WAAU,kCACb;AAAA,4BAAC,SAAI,WAAU,mBAAkB,eAAY,QAAO;AAAA,QACpD,qBAAC,SAAI,WAAU,4BACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,eAAY;AAAA,cACZ,yBAAyB,EAAE,QAAQ,SAAS;AAAA;AAAA,UAC9C;AAAA,UAEC,gBACC;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,eAAY;AAAA,cACZ,yBAAyB,EAAE,QAAQ,cAAc;AAAA;AAAA,UACnD,IACE;AAAA,WACN;AAAA,SACF;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
@@ -280,10 +280,10 @@ function VirtualizedDataTable({
280
280
  onMouseDown: header.getResizeHandler(),
281
281
  onTouchStart: header.getResizeHandler(),
282
282
  className: cn(
283
- "absolute right-0 top-0 z-20 h-full w-4 -mr-2 cursor-col-resize select-none touch-none",
284
- "after:absolute after:right-2 after:top-1 after:h-[calc(100%-0.5rem)] after:w-px after:rounded-full",
285
- "after:bg-border/70 after:transition-colors hover:after:bg-primary/60",
286
- header.column.getIsResizing() && "after:bg-primary/70"
283
+ "absolute right-0 top-0 h-full w-3 -mr-1.5 cursor-col-resize select-none touch-none",
284
+ "after:absolute after:right-1.5 after:top-0 after:h-full after:w-px",
285
+ "after:bg-transparent hover:after:bg-primary/30",
286
+ header.column.getIsResizing() && "after:bg-primary/50"
287
287
  ),
288
288
  role: "separator",
289
289
  "aria-orientation": "vertical"
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/virtualized-data-table.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { useVirtualizer } from \"@tanstack/react-virtual\"\nimport {\n useReactTable,\n getCoreRowModel,\n flexRender,\n type ColumnDef,\n type SortingState,\n type ColumnFiltersState,\n type VisibilityState,\n type ColumnSizingState,\n type OnChangeFn,\n} from \"@tanstack/react-table\"\nimport type { RowData } from \"@tanstack/react-table\"\nimport { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, EyeOff, Check, SearchX, Loader2 } from \"lucide-react\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuTrigger,\n DropdownMenuItem,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\"\n\nimport { cn } from \"../lib/utils\"\n\ndeclare module \"@tanstack/react-table\" {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface ColumnMeta<TData extends RowData, TValue> {\n /** Server-side sort key for this column. Enables sort in the header menu when onColumnSort is also provided. */\n sortKey?: string\n }\n}\n\nexport interface VirtualizedDataTableProps<TData> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n columns: ColumnDef<TData, any>[]\n data: TData[]\n\n // Virtualization\n height?: number | string\n estimateRowHeight?: number\n overscan?: number\n\n // Row interaction\n onRowClick?: (row: TData) => void\n getRowId?: (original: TData, index: number) => string\n\n // Infinite scroll\n onReachBottom?: () => void\n reachBottomThreshold?: number\n hasMore?: boolean\n isFetchingMore?: boolean\n\n // Column resizing\n enableColumnResizing?: boolean\n columnResizeMode?: \"onChange\" | \"onEnd\"\n columnSizing?: ColumnSizingState\n onColumnSizingChange?: OnChangeFn<ColumnSizingState>\n\n // Server-driven state (controlled) — omit for internal state\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n columnFilters?: ColumnFiltersState\n onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>\n columnVisibility?: VisibilityState\n onColumnVisibilityChange?: OnChangeFn<VisibilityState>\n\n // Loading / Empty state\n isLoading?: boolean\n emptyIcon?: React.ReactNode\n emptyMessage?: string\n emptyDescription?: string\n\n // Column header menu\n /** Called when user requests sorting from column header. columnId is the column's meta.sortKey. */\n onColumnSort?: (columnId: string, direction: \"asc\" | \"desc\") => void\n /** Called when user hides a column from the header menu. */\n onColumnHide?: (columnId: string) => void\n /** The currently active sort column ID — matches a column's meta.sortKey. Used for visual indicators and aria-sort. */\n activeSortColumn?: string | null\n /** The current sort direction. Used for visual indicators and aria-sort. */\n activeSortDirection?: \"asc\" | \"desc\"\n\n // Styling\n className?: string\n}\n\nexport function VirtualizedDataTable<TData>({\n columns,\n data,\n height = 600,\n estimateRowHeight = 48,\n overscan = 8,\n onRowClick,\n getRowId,\n onReachBottom,\n reachBottomThreshold = 5,\n hasMore = true,\n isFetchingMore,\n enableColumnResizing = false,\n columnResizeMode = \"onEnd\",\n columnSizing,\n onColumnSizingChange,\n sorting,\n onSortingChange,\n columnFilters,\n onColumnFiltersChange,\n columnVisibility,\n onColumnVisibilityChange,\n onColumnSort,\n onColumnHide,\n activeSortColumn,\n activeSortDirection,\n isLoading,\n emptyIcon,\n emptyMessage = \"No rows found\",\n emptyDescription = \"Try adjusting your filters\",\n className,\n}: VirtualizedDataTableProps<TData>) {\n // Controlled/uncontrolled state for sorting\n const [internalSorting, setInternalSorting] = React.useState<SortingState>([])\n const resolvedSorting = sorting ?? internalSorting\n const resolvedOnSortingChange = onSortingChange ?? setInternalSorting\n\n // Controlled/uncontrolled state for column filters\n const [internalColumnFilters, setInternalColumnFilters] =\n React.useState<ColumnFiltersState>([])\n const resolvedColumnFilters = columnFilters ?? internalColumnFilters\n const resolvedOnColumnFiltersChange =\n onColumnFiltersChange ?? setInternalColumnFilters\n\n // Controlled/uncontrolled state for column visibility\n const [internalColumnVisibility, setInternalColumnVisibility] =\n React.useState<VisibilityState>({})\n const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility\n const resolvedOnColumnVisibilityChange =\n onColumnVisibilityChange ?? setInternalColumnVisibility\n\n // Controlled/uncontrolled state for column sizing\n const [internalColumnSizing, setInternalColumnSizing] =\n React.useState<ColumnSizingState>({})\n const resolvedColumnSizing = columnSizing ?? internalColumnSizing\n const resolvedOnColumnSizingChange =\n onColumnSizingChange ?? setInternalColumnSizing\n\n // TanStack Table setup\n const table = useReactTable({\n data,\n columns,\n ...(getRowId ? { getRowId } : {}),\n state: {\n sorting: resolvedSorting,\n columnFilters: resolvedColumnFilters,\n columnVisibility: resolvedColumnVisibility,\n columnSizing: resolvedColumnSizing,\n },\n onSortingChange: resolvedOnSortingChange,\n onColumnFiltersChange: resolvedOnColumnFiltersChange,\n onColumnVisibilityChange: resolvedOnColumnVisibilityChange,\n onColumnSizingChange: resolvedOnColumnSizingChange,\n enableColumnResizing,\n columnResizeMode,\n manualSorting: true,\n manualFiltering: true,\n manualPagination: true,\n getCoreRowModel: getCoreRowModel(),\n })\n\n // Virtualizer setup\n const scrollContainerRef = React.useRef<HTMLDivElement>(null)\n const rows = table.getRowModel().rows\n\n const virtualizer = useVirtualizer({\n count: rows.length,\n getScrollElement: () => scrollContainerRef.current,\n estimateSize: () => estimateRowHeight,\n overscan,\n measureElement: (element) => element.getBoundingClientRect().height,\n })\n\n // Infinite scroll detection\n const lastTriggeredDataLengthRef = React.useRef<number>(0)\n\n // Derive a stable primitive for the last visible virtual-item index so the\n // effect below doesn't re-run on every render (getVirtualItems() returns a\n // new array reference each call).\n const virtualItems = virtualizer.getVirtualItems()\n const lastVirtualItemIndex =\n virtualItems.length > 0\n ? virtualItems[virtualItems.length - 1].index\n : -1\n\n React.useEffect(() => {\n if (!onReachBottom || isFetchingMore || hasMore === false) return\n if (lastVirtualItemIndex < 0) return\n if (lastVirtualItemIndex < rows.length - reachBottomThreshold) return\n\n // Prevent re-firing until data.length changes (i.e. new page loaded).\n if (lastTriggeredDataLengthRef.current === data.length) return\n lastTriggeredDataLengthRef.current = data.length\n\n onReachBottom()\n }, [\n lastVirtualItemIndex,\n rows.length,\n data.length,\n onReachBottom,\n isFetchingMore,\n hasMore,\n reachBottomThreshold,\n ])\n\n return (\n <div className={cn(\n \"w-full\",\n typeof height === \"string\" && height.trim().endsWith(\"%\") && \"h-full\",\n className,\n )}>\n <div\n ref={scrollContainerRef}\n className=\"relative overflow-auto\"\n style={{\n height: typeof height === \"number\" ? `${height}px` : height,\n contain: \"strict\",\n }}\n role=\"table\"\n aria-rowcount={data.length}\n aria-colcount={table.getVisibleLeafColumns().length}\n >\n {/* Sticky header */}\n <div className=\"sticky top-0 z-10 bg-background\" role=\"rowgroup\">\n {table.getHeaderGroups().map((headerGroup) => (\n <div\n key={headerGroup.id}\n className=\"flex w-max min-w-full border-b border-border/50\"\n role=\"row\"\n >\n {headerGroup.headers.map((header, colIdx) => {\n const sortKey = header.column.columnDef.meta?.sortKey\n const canServerSort = Boolean(sortKey && onColumnSort)\n\n const resolvedAriaSort = (() => {\n if (activeSortColumn !== undefined) {\n // Server-driven\n if (!sortKey) return undefined\n if (activeSortColumn === sortKey) return activeSortDirection === \"asc\" ? \"ascending\" as const : \"descending\" as const\n return \"none\" as const\n }\n // Fallback to TanStack state\n const sorted = header.column.getIsSorted()\n if (sorted === \"asc\") return \"ascending\" as const\n if (sorted === \"desc\") return \"descending\" as const\n if (header.column.getCanSort()) return \"none\" as const\n return undefined\n })()\n\n const sortIcon = (() => {\n if (!canServerSort) return null\n if (activeSortColumn === sortKey && activeSortDirection === \"asc\") return <ArrowUp className=\"w-3 h-3 shrink-0\" />\n if (activeSortColumn === sortKey && activeSortDirection === \"desc\") return <ArrowDown className=\"w-3 h-3 shrink-0\" />\n return <ArrowUpDown className=\"w-3 h-3 shrink-0 opacity-40\" />\n })()\n\n const handleHeaderClick = canServerSort ? () => {\n const newDir = activeSortColumn === sortKey\n ? (activeSortDirection === \"asc\" ? \"desc\" : \"asc\")\n : \"asc\"\n onColumnSort!(sortKey!, newDir)\n } : undefined\n\n return (\n <div\n key={header.id}\n className={cn(\n \"group/header h-9 min-w-0 px-3 flex items-center text-xs font-medium text-muted-foreground whitespace-nowrap relative\",\n header.column.getCanResize() && \"pr-4\",\n )}\n style={{\n width: header.getSize(),\n minWidth: header.getSize(),\n }}\n role=\"columnheader\"\n aria-colindex={colIdx + 1}\n aria-sort={resolvedAriaSort}\n >\n {header.isPlaceholder ? null : (\n <>\n {canServerSort ? (\n <button\n type=\"button\"\n className=\"flex min-w-0 flex-1 items-center gap-1 hover:text-foreground transition-colors\"\n onClick={handleHeaderClick}\n >\n <span className=\"min-w-0 truncate\">\n {flexRender(header.column.columnDef.header, header.getContext())}\n </span>\n {sortIcon}\n </button>\n ) : header.column.getCanSort() ? (\n <button\n type=\"button\"\n className=\"flex min-w-0 flex-1 items-center gap-1 hover:text-foreground transition-colors\"\n onClick={header.column.getToggleSortingHandler()}\n >\n <span className=\"min-w-0 truncate\">\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </span>\n {header.column.getIsSorted() === \"asc\" ? (\n <ArrowUp className=\"w-3 h-3 shrink-0\" />\n ) : header.column.getIsSorted() === \"desc\" ? (\n <ArrowDown className=\"w-3 h-3 shrink-0\" />\n ) : (\n <ArrowUpDown className=\"w-3 h-3 shrink-0 opacity-40\" />\n )}\n </button>\n ) : (\n <span className=\"min-w-0 flex-1 truncate\">\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </span>\n )}\n {(canServerSort || header.column.getCanSort() || header.column.getCanHide()) && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n className=\"ml-1 inline-flex shrink-0 items-center hover:text-foreground transition-all opacity-0 group-hover/header:opacity-100\"\n aria-label=\"Column actions\"\n >\n <ChevronDown className=\"w-3 h-3\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-48\">\n <DropdownMenuItem\n disabled={!canServerSort}\n onClick={() => canServerSort && onColumnSort!(sortKey!, \"asc\")}\n >\n <ArrowUp className=\"w-3.5 h-3.5 mr-2\" />\n Sort ascending\n {activeSortColumn === sortKey && activeSortDirection === \"asc\" && <Check className=\"w-3.5 h-3.5 ml-auto\" />}\n </DropdownMenuItem>\n <DropdownMenuItem\n disabled={!canServerSort}\n onClick={() => canServerSort && onColumnSort!(sortKey!, \"desc\")}\n >\n <ArrowDown className=\"w-3.5 h-3.5 mr-2\" />\n Sort descending\n {activeSortColumn === sortKey && activeSortDirection === \"desc\" && <Check className=\"w-3.5 h-3.5 ml-auto\" />}\n </DropdownMenuItem>\n {header.column.getCanHide() && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onClick={() => onColumnHide ? onColumnHide(header.column.id) : header.column.toggleVisibility(false)}\n >\n <EyeOff className=\"w-3.5 h-3.5 mr-2\" />\n Hide column\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </>\n )}\n {header.column.getCanResize() && (\n <div\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n className={cn(\n \"absolute right-0 top-0 z-20 h-full w-4 -mr-2 cursor-col-resize select-none touch-none\",\n \"after:absolute after:right-2 after:top-1 after:h-[calc(100%-0.5rem)] after:w-px after:rounded-full\",\n \"after:bg-border/70 after:transition-colors hover:after:bg-primary/60\",\n header.column.getIsResizing() && \"after:bg-primary/70\",\n )}\n role=\"separator\"\n aria-orientation=\"vertical\"\n />\n )}\n </div>\n )\n })}\n </div>\n ))}\n </div>\n\n {/* Virtualized body or empty state */}\n {rows.length > 0 ? (\n <div\n role=\"rowgroup\"\n style={{\n height: virtualizer.getTotalSize(),\n width: \"100%\",\n position: \"relative\",\n }}\n >\n {virtualizer.getVirtualItems().map((virtualRow) => {\n const row = rows[virtualRow.index]\n return (\n <div\n key={row.id}\n data-index={virtualRow.index}\n ref={virtualizer.measureElement}\n className={cn(\n \"absolute left-0 w-max min-w-full flex group transition-colors\",\n onRowClick && \"cursor-pointer\",\n )}\n style={{\n transform: `translateY(${virtualRow.start}px)`,\n }}\n role=\"row\"\n aria-rowindex={virtualRow.index + 2}\n onClick={() => onRowClick?.(row.original)}\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n onRowClick(row.original)\n }\n }\n : undefined\n }\n >\n {row.getVisibleCells().map((cell, colIdx) => (\n <div\n key={cell.id}\n className=\"px-3 py-3 flex items-center whitespace-nowrap group-hover:bg-muted/50\"\n style={{\n width: cell.column.getSize(),\n minWidth: cell.column.getSize(),\n }}\n role=\"cell\"\n aria-colindex={colIdx + 1}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </div>\n ))}\n </div>\n )\n })}\n </div>\n ) : isLoading ? (\n <div className=\"flex flex-col items-center justify-center gap-2 text-muted-foreground py-20\">\n <Loader2 className=\"h-7 w-7 animate-spin opacity-60\" />\n <p className=\"text-sm font-medium\">Loading...</p>\n </div>\n ) : (\n <div className=\"flex flex-col items-center justify-center gap-1 text-muted-foreground py-20\">\n {emptyIcon ?? <SearchX className=\"h-7 w-7 opacity-40\" />}\n <p className=\"text-sm font-medium\">{emptyMessage}</p>\n <p className=\"text-xs\">{emptyDescription}</p>\n </div>\n )}\n\n {/* Loading indicator */}\n {isFetchingMore && (\n <div className=\"flex items-center justify-center py-4\">\n <Loader2 className=\"h-5 w-5 animate-spin text-muted-foreground\" />\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/virtualized-data-table.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { useVirtualizer } from \"@tanstack/react-virtual\"\nimport {\n useReactTable,\n getCoreRowModel,\n flexRender,\n type ColumnDef,\n type SortingState,\n type ColumnFiltersState,\n type VisibilityState,\n type ColumnSizingState,\n type OnChangeFn,\n} from \"@tanstack/react-table\"\nimport type { RowData } from \"@tanstack/react-table\"\nimport { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, EyeOff, Check, SearchX, Loader2 } from \"lucide-react\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuTrigger,\n DropdownMenuItem,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\"\n\nimport { cn } from \"../lib/utils\"\n\ndeclare module \"@tanstack/react-table\" {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface ColumnMeta<TData extends RowData, TValue> {\n /** Server-side sort key for this column. Enables sort in the header menu when onColumnSort is also provided. */\n sortKey?: string\n }\n}\n\nexport interface VirtualizedDataTableProps<TData> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n columns: ColumnDef<TData, any>[]\n data: TData[]\n\n // Virtualization\n height?: number | string\n estimateRowHeight?: number\n overscan?: number\n\n // Row interaction\n onRowClick?: (row: TData) => void\n getRowId?: (original: TData, index: number) => string\n\n // Infinite scroll\n onReachBottom?: () => void\n reachBottomThreshold?: number\n hasMore?: boolean\n isFetchingMore?: boolean\n\n // Column resizing\n enableColumnResizing?: boolean\n columnResizeMode?: \"onChange\" | \"onEnd\"\n columnSizing?: ColumnSizingState\n onColumnSizingChange?: OnChangeFn<ColumnSizingState>\n\n // Server-driven state (controlled) — omit for internal state\n sorting?: SortingState\n onSortingChange?: OnChangeFn<SortingState>\n columnFilters?: ColumnFiltersState\n onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>\n columnVisibility?: VisibilityState\n onColumnVisibilityChange?: OnChangeFn<VisibilityState>\n\n // Loading / Empty state\n isLoading?: boolean\n emptyIcon?: React.ReactNode\n emptyMessage?: string\n emptyDescription?: string\n\n // Column header menu\n /** Called when user requests sorting from column header. columnId is the column's meta.sortKey. */\n onColumnSort?: (columnId: string, direction: \"asc\" | \"desc\") => void\n /** Called when user hides a column from the header menu. */\n onColumnHide?: (columnId: string) => void\n /** The currently active sort column ID — matches a column's meta.sortKey. Used for visual indicators and aria-sort. */\n activeSortColumn?: string | null\n /** The current sort direction. Used for visual indicators and aria-sort. */\n activeSortDirection?: \"asc\" | \"desc\"\n\n // Styling\n className?: string\n}\n\nexport function VirtualizedDataTable<TData>({\n columns,\n data,\n height = 600,\n estimateRowHeight = 48,\n overscan = 8,\n onRowClick,\n getRowId,\n onReachBottom,\n reachBottomThreshold = 5,\n hasMore = true,\n isFetchingMore,\n enableColumnResizing = false,\n columnResizeMode = \"onEnd\",\n columnSizing,\n onColumnSizingChange,\n sorting,\n onSortingChange,\n columnFilters,\n onColumnFiltersChange,\n columnVisibility,\n onColumnVisibilityChange,\n onColumnSort,\n onColumnHide,\n activeSortColumn,\n activeSortDirection,\n isLoading,\n emptyIcon,\n emptyMessage = \"No rows found\",\n emptyDescription = \"Try adjusting your filters\",\n className,\n}: VirtualizedDataTableProps<TData>) {\n // Controlled/uncontrolled state for sorting\n const [internalSorting, setInternalSorting] = React.useState<SortingState>([])\n const resolvedSorting = sorting ?? internalSorting\n const resolvedOnSortingChange = onSortingChange ?? setInternalSorting\n\n // Controlled/uncontrolled state for column filters\n const [internalColumnFilters, setInternalColumnFilters] =\n React.useState<ColumnFiltersState>([])\n const resolvedColumnFilters = columnFilters ?? internalColumnFilters\n const resolvedOnColumnFiltersChange =\n onColumnFiltersChange ?? setInternalColumnFilters\n\n // Controlled/uncontrolled state for column visibility\n const [internalColumnVisibility, setInternalColumnVisibility] =\n React.useState<VisibilityState>({})\n const resolvedColumnVisibility = columnVisibility ?? internalColumnVisibility\n const resolvedOnColumnVisibilityChange =\n onColumnVisibilityChange ?? setInternalColumnVisibility\n\n // Controlled/uncontrolled state for column sizing\n const [internalColumnSizing, setInternalColumnSizing] =\n React.useState<ColumnSizingState>({})\n const resolvedColumnSizing = columnSizing ?? internalColumnSizing\n const resolvedOnColumnSizingChange =\n onColumnSizingChange ?? setInternalColumnSizing\n\n // TanStack Table setup\n const table = useReactTable({\n data,\n columns,\n ...(getRowId ? { getRowId } : {}),\n state: {\n sorting: resolvedSorting,\n columnFilters: resolvedColumnFilters,\n columnVisibility: resolvedColumnVisibility,\n columnSizing: resolvedColumnSizing,\n },\n onSortingChange: resolvedOnSortingChange,\n onColumnFiltersChange: resolvedOnColumnFiltersChange,\n onColumnVisibilityChange: resolvedOnColumnVisibilityChange,\n onColumnSizingChange: resolvedOnColumnSizingChange,\n enableColumnResizing,\n columnResizeMode,\n manualSorting: true,\n manualFiltering: true,\n manualPagination: true,\n getCoreRowModel: getCoreRowModel(),\n })\n\n // Virtualizer setup\n const scrollContainerRef = React.useRef<HTMLDivElement>(null)\n const rows = table.getRowModel().rows\n\n const virtualizer = useVirtualizer({\n count: rows.length,\n getScrollElement: () => scrollContainerRef.current,\n estimateSize: () => estimateRowHeight,\n overscan,\n measureElement: (element) => element.getBoundingClientRect().height,\n })\n\n // Infinite scroll detection\n const lastTriggeredDataLengthRef = React.useRef<number>(0)\n\n // Derive a stable primitive for the last visible virtual-item index so the\n // effect below doesn't re-run on every render (getVirtualItems() returns a\n // new array reference each call).\n const virtualItems = virtualizer.getVirtualItems()\n const lastVirtualItemIndex =\n virtualItems.length > 0\n ? virtualItems[virtualItems.length - 1].index\n : -1\n\n React.useEffect(() => {\n if (!onReachBottom || isFetchingMore || hasMore === false) return\n if (lastVirtualItemIndex < 0) return\n if (lastVirtualItemIndex < rows.length - reachBottomThreshold) return\n\n // Prevent re-firing until data.length changes (i.e. new page loaded).\n if (lastTriggeredDataLengthRef.current === data.length) return\n lastTriggeredDataLengthRef.current = data.length\n\n onReachBottom()\n }, [\n lastVirtualItemIndex,\n rows.length,\n data.length,\n onReachBottom,\n isFetchingMore,\n hasMore,\n reachBottomThreshold,\n ])\n\n return (\n <div className={cn(\n \"w-full\",\n typeof height === \"string\" && height.trim().endsWith(\"%\") && \"h-full\",\n className,\n )}>\n <div\n ref={scrollContainerRef}\n className=\"relative overflow-auto\"\n style={{\n height: typeof height === \"number\" ? `${height}px` : height,\n contain: \"strict\",\n }}\n role=\"table\"\n aria-rowcount={data.length}\n aria-colcount={table.getVisibleLeafColumns().length}\n >\n {/* Sticky header */}\n <div className=\"sticky top-0 z-10 bg-background\" role=\"rowgroup\">\n {table.getHeaderGroups().map((headerGroup) => (\n <div\n key={headerGroup.id}\n className=\"flex w-max min-w-full border-b border-border/50\"\n role=\"row\"\n >\n {headerGroup.headers.map((header, colIdx) => {\n const sortKey = header.column.columnDef.meta?.sortKey\n const canServerSort = Boolean(sortKey && onColumnSort)\n\n const resolvedAriaSort = (() => {\n if (activeSortColumn !== undefined) {\n // Server-driven\n if (!sortKey) return undefined\n if (activeSortColumn === sortKey) return activeSortDirection === \"asc\" ? \"ascending\" as const : \"descending\" as const\n return \"none\" as const\n }\n // Fallback to TanStack state\n const sorted = header.column.getIsSorted()\n if (sorted === \"asc\") return \"ascending\" as const\n if (sorted === \"desc\") return \"descending\" as const\n if (header.column.getCanSort()) return \"none\" as const\n return undefined\n })()\n\n const sortIcon = (() => {\n if (!canServerSort) return null\n if (activeSortColumn === sortKey && activeSortDirection === \"asc\") return <ArrowUp className=\"w-3 h-3 shrink-0\" />\n if (activeSortColumn === sortKey && activeSortDirection === \"desc\") return <ArrowDown className=\"w-3 h-3 shrink-0\" />\n return <ArrowUpDown className=\"w-3 h-3 shrink-0 opacity-40\" />\n })()\n\n const handleHeaderClick = canServerSort ? () => {\n const newDir = activeSortColumn === sortKey\n ? (activeSortDirection === \"asc\" ? \"desc\" : \"asc\")\n : \"asc\"\n onColumnSort!(sortKey!, newDir)\n } : undefined\n\n return (\n <div\n key={header.id}\n className={cn(\n \"group/header h-9 min-w-0 px-3 flex items-center text-xs font-medium text-muted-foreground whitespace-nowrap relative\",\n header.column.getCanResize() && \"pr-4\",\n )}\n style={{\n width: header.getSize(),\n minWidth: header.getSize(),\n }}\n role=\"columnheader\"\n aria-colindex={colIdx + 1}\n aria-sort={resolvedAriaSort}\n >\n {header.isPlaceholder ? null : (\n <>\n {canServerSort ? (\n <button\n type=\"button\"\n className=\"flex min-w-0 flex-1 items-center gap-1 hover:text-foreground transition-colors\"\n onClick={handleHeaderClick}\n >\n <span className=\"min-w-0 truncate\">\n {flexRender(header.column.columnDef.header, header.getContext())}\n </span>\n {sortIcon}\n </button>\n ) : header.column.getCanSort() ? (\n <button\n type=\"button\"\n className=\"flex min-w-0 flex-1 items-center gap-1 hover:text-foreground transition-colors\"\n onClick={header.column.getToggleSortingHandler()}\n >\n <span className=\"min-w-0 truncate\">\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </span>\n {header.column.getIsSorted() === \"asc\" ? (\n <ArrowUp className=\"w-3 h-3 shrink-0\" />\n ) : header.column.getIsSorted() === \"desc\" ? (\n <ArrowDown className=\"w-3 h-3 shrink-0\" />\n ) : (\n <ArrowUpDown className=\"w-3 h-3 shrink-0 opacity-40\" />\n )}\n </button>\n ) : (\n <span className=\"min-w-0 flex-1 truncate\">\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n </span>\n )}\n {(canServerSort || header.column.getCanSort() || header.column.getCanHide()) && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n className=\"ml-1 inline-flex shrink-0 items-center hover:text-foreground transition-all opacity-0 group-hover/header:opacity-100\"\n aria-label=\"Column actions\"\n >\n <ChevronDown className=\"w-3 h-3\" />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-48\">\n <DropdownMenuItem\n disabled={!canServerSort}\n onClick={() => canServerSort && onColumnSort!(sortKey!, \"asc\")}\n >\n <ArrowUp className=\"w-3.5 h-3.5 mr-2\" />\n Sort ascending\n {activeSortColumn === sortKey && activeSortDirection === \"asc\" && <Check className=\"w-3.5 h-3.5 ml-auto\" />}\n </DropdownMenuItem>\n <DropdownMenuItem\n disabled={!canServerSort}\n onClick={() => canServerSort && onColumnSort!(sortKey!, \"desc\")}\n >\n <ArrowDown className=\"w-3.5 h-3.5 mr-2\" />\n Sort descending\n {activeSortColumn === sortKey && activeSortDirection === \"desc\" && <Check className=\"w-3.5 h-3.5 ml-auto\" />}\n </DropdownMenuItem>\n {header.column.getCanHide() && (\n <>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onClick={() => onColumnHide ? onColumnHide(header.column.id) : header.column.toggleVisibility(false)}\n >\n <EyeOff className=\"w-3.5 h-3.5 mr-2\" />\n Hide column\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </>\n )}\n {header.column.getCanResize() && (\n <div\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n className={cn(\n \"absolute right-0 top-0 h-full w-3 -mr-1.5 cursor-col-resize select-none touch-none\",\n \"after:absolute after:right-1.5 after:top-0 after:h-full after:w-px\",\n \"after:bg-transparent hover:after:bg-primary/30\",\n header.column.getIsResizing() && \"after:bg-primary/50\",\n )}\n role=\"separator\"\n aria-orientation=\"vertical\"\n />\n )}\n </div>\n )\n })}\n </div>\n ))}\n </div>\n\n {/* Virtualized body or empty state */}\n {rows.length > 0 ? (\n <div\n role=\"rowgroup\"\n style={{\n height: virtualizer.getTotalSize(),\n width: \"100%\",\n position: \"relative\",\n }}\n >\n {virtualizer.getVirtualItems().map((virtualRow) => {\n const row = rows[virtualRow.index]\n return (\n <div\n key={row.id}\n data-index={virtualRow.index}\n ref={virtualizer.measureElement}\n className={cn(\n \"absolute left-0 w-max min-w-full flex group transition-colors\",\n onRowClick && \"cursor-pointer\",\n )}\n style={{\n transform: `translateY(${virtualRow.start}px)`,\n }}\n role=\"row\"\n aria-rowindex={virtualRow.index + 2}\n onClick={() => onRowClick?.(row.original)}\n tabIndex={onRowClick ? 0 : undefined}\n onKeyDown={\n onRowClick\n ? (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault()\n onRowClick(row.original)\n }\n }\n : undefined\n }\n >\n {row.getVisibleCells().map((cell, colIdx) => (\n <div\n key={cell.id}\n className=\"px-3 py-3 flex items-center whitespace-nowrap group-hover:bg-muted/50\"\n style={{\n width: cell.column.getSize(),\n minWidth: cell.column.getSize(),\n }}\n role=\"cell\"\n aria-colindex={colIdx + 1}\n >\n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n </div>\n ))}\n </div>\n )\n })}\n </div>\n ) : isLoading ? (\n <div className=\"flex flex-col items-center justify-center gap-2 text-muted-foreground py-20\">\n <Loader2 className=\"h-7 w-7 animate-spin opacity-60\" />\n <p className=\"text-sm font-medium\">Loading...</p>\n </div>\n ) : (\n <div className=\"flex flex-col items-center justify-center gap-1 text-muted-foreground py-20\">\n {emptyIcon ?? <SearchX className=\"h-7 w-7 opacity-40\" />}\n <p className=\"text-sm font-medium\">{emptyMessage}</p>\n <p className=\"text-xs\">{emptyDescription}</p>\n </div>\n )}\n\n {/* Loading indicator */}\n {isFetchingMore && (\n <div className=\"flex items-center justify-center py-4\">\n <Loader2 className=\"h-5 w-5 animate-spin text-muted-foreground\" />\n </div>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handled-ai/design-system",
3
- "version": "0.18.54",
3
+ "version": "0.18.55",
4
4
  "description": "Handled UI component library (shadcn-style, New York)",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.0",
@@ -31,7 +31,7 @@ describe("EmailPreviewCard", () => {
31
31
  render(<EmailPreviewCard from={from} to="jane@acme.com" />)
32
32
  expect(screen.getByText("to jane@acme.com")).toBeTruthy()
33
33
  expect(
34
- screen.getByText(/This is how your email lands in jane@acme.com's inbox/),
34
+ screen.getByText(/This is a preview for jane@acme.com\. Nothing has been sent yet\./),
35
35
  ).toBeTruthy()
36
36
  })
37
37
 
@@ -39,24 +39,30 @@ describe("EmailPreviewCard", () => {
39
39
  render(<EmailPreviewCard from={from} />)
40
40
  expect(screen.getByText("to no recipient yet")).toBeTruthy()
41
41
  expect(
42
- screen.getByText(/This is how your email lands in the recipient's inbox/),
42
+ screen.getByText(/This is a preview for the recipient\. Nothing has been sent yet\./),
43
43
  ).toBeTruthy()
44
44
  })
45
45
 
46
- it("renders html body markup", () => {
46
+ it("renders html body markup in the message body area", () => {
47
47
  const { container } = render(
48
48
  <EmailPreviewCard from={from} htmlBody="<strong>Hello world</strong>" />,
49
49
  )
50
- expect(container.querySelector("strong")?.textContent).toBe("Hello world")
50
+ expect(screen.getByTestId("email-preview-body").querySelector("strong")?.textContent).toBe("Hello world")
51
+ expect(container.querySelector("[data-testid='email-preview-body']")?.className).not.toContain("ml-")
51
52
  })
52
53
 
53
- it("renders the signature when provided", () => {
54
- const { container } = render(
54
+ it("renders the signature as part of the aligned message content", () => {
55
+ render(
55
56
  <EmailPreviewCard
56
57
  from={from}
58
+ htmlBody="<p>Hello</p>"
57
59
  signatureHtml="<em>Best, Cory</em>"
58
60
  />,
59
61
  )
60
- expect(container.querySelector("em")?.textContent).toBe("Best, Cory")
62
+
63
+ expect(screen.getByTestId("email-preview-signature").querySelector("em")?.textContent).toBe("Best, Cory")
64
+ expect(screen.getByTestId("email-preview-body").parentElement).toBe(
65
+ screen.getByTestId("email-preview-signature").parentElement,
66
+ )
61
67
  })
62
68
  })
@@ -84,24 +84,6 @@ describe("VirtualizedDataTable — resize handles render when enabled", () => {
84
84
  });
85
85
  });
86
86
 
87
- it("each handle has a wider grab target and visible divider", () => {
88
- const { container } = render(
89
- <VirtualizedDataTable
90
- columns={testColumns}
91
- data={testData}
92
- height={300}
93
- enableColumnResizing
94
- />,
95
- );
96
-
97
- container.querySelectorAll('[role="separator"]').forEach((sep) => {
98
- expect(sep.classList.contains("w-4")).toBe(true);
99
- expect(sep.classList.contains("-mr-2")).toBe(true);
100
- expect(sep.className).toContain("after:bg-border/70");
101
- expect(sep.className).toContain("hover:after:bg-primary/60");
102
- });
103
- });
104
-
105
87
  it("header cells have 'relative' class when resizing is enabled", () => {
106
88
  const { container } = render(
107
89
  <VirtualizedDataTable
@@ -43,7 +43,7 @@ export function EmailPreviewCard({
43
43
  signatureHtml,
44
44
  className,
45
45
  }: EmailPreviewCardProps) {
46
- const recipientLabel = to ? `${to}'s` : "the recipient's"
46
+ const recipientLabel = to || "the recipient"
47
47
  const bodyHtml = htmlBody ?? (textBody ? escapeHtml(textBody) : "")
48
48
 
49
49
  return (
@@ -51,8 +51,7 @@ export function EmailPreviewCard({
51
51
  <div className="flex items-start gap-2 mb-3 py-2.5 px-3 border rounded-lg bg-background text-[11.5px] text-muted-foreground">
52
52
  <Eye className="size-4 shrink-0 mt-0.5" />
53
53
  <span>
54
- This is how your email lands in {recipientLabel} inbox. Nothing has
55
- been sent yet.
54
+ This is a preview for {recipientLabel}. Nothing has been sent yet.
56
55
  </span>
57
56
  </div>
58
57
 
@@ -77,17 +76,24 @@ export function EmailPreviewCard({
77
76
  <div className="text-xs text-muted-foreground shrink-0">just now</div>
78
77
  </div>
79
78
 
80
- <div
81
- className="px-[18px] py-2 ml-[47px] text-[13.5px] leading-relaxed whitespace-pre-wrap"
82
- dangerouslySetInnerHTML={{ __html: bodyHtml }}
83
- />
79
+ <div className="flex gap-3 px-[18px] pt-3 pb-4">
80
+ <div className="size-9 shrink-0" aria-hidden="true" />
81
+ <div className="min-w-0 flex-1 space-y-3">
82
+ <div
83
+ className="text-[13.5px] leading-relaxed whitespace-pre-wrap"
84
+ data-testid="email-preview-body"
85
+ dangerouslySetInnerHTML={{ __html: bodyHtml }}
86
+ />
84
87
 
85
- {signatureHtml ? (
86
- <div
87
- className="ml-[47px] px-[18px] pt-3 mt-3 border-t border-border/50 pb-4 text-xs leading-relaxed text-muted-foreground"
88
- dangerouslySetInnerHTML={{ __html: signatureHtml }}
89
- />
90
- ) : null}
88
+ {signatureHtml ? (
89
+ <div
90
+ className="border-t border-border/50 pt-3 text-xs leading-relaxed text-muted-foreground"
91
+ data-testid="email-preview-signature"
92
+ dangerouslySetInnerHTML={{ __html: signatureHtml }}
93
+ />
94
+ ) : null}
95
+ </div>
96
+ </div>
91
97
  </div>
92
98
  </div>
93
99
  )
@@ -375,10 +375,10 @@ export function VirtualizedDataTable<TData>({
375
375
  onMouseDown={header.getResizeHandler()}
376
376
  onTouchStart={header.getResizeHandler()}
377
377
  className={cn(
378
- "absolute right-0 top-0 z-20 h-full w-4 -mr-2 cursor-col-resize select-none touch-none",
379
- "after:absolute after:right-2 after:top-1 after:h-[calc(100%-0.5rem)] after:w-px after:rounded-full",
380
- "after:bg-border/70 after:transition-colors hover:after:bg-primary/60",
381
- header.column.getIsResizing() && "after:bg-primary/70",
378
+ "absolute right-0 top-0 h-full w-3 -mr-1.5 cursor-col-resize select-none touch-none",
379
+ "after:absolute after:right-1.5 after:top-0 after:h-full after:w-px",
380
+ "after:bg-transparent hover:after:bg-primary/30",
381
+ header.column.getIsResizing() && "after:bg-primary/50",
382
382
  )}
383
383
  role="separator"
384
384
  aria-orientation="vertical"