@classytic/fluid 0.3.4 → 0.4.0

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.
@@ -1,7 +1,7 @@
1
1
  import { n as ApiPaginationData } from "../api-pagination-CJ0vR_w6.mjs";
2
2
  import * as react_jsx_runtime0 from "react/jsx-runtime";
3
- import React, { ReactNode } from "react";
4
- import { ColumnDef } from "@tanstack/react-table";
3
+ import React$1, { ReactNode } from "react";
4
+ import { ColumnDef, Table as TanstackTable, VisibilityState, VisibilityState as VisibilityState$1 } from "@tanstack/react-table";
5
5
 
6
6
  //#region src/components/data-table.d.ts
7
7
  interface DataTablePaginationProps extends Partial<ApiPaginationData> {
@@ -15,11 +15,15 @@ interface DataTableProps<TData, TValue> {
15
15
  enableSorting?: boolean;
16
16
  enableRowSelection?: boolean;
17
17
  onRowSelectionChange?: (selectedRows: TData[]) => void;
18
+ /** Controlled column visibility state. Keys are column IDs, values are booleans. */
19
+ columnVisibility?: VisibilityState$1;
20
+ /** Callback when column visibility changes. */
21
+ onColumnVisibilityChange?: (visibility: VisibilityState$1) => void;
18
22
  className?: string;
19
23
  /** Custom loading state renderer */
20
- loadingState?: React.ReactNode;
24
+ loadingState?: React$1.ReactNode;
21
25
  /** Custom empty state renderer */
22
- emptyState?: React.ReactNode;
26
+ emptyState?: React$1.ReactNode;
23
27
  }
24
28
  declare function DataTable<TData, TValue>({
25
29
  columns,
@@ -29,6 +33,8 @@ declare function DataTable<TData, TValue>({
29
33
  enableSorting,
30
34
  enableRowSelection,
31
35
  onRowSelectionChange,
36
+ columnVisibility: controlledVisibility,
37
+ onColumnVisibilityChange,
32
38
  className,
33
39
  loadingState: customLoadingState,
34
40
  emptyState: customEmptyState
@@ -81,4 +87,34 @@ declare function DataTableToolbar({
81
87
  showResultCount
82
88
  }: DataTableToolbarProps): react_jsx_runtime0.JSX.Element;
83
89
  //#endregion
84
- export { DataTable, type DataTablePaginationProps, type DataTableProps, DataTableToolbar, type DataTableToolbarProps };
90
+ //#region src/components/data-table-column-toggle.d.ts
91
+ interface ToggleableColumn {
92
+ /** Column ID (must match the column key used in VisibilityState) */
93
+ id: string;
94
+ /** Display name shown in the toggle list */
95
+ header: string;
96
+ }
97
+ interface DataTableColumnToggleProps {
98
+ /** List of columns that can be toggled. Derive from your ColumnDef array. */
99
+ columns: ToggleableColumn[];
100
+ /** Controlled visibility state. Keys are column IDs, values are booleans. */
101
+ columnVisibility: VisibilityState$1;
102
+ /** Callback when visibility changes. */
103
+ onColumnVisibilityChange: (visibility: VisibilityState$1) => void;
104
+ /** Custom trigger element. Defaults to a Settings2 icon button. */
105
+ trigger?: React.ReactNode;
106
+ /** Label shown at the top of the dropdown */
107
+ label?: string;
108
+ /** Additional className for the trigger button */
109
+ className?: string;
110
+ }
111
+ declare function DataTableColumnToggle({
112
+ columns,
113
+ columnVisibility,
114
+ onColumnVisibilityChange,
115
+ trigger,
116
+ label,
117
+ className
118
+ }: DataTableColumnToggleProps): react_jsx_runtime0.JSX.Element;
119
+ //#endregion
120
+ export { DataTable, DataTableColumnToggle, type DataTableColumnToggleProps, type DataTablePaginationProps, type DataTableProps, DataTableToolbar, type DataTableToolbarProps, type TanstackTable, type ToggleableColumn, type VisibilityState };
@@ -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 };
@@ -49,6 +49,8 @@ interface NavItem extends NavPermissions {
49
49
  url: string;
50
50
  icon?: LucideIcon;
51
51
  isActive?: boolean;
52
+ /** When true, the collapsible sub-menu is expanded by default (even when not active). */
53
+ defaultOpen?: boolean;
52
54
  badge?: string | number;
53
55
  items?: NavSubItem[];
54
56
  }
@@ -379,7 +379,7 @@ function SidebarBrand({ title, icon, href = "/", className, tooltip }) {
379
379
  function SidebarNavItem({ item, onClick }) {
380
380
  const hasSubItems = item.items && item.items.length > 0;
381
381
  const Icon = item.icon;
382
- const [open, setOpen] = useState(item.isActive ?? false);
382
+ const [open, setOpen] = useState(item.defaultOpen ?? item.isActive ?? false);
383
383
  if (hasSubItems) return /* @__PURE__ */ jsx(SidebarMenuItem, { children: /* @__PURE__ */ jsxs(Collapsible, {
384
384
  open,
385
385
  onOpenChange: setOpen,
@@ -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";
@@ -372,6 +372,7 @@ function SelectInput({ control, items = [], groups = [], name, label, placeholde
372
372
  const rawValue = field ? field.value?.toString() : localValue;
373
373
  const handleChange = (newValue) => {
374
374
  const actualValue = newValue === CLEAR_VALUE ? "" : newValue;
375
+ if ((field ? String(field.value ?? "") : localValue) === actualValue) return;
375
376
  if (field) field.onChange(actualValue);
376
377
  else setLocalValue(actualValue);
377
378
  onValueChange?.(actualValue);
@@ -1475,18 +1476,32 @@ TagChoiceInput.displayName = "TagChoiceInput";
1475
1476
  */
1476
1477
  function ComboboxInput({ control, name, label, placeholder = "Select...", emptyText = "No items found.", description, helperText, required, disabled, items = [], className, labelClassName, inputClassName, onValueChange, renderOption, value: propValue, onChange: propOnChange }) {
1477
1478
  const descriptionText = description || helperText;
1478
- const handleValueChange = useCallback((newItem, field) => {
1479
+ const resolvedMapRef = useRef(/* @__PURE__ */ new Map());
1480
+ const currentValueRef = useRef("");
1481
+ useEffect(() => {
1482
+ resolvedMapRef.current = new Map(items.map((item) => [item.value, item]));
1483
+ }, [items]);
1484
+ const onValueChangeRef = useRef(onValueChange);
1485
+ onValueChangeRef.current = onValueChange;
1486
+ const propOnChangeRef = useRef(propOnChange);
1487
+ propOnChangeRef.current = propOnChange;
1488
+ const fieldRef = useRef();
1489
+ const handleValueChange = useCallback((newItem) => {
1479
1490
  const safeValue = newItem?.value || "";
1491
+ if (safeValue === currentValueRef.current) return;
1492
+ const field = fieldRef.current;
1480
1493
  if (field) field.onChange(safeValue);
1481
- else if (propOnChange) propOnChange(safeValue);
1482
- onValueChange?.(safeValue);
1483
- }, [propOnChange, onValueChange]);
1494
+ else if (propOnChangeRef.current) propOnChangeRef.current(safeValue);
1495
+ onValueChangeRef.current?.(safeValue);
1496
+ }, []);
1484
1497
  const renderCombobox = (currentValue, field, isDisabled, fieldState) => {
1498
+ fieldRef.current = field;
1499
+ currentValueRef.current = currentValue ?? "";
1485
1500
  const ariaDescribedBy = [descriptionText ? `${name}-description` : void 0, fieldState?.invalid ? `${name}-error` : void 0].filter(Boolean).join(" ") || void 0;
1486
1501
  return /* @__PURE__ */ jsxs(Combobox, {
1487
1502
  items,
1488
- value: currentValue ? items.find((item) => item.value === currentValue) ?? null : null,
1489
- onValueChange: (item) => handleValueChange(item, field),
1503
+ value: currentValue ? resolvedMapRef.current.get(currentValue) ?? items.find((item) => item.value === currentValue) ?? null : null,
1504
+ onValueChange: handleValueChange,
1490
1505
  disabled: isDisabled,
1491
1506
  children: [/* @__PURE__ */ jsx(ComboboxInput$1, {
1492
1507
  placeholder,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/fluid",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
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",