@classytic/fluid 0.3.6 → 0.4.1

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.
@@ -3,12 +3,12 @@
3
3
  import { t as cn } from "../utils-DQ5SCVoW.mjs";
4
4
  import { t as useIsMobile } from "../use-mobile-BX3SQVo2.mjs";
5
5
  import { t as useScrollDetection } from "../use-scroll-detection-CsgsQYvy.mjs";
6
- import { t as ApiPagination } from "../api-pagination-DBTE0yk4.mjs";
6
+ import { o as ApiPagination, r as DropdownWrapper } from "../dropdown-wrapper-B86u9Fri.mjs";
7
7
  import { n as useSearch } from "../search-context-DR7DBs7S.mjs";
8
8
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
9
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
10
10
  import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
11
- import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, Search, SlidersHorizontal, X } from "lucide-react";
11
+ import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, Search, Settings2, SlidersHorizontal, X } from "lucide-react";
12
12
  import { Button } from "@/components/ui/button";
13
13
  import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
14
14
  import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
@@ -16,6 +16,7 @@ import { Badge } from "@/components/ui/badge";
16
16
  import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
17
17
  import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
18
18
  import { Input } from "@/components/ui/input";
19
+ import { Checkbox } from "@/components/ui/checkbox";
19
20
 
20
21
  //#region src/components/data-table.tsx
21
22
  const ScrollButton = React.memo(function ScrollButton({ direction, onClick, visible, className }) {
@@ -34,11 +35,14 @@ const ScrollButton = React.memo(function ScrollButton({ direction, onClick, visi
34
35
  children: direction === "left" ? /* @__PURE__ */ jsx(ChevronLeft, { className: "size-5" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "size-5" })
35
36
  });
36
37
  });
37
- function DataTable({ columns, data, isLoading = false, pagination, enableSorting = false, enableRowSelection = false, onRowSelectionChange, className, loadingState: customLoadingState, emptyState: customEmptyState }) {
38
+ function DataTable({ columns, data, isLoading = false, pagination, enableSorting = false, enableRowSelection = false, onRowSelectionChange, columnVisibility: controlledVisibility, onColumnVisibilityChange, className, loadingState: customLoadingState, emptyState: customEmptyState }) {
38
39
  const [sorting, setSorting] = useState([]);
39
40
  const [rowSelection, setRowSelection] = useState({});
41
+ const [internalVisibility, setInternalVisibility] = useState({});
40
42
  const scrollAreaRef = useRef(null);
41
43
  const { canScrollLeft, canScrollRight, isScrollable, checkScroll } = useScrollDetection(scrollAreaRef);
44
+ const visibility = controlledVisibility ?? internalVisibility;
45
+ const setVisibility = onColumnVisibilityChange ?? setInternalVisibility;
42
46
  const { total = 0, limit = 10, pages = 1, page = 1, hasNext = false, hasPrev = false, onPageChange = () => {} } = pagination ?? {};
43
47
  const table = useReactTable({
44
48
  data,
@@ -46,6 +50,9 @@ function DataTable({ columns, data, isLoading = false, pagination, enableSorting
46
50
  getCoreRowModel: getCoreRowModel(),
47
51
  getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
48
52
  onSortingChange: setSorting,
53
+ onColumnVisibilityChange: (updater) => {
54
+ setVisibility(typeof updater === "function" ? updater(visibility) : updater);
55
+ },
49
56
  onRowSelectionChange: enableRowSelection ? (updater) => {
50
57
  setRowSelection(updater);
51
58
  if (onRowSelectionChange) {
@@ -55,6 +62,7 @@ function DataTable({ columns, data, isLoading = false, pagination, enableSorting
55
62
  } : void 0,
56
63
  state: {
57
64
  sorting,
65
+ columnVisibility: visibility,
58
66
  rowSelection: enableRowSelection ? rowSelection : void 0
59
67
  },
60
68
  enableRowSelection
@@ -106,8 +114,9 @@ function DataTable({ columns, data, isLoading = false, pagination, enableSorting
106
114
  })]
107
115
  })
108
116
  }), []);
117
+ const visibleColumnCount = table.getVisibleLeafColumns().length;
109
118
  const defaultEmptyState = useMemo(() => /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
110
- colSpan: columns.length,
119
+ colSpan: visibleColumnCount,
111
120
  className: "h-32 text-center",
112
121
  children: /* @__PURE__ */ jsxs("div", {
113
122
  className: "flex flex-col items-center justify-center py-8",
@@ -126,7 +135,7 @@ function DataTable({ columns, data, isLoading = false, pagination, enableSorting
126
135
  })
127
136
  ]
128
137
  })
129
- }) }), [columns.length]);
138
+ }) }), [visibleColumnCount]);
130
139
  if (isLoading) return /* @__PURE__ */ jsx(Fragment, { children: customLoadingState || defaultLoadingState });
131
140
  return /* @__PURE__ */ jsxs("div", {
132
141
  className: cn("flex flex-col h-full gap-4", className),
@@ -159,7 +168,7 @@ function DataTable({ columns, data, isLoading = false, pagination, enableSorting
159
168
  children: flexRender(cell.column.columnDef.cell, cell.getContext())
160
169
  }, cell.id))
161
170
  }, row.id)) : customEmptyState ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
162
- colSpan: columns.length,
171
+ colSpan: visibleColumnCount,
163
172
  className: "h-32 text-center",
164
173
  children: customEmptyState
165
174
  }) }) : defaultEmptyState })] })
@@ -370,4 +379,42 @@ function DataTableToolbar({ children, className, showSearch = true, searchPlaceh
370
379
  }
371
380
 
372
381
  //#endregion
373
- export { DataTable, DataTableToolbar };
382
+ //#region src/components/data-table-column-toggle.tsx
383
+ function DataTableColumnToggle({ columns, columnVisibility, onColumnVisibilityChange, trigger, label = "Columns", className }) {
384
+ const defaultTrigger = /* @__PURE__ */ jsxs(Button, {
385
+ variant: "outline",
386
+ size: "sm",
387
+ className: cn("shrink-0", className),
388
+ children: [/* @__PURE__ */ jsx(Settings2, { className: "mr-2 h-4 w-4" }), label]
389
+ });
390
+ return /* @__PURE__ */ jsxs(DropdownWrapper, {
391
+ trigger: trigger ?? defaultTrigger,
392
+ contentClassName: "min-w-[150px]",
393
+ children: [
394
+ /* @__PURE__ */ jsx("div", {
395
+ className: "px-1.5 py-1 text-xs font-medium text-muted-foreground",
396
+ children: label
397
+ }),
398
+ /* @__PURE__ */ jsx("div", { className: "bg-border -mx-1 my-1 h-px" }),
399
+ columns.map((col) => {
400
+ const isVisible = columnVisibility[col.id] !== false;
401
+ return /* @__PURE__ */ jsxs("div", {
402
+ role: "menuitemcheckbox",
403
+ "aria-checked": isVisible,
404
+ className: "flex items-center gap-2 rounded-md px-2 py-1.5 text-sm cursor-pointer hover:bg-accent select-none",
405
+ onClick: () => onColumnVisibilityChange({
406
+ ...columnVisibility,
407
+ [col.id]: !isVisible
408
+ }),
409
+ children: [/* @__PURE__ */ jsx(Checkbox, {
410
+ checked: isVisible,
411
+ className: "pointer-events-none"
412
+ }), col.header]
413
+ }, col.id);
414
+ })
415
+ ]
416
+ });
417
+ }
418
+
419
+ //#endregion
420
+ export { DataTable, DataTableColumnToggle, DataTableToolbar };
@@ -0,0 +1,357 @@
1
+ import { t as cn } from "./utils-DQ5SCVoW.mjs";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { ChevronDown, MoreHorizontal } from "lucide-react";
5
+ import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
6
+ import { Button } from "@/components/ui/button";
7
+ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
8
+
9
+ //#region src/components/custom-pagination.tsx
10
+ function CustomPagination({ page, onPageChange, pages, hasPrev, hasNext }) {
11
+ const [isMobile, setIsMobile] = useState(false);
12
+ const currentPageNum = Number(page);
13
+ useEffect(() => {
14
+ const checkMobile = () => {
15
+ setIsMobile(window.innerWidth < 640);
16
+ };
17
+ checkMobile();
18
+ window.addEventListener("resize", checkMobile);
19
+ return () => window.removeEventListener("resize", checkMobile);
20
+ }, []);
21
+ if (pages <= 1) return null;
22
+ const getPageNumbers = () => {
23
+ const delta = isMobile ? 1 : 2;
24
+ const range = [];
25
+ const rangeWithDots = [];
26
+ for (let i = Math.max(2, currentPageNum - delta); i <= Math.min(pages - 1, currentPageNum + delta); i++) range.push(i);
27
+ if (currentPageNum - delta > 2) rangeWithDots.push(1, "...");
28
+ else rangeWithDots.push(1);
29
+ rangeWithDots.push(...range);
30
+ if (currentPageNum + delta < pages - 1) rangeWithDots.push("...", pages);
31
+ else if (pages > 1) {
32
+ if (!range.includes(pages)) rangeWithDots.push(pages);
33
+ }
34
+ return [...new Set(rangeWithDots)];
35
+ };
36
+ if (isMobile && pages > 5) return /* @__PURE__ */ jsx(Pagination, {
37
+ className: "mx-0 w-auto",
38
+ children: /* @__PURE__ */ jsxs(PaginationContent, {
39
+ className: "gap-1",
40
+ children: [
41
+ /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationPrevious, {
42
+ onClick: () => hasPrev ? onPageChange(currentPageNum - 1) : void 0,
43
+ className: cn("cursor-pointer transition-colors h-8 px-2 text-xs", !hasPrev && "pointer-events-none opacity-50 cursor-not-allowed", hasPrev && "hover:bg-accent hover:text-accent-foreground"),
44
+ "aria-disabled": !hasPrev
45
+ }) }),
46
+ /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsxs("div", {
47
+ className: "flex h-8 items-center justify-center px-3 text-xs font-medium bg-primary text-primary-foreground rounded-md",
48
+ children: [
49
+ currentPageNum,
50
+ " / ",
51
+ pages
52
+ ]
53
+ }) }),
54
+ /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationNext, {
55
+ onClick: () => hasNext ? onPageChange(currentPageNum + 1) : void 0,
56
+ className: cn("cursor-pointer transition-colors h-8 px-2 text-xs", !hasNext && "pointer-events-none opacity-50 cursor-not-allowed", hasNext && "hover:bg-accent hover:text-accent-foreground"),
57
+ "aria-disabled": !hasNext
58
+ }) })
59
+ ]
60
+ })
61
+ });
62
+ return /* @__PURE__ */ jsx(Pagination, {
63
+ className: "mx-0 w-auto",
64
+ children: /* @__PURE__ */ jsxs(PaginationContent, {
65
+ className: "gap-1 flex-wrap",
66
+ children: [
67
+ /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationPrevious, {
68
+ onClick: () => hasPrev ? onPageChange(currentPageNum - 1) : void 0,
69
+ className: cn("cursor-pointer transition-colors", isMobile ? "h-8 px-2 text-xs" : "h-9 px-3 text-sm", !hasPrev && "pointer-events-none opacity-50 cursor-not-allowed", hasPrev && "hover:bg-accent hover:text-accent-foreground"),
70
+ "aria-disabled": !hasPrev
71
+ }) }),
72
+ getPageNumbers().map((pageNum, index) => /* @__PURE__ */ jsx(PaginationItem, { children: pageNum === "..." ? /* @__PURE__ */ jsx("span", {
73
+ className: cn("flex items-center justify-center text-muted-foreground", isMobile ? "h-8 w-8 text-xs" : "h-9 w-9 text-sm"),
74
+ children: "..."
75
+ }) : /* @__PURE__ */ jsx(PaginationLink, {
76
+ className: cn("cursor-pointer transition-colors", isMobile ? "h-8 w-8 text-xs p-0" : "h-9 w-9 text-sm p-0", "hover:bg-accent hover:text-accent-foreground", Number(currentPageNum) === Number(pageNum) && "bg-primary text-primary-foreground hover:bg-primary/90"),
77
+ onClick: () => typeof pageNum === "number" ? onPageChange(pageNum) : void 0,
78
+ isActive: Number(currentPageNum) === Number(pageNum),
79
+ "aria-label": `Go to page ${pageNum}`,
80
+ "aria-current": Number(currentPageNum) === Number(pageNum) ? "page" : void 0,
81
+ children: pageNum
82
+ }) }, `pagination-page-${pageNum}-${index}`)),
83
+ /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationNext, {
84
+ onClick: () => hasNext ? onPageChange(currentPageNum + 1) : void 0,
85
+ className: cn("cursor-pointer transition-colors", isMobile ? "h-8 px-2 text-xs" : "h-9 px-3 text-sm", !hasNext && "pointer-events-none opacity-50 cursor-not-allowed", hasNext && "hover:bg-accent hover:text-accent-foreground"),
86
+ "aria-disabled": !hasNext
87
+ }) })
88
+ ]
89
+ })
90
+ });
91
+ }
92
+ function PaginationInfo({ page, total, limit = 10 }) {
93
+ const [isMobile, setIsMobile] = useState(false);
94
+ useEffect(() => {
95
+ const checkMobile = () => {
96
+ setIsMobile(window.innerWidth < 640);
97
+ };
98
+ checkMobile();
99
+ window.addEventListener("resize", checkMobile);
100
+ return () => window.removeEventListener("resize", checkMobile);
101
+ }, []);
102
+ if (total === 0) return /* @__PURE__ */ jsx("div", {
103
+ className: "flex-1 text-sm text-muted-foreground",
104
+ children: /* @__PURE__ */ jsx("p", { children: "No entries found" })
105
+ });
106
+ const startEntry = (page - 1) * limit + 1;
107
+ const endEntry = Math.min(page * limit, total);
108
+ return /* @__PURE__ */ jsx("div", {
109
+ className: "flex-1 text-sm text-muted-foreground",
110
+ children: /* @__PURE__ */ jsx("p", {
111
+ className: cn("whitespace-nowrap", isMobile && "text-xs"),
112
+ children: isMobile ? /* @__PURE__ */ jsxs("span", { children: [
113
+ startEntry.toLocaleString(),
114
+ "-",
115
+ endEntry.toLocaleString(),
116
+ " of",
117
+ " ",
118
+ total.toLocaleString()
119
+ ] }) : /* @__PURE__ */ jsxs("span", { children: [
120
+ "Showing",
121
+ " ",
122
+ /* @__PURE__ */ jsx("span", {
123
+ className: "font-medium text-foreground",
124
+ children: startEntry.toLocaleString()
125
+ }),
126
+ " ",
127
+ "to",
128
+ " ",
129
+ /* @__PURE__ */ jsx("span", {
130
+ className: "font-medium text-foreground",
131
+ children: endEntry.toLocaleString()
132
+ }),
133
+ " ",
134
+ "of",
135
+ " ",
136
+ /* @__PURE__ */ jsx("span", {
137
+ className: "font-medium text-foreground",
138
+ children: total.toLocaleString()
139
+ }),
140
+ " ",
141
+ total === 1 ? "result" : "results"
142
+ ] })
143
+ })
144
+ });
145
+ }
146
+
147
+ //#endregion
148
+ //#region src/components/api-pagination.tsx
149
+ /**
150
+ * ApiPagination - A reusable pagination component for API-driven data
151
+ */
152
+ function ApiPagination({ total = 0, limit = 10, pages = 1, page = 1, hasNext = false, hasPrev = false, onPageChange = () => {}, className, showInfo = true, infoPosition = "left" }) {
153
+ const infoComponent = showInfo && /* @__PURE__ */ jsx("div", {
154
+ className: "shrink-0",
155
+ children: /* @__PURE__ */ jsx(PaginationInfo, {
156
+ total,
157
+ page,
158
+ limit
159
+ })
160
+ });
161
+ const paginationComponent = /* @__PURE__ */ jsx("div", {
162
+ className: "flex justify-center sm:justify-end",
163
+ children: /* @__PURE__ */ jsx(CustomPagination, {
164
+ page,
165
+ pages,
166
+ hasPrev,
167
+ hasNext,
168
+ onPageChange
169
+ })
170
+ });
171
+ return /* @__PURE__ */ jsx("div", {
172
+ className: cn("shrink-0 bg-muted/30 rounded-lg border border-border p-3 mb-2", className),
173
+ children: /* @__PURE__ */ jsx("div", {
174
+ className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4",
175
+ children: infoPosition === "left" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
176
+ className: "order-2 sm:order-1",
177
+ children: infoComponent
178
+ }), /* @__PURE__ */ jsx("div", {
179
+ className: "order-1 sm:order-2",
180
+ children: paginationComponent
181
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
182
+ className: "order-1 sm:order-1",
183
+ children: paginationComponent
184
+ }), /* @__PURE__ */ jsx("div", {
185
+ className: "order-2 sm:order-2",
186
+ children: infoComponent
187
+ })] })
188
+ })
189
+ });
190
+ }
191
+
192
+ //#endregion
193
+ //#region src/components/dropdown-wrapper.tsx
194
+ function DropdownWrapper({ trigger, children, align = "end", side = "bottom", sideOffset = 4, className, contentClassName, ...props }) {
195
+ return /* @__PURE__ */ jsxs(DropdownMenu, {
196
+ ...props,
197
+ children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, { render: trigger }), /* @__PURE__ */ jsx(DropdownMenuContent, {
198
+ align,
199
+ side,
200
+ sideOffset,
201
+ className: cn("min-w-[160px]", contentClassName),
202
+ children
203
+ })]
204
+ });
205
+ }
206
+ function ActionDropdown({ items = [], triggerIcon: TriggerIcon = MoreHorizontal, triggerVariant = "ghost", triggerSize = "sm", triggerClassName, triggerLabel, showOnHover = false, align = "end", contentClassName, onOpenChange, stopPropagation = true, ...props }) {
207
+ const handleTriggerClick = (e) => {
208
+ if (stopPropagation) e.stopPropagation();
209
+ };
210
+ const trigger = /* @__PURE__ */ jsxs(Button, {
211
+ variant: triggerVariant,
212
+ size: triggerSize,
213
+ onClick: handleTriggerClick,
214
+ className: cn(showOnHover && "opacity-0 group-hover:opacity-100 transition-opacity", triggerClassName),
215
+ children: [
216
+ /* @__PURE__ */ jsx(TriggerIcon, { className: "h-4 w-4" }),
217
+ triggerLabel && /* @__PURE__ */ jsx("span", {
218
+ className: "ml-2",
219
+ children: triggerLabel
220
+ }),
221
+ /* @__PURE__ */ jsx("span", {
222
+ className: "sr-only",
223
+ children: "Open menu"
224
+ })
225
+ ]
226
+ });
227
+ const handleItemClick = (item) => (e) => {
228
+ if (stopPropagation) e.stopPropagation();
229
+ item.onClick?.(e);
230
+ };
231
+ return /* @__PURE__ */ jsxs(DropdownMenu, {
232
+ onOpenChange,
233
+ ...props,
234
+ children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, { render: trigger }), /* @__PURE__ */ jsx(DropdownMenuContent, {
235
+ align,
236
+ className: cn("min-w-[180px]", contentClassName),
237
+ children: items.map((item, index) => {
238
+ if (item.hidden) return null;
239
+ if (item.type === "separator") return /* @__PURE__ */ jsx(DropdownMenuSeparator, {}, item.key || `separator-${index}`);
240
+ if (item.type === "label") return /* @__PURE__ */ jsx(DropdownMenuGroup, { children: /* @__PURE__ */ jsx(DropdownMenuLabel, {
241
+ className: item.className,
242
+ children: item.label
243
+ }) }, item.key || `label-${index}`);
244
+ if (item.type === "group") return /* @__PURE__ */ jsx(DropdownMenuGroup, { children: item.items?.map((groupItem, groupIndex) => {
245
+ if (groupItem.hidden) return null;
246
+ const GroupItemIcon = groupItem.icon;
247
+ const groupItemLabel = typeof groupItem.label === "function" ? groupItem.label() : groupItem.label;
248
+ return /* @__PURE__ */ jsxs(DropdownMenuItem, {
249
+ onClick: handleItemClick(groupItem),
250
+ disabled: groupItem.disabled,
251
+ className: cn(groupItem.variant === "destructive" && "text-destructive focus:text-destructive", groupItem.className),
252
+ children: [
253
+ GroupItemIcon && /* @__PURE__ */ jsx(GroupItemIcon, { className: "h-4 w-4" }),
254
+ /* @__PURE__ */ jsx("span", { children: groupItemLabel }),
255
+ groupItem.shortcut && /* @__PURE__ */ jsx("span", {
256
+ className: "ml-auto text-xs tracking-widest text-muted-foreground",
257
+ children: groupItem.shortcut
258
+ })
259
+ ]
260
+ }, groupItem.key || `group-item-${groupIndex}`);
261
+ }) }, item.key || `group-${index}`);
262
+ const ItemIcon = item.icon;
263
+ const displayLabel = typeof item.label === "function" ? item.label() : item.label;
264
+ return /* @__PURE__ */ jsxs(DropdownMenuItem, {
265
+ onClick: handleItemClick(item),
266
+ disabled: item.disabled,
267
+ className: cn(item.variant === "destructive" && "text-destructive focus:text-destructive", item.className),
268
+ children: [
269
+ ItemIcon && /* @__PURE__ */ jsx(ItemIcon, { className: "h-4 w-4" }),
270
+ /* @__PURE__ */ jsx("span", { children: displayLabel }),
271
+ item.shortcut && /* @__PURE__ */ jsx("span", {
272
+ className: "ml-auto text-xs tracking-widest text-muted-foreground",
273
+ children: item.shortcut
274
+ })
275
+ ]
276
+ }, item.key || `item-${index}`);
277
+ })
278
+ })]
279
+ });
280
+ }
281
+ function SelectDropdown({ value, onValueChange, placeholder = "Select option...", options = [], triggerClassName, contentClassName, disabled = false, ...props }) {
282
+ const selectedOption = options.find((option) => option.value === value);
283
+ const trigger = /* @__PURE__ */ jsxs(Button, {
284
+ variant: "outline",
285
+ disabled,
286
+ className: cn("w-full justify-between", !selectedOption && "text-muted-foreground", triggerClassName),
287
+ children: [selectedOption ? selectedOption.label : placeholder, /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
288
+ });
289
+ return /* @__PURE__ */ jsxs(DropdownMenu, {
290
+ ...props,
291
+ children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, { render: trigger }), /* @__PURE__ */ jsx(DropdownMenuContent, {
292
+ align: "start",
293
+ className: cn("w-full min-w-(--anchor-width)", contentClassName),
294
+ children: options.map((option) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
295
+ onClick: () => onValueChange?.(option.value),
296
+ className: cn("cursor-pointer", value === option.value && "bg-accent"),
297
+ children: [option.icon && /* @__PURE__ */ jsx(option.icon, { className: "mr-2 h-4 w-4" }), option.label]
298
+ }, option.value))
299
+ })]
300
+ });
301
+ }
302
+ function CheckboxDropdown({ values = [], onValuesChange, placeholder = "Select options...", options = [], triggerClassName, contentClassName, disabled = false, showSelectedCount = true, ...props }) {
303
+ const selectedOptions = options.filter((option) => values.includes(option.value));
304
+ const displayText = selectedOptions.length > 0 ? showSelectedCount ? `${selectedOptions.length} selected` : selectedOptions.map((opt) => opt.label).join(", ") : placeholder;
305
+ const trigger = /* @__PURE__ */ jsxs(Button, {
306
+ variant: "outline",
307
+ disabled,
308
+ className: cn("w-full justify-between", selectedOptions.length === 0 && "text-muted-foreground", triggerClassName),
309
+ children: [/* @__PURE__ */ jsx("span", {
310
+ className: "truncate",
311
+ children: displayText
312
+ }), /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
313
+ });
314
+ const handleCheckedChange = (optionValue, checked) => {
315
+ if (checked) onValuesChange?.([...values, optionValue]);
316
+ else onValuesChange?.(values.filter((value) => value !== optionValue));
317
+ };
318
+ return /* @__PURE__ */ jsxs(DropdownMenu, {
319
+ ...props,
320
+ children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, { render: trigger }), /* @__PURE__ */ jsx(DropdownMenuContent, {
321
+ align: "start",
322
+ className: cn("w-full min-w-(--anchor-width)", contentClassName),
323
+ children: options.map((option) => /* @__PURE__ */ jsxs(DropdownMenuCheckboxItem, {
324
+ checked: values.includes(option.value),
325
+ onCheckedChange: (checked) => handleCheckedChange(option.value, checked),
326
+ children: [option.icon && /* @__PURE__ */ jsx(option.icon, { className: "mr-2 h-4 w-4" }), option.label]
327
+ }, option.value))
328
+ })]
329
+ });
330
+ }
331
+ function RadioDropdown({ value, onValueChange, placeholder = "Select option...", options = [], triggerClassName, contentClassName, disabled = false, ...props }) {
332
+ const selectedOption = options.find((option) => option.value === value);
333
+ const trigger = /* @__PURE__ */ jsxs(Button, {
334
+ variant: "outline",
335
+ disabled,
336
+ className: cn("w-full justify-between", !selectedOption && "text-muted-foreground", triggerClassName),
337
+ children: [selectedOption ? selectedOption.label : placeholder, /* @__PURE__ */ jsx(ChevronDown, { className: "ml-2 h-4 w-4 shrink-0 opacity-50" })]
338
+ });
339
+ return /* @__PURE__ */ jsxs(DropdownMenu, {
340
+ ...props,
341
+ children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, { render: trigger }), /* @__PURE__ */ jsx(DropdownMenuContent, {
342
+ align: "start",
343
+ className: cn("w-full min-w-(--anchor-width)", contentClassName),
344
+ children: /* @__PURE__ */ jsx(DropdownMenuRadioGroup, {
345
+ value,
346
+ onValueChange,
347
+ children: options.map((option) => /* @__PURE__ */ jsxs(DropdownMenuRadioItem, {
348
+ value: option.value,
349
+ children: [option.icon && /* @__PURE__ */ jsx(option.icon, { className: "mr-2 h-4 w-4" }), option.label]
350
+ }, option.value))
351
+ })
352
+ })]
353
+ });
354
+ }
355
+
356
+ //#endregion
357
+ export { SelectDropdown as a, PaginationInfo as c, RadioDropdown as i, CheckboxDropdown as n, ApiPagination as o, DropdownWrapper as r, CustomPagination as s, ActionDropdown as t };
package/dist/forms.mjs CHANGED
@@ -12,12 +12,12 @@ import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGr
12
12
  import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
13
13
  import { Badge } from "@/components/ui/badge";
14
14
  import { Input } from "@/components/ui/input";
15
+ import { Checkbox } from "@/components/ui/checkbox";
15
16
  import { format, isValid, set } from "date-fns";
16
17
  import { Controller } from "react-hook-form";
17
18
  import { Field, FieldContent, FieldDescription, FieldError as FieldError$1, FieldLabel } from "@/components/ui/field";
18
19
  import { Textarea } from "@/components/ui/textarea";
19
20
  import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
20
- import { Checkbox } from "@/components/ui/checkbox";
21
21
  import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
22
22
  import { Switch } from "@/components/ui/switch";
23
23
  import { Calendar } from "@/components/ui/calendar";
@@ -1325,7 +1325,7 @@ function TagChoiceInputInternal({ name, label, description, placeholder = "Selec
1325
1325
  }),
1326
1326
  children: [/* @__PURE__ */ jsx("div", {
1327
1327
  className: "flex flex-1 flex-wrap items-center gap-1 px-3 py-1.5 min-h-[2.25rem]",
1328
- children: selectedValues.length > 0 ? selectedValues.map((val) => {
1328
+ children: selectedValues.length > 0 ? selectedValues.map((val, idx) => {
1329
1329
  const item = items.find((i) => i.value === val);
1330
1330
  return /* @__PURE__ */ jsxs(Badge, {
1331
1331
  variant: "secondary",
@@ -1338,7 +1338,7 @@ function TagChoiceInputInternal({ name, label, description, placeholder = "Selec
1338
1338
  "aria-label": `Remove ${item?.label || val}`,
1339
1339
  children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
1340
1340
  })]
1341
- }, val);
1341
+ }, val || `tag-${idx}`);
1342
1342
  }) : /* @__PURE__ */ jsx("span", {
1343
1343
  className: "text-sm text-muted-foreground",
1344
1344
  children: placeholder
@@ -1485,7 +1485,7 @@ function ComboboxInput({ control, name, label, placeholder = "Select...", emptyT
1485
1485
  onValueChangeRef.current = onValueChange;
1486
1486
  const propOnChangeRef = useRef(propOnChange);
1487
1487
  propOnChangeRef.current = propOnChange;
1488
- const fieldRef = useRef();
1488
+ const fieldRef = useRef(void 0);
1489
1489
  const handleValueChange = useCallback((newItem) => {
1490
1490
  const safeValue = newItem?.value || "";
1491
1491
  if (safeValue === currentValueRef.current) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/fluid",
3
- "version": "0.3.6",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "Fluid UI - Custom components built on shadcn/ui and base ui by Classytic",
6
6
  "main": "./dist/index.mjs",
@@ -35,6 +35,10 @@
35
35
  "types": "./dist/client/calendar.d.mts",
36
36
  "default": "./dist/client/calendar.mjs"
37
37
  },
38
+ "./client/spreadsheet": {
39
+ "types": "./dist/client/spreadsheet.d.mts",
40
+ "default": "./dist/client/spreadsheet.mjs"
41
+ },
38
42
  "./forms": {
39
43
  "types": "./dist/forms.d.mts",
40
44
  "default": "./dist/forms.mjs"
@@ -71,30 +75,36 @@
71
75
  "scripts": {
72
76
  "build": "tsdown",
73
77
  "dev": "tsdown --watch",
78
+ "test": "vitest run",
79
+ "test:watch": "vitest",
74
80
  "typecheck": "tsc --noEmit",
75
81
  "lint": "tsc --noEmit",
76
82
  "clean": "rimraf dist",
77
- "prepublishOnly": "npm run clean && npm run build"
83
+ "prepublishOnly": "npm run typecheck && npm test && npm run clean && npm run build"
78
84
  },
79
85
  "peerDependencies": {
80
- "@tanstack/react-table": "^8.21.0",
81
- "class-variance-authority": "^0.7.0",
82
- "clsx": "^2.1.0",
83
- "date-fns": "^4.1.0",
86
+ "@tanstack/react-table": ">=8.21.0",
87
+ "@tanstack/react-virtual": ">=3.11.2",
88
+ "class-variance-authority": ">=0.7.0",
89
+ "clsx": ">=2.1.0",
90
+ "date-fns": ">=4.1.0",
84
91
  "lucide-react": ">=0.460.0",
85
- "next": "^15.0.0 || ^16.0.0",
86
- "next-themes": "^0.4.0",
87
- "react": "^19.0.0",
88
- "react-dom": "^19.0.0",
89
- "react-error-boundary": "^4.0.0 || ^5.0.0 || ^6.0.0",
90
- "react-hook-form": "^7.54.0",
91
- "tailwind-merge": "^2.2.0 || ^3.0.0",
92
- "vaul": "^1.0.0"
92
+ "next": ">=16.0.0",
93
+ "next-themes": ">=0.4.0",
94
+ "react": ">=19.0.0",
95
+ "react-dom": ">=19.0.0",
96
+ "react-error-boundary": ">=6.0.0",
97
+ "react-hook-form": ">=7.54.0",
98
+ "tailwind-merge": ">=2.2.0",
99
+ "vaul": ">=1.0.0"
93
100
  },
94
101
  "peerDependenciesMeta": {
95
102
  "@tanstack/react-table": {
96
103
  "optional": true
97
104
  },
105
+ "@tanstack/react-virtual": {
106
+ "optional": true
107
+ },
98
108
  "next-themes": {
99
109
  "optional": true
100
110
  },
@@ -113,12 +123,18 @@
113
123
  },
114
124
  "devDependencies": {
115
125
  "@tanstack/react-table": "^8.21.0",
126
+ "@tanstack/react-virtual": "^3.13.23",
127
+ "@testing-library/dom": "^10.4.1",
128
+ "@testing-library/jest-dom": "^6.9.1",
129
+ "@testing-library/react": "^16.3.2",
130
+ "@testing-library/user-event": "^14.6.1",
116
131
  "@types/node": "^22.0.0",
117
132
  "@types/react": "^19.0.0",
118
133
  "@types/react-dom": "^19.0.0",
119
134
  "class-variance-authority": "^0.7.0",
120
135
  "clsx": "^2.1.0",
121
136
  "date-fns": "^4.1.0",
137
+ "jsdom": "^29.0.0",
122
138
  "lucide-react": "^0.460.0",
123
139
  "next-themes": "^0.4.0",
124
140
  "react": "^19.0.0",
@@ -128,7 +144,8 @@
128
144
  "rimraf": "^5.0.0",
129
145
  "tailwind-merge": "^3.5.0",
130
146
  "tsdown": "^0.21.0-beta.2",
131
- "typescript": "^5.7.0"
147
+ "typescript": "^5.7.0",
148
+ "vitest": "^4.1.0"
132
149
  },
133
150
  "publishConfig": {
134
151
  "access": "public"