@inui/design-system 0.0.0-stage → 1.22.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.
- package/README.md +360 -2
- package/dist/components/ui/accordion.d.ts +44 -0
- package/dist/components/ui/accordion.d.ts.map +1 -0
- package/dist/components/ui/accordion.js +86 -0
- package/dist/components/ui/accordion.js.map +1 -0
- package/dist/components/ui/ai-avatar.d.ts +46 -0
- package/dist/components/ui/ai-avatar.d.ts.map +1 -0
- package/dist/components/ui/ai-avatar.js +43 -0
- package/dist/components/ui/ai-avatar.js.map +1 -0
- package/dist/components/ui/ai-label.d.ts +118 -0
- package/dist/components/ui/ai-label.d.ts.map +1 -0
- package/dist/components/ui/ai-label.js +184 -0
- package/dist/components/ui/ai-label.js.map +1 -0
- package/dist/components/ui/ai-sparkle.d.ts +91 -0
- package/dist/components/ui/ai-sparkle.d.ts.map +1 -0
- package/dist/components/ui/ai-sparkle.js +200 -0
- package/dist/components/ui/ai-sparkle.js.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +79 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.js +147 -0
- package/dist/components/ui/alert-dialog.js.map +1 -0
- package/dist/components/ui/alert.d.ts +71 -0
- package/dist/components/ui/alert.d.ts.map +1 -0
- package/dist/components/ui/alert.js +108 -0
- package/dist/components/ui/alert.js.map +1 -0
- package/dist/components/ui/aspect-ratio.d.ts +30 -0
- package/dist/components/ui/aspect-ratio.d.ts.map +1 -0
- package/dist/components/ui/aspect-ratio.js +40 -0
- package/dist/components/ui/aspect-ratio.js.map +1 -0
- package/dist/components/ui/attachment.d.ts +74 -0
- package/dist/components/ui/attachment.d.ts.map +1 -0
- package/dist/components/ui/attachment.js +142 -0
- package/dist/components/ui/attachment.js.map +1 -0
- package/dist/components/ui/avatar.d.ts +82 -0
- package/dist/components/ui/avatar.d.ts.map +1 -0
- package/dist/components/ui/avatar.js +123 -0
- package/dist/components/ui/avatar.js.map +1 -0
- package/dist/components/ui/badge.d.ts +130 -0
- package/dist/components/ui/badge.d.ts.map +1 -0
- package/dist/components/ui/badge.js +226 -0
- package/dist/components/ui/badge.js.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +43 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.js +98 -0
- package/dist/components/ui/breadcrumb.js.map +1 -0
- package/dist/components/ui/browser-tabs.d.ts +218 -0
- package/dist/components/ui/browser-tabs.d.ts.map +1 -0
- package/dist/components/ui/browser-tabs.js +528 -0
- package/dist/components/ui/browser-tabs.js.map +1 -0
- package/dist/components/ui/bubble.d.ts +80 -0
- package/dist/components/ui/bubble.d.ts.map +1 -0
- package/dist/components/ui/bubble.js +235 -0
- package/dist/components/ui/bubble.js.map +1 -0
- package/dist/components/ui/button-group.d.ts +60 -0
- package/dist/components/ui/button-group.d.ts.map +1 -0
- package/dist/components/ui/button-group.js +89 -0
- package/dist/components/ui/button-group.js.map +1 -0
- package/dist/components/ui/button.d.ts +157 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/button.js +144 -0
- package/dist/components/ui/button.js.map +1 -0
- package/dist/components/ui/calendar.d.ts +57 -0
- package/dist/components/ui/calendar.d.ts.map +1 -0
- package/dist/components/ui/calendar.js +163 -0
- package/dist/components/ui/calendar.js.map +1 -0
- package/dist/components/ui/card.d.ts +96 -0
- package/dist/components/ui/card.d.ts.map +1 -0
- package/dist/components/ui/card.js +131 -0
- package/dist/components/ui/card.js.map +1 -0
- package/dist/components/ui/carousel.d.ts +86 -0
- package/dist/components/ui/carousel.d.ts.map +1 -0
- package/dist/components/ui/carousel.js +207 -0
- package/dist/components/ui/carousel.js.map +1 -0
- package/dist/components/ui/checkbox.d.ts +54 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/components/ui/checkbox.js +78 -0
- package/dist/components/ui/checkbox.js.map +1 -0
- package/dist/components/ui/collapsible.d.ts +45 -0
- package/dist/components/ui/collapsible.d.ts.map +1 -0
- package/dist/components/ui/collapsible.js +65 -0
- package/dist/components/ui/collapsible.js.map +1 -0
- package/dist/components/ui/combobox.d.ts +111 -0
- package/dist/components/ui/combobox.d.ts.map +1 -0
- package/dist/components/ui/combobox.js +264 -0
- package/dist/components/ui/combobox.js.map +1 -0
- package/dist/components/ui/command.d.ts +71 -0
- package/dist/components/ui/command.d.ts.map +1 -0
- package/dist/components/ui/command.js +133 -0
- package/dist/components/ui/command.js.map +1 -0
- package/dist/components/ui/context-menu.d.ts +70 -0
- package/dist/components/ui/context-menu.d.ts.map +1 -0
- package/dist/components/ui/context-menu.js +163 -0
- package/dist/components/ui/context-menu.js.map +1 -0
- package/dist/components/ui/count-badge.d.ts +40 -0
- package/dist/components/ui/count-badge.d.ts.map +1 -0
- package/dist/components/ui/count-badge.js +43 -0
- package/dist/components/ui/count-badge.js.map +1 -0
- package/dist/components/ui/data-table.d.ts +1026 -0
- package/dist/components/ui/data-table.d.ts.map +1 -0
- package/dist/components/ui/data-table.js +3423 -0
- package/dist/components/ui/data-table.js.map +1 -0
- package/dist/components/ui/dialog.d.ts +73 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/dialog.js +145 -0
- package/dist/components/ui/dialog.js.map +1 -0
- package/dist/components/ui/direction.d.ts +35 -0
- package/dist/components/ui/direction.d.ts.map +1 -0
- package/dist/components/ui/direction.js +35 -0
- package/dist/components/ui/direction.js.map +1 -0
- package/dist/components/ui/drawer.d.ts +53 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/drawer.js +115 -0
- package/dist/components/ui/drawer.js.map +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +75 -0
- package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/components/ui/dropdown-menu.js +173 -0
- package/dist/components/ui/dropdown-menu.js.map +1 -0
- package/dist/components/ui/dropzone.d.ts +141 -0
- package/dist/components/ui/dropzone.d.ts.map +1 -0
- package/dist/components/ui/dropzone.js +257 -0
- package/dist/components/ui/dropzone.js.map +1 -0
- package/dist/components/ui/empty.d.ts +94 -0
- package/dist/components/ui/empty.d.ts.map +1 -0
- package/dist/components/ui/empty.js +149 -0
- package/dist/components/ui/empty.js.map +1 -0
- package/dist/components/ui/field.d.ts +122 -0
- package/dist/components/ui/field.d.ts.map +1 -0
- package/dist/components/ui/field.js +254 -0
- package/dist/components/ui/field.js.map +1 -0
- package/dist/components/ui/hover-card.d.ts +51 -0
- package/dist/components/ui/hover-card.d.ts.map +1 -0
- package/dist/components/ui/hover-card.js +101 -0
- package/dist/components/ui/hover-card.js.map +1 -0
- package/dist/components/ui/input-group.d.ts +62 -0
- package/dist/components/ui/input-group.d.ts.map +1 -0
- package/dist/components/ui/input-group.js +115 -0
- package/dist/components/ui/input-group.js.map +1 -0
- package/dist/components/ui/input-otp.d.ts +52 -0
- package/dist/components/ui/input-otp.d.ts.map +1 -0
- package/dist/components/ui/input-otp.js +89 -0
- package/dist/components/ui/input-otp.js.map +1 -0
- package/dist/components/ui/input.d.ts +47 -0
- package/dist/components/ui/input.d.ts.map +1 -0
- package/dist/components/ui/input.js +47 -0
- package/dist/components/ui/input.js.map +1 -0
- package/dist/components/ui/item.d.ts +74 -0
- package/dist/components/ui/item.d.ts.map +1 -0
- package/dist/components/ui/item.js +176 -0
- package/dist/components/ui/item.js.map +1 -0
- package/dist/components/ui/kbd.d.ts +41 -0
- package/dist/components/ui/kbd.d.ts.map +1 -0
- package/dist/components/ui/kbd.js +58 -0
- package/dist/components/ui/kbd.js.map +1 -0
- package/dist/components/ui/key-value-pair.d.ts +56 -0
- package/dist/components/ui/key-value-pair.d.ts.map +1 -0
- package/dist/components/ui/key-value-pair.js +110 -0
- package/dist/components/ui/key-value-pair.js.map +1 -0
- package/dist/components/ui/label.d.ts +83 -0
- package/dist/components/ui/label.d.ts.map +1 -0
- package/dist/components/ui/label.js +85 -0
- package/dist/components/ui/label.js.map +1 -0
- package/dist/components/ui/marker.d.ts +54 -0
- package/dist/components/ui/marker.d.ts.map +1 -0
- package/dist/components/ui/marker.js +79 -0
- package/dist/components/ui/marker.js.map +1 -0
- package/dist/components/ui/menubar.d.ts +72 -0
- package/dist/components/ui/menubar.d.ts.map +1 -0
- package/dist/components/ui/menubar.js +174 -0
- package/dist/components/ui/menubar.js.map +1 -0
- package/dist/components/ui/message-scroller.d.ts +53 -0
- package/dist/components/ui/message-scroller.d.ts.map +1 -0
- package/dist/components/ui/message-scroller.js +95 -0
- package/dist/components/ui/message-scroller.js.map +1 -0
- package/dist/components/ui/message.d.ts +63 -0
- package/dist/components/ui/message.d.ts.map +1 -0
- package/dist/components/ui/message.js +92 -0
- package/dist/components/ui/message.js.map +1 -0
- package/dist/components/ui/meta-list.d.ts +55 -0
- package/dist/components/ui/meta-list.d.ts.map +1 -0
- package/dist/components/ui/meta-list.js +75 -0
- package/dist/components/ui/meta-list.js.map +1 -0
- package/dist/components/ui/native-select.d.ts +51 -0
- package/dist/components/ui/native-select.d.ts.map +1 -0
- package/dist/components/ui/native-select.js +75 -0
- package/dist/components/ui/native-select.js.map +1 -0
- package/dist/components/ui/navigation-menu.d.ts +56 -0
- package/dist/components/ui/navigation-menu.d.ts.map +1 -0
- package/dist/components/ui/navigation-menu.js +116 -0
- package/dist/components/ui/navigation-menu.js.map +1 -0
- package/dist/components/ui/page-header.d.ts +223 -0
- package/dist/components/ui/page-header.d.ts.map +1 -0
- package/dist/components/ui/page-header.js +233 -0
- package/dist/components/ui/page-header.js.map +1 -0
- package/dist/components/ui/pagination.d.ts +68 -0
- package/dist/components/ui/pagination.d.ts.map +1 -0
- package/dist/components/ui/pagination.js +145 -0
- package/dist/components/ui/pagination.js.map +1 -0
- package/dist/components/ui/popover.d.ts +61 -0
- package/dist/components/ui/popover.d.ts.map +1 -0
- package/dist/components/ui/popover.js +144 -0
- package/dist/components/ui/popover.js.map +1 -0
- package/dist/components/ui/progress.d.ts +35 -0
- package/dist/components/ui/progress.d.ts.map +1 -0
- package/dist/components/ui/progress.js +46 -0
- package/dist/components/ui/progress.js.map +1 -0
- package/dist/components/ui/questionnaire.d.ts +92 -0
- package/dist/components/ui/questionnaire.d.ts.map +1 -0
- package/dist/components/ui/questionnaire.js +277 -0
- package/dist/components/ui/questionnaire.js.map +1 -0
- package/dist/components/ui/radio-group.d.ts +50 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -0
- package/dist/components/ui/radio-group.js +65 -0
- package/dist/components/ui/radio-group.js.map +1 -0
- package/dist/components/ui/resizable.d.ts +44 -0
- package/dist/components/ui/resizable.d.ts.map +1 -0
- package/dist/components/ui/resizable.js +73 -0
- package/dist/components/ui/resizable.js.map +1 -0
- package/dist/components/ui/scroll-area.d.ts +55 -0
- package/dist/components/ui/scroll-area.d.ts.map +1 -0
- package/dist/components/ui/scroll-area.js +72 -0
- package/dist/components/ui/scroll-area.js.map +1 -0
- package/dist/components/ui/select.d.ts +274 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +609 -0
- package/dist/components/ui/select.js.map +1 -0
- package/dist/components/ui/separator.d.ts +31 -0
- package/dist/components/ui/separator.d.ts.map +1 -0
- package/dist/components/ui/separator.js +38 -0
- package/dist/components/ui/separator.js.map +1 -0
- package/dist/components/ui/sheet.d.ts +59 -0
- package/dist/components/ui/sheet.d.ts.map +1 -0
- package/dist/components/ui/sheet.js +128 -0
- package/dist/components/ui/sheet.js.map +1 -0
- package/dist/components/ui/sidebar.d.ts +166 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sidebar.js +512 -0
- package/dist/components/ui/sidebar.js.map +1 -0
- package/dist/components/ui/skeleton.d.ts +35 -0
- package/dist/components/ui/skeleton.d.ts.map +1 -0
- package/dist/components/ui/skeleton.js +46 -0
- package/dist/components/ui/skeleton.js.map +1 -0
- package/dist/components/ui/slider.d.ts +39 -0
- package/dist/components/ui/slider.d.ts.map +1 -0
- package/dist/components/ui/slider.js +75 -0
- package/dist/components/ui/slider.js.map +1 -0
- package/dist/components/ui/sonner.d.ts +56 -0
- package/dist/components/ui/sonner.d.ts.map +1 -0
- package/dist/components/ui/sonner.js +96 -0
- package/dist/components/ui/sonner.js.map +1 -0
- package/dist/components/ui/spinner.d.ts +37 -0
- package/dist/components/ui/spinner.d.ts.map +1 -0
- package/dist/components/ui/spinner.js +51 -0
- package/dist/components/ui/spinner.js.map +1 -0
- package/dist/components/ui/stepper.d.ts +84 -0
- package/dist/components/ui/stepper.d.ts.map +1 -0
- package/dist/components/ui/stepper.js +145 -0
- package/dist/components/ui/stepper.js.map +1 -0
- package/dist/components/ui/switch.d.ts +36 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/components/ui/switch.js +44 -0
- package/dist/components/ui/switch.js.map +1 -0
- package/dist/components/ui/table.d.ts +80 -0
- package/dist/components/ui/table.d.ts.map +1 -0
- package/dist/components/ui/table.js +130 -0
- package/dist/components/ui/table.js.map +1 -0
- package/dist/components/ui/tabs.d.ts +87 -0
- package/dist/components/ui/tabs.d.ts.map +1 -0
- package/dist/components/ui/tabs.js +128 -0
- package/dist/components/ui/tabs.js.map +1 -0
- package/dist/components/ui/textarea.d.ts +35 -0
- package/dist/components/ui/textarea.d.ts.map +1 -0
- package/dist/components/ui/textarea.js +38 -0
- package/dist/components/ui/textarea.js.map +1 -0
- package/dist/components/ui/time-picker.d.ts +58 -0
- package/dist/components/ui/time-picker.d.ts.map +1 -0
- package/dist/components/ui/time-picker.js +188 -0
- package/dist/components/ui/time-picker.js.map +1 -0
- package/dist/components/ui/toggle-group.d.ts +52 -0
- package/dist/components/ui/toggle-group.d.ts.map +1 -0
- package/dist/components/ui/toggle-group.js +94 -0
- package/dist/components/ui/toggle-group.js.map +1 -0
- package/dist/components/ui/toggle.d.ts +50 -0
- package/dist/components/ui/toggle.d.ts.map +1 -0
- package/dist/components/ui/toggle.js +70 -0
- package/dist/components/ui/toggle.js.map +1 -0
- package/dist/components/ui/tooltip.d.ts +51 -0
- package/dist/components/ui/tooltip.d.ts.map +1 -0
- package/dist/components/ui/tooltip.js +78 -0
- package/dist/components/ui/tooltip.js.map +1 -0
- package/dist/hooks/use-dark-mode.d.ts +2 -0
- package/dist/hooks/use-dark-mode.d.ts.map +1 -0
- package/dist/hooks/use-dark-mode.js +36 -0
- package/dist/hooks/use-dark-mode.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +2 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +20 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/lib/scope.d.ts +64 -0
- package/dist/lib/scope.d.ts.map +1 -0
- package/dist/lib/scope.js +74 -0
- package/dist/lib/scope.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +11 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/theme-tokens.css +537 -0
- package/dist/theme.css +417 -0
- package/package.json +103 -3
|
@@ -0,0 +1,3423 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { AISparkle } from "./ai-sparkle.js";
|
|
4
|
+
import { Button } from "./button.js";
|
|
5
|
+
import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "./popover.js";
|
|
6
|
+
import { AILabel, AILabelContent, AILabelTrigger } from "./ai-label.js";
|
|
7
|
+
import { Input } from "./input.js";
|
|
8
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
9
|
+
import { Label } from "./label.js";
|
|
10
|
+
import { Checkbox } from "./checkbox.js";
|
|
11
|
+
import { InputGroup, InputGroupAddon, InputGroupInput } from "./input-group.js";
|
|
12
|
+
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "./table.js";
|
|
13
|
+
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "./pagination.js";
|
|
14
|
+
import { Skeleton } from "./skeleton.js";
|
|
15
|
+
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "./empty.js";
|
|
16
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./dropdown-menu.js";
|
|
17
|
+
import { Select, SelectContent, SelectItem, SelectMultiple, SelectMultipleContent, SelectMultipleFooter, SelectMultipleGroup, SelectMultipleItem, SelectMultipleLabel, SelectMultipleTrigger, SelectTrigger, SelectValue } from "./select.js";
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import { ArrowDownIcon, ArrowLeftIcon, ArrowLeftToLineIcon, ArrowRightIcon, ArrowRightToLineIcon, ArrowUpIcon, CheckCheckIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsUpDownIcon, CircleAlertIcon, Columns3Icon, EllipsisIcon, EyeOffIcon, ListFilterIcon, PinIcon, PinOffIcon, RotateCcwIcon, SearchIcon, SearchXIcon, UnfoldHorizontalIcon, XIcon } from "lucide-react";
|
|
20
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
21
|
+
import { flexRender, getCoreRowModel, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
|
|
22
|
+
//#region src/components/ui/data-table.tsx
|
|
23
|
+
/**
|
|
24
|
+
* DataTable — Table plus sorting, filtering, search, row selection,
|
|
25
|
+
* row actions, expandable rows, AI affordances, column management, and
|
|
26
|
+
* pagination.
|
|
27
|
+
*
|
|
28
|
+
* A composite over the existing `Table` primitive (unmodified) — state is
|
|
29
|
+
* owned by `@tanstack/react-table`, a headless logic-only library with no
|
|
30
|
+
* rendered markup of its own (same "headless engine, MMDS renders every
|
|
31
|
+
* pixel" shape as RichTextEditor's use of draft-js), not hand-rolled here.
|
|
32
|
+
* Every visible piece (checkboxes, buttons, the search field, filter
|
|
33
|
+
* popovers, the column menu, pagination, loading/empty states) is an
|
|
34
|
+
* existing MMDS primitive — Checkbox, Button, InputGroup, Input, Select,
|
|
35
|
+
* Popover, DropdownMenu, Skeleton, Empty, AISparkle.
|
|
36
|
+
*/
|
|
37
|
+
var SELECTION_CHECKBOX_CLASS = "size-6 rounded-[6px] border-0 p-1 bg-clip-content [outline-style:solid]! outline-1 -outline-offset-4 outline-input data-[state=checked]:[outline-style:none]! data-[state=indeterminate]:[outline-style:none]!";
|
|
38
|
+
var SELECTION_COLUMN_ID = "__select__";
|
|
39
|
+
var EXPAND_COLUMN_ID = "__expand__";
|
|
40
|
+
var AI_COLUMN_ID = "__ai__";
|
|
41
|
+
var INJECTED_COLUMN_IDS = /* @__PURE__ */ new Set([
|
|
42
|
+
SELECTION_COLUMN_ID,
|
|
43
|
+
EXPAND_COLUMN_ID,
|
|
44
|
+
AI_COLUMN_ID
|
|
45
|
+
]);
|
|
46
|
+
function AIMarkerTrigger({ size, ariaLabel }) {
|
|
47
|
+
return /* @__PURE__ */ jsx(AILabelTrigger, {
|
|
48
|
+
size: size === "default" ? "default" : "sm",
|
|
49
|
+
"aria-label": ariaLabel
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function getColumnId(column) {
|
|
53
|
+
if (column.id) return column.id;
|
|
54
|
+
if ("accessorKey" in column && typeof column.accessorKey === "string") return column.accessorKey;
|
|
55
|
+
}
|
|
56
|
+
/** The dev-configured default pinning — every column's own `meta.pin`, collected into TanStack's `{ left, right }` shape. Seeds `columnPinning`'s initial state and is what "Reset to defaults" restores it to. */
|
|
57
|
+
function derivePinningFromMeta(columns) {
|
|
58
|
+
const left = [];
|
|
59
|
+
const right = [];
|
|
60
|
+
columns.forEach((col) => {
|
|
61
|
+
const id = getColumnId(col);
|
|
62
|
+
if (!id) return;
|
|
63
|
+
if (col.meta?.pin === "left") left.push(id);
|
|
64
|
+
if (col.meta?.pin === "right") right.push(id);
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
left,
|
|
68
|
+
right
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function idListsEqual(a, b) {
|
|
72
|
+
const left = a ?? [];
|
|
73
|
+
const right = b ?? [];
|
|
74
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
75
|
+
}
|
|
76
|
+
var measureCanvasContext;
|
|
77
|
+
function getMeasureContext() {
|
|
78
|
+
if (measureCanvasContext !== void 0) return measureCanvasContext;
|
|
79
|
+
measureCanvasContext = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d") ?? null;
|
|
80
|
+
return measureCanvasContext;
|
|
81
|
+
}
|
|
82
|
+
var MIN_SIZE_FALLBACK_CHAR_WIDTH = 8;
|
|
83
|
+
function measureTextWidth(text, font) {
|
|
84
|
+
const ctx = getMeasureContext();
|
|
85
|
+
if (!ctx) return text.length * MIN_SIZE_FALLBACK_CHAR_WIDTH;
|
|
86
|
+
ctx.font = font;
|
|
87
|
+
return ctx.measureText(text).width;
|
|
88
|
+
}
|
|
89
|
+
var HEADER_ICON_CONTROL_ALLOWANCE = 26;
|
|
90
|
+
/**
|
|
91
|
+
* The narrowest a column should ever resize to — just enough for its
|
|
92
|
+
* header's first character, an ellipsis, the sort icon (if sortable),
|
|
93
|
+
* and the filter icon/headerActions cluster (if this column has them —
|
|
94
|
+
* both occupy real layout space in the header row regardless of their
|
|
95
|
+
* own hover-reveal opacity), so a squeezed column still reads as "this
|
|
96
|
+
* column, truncated" rather than disappearing into an unreadable sliver
|
|
97
|
+
* or squeezing its own label to nothing to make room for its sibling
|
|
98
|
+
* controls. Measured per column via an offscreen canvas (the "W" in
|
|
99
|
+
* "Website" needs more room than the "I" in "Invoice") rather than one
|
|
100
|
+
* flat constant for every column — that's what makes this "dynamic."
|
|
101
|
+
* Only works out the real first character when `header` is a plain
|
|
102
|
+
* string; a render-function header falls back to
|
|
103
|
+
* `MIN_SIZE_FALLBACK_CHAR_WIDTH` since there's no text to introspect
|
|
104
|
+
* without actually mounting it.
|
|
105
|
+
*/
|
|
106
|
+
function computeColumnMinSize(col, tableSize) {
|
|
107
|
+
const font = tableSize === "xs" ? "500 12px system-ui, sans-serif" : "500 14px system-ui, sans-serif";
|
|
108
|
+
const label = (col.meta?.label ?? (typeof col.header === "string" ? col.header : "")).trim();
|
|
109
|
+
const charWidth = label ? measureTextWidth(label.charAt(0), font) : MIN_SIZE_FALLBACK_CHAR_WIDTH;
|
|
110
|
+
const ellipsisWidth = measureTextWidth("…", font);
|
|
111
|
+
const canSort = col.enableSorting !== false;
|
|
112
|
+
const sortIconAllowance = canSort ? 18 : 0;
|
|
113
|
+
const canFilter = col.enableColumnFilter !== false;
|
|
114
|
+
const hasHeaderActions = Boolean(col.meta?.headerActions);
|
|
115
|
+
const siblingControlsAllowance = (canFilter ? HEADER_ICON_CONTROL_ALLOWANCE : 0) + (hasHeaderActions ? HEADER_ICON_CONTROL_ALLOWANCE : 0);
|
|
116
|
+
const cellPadding = 16;
|
|
117
|
+
const buttonPadding = canSort ? 8 : 0;
|
|
118
|
+
return Math.ceil(charWidth + ellipsisWidth + sortIconAllowance + siblingControlsAllowance + cellPadding + buttonPadding);
|
|
119
|
+
}
|
|
120
|
+
/** A column absent from either map is implicitly visible (`true`) — comparing raw keys would treat "never touched" and "explicitly shown again" as different when they're not. */
|
|
121
|
+
function visibilityEqual(a, b) {
|
|
122
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].every((key) => (a[key] ?? true) === (b[key] ?? true));
|
|
123
|
+
}
|
|
124
|
+
/** A column absent from either map has no explicit width set — same "missing means untouched" comparison as `visibilityEqual`, just with no shared implicit value to fall back to (there's no one universal default width), so a column present in only one side always counts as a difference. */
|
|
125
|
+
function sizingEqual(a, b) {
|
|
126
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].every((key) => a[key] === b[key]);
|
|
127
|
+
}
|
|
128
|
+
var selectFilterFn = (row, columnId, filterValue) => {
|
|
129
|
+
if (!Array.isArray(filterValue) || filterValue.length === 0) return true;
|
|
130
|
+
return filterValue.includes(String(row.getValue(columnId)));
|
|
131
|
+
};
|
|
132
|
+
var singleSelectFilterFn = (row, columnId, filterValue) => {
|
|
133
|
+
if (!filterValue) return true;
|
|
134
|
+
return String(row.getValue(columnId)) === filterValue;
|
|
135
|
+
};
|
|
136
|
+
var DEFAULT_RANGE_BUCKET_SIZE = 3;
|
|
137
|
+
var ALL_VALUE = "__all__";
|
|
138
|
+
/** The last run of digits in a string — "INV003" -> 3. Powers `filterVariant: "range"`. */
|
|
139
|
+
function extractTrailingNumber(value) {
|
|
140
|
+
const match = String(value ?? "").match(/(\d+)(?!.*\d)/);
|
|
141
|
+
return match ? Number(match[0]) : null;
|
|
142
|
+
}
|
|
143
|
+
var rangeBucketCache = /* @__PURE__ */ new WeakMap();
|
|
144
|
+
/** Buckets a column's sorted unique values (by trailing number) into fixed-size groups, each rendered as a "min-max" (or just "min" for a single-value bucket) label. */
|
|
145
|
+
function computeRangeBuckets(column, bucketSize) {
|
|
146
|
+
const uniqueValues = column.getFacetedUniqueValues();
|
|
147
|
+
const cached = rangeBucketCache.get(uniqueValues)?.get(bucketSize);
|
|
148
|
+
if (cached) return cached;
|
|
149
|
+
const numbers = Array.from(uniqueValues.keys()).map(extractTrailingNumber).filter((value) => value !== null).sort((a, b) => a - b);
|
|
150
|
+
const buckets = [];
|
|
151
|
+
for (let index = 0; index < numbers.length; index += bucketSize) {
|
|
152
|
+
const slice = numbers.slice(index, index + bucketSize);
|
|
153
|
+
const min = slice[0];
|
|
154
|
+
const max = slice[slice.length - 1];
|
|
155
|
+
buckets.push(min === max ? `${min}` : `${min}-${max}`);
|
|
156
|
+
}
|
|
157
|
+
const byBucketSize = rangeBucketCache.get(uniqueValues) ?? /* @__PURE__ */ new Map();
|
|
158
|
+
byBucketSize.set(bucketSize, buckets);
|
|
159
|
+
rangeBucketCache.set(uniqueValues, byBucketSize);
|
|
160
|
+
return buckets;
|
|
161
|
+
}
|
|
162
|
+
/** A "min-max" (or single "n") bucket label back into its numeric bounds. */
|
|
163
|
+
function parseRangeBucket(label) {
|
|
164
|
+
const [min, max] = label.split("-").map(Number);
|
|
165
|
+
return max === void 0 ? [min, min] : [min, max];
|
|
166
|
+
}
|
|
167
|
+
var rangeFilterFn = (row, columnId, filterValue) => {
|
|
168
|
+
if (!Array.isArray(filterValue) || filterValue.length === 0) return true;
|
|
169
|
+
const value = extractTrailingNumber(row.getValue(columnId));
|
|
170
|
+
if (value === null) return false;
|
|
171
|
+
return filterValue.some((bucket) => {
|
|
172
|
+
const [min, max] = parseRangeBucket(bucket);
|
|
173
|
+
return value >= min && value <= max;
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
/** Matches `query` only where a word starts — at the beginning of the text or right after anything that isn't a letter or digit. The one pattern behind both the `searchMatch="startsWith"` filter and its highlight, so they can never disagree. Capture group 1 is the (possibly empty) boundary character, so the highlight can skip it; a consuming prefix instead of a lookbehind keeps this working on Safari before 16.4. */
|
|
177
|
+
function wordStartPattern(query, flags) {
|
|
178
|
+
return new RegExp(`(^|[^\\p{L}\\p{N}])${query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, flags);
|
|
179
|
+
}
|
|
180
|
+
var startsWithWordFilterFn = (row, columnId, filterValue) => {
|
|
181
|
+
const query = String(filterValue ?? "").trim();
|
|
182
|
+
if (!query) return true;
|
|
183
|
+
return wordStartPattern(query, "iu").test(String(row.getValue(columnId) ?? ""));
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Wraps the substring of `text` matching `query` in a `<mark>`. Exported so a
|
|
187
|
+
* column with a custom `cell` can highlight matches itself — read the live
|
|
188
|
+
* query off `info.table.getState().globalFilter` inside that `cell`. Pass
|
|
189
|
+
* `startsWith` when the table's `searchMatch` is `"startsWith"`, so only
|
|
190
|
+
* word-start matches are marked.
|
|
191
|
+
*/
|
|
192
|
+
function HighlightMatch({ text, query, startsWith = false }) {
|
|
193
|
+
const trimmed = query.trim();
|
|
194
|
+
if (!trimmed) return /* @__PURE__ */ jsx(Fragment, { children: text });
|
|
195
|
+
const mark = (part, index) => /* @__PURE__ */ jsx("mark", {
|
|
196
|
+
className: "rounded-[2px] bg-primary/15 text-inherit",
|
|
197
|
+
children: part
|
|
198
|
+
}, index);
|
|
199
|
+
if (startsWith) {
|
|
200
|
+
const parts = [];
|
|
201
|
+
let last = 0;
|
|
202
|
+
for (const match of text.matchAll(wordStartPattern(trimmed, "giu"))) {
|
|
203
|
+
const start = match.index + match[1].length;
|
|
204
|
+
if (start > last) parts.push(text.slice(last, start));
|
|
205
|
+
parts.push(mark(match[0].slice(match[1].length), parts.length));
|
|
206
|
+
last = match.index + match[0].length;
|
|
207
|
+
}
|
|
208
|
+
if (last < text.length) parts.push(text.slice(last));
|
|
209
|
+
return /* @__PURE__ */ jsx(Fragment, { children: parts });
|
|
210
|
+
}
|
|
211
|
+
const pattern = new RegExp(`(${trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
|
|
212
|
+
const parts = text.split(pattern);
|
|
213
|
+
return /* @__PURE__ */ jsx(Fragment, { children: parts.map((part, index) => part.toLowerCase() === trimmed.toLowerCase() ? mark(part, index) : part) });
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Renders `actions` as inline icon buttons (≤2 actions) or an overflow
|
|
217
|
+
* menu (>2) — `mode` overrides the auto pick. Stops its own click from
|
|
218
|
+
* bubbling, so it composes safely inside a row that also has `onRowClick`.
|
|
219
|
+
*/
|
|
220
|
+
function RowActions({ actions, mode = "auto", revealClassName, onOpenChange }) {
|
|
221
|
+
const [overflowOpen, setOverflowOpenState] = React.useState(false);
|
|
222
|
+
const setOverflowOpen = (open) => {
|
|
223
|
+
setOverflowOpenState(open);
|
|
224
|
+
onOpenChange?.(open);
|
|
225
|
+
};
|
|
226
|
+
if ((mode === "auto" ? actions.length <= 2 ? "inline" : "overflow" : mode) === "inline") return /* @__PURE__ */ jsx("div", {
|
|
227
|
+
"data-fit-exclude": true,
|
|
228
|
+
draggable: false,
|
|
229
|
+
className: cn("flex items-center justify-end gap-1", revealClassName),
|
|
230
|
+
onClick: (event) => event.stopPropagation(),
|
|
231
|
+
children: actions.map((action) => /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
232
|
+
asChild: true,
|
|
233
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
234
|
+
type: "button",
|
|
235
|
+
variant: "ghost",
|
|
236
|
+
size: "xs",
|
|
237
|
+
iconOnly: true,
|
|
238
|
+
"aria-label": action.label,
|
|
239
|
+
className: cn(action.variant === "destructive" && "text-destructive hover:text-destructive"),
|
|
240
|
+
onClick: action.onClick,
|
|
241
|
+
onMouseEnter: action.onMouseEnter,
|
|
242
|
+
onMouseLeave: action.onMouseLeave,
|
|
243
|
+
onFocus: action.onFocus,
|
|
244
|
+
onBlur: action.onBlur,
|
|
245
|
+
children: action.icon ? /* @__PURE__ */ jsx(action.icon, {}) : null
|
|
246
|
+
})
|
|
247
|
+
}), /* @__PURE__ */ jsx(TooltipContent, { children: action.label })] }, action.label))
|
|
248
|
+
});
|
|
249
|
+
return /* @__PURE__ */ jsx("div", {
|
|
250
|
+
"data-fit-exclude": true,
|
|
251
|
+
draggable: false,
|
|
252
|
+
className: cn("flex justify-end", overflowOpen ? "opacity-100" : revealClassName),
|
|
253
|
+
onClick: (event) => event.stopPropagation(),
|
|
254
|
+
children: /* @__PURE__ */ jsxs(DropdownMenu, {
|
|
255
|
+
open: overflowOpen,
|
|
256
|
+
onOpenChange: setOverflowOpen,
|
|
257
|
+
children: [/* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
258
|
+
asChild: true,
|
|
259
|
+
children: /* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
260
|
+
asChild: true,
|
|
261
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
262
|
+
type: "button",
|
|
263
|
+
variant: "ghost",
|
|
264
|
+
size: "xs",
|
|
265
|
+
iconOnly: true,
|
|
266
|
+
"aria-label": "Row actions",
|
|
267
|
+
children: /* @__PURE__ */ jsx(EllipsisIcon, {})
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
}), /* @__PURE__ */ jsx(TooltipContent, { children: "Row actions" })] }), /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
271
|
+
align: "end",
|
|
272
|
+
children: actions.map((action) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
273
|
+
variant: action.variant,
|
|
274
|
+
onClick: action.onClick,
|
|
275
|
+
onMouseEnter: action.onMouseEnter,
|
|
276
|
+
onMouseLeave: action.onMouseLeave,
|
|
277
|
+
onFocus: action.onFocus,
|
|
278
|
+
onBlur: action.onBlur,
|
|
279
|
+
children: [action.icon ? /* @__PURE__ */ jsx(action.icon, { "data-icon": "inline-start" }) : null, action.label]
|
|
280
|
+
}, action.label))
|
|
281
|
+
})]
|
|
282
|
+
})
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
var FIT_TO_CONTENT_PADDING = 2;
|
|
286
|
+
var DEFAULT_MAX_COLUMN_SIZE = 1200;
|
|
287
|
+
var PAGE_SELECT_THRESHOLD = 50;
|
|
288
|
+
/**
|
|
289
|
+
* The page numbers (and `"ellipsis"` gaps) a numbered `Pagination` control
|
|
290
|
+
* actually renders for `current` (1-indexed) out of `total` pages — always
|
|
291
|
+
* page 1 and `total`, always `current`'s own immediate neighbors, an
|
|
292
|
+
* ellipsis standing in for whatever's skipped between those two clusters.
|
|
293
|
+
*/
|
|
294
|
+
function getPaginationWindow(current, total) {
|
|
295
|
+
const pages = /* @__PURE__ */ new Set([1, total]);
|
|
296
|
+
for (let page = current - 1; page <= current + 1; page++) if (page >= 1 && page <= total) pages.add(page);
|
|
297
|
+
const sorted = [...pages].sort((a, b) => a - b);
|
|
298
|
+
const result = [];
|
|
299
|
+
let previous = 0;
|
|
300
|
+
for (const page of sorted) {
|
|
301
|
+
if (page - previous > 1) result.push("ellipsis");
|
|
302
|
+
result.push(page);
|
|
303
|
+
previous = page;
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Clones every element in `elements` into one shared, unconstrained, hidden
|
|
309
|
+
* container appended to `document.body` exactly once, to measure each
|
|
310
|
+
* clone's natural (unwrapped, unclipped) content width independent of
|
|
311
|
+
* whatever width the live layout currently gives it — one batch of
|
|
312
|
+
* append/measure/remove instead of one full reflow-triggering cycle per
|
|
313
|
+
* cell, which is what a header plus a full page of body rows would
|
|
314
|
+
* otherwise cost on every "Fit to content" click. Measuring the live
|
|
315
|
+
* elements' own `scrollWidth` directly doesn't work here: once a column
|
|
316
|
+
* has already been fit once, its cells are exactly as wide as their
|
|
317
|
+
* content, so `scrollWidth` on those *live* cells just reports back their
|
|
318
|
+
* own current box width (there's nothing left to overflow) rather than the
|
|
319
|
+
* true minimum — clicking "Fit to content" again would then add
|
|
320
|
+
* `FIT_TO_CONTENT_PADDING` on top of that every time, growing the column a
|
|
321
|
+
* couple pixels on every click forever. The clones sidestep that by never
|
|
322
|
+
* being constrained by the table's own column width to begin with. Strips
|
|
323
|
+
* anything marked `data-fit-exclude` first — hover-only header actions
|
|
324
|
+
* shouldn't force permanent extra width onto a column just because they're
|
|
325
|
+
* in the DOM.
|
|
326
|
+
*/
|
|
327
|
+
function measureNaturalWidths(elements) {
|
|
328
|
+
const container = document.createElement("div");
|
|
329
|
+
Object.assign(container.style, {
|
|
330
|
+
position: "absolute",
|
|
331
|
+
visibility: "hidden",
|
|
332
|
+
left: "-9999px",
|
|
333
|
+
top: "-9999px"
|
|
334
|
+
});
|
|
335
|
+
const clones = elements.map((element) => {
|
|
336
|
+
const clone = element.cloneNode(true);
|
|
337
|
+
clone.querySelectorAll("[data-fit-exclude]").forEach((node) => node.remove());
|
|
338
|
+
Object.assign(clone.style, {
|
|
339
|
+
display: "inline-block",
|
|
340
|
+
width: "auto",
|
|
341
|
+
maxWidth: "none",
|
|
342
|
+
whiteSpace: "nowrap"
|
|
343
|
+
});
|
|
344
|
+
container.appendChild(clone);
|
|
345
|
+
return clone;
|
|
346
|
+
});
|
|
347
|
+
document.body.appendChild(container);
|
|
348
|
+
const widths = clones.map((clone) => clone.scrollWidth);
|
|
349
|
+
container.remove();
|
|
350
|
+
return widths;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Measures every currently-rendered cell in `column` — its header plus
|
|
354
|
+
* this page's body rows, via `measureNaturalWidths` — and sets the
|
|
355
|
+
* column's size to the widest one found, via `table.setColumnSizing`.
|
|
356
|
+
* Only rows rendered on the *current* page are measured — a deliberate,
|
|
357
|
+
* cheaper scope than the whole dataset. Requires `enableColumnResizing`
|
|
358
|
+
* on `DataTable` (a column that can't resize can't be fit either — see
|
|
359
|
+
* `columnMenuActions`). The measuring pass runs one frame later
|
|
360
|
+
* (`requestAnimationFrame`) so it never shares a frame with whatever
|
|
361
|
+
* triggered it (a click, a double-click) — it's a batch of forced
|
|
362
|
+
* layout reads either way, just not fighting that frame's own paint too.
|
|
363
|
+
*/
|
|
364
|
+
function fitColumnToContent(column, table) {
|
|
365
|
+
const elements = table.options.meta?.getColumnCellElements?.(column.id) ?? [];
|
|
366
|
+
if (elements.length === 0) return;
|
|
367
|
+
requestAnimationFrame(() => {
|
|
368
|
+
const widest = Math.max(...measureNaturalWidths(elements));
|
|
369
|
+
table.setColumnSizing((old) => ({
|
|
370
|
+
...old,
|
|
371
|
+
[column.id]: widest + FIT_TO_CONTENT_PADDING
|
|
372
|
+
}));
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Pin left/Pin right `RowAction`s for `column` — each becomes "Unpin"
|
|
377
|
+
* instead once the column's already pinned to that side. Empty when the
|
|
378
|
+
* column can't pin (`getCanPin()`) or `meta.pinLocked` is set. Shared by
|
|
379
|
+
* `columnMenuActions` (the header-cell menu) and the Columns-menu popover
|
|
380
|
+
* row, which renders these same two actions as plain inline icon buttons
|
|
381
|
+
* instead of inside a dropdown.
|
|
382
|
+
*/
|
|
383
|
+
function pinActions(column) {
|
|
384
|
+
if (!column.getCanPin() || column.columnDef.meta?.pinLocked) return [];
|
|
385
|
+
const pinned = column.getIsPinned();
|
|
386
|
+
return [pinned === "left" ? {
|
|
387
|
+
label: "Unpin",
|
|
388
|
+
icon: PinOffIcon,
|
|
389
|
+
onClick: () => column.pin(false)
|
|
390
|
+
} : {
|
|
391
|
+
label: "Pin left",
|
|
392
|
+
icon: ArrowLeftToLineIcon,
|
|
393
|
+
onClick: () => column.pin("left")
|
|
394
|
+
}, pinned === "right" ? {
|
|
395
|
+
label: "Unpin",
|
|
396
|
+
icon: PinOffIcon,
|
|
397
|
+
onClick: () => column.pin(false)
|
|
398
|
+
} : {
|
|
399
|
+
label: "Pin right",
|
|
400
|
+
icon: ArrowRightToLineIcon,
|
|
401
|
+
onClick: () => column.pin("right")
|
|
402
|
+
}];
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* A ready-made column-menu bundle for `headerActions` — Pin left, Pin
|
|
406
|
+
* right (via `pinActions`), Hide column, and Fit to content — each
|
|
407
|
+
* omitted when the column can't do it (`getCanPin()`/`getCanHide()`/
|
|
408
|
+
* `getCanResize()`, the last only true once `enableColumnResizing` is
|
|
409
|
+
* set). Typically 3-4 actions, so `RowActions`' own auto mode already
|
|
410
|
+
* renders it as a single overflow "more" menu — no need to force
|
|
411
|
+
* `mode="overflow"`.
|
|
412
|
+
*/
|
|
413
|
+
function columnMenuActions(column, table) {
|
|
414
|
+
const actions = [...pinActions(column)];
|
|
415
|
+
if (column.getCanHide()) actions.push({
|
|
416
|
+
label: "Hide column",
|
|
417
|
+
icon: EyeOffIcon,
|
|
418
|
+
onClick: () => column.toggleVisibility(false)
|
|
419
|
+
});
|
|
420
|
+
if (column.getCanResize()) actions.push({
|
|
421
|
+
label: "Fit to content",
|
|
422
|
+
icon: UnfoldHorizontalIcon,
|
|
423
|
+
onClick: () => fitColumnToContent(column, table)
|
|
424
|
+
});
|
|
425
|
+
return actions;
|
|
426
|
+
}
|
|
427
|
+
var numericAwareCollator = new Intl.Collator(void 0, { numeric: true });
|
|
428
|
+
/** `"bucket"` is the canonical name; `"range"` is still accepted (with a dev warning — see the deprecation checks near the component's other dev warnings) since it originally had no more specific name to disambiguate against. */
|
|
429
|
+
function isBucketFilterVariant(column) {
|
|
430
|
+
const variant = column.columnDef.meta?.filterVariant;
|
|
431
|
+
return variant === "bucket" || variant === "range";
|
|
432
|
+
}
|
|
433
|
+
/** `bucketSize` is the canonical name; `rangeBucketSize` is still accepted (same deprecation as `"range"` above). */
|
|
434
|
+
function resolveBucketSize(column) {
|
|
435
|
+
const meta = column.columnDef.meta;
|
|
436
|
+
return meta?.bucketSize ?? meta?.rangeBucketSize ?? DEFAULT_RANGE_BUCKET_SIZE;
|
|
437
|
+
}
|
|
438
|
+
/** The resolved option list for an enumerable filter (`select`/`singleSelect`/`bucket`) — `meta.filterOptions` if given, else raw unique values or computed range buckets, sorted per `meta.filterOptionsSort` and decorated with a facet `count` when `showFilterCounts`. Shared by the header filter icon, the inline pinned filter, and the "more filters" pane — all three render the same resolved list the same way, see `renderFilterOptionContent`. */
|
|
439
|
+
function resolveFilterOptions(column, showFilterCounts) {
|
|
440
|
+
const meta = column.columnDef.meta;
|
|
441
|
+
const explicit = meta?.filterOptions;
|
|
442
|
+
let options;
|
|
443
|
+
if (explicit) options = typeof explicit === "function" ? explicit(column) : explicit;
|
|
444
|
+
else if (isBucketFilterVariant(column)) options = computeRangeBuckets(column, resolveBucketSize(column)).map((value) => ({ value }));
|
|
445
|
+
else options = Array.from(column.getFacetedUniqueValues().keys()).map((value) => ({ value: String(value) }));
|
|
446
|
+
const sortMode = meta?.filterOptionsSort ?? "auto";
|
|
447
|
+
if (typeof sortMode === "function") options = [...options].sort(sortMode);
|
|
448
|
+
else if (sortMode === "auto") options = [...options].sort((a, b) => numericAwareCollator.compare(a.textValue ?? (typeof a.label === "string" ? a.label : a.value), b.textValue ?? (typeof b.label === "string" ? b.label : b.value)));
|
|
449
|
+
if (showFilterCounts) {
|
|
450
|
+
const counts = column.getFacetedUniqueValues();
|
|
451
|
+
options = options.map((option) => ({
|
|
452
|
+
...option,
|
|
453
|
+
count: counts.get(option.value)
|
|
454
|
+
}));
|
|
455
|
+
}
|
|
456
|
+
return options;
|
|
457
|
+
}
|
|
458
|
+
/** Whether any option in the list opts into grouping — grouping only activates once at least one option sets `group`; a list with none renders flat. */
|
|
459
|
+
function hasGroupedFilterOptions(options) {
|
|
460
|
+
return options.some((option) => option.group);
|
|
461
|
+
}
|
|
462
|
+
/** Stable-orders `options` into `{ group, options }` buckets, in whichever order each group first appears — used by every surface that renders grouped options the same way. */
|
|
463
|
+
function groupFilterOptions(options) {
|
|
464
|
+
const order = [];
|
|
465
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const option of options) {
|
|
467
|
+
if (!byGroup.has(option.group)) {
|
|
468
|
+
order.push(option.group);
|
|
469
|
+
byGroup.set(option.group, []);
|
|
470
|
+
}
|
|
471
|
+
byGroup.get(option.group).push(option);
|
|
472
|
+
}
|
|
473
|
+
return order.map((group) => ({
|
|
474
|
+
group,
|
|
475
|
+
options: byGroup.get(group)
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
/** The label content shared by every filter surface for one resolved option — icon, label (falling back to the raw value), a muted count when present, and a muted `description` line underneath when given. `size-4` is explicit, not left to each surface's own ambient `[&_svg:not([class*='size-'])]:size-4` ("skip if the class already sets a size") sizing rule — the checklist's plain `Label`+`Checkbox` row has no such rule the way `SelectItem`/`SelectMultipleItem` do, so an unsized icon there falls back to the browser's native (much larger) SVG default instead. */
|
|
479
|
+
function FilterOptionContent({ option }) {
|
|
480
|
+
const Icon = option.icon;
|
|
481
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
482
|
+
className: "flex min-w-0 flex-1 items-center gap-1.5",
|
|
483
|
+
children: [Icon && /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }), /* @__PURE__ */ jsxs("span", {
|
|
484
|
+
className: "flex min-w-0 flex-col",
|
|
485
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
486
|
+
className: "flex items-center gap-1 truncate",
|
|
487
|
+
children: [option.label ?? option.value, option.count != null && /* @__PURE__ */ jsxs("span", {
|
|
488
|
+
className: "text-muted-foreground",
|
|
489
|
+
children: [
|
|
490
|
+
"(",
|
|
491
|
+
option.count,
|
|
492
|
+
")"
|
|
493
|
+
]
|
|
494
|
+
})]
|
|
495
|
+
}), option.description && /* @__PURE__ */ jsx("span", {
|
|
496
|
+
className: "truncate text-xs text-muted-foreground",
|
|
497
|
+
children: option.description
|
|
498
|
+
})]
|
|
499
|
+
})]
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
/** Stand-in for the option list while `meta.filterOptionsLoading` is set — same shape in every surface (checklist, `SelectMultipleContent`, plain `Select`'s content), since DataTable never fetches anything itself and has no real option count to size the skeleton to. */
|
|
503
|
+
function FilterOptionsSkeleton() {
|
|
504
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
505
|
+
className: "flex flex-col gap-1.5 p-1",
|
|
506
|
+
children: [
|
|
507
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }),
|
|
508
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-4/5" }),
|
|
509
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-3/5" })
|
|
510
|
+
]
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
var FILTER_SEARCH_THRESHOLD = 8;
|
|
514
|
+
var FILTER_SELECT_ALL_THRESHOLD = 2;
|
|
515
|
+
function optionSearchText(option) {
|
|
516
|
+
return option.textValue ?? (typeof option.label === "string" ? option.label : option.value);
|
|
517
|
+
}
|
|
518
|
+
/** The always-open checklist `ColumnFilterField`'s `select`/`range` branch renders — its own component so the search/select-all state doesn't leak into the parent's. */
|
|
519
|
+
function FilterOptionChecklist({ options, selected, onValueChange, stickySelectAll }) {
|
|
520
|
+
const [query, setQuery] = React.useState("");
|
|
521
|
+
const showSearch = options.length > FILTER_SEARCH_THRESHOLD;
|
|
522
|
+
const grouped = hasGroupedFilterOptions(options);
|
|
523
|
+
const visibleOptions = showSearch ? options.filter((option) => optionSearchText(option).toLowerCase().includes(query.toLowerCase())) : options;
|
|
524
|
+
const allVisibleSelected = visibleOptions.length > 0 && visibleOptions.every((option) => selected.includes(option.value));
|
|
525
|
+
const someVisibleSelected = visibleOptions.some((option) => selected.includes(option.value));
|
|
526
|
+
function toggleAll() {
|
|
527
|
+
if (allVisibleSelected) {
|
|
528
|
+
const visibleValues = new Set(visibleOptions.map((option) => option.value));
|
|
529
|
+
const next = selected.filter((value) => !visibleValues.has(value));
|
|
530
|
+
onValueChange(next.length ? next : void 0);
|
|
531
|
+
} else onValueChange(Array.from(/* @__PURE__ */ new Set([...selected, ...visibleOptions.map((option) => option.value)])));
|
|
532
|
+
}
|
|
533
|
+
function renderOption(option) {
|
|
534
|
+
return /* @__PURE__ */ jsxs(Label, {
|
|
535
|
+
className: "flex items-center gap-2 font-normal",
|
|
536
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
537
|
+
checked: selected.includes(option.value),
|
|
538
|
+
disabled: option.disabled,
|
|
539
|
+
onCheckedChange: (checked) => {
|
|
540
|
+
const next = checked ? [...selected, option.value] : selected.filter((value) => value !== option.value);
|
|
541
|
+
onValueChange(next.length ? next : void 0);
|
|
542
|
+
}
|
|
543
|
+
}), /* @__PURE__ */ jsx(FilterOptionContent, { option })]
|
|
544
|
+
}, option.value);
|
|
545
|
+
}
|
|
546
|
+
const selectAllRow = options.length > FILTER_SELECT_ALL_THRESHOLD && visibleOptions.length > 0 && /* @__PURE__ */ jsxs(Label, {
|
|
547
|
+
className: "flex items-center gap-2 pb-1.5 font-normal",
|
|
548
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
549
|
+
checked: allVisibleSelected ? true : someVisibleSelected ? "indeterminate" : false,
|
|
550
|
+
onCheckedChange: toggleAll
|
|
551
|
+
}), "Select all"]
|
|
552
|
+
});
|
|
553
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
554
|
+
className: "flex flex-col gap-1.5",
|
|
555
|
+
children: [
|
|
556
|
+
showSearch && /* @__PURE__ */ jsx(Input, {
|
|
557
|
+
placeholder: "Search...",
|
|
558
|
+
value: query,
|
|
559
|
+
onChange: (event) => setQuery(event.target.value)
|
|
560
|
+
}),
|
|
561
|
+
stickySelectAll && selectAllRow,
|
|
562
|
+
/* @__PURE__ */ jsxs("div", {
|
|
563
|
+
className: "flex max-h-64 flex-col gap-1.5 overflow-y-auto",
|
|
564
|
+
children: [
|
|
565
|
+
!stickySelectAll && selectAllRow,
|
|
566
|
+
visibleOptions.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
567
|
+
className: "text-sm text-muted-foreground",
|
|
568
|
+
children: "No values"
|
|
569
|
+
}),
|
|
570
|
+
grouped ? groupFilterOptions(visibleOptions).map(({ group, options }) => /* @__PURE__ */ jsxs("div", { children: [group && /* @__PURE__ */ jsx(Label, {
|
|
571
|
+
className: "mb-1 block text-xs text-muted-foreground",
|
|
572
|
+
children: group
|
|
573
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
574
|
+
className: "flex flex-col gap-1.5",
|
|
575
|
+
children: options.map(renderOption)
|
|
576
|
+
})] }, group ?? "__ungrouped__")) : visibleOptions.map(renderOption)
|
|
577
|
+
]
|
|
578
|
+
})
|
|
579
|
+
]
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* `value`/`onValueChange` are handed in rather than read straight off
|
|
584
|
+
* `column` so this same field can drive either the real, immediately-applied
|
|
585
|
+
* filter state or a popover's own pending draft — see `ColumnFilterTrigger`'s
|
|
586
|
+
* `deferred` prop for the latter. Both still read `column` itself for
|
|
587
|
+
* `filterVariant`/unique values; only the value being edited is decoupled.
|
|
588
|
+
*/
|
|
589
|
+
/** A text filter input whose own displayed value updates every keystroke, while the real filter it drives only catches up 200ms after typing stops (flushed immediately on blur or Enter) — same "instant to type into, debounced to act on" split as the toolbar's global search. */
|
|
590
|
+
function DebouncedFilterInput({ value, onValueChange, placeholder = "Filter..." }) {
|
|
591
|
+
const [inputValue, setInputValue] = React.useState(value);
|
|
592
|
+
const debouncedOnValueChange = useDebouncedCallback(onValueChange, 200);
|
|
593
|
+
React.useEffect(() => {
|
|
594
|
+
setInputValue(value);
|
|
595
|
+
}, [value]);
|
|
596
|
+
return /* @__PURE__ */ jsx(Input, {
|
|
597
|
+
placeholder,
|
|
598
|
+
value: inputValue,
|
|
599
|
+
onChange: (event) => {
|
|
600
|
+
setInputValue(event.target.value);
|
|
601
|
+
debouncedOnValueChange(event.target.value || void 0);
|
|
602
|
+
},
|
|
603
|
+
onBlur: () => onValueChange(inputValue || void 0),
|
|
604
|
+
onKeyDown: (event) => {
|
|
605
|
+
if (event.key === "Enter") onValueChange(inputValue || void 0);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function ColumnFilterField({ column, value, onValueChange, showFilterCounts, stickyFilterSelectAll }) {
|
|
610
|
+
const meta = column.columnDef.meta;
|
|
611
|
+
if (meta?.filterRender) return meta.filterRender({
|
|
612
|
+
column,
|
|
613
|
+
value,
|
|
614
|
+
onValueChange
|
|
615
|
+
});
|
|
616
|
+
if (meta?.filterOptionsLoading) return /* @__PURE__ */ jsx(FilterOptionsSkeleton, {});
|
|
617
|
+
if (meta?.filterVariant === "select" || isBucketFilterVariant(column)) {
|
|
618
|
+
const options = resolveFilterOptions(column, showFilterCounts);
|
|
619
|
+
return /* @__PURE__ */ jsx(FilterOptionChecklist, {
|
|
620
|
+
options,
|
|
621
|
+
selected: value ?? [],
|
|
622
|
+
onValueChange,
|
|
623
|
+
stickySelectAll: stickyFilterSelectAll
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (meta?.filterVariant === "singleSelect") {
|
|
627
|
+
const options = resolveFilterOptions(column, showFilterCounts);
|
|
628
|
+
return /* @__PURE__ */ jsxs(Select, {
|
|
629
|
+
value: value ?? ALL_VALUE,
|
|
630
|
+
onValueChange: (next) => onValueChange(next === ALL_VALUE ? void 0 : next),
|
|
631
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
632
|
+
className: "w-full",
|
|
633
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
634
|
+
}), /* @__PURE__ */ jsxs(SelectContent, { children: [/* @__PURE__ */ jsx(SelectItem, {
|
|
635
|
+
value: ALL_VALUE,
|
|
636
|
+
children: "All"
|
|
637
|
+
}), options.map((option) => /* @__PURE__ */ jsx(SelectItem, {
|
|
638
|
+
value: option.value,
|
|
639
|
+
disabled: option.disabled,
|
|
640
|
+
children: /* @__PURE__ */ jsx(FilterOptionContent, { option })
|
|
641
|
+
}, option.value))] })]
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
return /* @__PURE__ */ jsx(DebouncedFilterInput, {
|
|
645
|
+
value: value ?? "",
|
|
646
|
+
onValueChange
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
/** The search input + select-all/clear row + grouped-or-flat `SelectMultipleItem`s shared by every `SelectMultiple`-based filter surface (inline toolbar chip and pane, for `select`/`range`/`singleSelect` alike) — matches `select.tsx`'s own documented search recipe: hidden via `className`, never unmounted, so a selected-but-filtered-out item keeps its label registered for the trigger's own comma-list. `showSelectAll` is only meaningful for true multi-select (`select`/`range`) — a `singleSelect` list has nothing for "select all" to mean. */
|
|
650
|
+
function SelectMultipleOptionsContent({ options, value, onValueChangeAll, showSelectAll = true, stickySelectAll }) {
|
|
651
|
+
const [query, setQuery] = React.useState("");
|
|
652
|
+
const showSearch = options.length > FILTER_SEARCH_THRESHOLD;
|
|
653
|
+
const grouped = hasGroupedFilterOptions(options);
|
|
654
|
+
const matches = (option) => !showSearch || !query || optionSearchText(option).toLowerCase().includes(query.toLowerCase());
|
|
655
|
+
const visibleOptions = options.filter(matches);
|
|
656
|
+
const allVisibleSelected = visibleOptions.length > 0 && visibleOptions.every((option) => value.includes(option.value));
|
|
657
|
+
const someVisibleSelected = visibleOptions.some((option) => value.includes(option.value));
|
|
658
|
+
function toggleAll() {
|
|
659
|
+
if (allVisibleSelected) {
|
|
660
|
+
const visibleValues = new Set(visibleOptions.map((option) => option.value));
|
|
661
|
+
const next = value.filter((v) => !visibleValues.has(v));
|
|
662
|
+
onValueChangeAll(next.length ? next : void 0);
|
|
663
|
+
} else onValueChangeAll(Array.from(/* @__PURE__ */ new Set([...value, ...visibleOptions.map((option) => option.value)])));
|
|
664
|
+
}
|
|
665
|
+
function renderItem(option) {
|
|
666
|
+
return /* @__PURE__ */ jsx(SelectMultipleItem, {
|
|
667
|
+
value: option.value,
|
|
668
|
+
disabled: option.disabled,
|
|
669
|
+
textValue: option.textValue ?? (typeof option.label === "string" ? option.label : void 0),
|
|
670
|
+
className: cn(!matches(option) && "hidden"),
|
|
671
|
+
children: /* @__PURE__ */ jsx(FilterOptionContent, { option })
|
|
672
|
+
}, option.value);
|
|
673
|
+
}
|
|
674
|
+
const selectAllRow = showSelectAll && options.length > FILTER_SELECT_ALL_THRESHOLD && options.length > 0 && /* @__PURE__ */ jsxs(Label, {
|
|
675
|
+
className: "flex items-center gap-2 p-1.5 font-normal",
|
|
676
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
677
|
+
checked: allVisibleSelected ? true : someVisibleSelected ? "indeterminate" : false,
|
|
678
|
+
onCheckedChange: toggleAll
|
|
679
|
+
}), "Select all"]
|
|
680
|
+
});
|
|
681
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
682
|
+
showSearch && /* @__PURE__ */ jsx("div", {
|
|
683
|
+
className: "p-1 pb-0",
|
|
684
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
685
|
+
placeholder: "Search...",
|
|
686
|
+
value: query,
|
|
687
|
+
onChange: (event) => setQuery(event.target.value)
|
|
688
|
+
})
|
|
689
|
+
}),
|
|
690
|
+
stickySelectAll && selectAllRow,
|
|
691
|
+
/* @__PURE__ */ jsxs("div", {
|
|
692
|
+
className: "max-h-64 overflow-y-auto",
|
|
693
|
+
children: [!stickySelectAll && selectAllRow, grouped ? groupFilterOptions(options).map(({ group, options: groupOptions }) => /* @__PURE__ */ jsxs(SelectMultipleGroup, { children: [group && /* @__PURE__ */ jsx(SelectMultipleLabel, { children: group }), groupOptions.map(renderItem)] }, group ?? "__ungrouped__")) : options.map(renderItem)]
|
|
694
|
+
})
|
|
695
|
+
] });
|
|
696
|
+
}
|
|
697
|
+
/** Same filtering as `ColumnFilterField` (including `meta.filterRender`, checked first), but the select/range/singleSelect variants stay closed by default (a `SelectMultiple`/`Select`) instead of listing every value upfront — the filters pane holds every filterable column at once, so an always-open checklist per column would be too much to scan. `value`/`onValueChange` are the pane's own shared draft under `filterApplyMode="deferred"` (see the pane's footer), or the real filter directly otherwise — same dual-mode shape `ColumnFilterField` already uses. */
|
|
698
|
+
/**
|
|
699
|
+
* Same filter shapes as `ColumnFilterField`, but self-contained rather
|
|
700
|
+
* than taking `value`/`onValueChange` from a caller — the pane has no
|
|
701
|
+
* shared batch draft or footer of its own (just "Clear filters", which
|
|
702
|
+
* clears the real, already-applied filters). A multiselect field
|
|
703
|
+
* (`select`/`range`) is still always deferred, same as everywhere else,
|
|
704
|
+
* but that's *this* field's own popover — its own draft, its own
|
|
705
|
+
* Cancel/Apply footer inside it — exactly like the inline toolbar's
|
|
706
|
+
* identical field, not a pane-wide batch.
|
|
707
|
+
*/
|
|
708
|
+
/** The pane's `filterRender` field under a deferred apply mode — a local draft plus an inline Cancel/Apply row, since `filterRender` in the pane isn't wrapped in a popover of its own the way every other variant here is (it just renders directly, always visible), so there's no popover footer to hang Cancel/Apply off of the way `SelectMultipleFooter`/`FilterPopoverFooter` do elsewhere. */
|
|
709
|
+
function DeferredPaneFilterRender({ column, filterRender }) {
|
|
710
|
+
const committedValue = column.getFilterValue();
|
|
711
|
+
const [pendingValue, setPendingValue] = React.useState(committedValue);
|
|
712
|
+
React.useEffect(() => {
|
|
713
|
+
setPendingValue(committedValue);
|
|
714
|
+
}, [committedValue]);
|
|
715
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
716
|
+
className: "flex flex-col gap-2",
|
|
717
|
+
children: [filterRender({
|
|
718
|
+
column,
|
|
719
|
+
value: pendingValue,
|
|
720
|
+
onValueChange: setPendingValue
|
|
721
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
722
|
+
className: "flex items-center justify-end gap-1",
|
|
723
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
724
|
+
type: "button",
|
|
725
|
+
variant: "ghost",
|
|
726
|
+
size: "xs",
|
|
727
|
+
onClick: () => setPendingValue(committedValue),
|
|
728
|
+
children: "Cancel"
|
|
729
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
730
|
+
type: "button",
|
|
731
|
+
size: "xs",
|
|
732
|
+
disabled: filterDraftUnchanged(committedValue, pendingValue),
|
|
733
|
+
onClick: () => column.setFilterValue(pendingValue),
|
|
734
|
+
children: "Apply"
|
|
735
|
+
})]
|
|
736
|
+
})]
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
function PaneColumnFilterField({ column, showFilterCounts, tableDeferred, stickyFilterSelectAll }) {
|
|
740
|
+
const meta = column.columnDef.meta;
|
|
741
|
+
const [open, setOpenState] = React.useState(false);
|
|
742
|
+
const [pendingValue, setPendingValue] = React.useState(void 0);
|
|
743
|
+
const setOpen = (value) => {
|
|
744
|
+
if (value) setPendingValue(column.getFilterValue());
|
|
745
|
+
setOpenState(value);
|
|
746
|
+
};
|
|
747
|
+
if (meta?.filterRender) {
|
|
748
|
+
if (isColumnDeferred(column, !!tableDeferred)) return /* @__PURE__ */ jsx(DeferredPaneFilterRender, {
|
|
749
|
+
column,
|
|
750
|
+
filterRender: meta.filterRender
|
|
751
|
+
});
|
|
752
|
+
return meta.filterRender({
|
|
753
|
+
column,
|
|
754
|
+
value: column.getFilterValue(),
|
|
755
|
+
onValueChange: (value) => column.setFilterValue(value)
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
if (meta?.filterOptionsLoading) return /* @__PURE__ */ jsx(FilterOptionsSkeleton, {});
|
|
759
|
+
if (meta?.filterVariant === "select" || isBucketFilterVariant(column)) {
|
|
760
|
+
const options = resolveFilterOptions(column, showFilterCounts);
|
|
761
|
+
const selected = open ? pendingValue ?? [] : column.getFilterValue() ?? [];
|
|
762
|
+
return /* @__PURE__ */ jsxs(SelectMultiple, {
|
|
763
|
+
open,
|
|
764
|
+
onOpenChange: setOpen,
|
|
765
|
+
value: selected,
|
|
766
|
+
onValueChange: (next) => setPendingValue(next.length ? next : void 0),
|
|
767
|
+
children: [/* @__PURE__ */ jsx(SelectMultipleTrigger, {
|
|
768
|
+
placeholder: "All",
|
|
769
|
+
className: "w-full"
|
|
770
|
+
}), /* @__PURE__ */ jsxs(SelectMultipleContent, { children: [/* @__PURE__ */ jsx(SelectMultipleOptionsContent, {
|
|
771
|
+
options,
|
|
772
|
+
value: selected,
|
|
773
|
+
onValueChangeAll: setPendingValue,
|
|
774
|
+
stickySelectAll: stickyFilterSelectAll
|
|
775
|
+
}), /* @__PURE__ */ jsxs(SelectMultipleFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
776
|
+
type: "button",
|
|
777
|
+
variant: "ghost",
|
|
778
|
+
size: "xs",
|
|
779
|
+
onClick: () => setOpen(false),
|
|
780
|
+
children: "Cancel"
|
|
781
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
782
|
+
type: "button",
|
|
783
|
+
size: "xs",
|
|
784
|
+
disabled: filterDraftUnchanged(column.getFilterValue(), pendingValue),
|
|
785
|
+
onClick: () => {
|
|
786
|
+
column.setFilterValue(pendingValue);
|
|
787
|
+
setOpen(false);
|
|
788
|
+
},
|
|
789
|
+
children: "Apply"
|
|
790
|
+
})] })] })]
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
if (meta?.filterVariant === "singleSelect") {
|
|
794
|
+
const options = resolveFilterOptions(column, showFilterCounts);
|
|
795
|
+
const selected = column.getFilterValue() ?? "";
|
|
796
|
+
return /* @__PURE__ */ jsxs(SelectMultiple, {
|
|
797
|
+
closeOnSelect: true,
|
|
798
|
+
value: selected ? [selected] : [],
|
|
799
|
+
onValueChange: (next) => column.setFilterValue(next.at(-1)),
|
|
800
|
+
children: [/* @__PURE__ */ jsx(SelectMultipleTrigger, {
|
|
801
|
+
placeholder: "All",
|
|
802
|
+
className: "w-full"
|
|
803
|
+
}), /* @__PURE__ */ jsx(SelectMultipleContent, { children: /* @__PURE__ */ jsx(SelectMultipleOptionsContent, {
|
|
804
|
+
options,
|
|
805
|
+
value: selected ? [selected] : [],
|
|
806
|
+
onValueChangeAll: () => {},
|
|
807
|
+
showSelectAll: false,
|
|
808
|
+
stickySelectAll: stickyFilterSelectAll
|
|
809
|
+
}) })]
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
return /* @__PURE__ */ jsx(DebouncedFilterInput, {
|
|
813
|
+
value: column.getFilterValue() ?? "",
|
|
814
|
+
onValueChange: (value) => column.setFilterValue(value)
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
var warnedMissingLabelIds = /* @__PURE__ */ new Set();
|
|
818
|
+
function columnLabel(column) {
|
|
819
|
+
if (column.columnDef.meta?.label) return column.columnDef.meta.label;
|
|
820
|
+
if (typeof column.columnDef.header === "string") return column.columnDef.header;
|
|
821
|
+
if (process.env.NODE_ENV !== "production" && !warnedMissingLabelIds.has(column.id)) {
|
|
822
|
+
warnedMissingLabelIds.add(column.id);
|
|
823
|
+
console.warn(`DataTable: column "${column.id}" has a non-string \`header\` (an icon, a custom component, ...) and no \`meta.label\` — every place that needs this column's *name* rather than its rendered header (the Columns menu, the "All filters" pane, tooltips, applied-filter/chip copy) falls back to its raw \`id\` instead. Set \`meta.label\` to fix this.`);
|
|
824
|
+
}
|
|
825
|
+
return column.id;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Bulk-actions bar shown while any row is selected — hovers over the
|
|
829
|
+
* bottom edge of the table's own viewport, never the screen: `sticky`
|
|
830
|
+
* inside `tableContainerRef`, which is unconditionally a genuine scroll
|
|
831
|
+
* container (its own `overflow-auto`) by the time this renders. `sticky`
|
|
832
|
+
* (not `absolute`) is what actually matters here:
|
|
833
|
+
* `absolute` positions against the *content* box, so it scrolls away
|
|
834
|
+
* with the rows above it instead of staying put — `sticky` is what
|
|
835
|
+
* tracks the scroll *viewport* itself. Since `tableContainerRef` is the
|
|
836
|
+
* nearest scrolling ancestor either way, this never hits the Card
|
|
837
|
+
* `overflow-hidden` problem `sticky` usually has (that only happens when
|
|
838
|
+
* `sticky` has to bubble past a non-scrolling container to find one).
|
|
839
|
+
* Either way it's a child of `tableContainerRef`, not a portal, so it's
|
|
840
|
+
* bounded by the table's own box, not the viewport.
|
|
841
|
+
*/
|
|
842
|
+
var BulkActionsBar = React.forwardRef(function BulkActionsBar({ count, hiddenByFilterCount = 0, totalCount, isAllSelected, maxSelectedRows, atSelectionCap = false, onSelectAll, onClear, clearLabel = "Clear selection", controlSize, controlIconSize, children }, ref) {
|
|
843
|
+
return /* @__PURE__ */ jsx("div", {
|
|
844
|
+
ref,
|
|
845
|
+
className: "pointer-events-none sticky bottom-2 z-10 flex justify-center",
|
|
846
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
847
|
+
className: "pointer-events-auto flex items-center gap-1 rounded-full border border-border bg-popover p-1.5 shadow-popover",
|
|
848
|
+
children: [
|
|
849
|
+
/* @__PURE__ */ jsx("div", {
|
|
850
|
+
className: "flex items-center gap-1.5 px-2",
|
|
851
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
852
|
+
className: "text-sm text-muted-foreground",
|
|
853
|
+
children: isAllSelected ? `All ${totalCount} selected` : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
854
|
+
count,
|
|
855
|
+
" ",
|
|
856
|
+
maxSelectedRows != null ? `of ${maxSelectedRows} max selected` : "selected",
|
|
857
|
+
hiddenByFilterCount > 0 && ` (${hiddenByFilterCount} hidden by filters)`
|
|
858
|
+
] })
|
|
859
|
+
})
|
|
860
|
+
}),
|
|
861
|
+
/* @__PURE__ */ jsx(Button, {
|
|
862
|
+
type: "button",
|
|
863
|
+
variant: "ghost",
|
|
864
|
+
size: controlSize,
|
|
865
|
+
disabled: !isAllSelected && atSelectionCap,
|
|
866
|
+
title: !isAllSelected && atSelectionCap ? `Up to ${maxSelectedRows} rows can be selected` : void 0,
|
|
867
|
+
onClick: isAllSelected ? onClear : onSelectAll,
|
|
868
|
+
children: isAllSelected ? "Clear selection" : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
869
|
+
/* @__PURE__ */ jsx(CheckCheckIcon, {}),
|
|
870
|
+
"Select all ",
|
|
871
|
+
totalCount
|
|
872
|
+
] })
|
|
873
|
+
}),
|
|
874
|
+
children && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", { className: "h-5 w-px shrink-0 bg-border" }), /* @__PURE__ */ jsx("div", {
|
|
875
|
+
className: "flex items-center gap-1",
|
|
876
|
+
children
|
|
877
|
+
})] }),
|
|
878
|
+
/* @__PURE__ */ jsx("div", { className: "h-5 w-px shrink-0 bg-border" }),
|
|
879
|
+
/* @__PURE__ */ jsx(Button, {
|
|
880
|
+
type: "button",
|
|
881
|
+
variant: "ghost",
|
|
882
|
+
size: controlIconSize,
|
|
883
|
+
iconOnly: true,
|
|
884
|
+
"aria-label": clearLabel,
|
|
885
|
+
onClick: onClear,
|
|
886
|
+
children: /* @__PURE__ */ jsx(XIcon, {})
|
|
887
|
+
})
|
|
888
|
+
]
|
|
889
|
+
})
|
|
890
|
+
});
|
|
891
|
+
});
|
|
892
|
+
/** Sticky offset for a pinned column — `undefined` (no inline style) when the column isn't pinned. */
|
|
893
|
+
function getPinnedStyle(column) {
|
|
894
|
+
const pinned = column.getIsPinned();
|
|
895
|
+
if (!pinned) return void 0;
|
|
896
|
+
return pinned === "left" ? {
|
|
897
|
+
position: "sticky",
|
|
898
|
+
left: column.getStart("left")
|
|
899
|
+
} : {
|
|
900
|
+
position: "sticky",
|
|
901
|
+
right: column.getAfter("right")
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Solid `bg-card` (the table's own real white surface, not `bg-background`
|
|
906
|
+
* — that token is the page canvas gray, which read as a mismatched "grey
|
|
907
|
+
* bg" against the rest of the table) so scrolled content doesn't show
|
|
908
|
+
* through underneath, plus a thicker boundary divider on the outermost
|
|
909
|
+
* pinned column of each side — on its right edge for the left-pinned
|
|
910
|
+
* group, its left edge for the right-pinned group, i.e. towards whichever
|
|
911
|
+
* side actually borders the scrollable/center columns. `bg-card` is opaque
|
|
912
|
+
* and painted directly on this cell, which would otherwise hide this row's
|
|
913
|
+
* own hover/selected tint (both painted on the `<tr>`, a layer down) for
|
|
914
|
+
* just this one column — `group-hover/row`/`group-data-[state=selected]/row`
|
|
915
|
+
* (reacting to `TableRow`'s own `group/row`, see table.tsx) repaints the
|
|
916
|
+
* same tint directly on this cell instead of relying on it showing through.
|
|
917
|
+
*/
|
|
918
|
+
function getPinnedClassName(column, table) {
|
|
919
|
+
const pinned = column.getIsPinned();
|
|
920
|
+
if (!pinned) return "";
|
|
921
|
+
const visibleLeft = table.getLeftLeafColumns().filter((c) => c.getIsVisible());
|
|
922
|
+
const visibleRight = table.getRightLeafColumns().filter((c) => c.getIsVisible());
|
|
923
|
+
const isLastLeft = pinned === "left" && visibleLeft.at(-1)?.id === column.id;
|
|
924
|
+
const isFirstRight = pinned === "right" && visibleRight[0]?.id === column.id;
|
|
925
|
+
return cn("z-10 bg-card", "group-hover/row:bg-[color-mix(in_oklab,var(--color-muted)_50%,var(--color-card))]", "group-data-[state=selected]/row:bg-[color-mix(in_oklab,var(--color-muted)_100%,var(--color-card))]", isLastLeft && "border-r-2 border-border", isFirstRight && "border-l-2 border-border");
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* The "scroll fade" cue — an inset shadow indicating there's scrollable
|
|
929
|
+
* content sliding in underneath a pinned column — lives on the *scrolling*
|
|
930
|
+
* side of the pinned boundary (the first/last center column next to it),
|
|
931
|
+
* not on the pinned column itself: the fade represents content that's
|
|
932
|
+
* about to disappear under something fixed, which is a property of the
|
|
933
|
+
* content moving, not of the fixed thing it's moving under. Only rendered
|
|
934
|
+
* once `canScroll` is true (the table actually overflows horizontally) —
|
|
935
|
+
* a shadow implying "more content behind this edge" would be misleading
|
|
936
|
+
* otherwise.
|
|
937
|
+
*/
|
|
938
|
+
function getScrollFadeClassName(column, table, canScroll) {
|
|
939
|
+
if (!canScroll || column.getIsPinned()) return "";
|
|
940
|
+
const visibleLeft = table.getLeftLeafColumns().filter((c) => c.getIsVisible());
|
|
941
|
+
const visibleRight = table.getRightLeafColumns().filter((c) => c.getIsVisible());
|
|
942
|
+
const visibleCenter = table.getCenterLeafColumns().filter((c) => c.getIsVisible());
|
|
943
|
+
const isFirstCenter = visibleLeft.length > 0 && visibleCenter[0]?.id === column.id;
|
|
944
|
+
const isLastCenter = visibleRight.length > 0 && visibleCenter.at(-1)?.id === column.id;
|
|
945
|
+
return cn(isFirstCenter && "shadow-[inset_8px_0_8px_-8px_rgba(0,0,0,0.15)]", isLastCenter && "shadow-[inset_-8px_0_8px_-8px_rgba(0,0,0,0.15)]");
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Cancel/Apply footer for a plain-`Popover`-based filter field (see
|
|
949
|
+
* `ColumnFilterTrigger` and `InlineColumnFilter`'s own fallback branch) —
|
|
950
|
+
* picks edit a local draft only, never the real filter; Apply commits that
|
|
951
|
+
* draft, Cancel just closes and discards it, leaving the real filter
|
|
952
|
+
* exactly as it was before the popover opened.
|
|
953
|
+
*/
|
|
954
|
+
function FilterPopoverFooter({ onCancel, onApply, applyDisabled }) {
|
|
955
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
956
|
+
className: "-mx-2.5 mt-2 flex w-[calc(100%+--spacing(5))] items-center justify-end gap-1 border-t border-border px-2.5 pt-2",
|
|
957
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
958
|
+
type: "button",
|
|
959
|
+
variant: "ghost",
|
|
960
|
+
size: "xs",
|
|
961
|
+
onClick: onCancel,
|
|
962
|
+
children: "Cancel"
|
|
963
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
964
|
+
type: "button",
|
|
965
|
+
size: "xs",
|
|
966
|
+
disabled: applyDisabled,
|
|
967
|
+
onClick: onApply,
|
|
968
|
+
children: "Apply"
|
|
969
|
+
})]
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* A multiselect filter (`filterVariant: "select"` or `"range"` — either
|
|
974
|
+
* way, a checklist of several values at once) always needs a deliberate
|
|
975
|
+
* Apply, in every surface, regardless of `filterApplyMode` — picking one
|
|
976
|
+
* value out of a list of many isn't a complete thought the way a single
|
|
977
|
+
* text edit or a `singleSelect` pick already is; the whole point of
|
|
978
|
+
* checking several boxes is to review the set before it takes effect.
|
|
979
|
+
* `filterApplyMode="deferred"` still separately opts every *other* filter
|
|
980
|
+
* shape into the same Cancel/Apply step.
|
|
981
|
+
*/
|
|
982
|
+
/** True for every filter shape with more than one field/pick to review before it's a complete thought — `select`/`range`/`bucket` checklists. */
|
|
983
|
+
function isMultiSelectFilterVariant(column) {
|
|
984
|
+
const variant = column.columnDef.meta?.filterVariant;
|
|
985
|
+
return variant === "select" || variant === "range" || variant === "bucket";
|
|
986
|
+
}
|
|
987
|
+
/** `singleSelect` commits on a single click/pick — there's never a moment an open draft exists to defer in the first place, so it can never be put into deferred mode, the same way a multiselect variant can never be taken *out* of it (see `isMultiSelectFilterVariant`). */
|
|
988
|
+
function isNeverDeferredFilterVariant(column) {
|
|
989
|
+
return column.columnDef.meta?.filterVariant === "singleSelect";
|
|
990
|
+
}
|
|
991
|
+
/** Whether this column's filter should defer to an explicit Apply — `meta.filterApplyMode` overrides the table-wide `filterApplyMode` when set, but neither can turn a multiselect (`select`/`range`/`bucket`) *off* deferred, or a `singleSelect` *into* it. */
|
|
992
|
+
function isColumnDeferred(column, tableDeferred) {
|
|
993
|
+
const columnMode = column.columnDef.meta?.filterApplyMode;
|
|
994
|
+
if (column.columnDef.meta?.filterRender) return columnMode === "deferred" || !columnMode && tableDeferred;
|
|
995
|
+
if (isNeverDeferredFilterVariant(column)) return false;
|
|
996
|
+
return (columnMode ? columnMode === "deferred" : tableDeferred) || isMultiSelectFilterVariant(column);
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Whether a filter draft actually differs from what's currently applied —
|
|
1000
|
+
* gates every deferred filter surface's Apply button so it's disabled
|
|
1001
|
+
* (not just harmless) when clicking it would be a no-op. Filter values
|
|
1002
|
+
* here are always plain JSON-serializable data (strings, string arrays,
|
|
1003
|
+
* small plain objects from a custom `filterRender`), so a `JSON.stringify`
|
|
1004
|
+
* comparison is a legitimate deep-equality check, not a shortcut.
|
|
1005
|
+
*/
|
|
1006
|
+
function filterDraftUnchanged(committed, pending) {
|
|
1007
|
+
return JSON.stringify(committed ?? null) === JSON.stringify(pending ?? null);
|
|
1008
|
+
}
|
|
1009
|
+
/** Delays invoking `callback` until `delayMs` have passed with no new call — the returned function is stable across renders (safe in a dep array), always calling whatever `callback` currently is by the time the delay elapses, not whatever it was when the delay started. */
|
|
1010
|
+
function useDebouncedCallback(callback, delayMs) {
|
|
1011
|
+
const callbackRef = React.useRef(callback);
|
|
1012
|
+
React.useEffect(() => {
|
|
1013
|
+
callbackRef.current = callback;
|
|
1014
|
+
});
|
|
1015
|
+
const timeoutRef = React.useRef(void 0);
|
|
1016
|
+
React.useEffect(() => () => clearTimeout(timeoutRef.current), []);
|
|
1017
|
+
return React.useCallback((...args) => {
|
|
1018
|
+
clearTimeout(timeoutRef.current);
|
|
1019
|
+
timeoutRef.current = setTimeout(() => callbackRef.current(...args), delayMs);
|
|
1020
|
+
}, [delayMs]);
|
|
1021
|
+
}
|
|
1022
|
+
/** Resolves `column`'s own `meta.filterMergeWith` ids against `table`, dropping any that don't resolve to a real column — the list `ColumnFilterTrigger` actually renders a field for, `column` itself always first. */
|
|
1023
|
+
function mergedFilterColumns(column, table) {
|
|
1024
|
+
return [column, ...(column.columnDef.meta?.filterMergeWith ?? []).map((id) => table.getColumn(id)).filter((c) => !!c)];
|
|
1025
|
+
}
|
|
1026
|
+
function ColumnFilterTrigger({ column, table, revealClassName, onOpenChange, tableDeferred, showFilterCounts, stickyFilterSelectAll }) {
|
|
1027
|
+
const [open, setOpenState] = React.useState(false);
|
|
1028
|
+
const [pendingValues, setPendingValues] = React.useState({});
|
|
1029
|
+
const mergedColumns = mergedFilterColumns(column, table);
|
|
1030
|
+
const deferred = mergedColumns.some((c) => isColumnDeferred(c, !!tableDeferred));
|
|
1031
|
+
const setOpen = (value) => {
|
|
1032
|
+
if (value) setPendingValues(Object.fromEntries(mergedColumns.map((c) => [c.id, c.getFilterValue()])));
|
|
1033
|
+
setOpenState(value);
|
|
1034
|
+
onOpenChange?.(value);
|
|
1035
|
+
};
|
|
1036
|
+
const label = columnLabel(column);
|
|
1037
|
+
const filtered = mergedColumns.some((c) => c.getIsFiltered());
|
|
1038
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1039
|
+
"data-fit-exclude": true,
|
|
1040
|
+
draggable: false,
|
|
1041
|
+
className: cn("shrink-0", filtered || open ? "opacity-100" : revealClassName),
|
|
1042
|
+
children: /* @__PURE__ */ jsxs(Popover, {
|
|
1043
|
+
open,
|
|
1044
|
+
onOpenChange: setOpen,
|
|
1045
|
+
children: [/* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
1046
|
+
asChild: true,
|
|
1047
|
+
children: /* @__PURE__ */ jsx(PopoverTrigger, {
|
|
1048
|
+
asChild: true,
|
|
1049
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
1050
|
+
type: "button",
|
|
1051
|
+
variant: "ghost",
|
|
1052
|
+
size: "xs",
|
|
1053
|
+
iconOnly: true,
|
|
1054
|
+
"aria-label": `Filter ${label}`,
|
|
1055
|
+
className: cn("shrink-0", filtered && "text-primary"),
|
|
1056
|
+
children: /* @__PURE__ */ jsx(ListFilterIcon, {})
|
|
1057
|
+
})
|
|
1058
|
+
})
|
|
1059
|
+
}), /* @__PURE__ */ jsx(TooltipContent, { children: `Filter ${label}` })] }), /* @__PURE__ */ jsxs(PopoverContent, {
|
|
1060
|
+
align: "start",
|
|
1061
|
+
className: "max-w-(--radix-popover-content-available-width) min-w-56",
|
|
1062
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1063
|
+
className: "flex flex-col gap-3",
|
|
1064
|
+
children: mergedColumns.map((c, index) => /* @__PURE__ */ jsxs("div", {
|
|
1065
|
+
className: cn(index > 0 && "border-t border-border pt-3"),
|
|
1066
|
+
children: [mergedColumns.length > 1 && /* @__PURE__ */ jsx(Label, {
|
|
1067
|
+
className: "mb-1.5 block text-xs text-muted-foreground",
|
|
1068
|
+
children: columnLabel(c)
|
|
1069
|
+
}), /* @__PURE__ */ jsx(ColumnFilterField, {
|
|
1070
|
+
column: c,
|
|
1071
|
+
value: deferred ? pendingValues[c.id] : c.getFilterValue(),
|
|
1072
|
+
onValueChange: (value) => deferred ? setPendingValues((prev) => ({
|
|
1073
|
+
...prev,
|
|
1074
|
+
[c.id]: value
|
|
1075
|
+
})) : c.setFilterValue(value),
|
|
1076
|
+
showFilterCounts,
|
|
1077
|
+
stickyFilterSelectAll
|
|
1078
|
+
})]
|
|
1079
|
+
}, c.id))
|
|
1080
|
+
}), deferred && /* @__PURE__ */ jsx(FilterPopoverFooter, {
|
|
1081
|
+
onCancel: () => setOpen(false),
|
|
1082
|
+
onApply: () => {
|
|
1083
|
+
mergedColumns.forEach((c) => c.setFilterValue(pendingValues[c.id]));
|
|
1084
|
+
setOpen(false);
|
|
1085
|
+
},
|
|
1086
|
+
applyDisabled: mergedColumns.every((c) => filterDraftUnchanged(c.getFilterValue(), pendingValues[c.id]))
|
|
1087
|
+
})]
|
|
1088
|
+
})]
|
|
1089
|
+
})
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
/** A pinned/always-visible filter, shown as a labeled button in the toolbar next to search. */
|
|
1093
|
+
function InlineColumnFilter({ column, size, tableDeferred, showFilterCounts, stickyFilterSelectAll }) {
|
|
1094
|
+
const label = columnLabel(column);
|
|
1095
|
+
const filterVariant = column.columnDef.meta?.filterVariant;
|
|
1096
|
+
const deferred = isColumnDeferred(column, !!tableDeferred);
|
|
1097
|
+
const [open, setOpenState] = React.useState(false);
|
|
1098
|
+
const [pendingValue, setPendingValue] = React.useState(void 0);
|
|
1099
|
+
const setOpen = (value) => {
|
|
1100
|
+
if (value) setPendingValue(column.getFilterValue());
|
|
1101
|
+
setOpenState(value);
|
|
1102
|
+
};
|
|
1103
|
+
if (column.columnDef.meta?.filterRender && !deferred) return column.columnDef.meta.filterRender({
|
|
1104
|
+
column,
|
|
1105
|
+
value: column.getFilterValue(),
|
|
1106
|
+
onValueChange: (value) => column.setFilterValue(value)
|
|
1107
|
+
});
|
|
1108
|
+
if (column.columnDef.meta?.filterOptionsLoading) return /* @__PURE__ */ jsx(FilterOptionsSkeleton, {});
|
|
1109
|
+
if (filterVariant === "select" || isBucketFilterVariant(column)) {
|
|
1110
|
+
const options = resolveFilterOptions(column, showFilterCounts);
|
|
1111
|
+
const committed = column.getFilterValue() ?? [];
|
|
1112
|
+
const effectiveDeferred = deferred && open;
|
|
1113
|
+
const selected = effectiveDeferred ? pendingValue ?? [] : committed;
|
|
1114
|
+
return /* @__PURE__ */ jsxs(SelectMultiple, {
|
|
1115
|
+
size,
|
|
1116
|
+
value: selected,
|
|
1117
|
+
onValueChange: (next) => {
|
|
1118
|
+
const value = next.length ? next : void 0;
|
|
1119
|
+
if (effectiveDeferred) setPendingValue(value);
|
|
1120
|
+
else column.setFilterValue(value);
|
|
1121
|
+
},
|
|
1122
|
+
...deferred ? {
|
|
1123
|
+
open,
|
|
1124
|
+
onOpenChange: setOpen
|
|
1125
|
+
} : {},
|
|
1126
|
+
children: [/* @__PURE__ */ jsx(SelectMultipleTrigger, {
|
|
1127
|
+
placeholder: label,
|
|
1128
|
+
inlineLabel: label,
|
|
1129
|
+
maxLabelChars: 12
|
|
1130
|
+
}), /* @__PURE__ */ jsxs(SelectMultipleContent, { children: [/* @__PURE__ */ jsx(SelectMultipleOptionsContent, {
|
|
1131
|
+
options,
|
|
1132
|
+
value: selected,
|
|
1133
|
+
onValueChangeAll: (next) => {
|
|
1134
|
+
if (effectiveDeferred) setPendingValue(next);
|
|
1135
|
+
else column.setFilterValue(next);
|
|
1136
|
+
},
|
|
1137
|
+
stickySelectAll: stickyFilterSelectAll
|
|
1138
|
+
}), deferred && /* @__PURE__ */ jsxs(SelectMultipleFooter, { children: [/* @__PURE__ */ jsx(Button, {
|
|
1139
|
+
type: "button",
|
|
1140
|
+
variant: "ghost",
|
|
1141
|
+
size: "xs",
|
|
1142
|
+
onClick: () => setOpen(false),
|
|
1143
|
+
children: "Cancel"
|
|
1144
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1145
|
+
type: "button",
|
|
1146
|
+
size: "xs",
|
|
1147
|
+
disabled: filterDraftUnchanged(column.getFilterValue(), pendingValue),
|
|
1148
|
+
onClick: () => {
|
|
1149
|
+
column.setFilterValue(pendingValue);
|
|
1150
|
+
setOpen(false);
|
|
1151
|
+
},
|
|
1152
|
+
children: "Apply"
|
|
1153
|
+
})] })] })]
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
if (filterVariant === "singleSelect") {
|
|
1157
|
+
const options = resolveFilterOptions(column, showFilterCounts);
|
|
1158
|
+
const selected = column.getFilterValue() ?? "";
|
|
1159
|
+
return /* @__PURE__ */ jsxs(SelectMultiple, {
|
|
1160
|
+
size,
|
|
1161
|
+
closeOnSelect: true,
|
|
1162
|
+
value: selected ? [selected] : [],
|
|
1163
|
+
onValueChange: (next) => column.setFilterValue(next.at(-1)),
|
|
1164
|
+
children: [/* @__PURE__ */ jsx(SelectMultipleTrigger, {
|
|
1165
|
+
placeholder: label,
|
|
1166
|
+
inlineLabel: label
|
|
1167
|
+
}), /* @__PURE__ */ jsx(SelectMultipleContent, { children: /* @__PURE__ */ jsx(SelectMultipleOptionsContent, {
|
|
1168
|
+
options,
|
|
1169
|
+
value: selected ? [selected] : [],
|
|
1170
|
+
onValueChangeAll: () => {},
|
|
1171
|
+
showSelectAll: false,
|
|
1172
|
+
stickySelectAll: stickyFilterSelectAll
|
|
1173
|
+
}) })]
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
return /* @__PURE__ */ jsxs(Popover, {
|
|
1177
|
+
open: deferred ? open : void 0,
|
|
1178
|
+
onOpenChange: deferred ? setOpen : void 0,
|
|
1179
|
+
children: [/* @__PURE__ */ jsx(PopoverTrigger, {
|
|
1180
|
+
asChild: true,
|
|
1181
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
1182
|
+
type: "button",
|
|
1183
|
+
variant: "outline",
|
|
1184
|
+
size,
|
|
1185
|
+
className: cn(column.getIsFiltered() && "border-primary text-primary"),
|
|
1186
|
+
children: [label, /* @__PURE__ */ jsx(ChevronDownIcon, {
|
|
1187
|
+
"data-icon": "inline-end",
|
|
1188
|
+
className: "opacity-50"
|
|
1189
|
+
})]
|
|
1190
|
+
})
|
|
1191
|
+
}), /* @__PURE__ */ jsxs(PopoverContent, {
|
|
1192
|
+
align: "start",
|
|
1193
|
+
className: "max-w-(--radix-popover-content-available-width) min-w-56",
|
|
1194
|
+
children: [/* @__PURE__ */ jsx(ColumnFilterField, {
|
|
1195
|
+
column,
|
|
1196
|
+
value: deferred ? pendingValue : column.getFilterValue(),
|
|
1197
|
+
onValueChange: deferred ? setPendingValue : (value) => column.setFilterValue(value),
|
|
1198
|
+
showFilterCounts,
|
|
1199
|
+
stickyFilterSelectAll
|
|
1200
|
+
}), deferred && /* @__PURE__ */ jsx(FilterPopoverFooter, {
|
|
1201
|
+
onCancel: () => setOpen(false),
|
|
1202
|
+
onApply: () => {
|
|
1203
|
+
column.setFilterValue(pendingValue);
|
|
1204
|
+
setOpen(false);
|
|
1205
|
+
},
|
|
1206
|
+
applyDisabled: filterDraftUnchanged(column.getFilterValue(), pendingValue)
|
|
1207
|
+
})]
|
|
1208
|
+
})]
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
function DataTable({ columns: userColumns, data, loading = false, error, errorDescription, emptyTitle = "No results", emptyDescription = "Try adjusting your search.", searchPlaceholder = "Search...", searchableColumnIds, searchMatch = "contains", filterApplyMode = "immediate", defaultSorting, defaultColumnFilters, defaultGlobalFilter, defaultPageIndex, defaultColumnVisibility, defaultColumnOrder, defaultColumnPinning, defaultColumnSizing, defaultExpanded, onExpandedChange, tableRef, onColumnLayoutChange, defaultPinnedFilterIds, allowFilterPinning = true, showFilterCounts = false, stickyFilterSelectAll = false, clearFiltersOnHide = false, enableRowSelection = false, selectionScope = "filtered", maxSelectedRows, defaultRowSelection, selectionBarPlacement, onRowSelectionChange, onStateChange, renderBulkActions, onRowClick, renderExpandedRow, getRowId, lockedColumnIds = [], enableColumnResizing = false, enableMultiSort = false, revealColumnControlsOnHover = true, striped = false, cellDividers = false, aiAction, actionButtons, aiRowPredicate, aiColumnIds, aiCellPredicate, pageSize = 10, pageSizeOptions = [
|
|
1212
|
+
10,
|
|
1213
|
+
20,
|
|
1214
|
+
30
|
|
1215
|
+
], showPageSizeSelector = true, paginationStyle = "compact", onLoadMore, loadMoreOffset = 200, hasMore = false, showColumnToggle = true, loadingMore = false, maxHeight, stickyHeader = true, caption, captionHidden = true, size = "default", className, "aria-label": ariaLabel }) {
|
|
1216
|
+
React.useEffect(() => {
|
|
1217
|
+
if (process.env.NODE_ENV === "production") return;
|
|
1218
|
+
if (actionButtons && actionButtons.length > 3) console.warn(`DataTable: \`actionButtons\` has ${actionButtons.length} entries, but only the first 3 render in the toolbar — the rest are silently dropped. Move the overflow into a dropdown of your own, or trim the list.`);
|
|
1219
|
+
if (typeof error !== "boolean" && error != null) console.warn("DataTable: passing a string/node to `error` is deprecated — put that copy in `errorDescription` instead and pass `error={true}`. `error` will stop accepting anything but a boolean in a future version.");
|
|
1220
|
+
if (!ariaLabel && !caption) console.warn("DataTable: no `aria-label` or `caption` provided — the table has no accessible name, so assistive tech has nothing to announce it by. Pass one describing what the table shows.");
|
|
1221
|
+
if (userColumns.some((col) => col.meta?.filterVariant === "range")) console.warn("DataTable: `filterVariant: \"range\"` is deprecated — use `\"bucket\"` instead (same behavior, clearer name). `\"range\"` will stop being accepted in a future version.");
|
|
1222
|
+
if (userColumns.some((col) => col.meta?.rangeBucketSize != null)) console.warn("DataTable: `meta.rangeBucketSize` is deprecated — use `meta.bucketSize` instead (same option, renamed to match `filterVariant: \"bucket\"`).");
|
|
1223
|
+
}, [
|
|
1224
|
+
actionButtons,
|
|
1225
|
+
error,
|
|
1226
|
+
ariaLabel,
|
|
1227
|
+
caption
|
|
1228
|
+
]);
|
|
1229
|
+
const [sorting, setSorting] = React.useState(defaultSorting ?? []);
|
|
1230
|
+
const [columnVisibility, setColumnVisibility] = React.useState(defaultColumnVisibility ?? {});
|
|
1231
|
+
const [columnOrder, setColumnOrder] = React.useState(defaultColumnOrder ?? []);
|
|
1232
|
+
const [columnPinning, setColumnPinning] = React.useState(() => defaultColumnPinning ?? derivePinningFromMeta(userColumns));
|
|
1233
|
+
const [columnSizing, setColumnSizing] = React.useState(defaultColumnSizing ?? {});
|
|
1234
|
+
const tableContainerRef = React.useRef(null);
|
|
1235
|
+
const bulkActionsBarRef = React.useRef(null);
|
|
1236
|
+
const [bulkActionsBarHeight, setBulkActionsBarHeight] = React.useState(0);
|
|
1237
|
+
const mainColumnRef = React.useRef(null);
|
|
1238
|
+
const footerRef = React.useRef(null);
|
|
1239
|
+
const [columnSearchQuery, setColumnSearchQuery] = React.useState("");
|
|
1240
|
+
const [filtersPaneOpen, setFiltersPaneOpen] = React.useState(false);
|
|
1241
|
+
const [columnsMenuOpen, setColumnsMenuOpen] = React.useState(false);
|
|
1242
|
+
const columnsMenuSnapshotRef = React.useRef(null);
|
|
1243
|
+
const columnsMenuAppliedRef = React.useRef(false);
|
|
1244
|
+
const [paginationState, setPaginationState] = React.useState({
|
|
1245
|
+
pageSize,
|
|
1246
|
+
pageIndex: defaultPageIndex ?? 0
|
|
1247
|
+
});
|
|
1248
|
+
const [pageRestorePending, setPageRestorePending] = React.useState(Boolean(defaultPageIndex) && data.length === 0);
|
|
1249
|
+
React.useEffect(() => {
|
|
1250
|
+
if (pageRestorePending && data.length > 0) setPageRestorePending(false);
|
|
1251
|
+
}, [pageRestorePending, data.length]);
|
|
1252
|
+
const [rowSelection, setRowSelection] = React.useState(defaultRowSelection ?? {});
|
|
1253
|
+
const atSelectionCap = maxSelectedRows != null && Object.keys(rowSelection).length >= maxSelectedRows;
|
|
1254
|
+
const effectiveEnableRowSelection = React.useCallback((row) => {
|
|
1255
|
+
if (!(typeof enableRowSelection === "function" ? enableRowSelection(row) : enableRowSelection)) return false;
|
|
1256
|
+
return !atSelectionCap || row.getIsSelected();
|
|
1257
|
+
}, [enableRowSelection, atSelectionCap]);
|
|
1258
|
+
const [columnFilters, setColumnFilters] = React.useState(defaultColumnFilters ?? []);
|
|
1259
|
+
const filtersPaneRef = React.useRef(null);
|
|
1260
|
+
const filtersPaneId = React.useId();
|
|
1261
|
+
const filtersPaneHeadingId = React.useId();
|
|
1262
|
+
const filtersPaneTriggerRef = React.useRef(null);
|
|
1263
|
+
const filtersPaneCloseButtonRef = React.useRef(null);
|
|
1264
|
+
const filtersPaneWasOpenRef = React.useRef(false);
|
|
1265
|
+
React.useEffect(() => {
|
|
1266
|
+
if (filtersPaneOpen && !filtersPaneWasOpenRef.current) filtersPaneCloseButtonRef.current?.focus();
|
|
1267
|
+
else if (!filtersPaneOpen && filtersPaneWasOpenRef.current) filtersPaneTriggerRef.current?.focus();
|
|
1268
|
+
filtersPaneWasOpenRef.current = filtersPaneOpen;
|
|
1269
|
+
}, [filtersPaneOpen]);
|
|
1270
|
+
const [filtersPaneEdgeAdjust, setFiltersPaneEdgeAdjust] = React.useState(null);
|
|
1271
|
+
const [expanded, setExpanded] = React.useState(defaultExpanded ?? {});
|
|
1272
|
+
React.useEffect(() => {
|
|
1273
|
+
onExpandedChange?.(expanded);
|
|
1274
|
+
}, [expanded]);
|
|
1275
|
+
const [globalFilter, setGlobalFilter] = React.useState(defaultGlobalFilter ?? "");
|
|
1276
|
+
const [searchInput, setSearchInput] = React.useState(defaultGlobalFilter ?? "");
|
|
1277
|
+
const debouncedSetGlobalFilter = useDebouncedCallback(setGlobalFilter, 200);
|
|
1278
|
+
const [pinnedFilterIds, setPinnedFilterIds] = React.useState(() => {
|
|
1279
|
+
if (defaultPinnedFilterIds) return defaultPinnedFilterIds;
|
|
1280
|
+
const mergedAwayIds = new Set(userColumns.flatMap((col) => col.meta?.filterMergeWith ?? []));
|
|
1281
|
+
return userColumns.filter((col) => col.enableColumnFilter !== false && !col.meta?.filterOnly && !mergedAwayIds.has(getColumnId(col) ?? "")).slice(0, 2).map(getColumnId).filter((id) => !!id);
|
|
1282
|
+
});
|
|
1283
|
+
function toggleFilterPin(columnId) {
|
|
1284
|
+
setPinnedFilterIds((prev) => prev.includes(columnId) ? prev.filter((id) => id !== columnId) : [...prev, columnId]);
|
|
1285
|
+
}
|
|
1286
|
+
const searchableIds = React.useMemo(() => {
|
|
1287
|
+
if (searchableColumnIds === "all") return userColumns.filter((col) => "accessorKey" in col || "accessorFn" in col).map(getColumnId).filter((id) => !!id);
|
|
1288
|
+
return searchableColumnIds ?? userColumns.slice(0, 2).map(getColumnId).filter((id) => !!id);
|
|
1289
|
+
}, [searchableColumnIds, userColumns]);
|
|
1290
|
+
const columns = React.useMemo(() => {
|
|
1291
|
+
const processedUserColumns = userColumns.map((col) => {
|
|
1292
|
+
const id = getColumnId(col);
|
|
1293
|
+
const isSearchable = id !== void 0 && searchableIds.includes(id);
|
|
1294
|
+
const isLocked = id !== void 0 && lockedColumnIds.includes(id);
|
|
1295
|
+
const isFilterOnly = !!col.meta?.filterOnly;
|
|
1296
|
+
const next = {
|
|
1297
|
+
...col,
|
|
1298
|
+
enableGlobalFilter: col.enableGlobalFilter ?? isSearchable,
|
|
1299
|
+
enableColumnFilter: col.enableColumnFilter ?? true,
|
|
1300
|
+
enableHiding: isLocked || isFilterOnly ? false : col.enableHiding,
|
|
1301
|
+
minSize: col.minSize ?? computeColumnMinSize(col, size),
|
|
1302
|
+
maxSize: col.maxSize ?? (col.size && col.size > DEFAULT_MAX_COLUMN_SIZE ? void 0 : DEFAULT_MAX_COLUMN_SIZE),
|
|
1303
|
+
filterFn: col.filterFn ?? (col.meta?.filterVariant === "select" ? selectFilterFn : col.meta?.filterVariant === "range" || col.meta?.filterVariant === "bucket" ? rangeFilterFn : col.meta?.filterVariant === "singleSelect" ? singleSelectFilterFn : "includesString")
|
|
1304
|
+
};
|
|
1305
|
+
if (!col.cell && isSearchable) next.cell = (info) => /* @__PURE__ */ jsx(HighlightMatch, {
|
|
1306
|
+
text: String(info.getValue() ?? ""),
|
|
1307
|
+
query: String(info.table.getState().globalFilter ?? ""),
|
|
1308
|
+
startsWith: searchMatch === "startsWith"
|
|
1309
|
+
});
|
|
1310
|
+
return next;
|
|
1311
|
+
});
|
|
1312
|
+
const leadingColumns = [];
|
|
1313
|
+
if (aiRowPredicate) leadingColumns.push({
|
|
1314
|
+
id: AI_COLUMN_ID,
|
|
1315
|
+
header: () => /* @__PURE__ */ jsx("span", {
|
|
1316
|
+
className: "sr-only",
|
|
1317
|
+
children: "AI"
|
|
1318
|
+
}),
|
|
1319
|
+
cell: ({ row }) => aiRowPredicate(row.original) ? /* @__PURE__ */ jsxs(AILabel, { children: [/* @__PURE__ */ jsx(AIMarkerTrigger, {
|
|
1320
|
+
size,
|
|
1321
|
+
ariaLabel: "AI-suggested row"
|
|
1322
|
+
}), /* @__PURE__ */ jsxs(AILabelContent, { children: [/* @__PURE__ */ jsx(PopoverHeader, { children: /* @__PURE__ */ jsx(PopoverTitle, { children: "Why you're seeing this" }) }), /* @__PURE__ */ jsx(PopoverDescription, { children: "This row's values are generated or suggested by AI." })] })] }) : null,
|
|
1323
|
+
enableSorting: false,
|
|
1324
|
+
enableHiding: false,
|
|
1325
|
+
enableGlobalFilter: false,
|
|
1326
|
+
enableColumnFilter: false,
|
|
1327
|
+
enablePinning: false,
|
|
1328
|
+
enableResizing: false,
|
|
1329
|
+
size: 56
|
|
1330
|
+
});
|
|
1331
|
+
if (renderExpandedRow) leadingColumns.push({
|
|
1332
|
+
id: EXPAND_COLUMN_ID,
|
|
1333
|
+
header: () => /* @__PURE__ */ jsx("span", {
|
|
1334
|
+
className: "sr-only",
|
|
1335
|
+
children: "Expand"
|
|
1336
|
+
}),
|
|
1337
|
+
cell: ({ row }) => row.getCanExpand() ? /* @__PURE__ */ jsx(Button, {
|
|
1338
|
+
type: "button",
|
|
1339
|
+
variant: "ghost",
|
|
1340
|
+
size: "xs",
|
|
1341
|
+
iconOnly: true,
|
|
1342
|
+
"aria-label": row.getIsExpanded() ? "Collapse row" : "Expand row",
|
|
1343
|
+
"aria-expanded": row.getIsExpanded(),
|
|
1344
|
+
"aria-controls": `data-table-expanded-${row.id}`,
|
|
1345
|
+
onClick: (event) => {
|
|
1346
|
+
event.stopPropagation();
|
|
1347
|
+
row.toggleExpanded();
|
|
1348
|
+
},
|
|
1349
|
+
children: /* @__PURE__ */ jsx(ChevronRightIcon, { className: cn("transition-transform", row.getIsExpanded() && "rotate-90") })
|
|
1350
|
+
}) : null,
|
|
1351
|
+
enableSorting: false,
|
|
1352
|
+
enableHiding: false,
|
|
1353
|
+
enableGlobalFilter: false,
|
|
1354
|
+
enableColumnFilter: false,
|
|
1355
|
+
enablePinning: false,
|
|
1356
|
+
enableResizing: false,
|
|
1357
|
+
size: 32
|
|
1358
|
+
});
|
|
1359
|
+
if (enableRowSelection) leadingColumns.push({
|
|
1360
|
+
id: SELECTION_COLUMN_ID,
|
|
1361
|
+
header: ({ table }) => {
|
|
1362
|
+
const state = table.getState().rowSelection;
|
|
1363
|
+
const pageRows = table.getRowModel().rows.filter((row) => typeof enableRowSelection === "function" ? enableRowSelection(row) : !!enableRowSelection);
|
|
1364
|
+
const selectedCount = pageRows.filter((row) => state[row.id]).length;
|
|
1365
|
+
const allSelected = pageRows.length > 0 && selectedCount === pageRows.length;
|
|
1366
|
+
const capReached = maxSelectedRows != null && Object.keys(state).length >= maxSelectedRows;
|
|
1367
|
+
return /* @__PURE__ */ jsx(Checkbox, {
|
|
1368
|
+
className: SELECTION_CHECKBOX_CLASS,
|
|
1369
|
+
"aria-label": onLoadMore ? "Select all rows" : "Select all rows on this page",
|
|
1370
|
+
checked: allSelected ? true : selectedCount > 0 ? "indeterminate" : false,
|
|
1371
|
+
disabled: capReached && !allSelected && selectedCount === 0,
|
|
1372
|
+
title: capReached && !allSelected && selectedCount === 0 ? `Up to ${maxSelectedRows} rows can be selected` : void 0,
|
|
1373
|
+
onCheckedChange: (value) => {
|
|
1374
|
+
const next = { ...state };
|
|
1375
|
+
if (!value || capReached && !allSelected) pageRows.forEach((row) => delete next[row.id]);
|
|
1376
|
+
else {
|
|
1377
|
+
let remaining = maxSelectedRows != null ? Math.max(0, maxSelectedRows - Object.keys(next).length) : Infinity;
|
|
1378
|
+
for (const row of pageRows) {
|
|
1379
|
+
if (next[row.id]) continue;
|
|
1380
|
+
if (remaining <= 0) break;
|
|
1381
|
+
next[row.id] = true;
|
|
1382
|
+
remaining -= 1;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
table.setRowSelection(next);
|
|
1386
|
+
}
|
|
1387
|
+
});
|
|
1388
|
+
},
|
|
1389
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
1390
|
+
className: SELECTION_CHECKBOX_CLASS,
|
|
1391
|
+
"aria-label": "Select row",
|
|
1392
|
+
checked: row.getIsSelected(),
|
|
1393
|
+
disabled: !row.getCanSelect(),
|
|
1394
|
+
title: !row.getCanSelect() && atSelectionCap && !row.getIsSelected() ? `Up to ${maxSelectedRows} rows can be selected` : void 0,
|
|
1395
|
+
onCheckedChange: (value) => row.toggleSelected(!!value),
|
|
1396
|
+
onClick: (event) => event.stopPropagation()
|
|
1397
|
+
}),
|
|
1398
|
+
enableSorting: false,
|
|
1399
|
+
enableHiding: false,
|
|
1400
|
+
enableGlobalFilter: false,
|
|
1401
|
+
enableColumnFilter: false,
|
|
1402
|
+
enablePinning: false,
|
|
1403
|
+
enableResizing: false,
|
|
1404
|
+
size: 40
|
|
1405
|
+
});
|
|
1406
|
+
return [...leadingColumns, ...processedUserColumns];
|
|
1407
|
+
}, [
|
|
1408
|
+
userColumns,
|
|
1409
|
+
enableRowSelection,
|
|
1410
|
+
atSelectionCap,
|
|
1411
|
+
maxSelectedRows,
|
|
1412
|
+
searchableIds,
|
|
1413
|
+
searchMatch,
|
|
1414
|
+
renderExpandedRow,
|
|
1415
|
+
aiRowPredicate,
|
|
1416
|
+
lockedColumnIds,
|
|
1417
|
+
size,
|
|
1418
|
+
onLoadMore
|
|
1419
|
+
]);
|
|
1420
|
+
const effectiveColumnVisibility = React.useMemo(() => {
|
|
1421
|
+
const forcedHidden = userColumns.reduce((acc, col) => {
|
|
1422
|
+
const id = getColumnId(col);
|
|
1423
|
+
if (id && col.meta?.filterOnly) acc[id] = false;
|
|
1424
|
+
return acc;
|
|
1425
|
+
}, {});
|
|
1426
|
+
return {
|
|
1427
|
+
...columnVisibility,
|
|
1428
|
+
...forcedHidden
|
|
1429
|
+
};
|
|
1430
|
+
}, [columnVisibility, userColumns]);
|
|
1431
|
+
const effectiveColumnPinning = React.useMemo(() => {
|
|
1432
|
+
const leadingIds = [
|
|
1433
|
+
aiRowPredicate && AI_COLUMN_ID,
|
|
1434
|
+
renderExpandedRow && EXPAND_COLUMN_ID,
|
|
1435
|
+
enableRowSelection && SELECTION_COLUMN_ID
|
|
1436
|
+
].filter((id) => !!id);
|
|
1437
|
+
const left = columnPinning.left ?? [];
|
|
1438
|
+
if (leadingIds.length === 0 || left.length === 0) return columnPinning;
|
|
1439
|
+
return {
|
|
1440
|
+
...columnPinning,
|
|
1441
|
+
left: [...leadingIds, ...left.filter((id) => !leadingIds.includes(id))]
|
|
1442
|
+
};
|
|
1443
|
+
}, [
|
|
1444
|
+
columnPinning,
|
|
1445
|
+
aiRowPredicate,
|
|
1446
|
+
renderExpandedRow,
|
|
1447
|
+
enableRowSelection
|
|
1448
|
+
]);
|
|
1449
|
+
const needsFacetedFiltering = userColumns.some((col) => col.meta?.filterVariant === "select" || col.meta?.filterVariant === "singleSelect" || col.meta?.filterVariant === "range" || col.meta?.filterVariant === "bucket" || col.meta?.filterRender);
|
|
1450
|
+
const needsFacetedMinMax = userColumns.some((col) => col.meta?.filterRender);
|
|
1451
|
+
const table = useReactTable({
|
|
1452
|
+
data,
|
|
1453
|
+
columns,
|
|
1454
|
+
getRowId,
|
|
1455
|
+
state: {
|
|
1456
|
+
sorting,
|
|
1457
|
+
columnVisibility: effectiveColumnVisibility,
|
|
1458
|
+
columnOrder,
|
|
1459
|
+
columnPinning: effectiveColumnPinning,
|
|
1460
|
+
columnSizing,
|
|
1461
|
+
rowSelection,
|
|
1462
|
+
columnFilters,
|
|
1463
|
+
expanded,
|
|
1464
|
+
globalFilter,
|
|
1465
|
+
pagination: paginationState
|
|
1466
|
+
},
|
|
1467
|
+
onSortingChange: setSorting,
|
|
1468
|
+
onColumnVisibilityChange: (updater) => {
|
|
1469
|
+
setColumnVisibility((old) => {
|
|
1470
|
+
const next = typeof updater === "function" ? updater(old) : updater;
|
|
1471
|
+
if (clearFiltersOnHide) {
|
|
1472
|
+
const newlyHidden = Object.keys(next).filter((id) => next[id] === false && old[id] !== false);
|
|
1473
|
+
if (newlyHidden.length > 0) setColumnFilters((filters) => filters.filter((filter) => !newlyHidden.includes(filter.id)));
|
|
1474
|
+
}
|
|
1475
|
+
return next;
|
|
1476
|
+
});
|
|
1477
|
+
},
|
|
1478
|
+
onColumnOrderChange: setColumnOrder,
|
|
1479
|
+
onColumnPinningChange: setColumnPinning,
|
|
1480
|
+
onColumnSizingChange: setColumnSizing,
|
|
1481
|
+
onRowSelectionChange: setRowSelection,
|
|
1482
|
+
onColumnFiltersChange: setColumnFilters,
|
|
1483
|
+
onExpandedChange: setExpanded,
|
|
1484
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
1485
|
+
enableRowSelection: effectiveEnableRowSelection,
|
|
1486
|
+
getRowCanExpand: renderExpandedRow ? () => true : void 0,
|
|
1487
|
+
getCoreRowModel: getCoreRowModel(),
|
|
1488
|
+
getSortedRowModel: getSortedRowModel(),
|
|
1489
|
+
globalFilterFn: searchMatch === "startsWith" ? startsWithWordFilterFn : "auto",
|
|
1490
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
1491
|
+
getPaginationRowModel: onLoadMore ? void 0 : getPaginationRowModel(),
|
|
1492
|
+
getFacetedRowModel: needsFacetedFiltering || needsFacetedMinMax ? getFacetedRowModel() : void 0,
|
|
1493
|
+
getFacetedUniqueValues: needsFacetedFiltering ? getFacetedUniqueValues() : void 0,
|
|
1494
|
+
getFacetedMinMaxValues: needsFacetedMinMax ? getFacetedMinMaxValues() : void 0,
|
|
1495
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
1496
|
+
initialState: { pagination: {
|
|
1497
|
+
pageSize,
|
|
1498
|
+
pageIndex: 0
|
|
1499
|
+
} },
|
|
1500
|
+
onPaginationChange: setPaginationState,
|
|
1501
|
+
autoResetPageIndex: pageRestorePending ? false : void 0,
|
|
1502
|
+
enableColumnResizing,
|
|
1503
|
+
columnResizeMode: "onChange",
|
|
1504
|
+
enableMultiSort,
|
|
1505
|
+
meta: { getColumnCellElements: (columnId) => {
|
|
1506
|
+
const root = tableContainerRef.current;
|
|
1507
|
+
if (!root) return [];
|
|
1508
|
+
return Array.from(root.querySelectorAll(`[data-column-id="${columnId}"]`));
|
|
1509
|
+
} }
|
|
1510
|
+
});
|
|
1511
|
+
React.useEffect(() => {
|
|
1512
|
+
if (!tableRef) return;
|
|
1513
|
+
tableRef(table);
|
|
1514
|
+
return () => tableRef(null);
|
|
1515
|
+
}, [tableRef, table]);
|
|
1516
|
+
const clampPageCount = onLoadMore ? 0 : table.getPageCount();
|
|
1517
|
+
const clampPageIndex = paginationState.pageIndex;
|
|
1518
|
+
React.useEffect(() => {
|
|
1519
|
+
if (clampPageCount > 0 && clampPageIndex >= clampPageCount) setPaginationState((prev) => ({
|
|
1520
|
+
...prev,
|
|
1521
|
+
pageIndex: clampPageCount - 1
|
|
1522
|
+
}));
|
|
1523
|
+
}, [clampPageCount, clampPageIndex]);
|
|
1524
|
+
const previousPageSizeRef = React.useRef(pageSize);
|
|
1525
|
+
React.useEffect(() => {
|
|
1526
|
+
if (previousPageSizeRef.current !== pageSize) {
|
|
1527
|
+
previousPageSizeRef.current = pageSize;
|
|
1528
|
+
table.setPageSize(pageSize);
|
|
1529
|
+
}
|
|
1530
|
+
}, [pageSize, table]);
|
|
1531
|
+
const isResizingColumn = table.getState().columnSizingInfo.isResizingColumn;
|
|
1532
|
+
const previousIsResizingColumnRef = React.useRef(isResizingColumn);
|
|
1533
|
+
React.useEffect(() => {
|
|
1534
|
+
if (previousIsResizingColumnRef.current && !isResizingColumn) onColumnLayoutChange?.({
|
|
1535
|
+
visibility: columnVisibility,
|
|
1536
|
+
order: columnOrder,
|
|
1537
|
+
pinning: columnPinning,
|
|
1538
|
+
sizing: columnSizing
|
|
1539
|
+
});
|
|
1540
|
+
previousIsResizingColumnRef.current = isResizingColumn;
|
|
1541
|
+
}, [
|
|
1542
|
+
isResizingColumn,
|
|
1543
|
+
columnVisibility,
|
|
1544
|
+
columnOrder,
|
|
1545
|
+
columnPinning,
|
|
1546
|
+
columnSizing,
|
|
1547
|
+
onColumnLayoutChange
|
|
1548
|
+
]);
|
|
1549
|
+
const pagination = table.getState().pagination;
|
|
1550
|
+
React.useEffect(() => {
|
|
1551
|
+
onStateChange?.({
|
|
1552
|
+
sorting,
|
|
1553
|
+
columnFilters,
|
|
1554
|
+
globalFilter,
|
|
1555
|
+
pagination,
|
|
1556
|
+
expanded,
|
|
1557
|
+
pinnedFilterIds
|
|
1558
|
+
});
|
|
1559
|
+
}, [
|
|
1560
|
+
sorting,
|
|
1561
|
+
columnFilters,
|
|
1562
|
+
globalFilter,
|
|
1563
|
+
pagination,
|
|
1564
|
+
expanded,
|
|
1565
|
+
pinnedFilterIds
|
|
1566
|
+
]);
|
|
1567
|
+
const lockedIds = React.useMemo(() => /* @__PURE__ */ new Set([...INJECTED_COLUMN_IDS, ...lockedColumnIds]), [lockedColumnIds]);
|
|
1568
|
+
const [draggedColumnId, setDraggedColumnId] = React.useState(null);
|
|
1569
|
+
const [dragOverColumnId, setDragOverColumnId] = React.useState(null);
|
|
1570
|
+
const orderedRealColumns = React.useMemo(() => {
|
|
1571
|
+
const leaf = table.getAllLeafColumns().filter((column) => !INJECTED_COLUMN_IDS.has(column.id));
|
|
1572
|
+
if (!columnOrder.length) return leaf;
|
|
1573
|
+
const rank = new Map(columnOrder.map((id, index) => [id, index]));
|
|
1574
|
+
return [...leaf].sort((a, b) => (rank.get(a.id) ?? 0) - (rank.get(b.id) ?? 0));
|
|
1575
|
+
}, [table, columnOrder]);
|
|
1576
|
+
const manageableColumns = React.useMemo(() => orderedRealColumns.filter((column) => !column.columnDef.meta?.filterOnly), [orderedRealColumns]);
|
|
1577
|
+
function columnSection(columnId) {
|
|
1578
|
+
if (columnPinning.left?.includes(columnId)) return "left";
|
|
1579
|
+
if (columnPinning.right?.includes(columnId)) return "right";
|
|
1580
|
+
return "center";
|
|
1581
|
+
}
|
|
1582
|
+
const manageableColumnsBySection = React.useMemo(() => {
|
|
1583
|
+
const byId = new Map(manageableColumns.map((column) => [column.id, column]));
|
|
1584
|
+
const toColumns = (ids) => (ids ?? []).map((id) => byId.get(id)).filter((column) => column !== void 0);
|
|
1585
|
+
const left = toColumns(columnPinning.left);
|
|
1586
|
+
const right = toColumns(columnPinning.right);
|
|
1587
|
+
const pinnedIds = /* @__PURE__ */ new Set([...columnPinning.left ?? [], ...columnPinning.right ?? []]);
|
|
1588
|
+
return {
|
|
1589
|
+
left,
|
|
1590
|
+
center: manageableColumns.filter((column) => !pinnedIds.has(column.id)),
|
|
1591
|
+
right
|
|
1592
|
+
};
|
|
1593
|
+
}, [manageableColumns, columnPinning]);
|
|
1594
|
+
function computeReorderedIds(draggedId, targetId) {
|
|
1595
|
+
if (!draggedId || !targetId || draggedId === targetId) return null;
|
|
1596
|
+
if (lockedIds.has(draggedId) || lockedIds.has(targetId)) return null;
|
|
1597
|
+
const section = columnSection(draggedId);
|
|
1598
|
+
if (section !== columnSection(targetId)) return null;
|
|
1599
|
+
const order = manageableColumnsBySection[section].map((column) => column.id);
|
|
1600
|
+
const fromIndex = order.indexOf(draggedId);
|
|
1601
|
+
const toIndex = order.indexOf(targetId);
|
|
1602
|
+
if (fromIndex === -1 || toIndex === -1) return null;
|
|
1603
|
+
const next = [...order];
|
|
1604
|
+
next.splice(fromIndex, 1);
|
|
1605
|
+
next.splice(toIndex, 0, draggedId);
|
|
1606
|
+
return {
|
|
1607
|
+
section,
|
|
1608
|
+
order: next
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function dropInsertSide(draggedId, targetId) {
|
|
1612
|
+
if (!draggedId || !targetId || draggedId === targetId) return null;
|
|
1613
|
+
if (lockedIds.has(draggedId) || lockedIds.has(targetId)) return null;
|
|
1614
|
+
const section = columnSection(draggedId);
|
|
1615
|
+
if (section !== columnSection(targetId)) return null;
|
|
1616
|
+
const order = manageableColumnsBySection[section].map((column) => column.id);
|
|
1617
|
+
const fromIndex = order.indexOf(draggedId);
|
|
1618
|
+
const toIndex = order.indexOf(targetId);
|
|
1619
|
+
if (fromIndex === -1 || toIndex === -1) return null;
|
|
1620
|
+
return fromIndex < toIndex ? "after" : "before";
|
|
1621
|
+
}
|
|
1622
|
+
const previewReorder = React.useMemo(() => {
|
|
1623
|
+
if (!draggedColumnId || !dragOverColumnId) return null;
|
|
1624
|
+
return computeReorderedIds(draggedColumnId, dragOverColumnId);
|
|
1625
|
+
}, [
|
|
1626
|
+
draggedColumnId,
|
|
1627
|
+
dragOverColumnId,
|
|
1628
|
+
manageableColumnsBySection,
|
|
1629
|
+
lockedIds
|
|
1630
|
+
]);
|
|
1631
|
+
function displayedSection(section) {
|
|
1632
|
+
const base = manageableColumnsBySection[section];
|
|
1633
|
+
if (!previewReorder || previewReorder.section !== section) return base;
|
|
1634
|
+
const byId = new Map(base.map((column) => [column.id, column]));
|
|
1635
|
+
return previewReorder.order.map((id) => byId.get(id)).filter((column) => column !== void 0);
|
|
1636
|
+
}
|
|
1637
|
+
const displayedLeft = displayedSection("left");
|
|
1638
|
+
const displayedCenter = displayedSection("center");
|
|
1639
|
+
const displayedRight = displayedSection("right");
|
|
1640
|
+
const displayedColumns = [
|
|
1641
|
+
...displayedLeft,
|
|
1642
|
+
...displayedCenter,
|
|
1643
|
+
...displayedRight
|
|
1644
|
+
];
|
|
1645
|
+
const derivedDefaultColumnPinning = React.useMemo(() => derivePinningFromMeta(userColumns), [userColumns]);
|
|
1646
|
+
const effectiveDefaultColumnPinning = defaultColumnPinning ?? derivedDefaultColumnPinning;
|
|
1647
|
+
const effectiveDefaultColumnVisibility = defaultColumnVisibility ?? {};
|
|
1648
|
+
const effectiveDefaultColumnOrder = defaultColumnOrder ?? [];
|
|
1649
|
+
const effectiveDefaultColumnSizing = defaultColumnSizing ?? {};
|
|
1650
|
+
const hasColumnCustomization = !idListsEqual(columnOrder, effectiveDefaultColumnOrder) || !visibilityEqual(columnVisibility, effectiveDefaultColumnVisibility) || !idListsEqual(columnPinning.left, effectiveDefaultColumnPinning.left) || !idListsEqual(columnPinning.right, effectiveDefaultColumnPinning.right) || !sizingEqual(columnSizing, effectiveDefaultColumnSizing);
|
|
1651
|
+
const columnsMenuSnapshot = columnsMenuSnapshotRef.current;
|
|
1652
|
+
const hasPendingColumnsMenuChanges = columnsMenuSnapshot ? !visibilityEqual(columnVisibility, columnsMenuSnapshot.visibility) || !idListsEqual(columnOrder, columnsMenuSnapshot.order) || !idListsEqual(columnPinning.left, columnsMenuSnapshot.pinning.left) || !idListsEqual(columnPinning.right, columnsMenuSnapshot.pinning.right) : false;
|
|
1653
|
+
function commitColumnOrder(centerOrder) {
|
|
1654
|
+
const leadingIds = table.getAllLeafColumns().filter((column) => INJECTED_COLUMN_IDS.has(column.id)).map((column) => column.id);
|
|
1655
|
+
const pinnedIds = [...columnPinning.left ?? [], ...columnPinning.right ?? []];
|
|
1656
|
+
setColumnOrder([
|
|
1657
|
+
...leadingIds,
|
|
1658
|
+
...centerOrder,
|
|
1659
|
+
...pinnedIds
|
|
1660
|
+
]);
|
|
1661
|
+
}
|
|
1662
|
+
function commitPinOrder(section, order) {
|
|
1663
|
+
setColumnPinning((old) => ({
|
|
1664
|
+
...old,
|
|
1665
|
+
[section]: order
|
|
1666
|
+
}));
|
|
1667
|
+
}
|
|
1668
|
+
function commitSectionOrder(section, order) {
|
|
1669
|
+
if (section === "center") commitColumnOrder(order);
|
|
1670
|
+
else commitPinOrder(section, order);
|
|
1671
|
+
}
|
|
1672
|
+
function findMoveTargetIndex(columnId, direction) {
|
|
1673
|
+
const section = columnSection(columnId);
|
|
1674
|
+
const order = manageableColumnsBySection[section].map((column) => column.id);
|
|
1675
|
+
const index = order.indexOf(columnId);
|
|
1676
|
+
if (index === -1) return null;
|
|
1677
|
+
let targetIndex = direction === "up" ? index - 1 : index + 1;
|
|
1678
|
+
while (targetIndex >= 0 && targetIndex < order.length && lockedIds.has(order[targetIndex])) targetIndex += direction === "up" ? -1 : 1;
|
|
1679
|
+
return targetIndex >= 0 && targetIndex < order.length ? targetIndex : null;
|
|
1680
|
+
}
|
|
1681
|
+
function canMoveColumn(columnId, direction) {
|
|
1682
|
+
return findMoveTargetIndex(columnId, direction) !== null;
|
|
1683
|
+
}
|
|
1684
|
+
function moveColumn(columnId, direction) {
|
|
1685
|
+
const section = columnSection(columnId);
|
|
1686
|
+
const order = manageableColumnsBySection[section].map((column) => column.id);
|
|
1687
|
+
const index = order.indexOf(columnId);
|
|
1688
|
+
const targetIndex = findMoveTargetIndex(columnId, direction);
|
|
1689
|
+
if (index === -1 || targetIndex === null) return;
|
|
1690
|
+
const next = [...order];
|
|
1691
|
+
next.splice(index, 1);
|
|
1692
|
+
next.splice(targetIndex, 0, columnId);
|
|
1693
|
+
commitSectionOrder(section, next);
|
|
1694
|
+
const newPosition = (section === "left" ? [] : section === "center" ? manageableColumnsBySection.left : [...manageableColumnsBySection.left, ...manageableColumnsBySection.center]).length + targetIndex + 1;
|
|
1695
|
+
const column = manageableColumnsBySection[section].find((c) => c.id === columnId);
|
|
1696
|
+
if (column) setLiveAnnouncement(`${columnLabel(column)} moved ${direction === "up" ? "left" : "right"}, now column ${newPosition} of ${manageableColumns.length}`);
|
|
1697
|
+
}
|
|
1698
|
+
function resetColumns() {
|
|
1699
|
+
setColumnOrder(effectiveDefaultColumnOrder);
|
|
1700
|
+
setColumnVisibility(effectiveDefaultColumnVisibility);
|
|
1701
|
+
setColumnPinning(effectiveDefaultColumnPinning);
|
|
1702
|
+
setColumnSizing(effectiveDefaultColumnSizing);
|
|
1703
|
+
onColumnLayoutChange?.({
|
|
1704
|
+
visibility: effectiveDefaultColumnVisibility,
|
|
1705
|
+
order: effectiveDefaultColumnOrder,
|
|
1706
|
+
pinning: effectiveDefaultColumnPinning,
|
|
1707
|
+
sizing: effectiveDefaultColumnSizing
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
function handleColumnsMenuOpenChange(open) {
|
|
1711
|
+
if (open) {
|
|
1712
|
+
columnsMenuSnapshotRef.current = {
|
|
1713
|
+
visibility: columnVisibility,
|
|
1714
|
+
order: columnOrder,
|
|
1715
|
+
pinning: columnPinning
|
|
1716
|
+
};
|
|
1717
|
+
columnsMenuAppliedRef.current = false;
|
|
1718
|
+
} else {
|
|
1719
|
+
if (!columnsMenuAppliedRef.current && columnsMenuSnapshotRef.current) {
|
|
1720
|
+
setColumnVisibility(columnsMenuSnapshotRef.current.visibility);
|
|
1721
|
+
setColumnOrder(columnsMenuSnapshotRef.current.order);
|
|
1722
|
+
setColumnPinning(columnsMenuSnapshotRef.current.pinning);
|
|
1723
|
+
}
|
|
1724
|
+
setColumnSearchQuery("");
|
|
1725
|
+
}
|
|
1726
|
+
setColumnsMenuOpen(open);
|
|
1727
|
+
}
|
|
1728
|
+
React.useLayoutEffect(() => {
|
|
1729
|
+
if (!filtersPaneOpen) {
|
|
1730
|
+
setFiltersPaneEdgeAdjust(null);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const pane = filtersPaneRef.current;
|
|
1734
|
+
const surface = pane?.closest("[data-slot=\"card\"]");
|
|
1735
|
+
if (!pane || !surface) {
|
|
1736
|
+
setFiltersPaneEdgeAdjust(null);
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
const paneRect = pane.getBoundingClientRect();
|
|
1740
|
+
const surfaceRect = surface.getBoundingClientRect();
|
|
1741
|
+
const topDelta = Math.max(0, paneRect.top - surfaceRect.top);
|
|
1742
|
+
const bottomDelta = Math.max(0, surfaceRect.bottom - paneRect.bottom);
|
|
1743
|
+
if (topDelta === 0 && bottomDelta === 0) {
|
|
1744
|
+
setFiltersPaneEdgeAdjust(null);
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
setFiltersPaneEdgeAdjust({
|
|
1748
|
+
marginTop: -topDelta,
|
|
1749
|
+
height: paneRect.height + topDelta + bottomDelta
|
|
1750
|
+
});
|
|
1751
|
+
}, [filtersPaneOpen]);
|
|
1752
|
+
const columnRowRefs = React.useRef(/* @__PURE__ */ new Map());
|
|
1753
|
+
const columnRowRefCallbacks = React.useRef(/* @__PURE__ */ new Map());
|
|
1754
|
+
const columnRowRectsRef = React.useRef(/* @__PURE__ */ new Map());
|
|
1755
|
+
const pendingFrameRef = React.useRef(null);
|
|
1756
|
+
const dragOverDebounceRef = React.useRef(null);
|
|
1757
|
+
const displayedOrderKey = displayedColumns.map((column) => column.id).join(",");
|
|
1758
|
+
function getColumnRowRef(id) {
|
|
1759
|
+
let callback = columnRowRefCallbacks.current.get(id);
|
|
1760
|
+
if (!callback) {
|
|
1761
|
+
callback = (element) => {
|
|
1762
|
+
if (element) columnRowRefs.current.set(id, element);
|
|
1763
|
+
else columnRowRefs.current.delete(id);
|
|
1764
|
+
};
|
|
1765
|
+
columnRowRefCallbacks.current.set(id, callback);
|
|
1766
|
+
}
|
|
1767
|
+
return callback;
|
|
1768
|
+
}
|
|
1769
|
+
React.useLayoutEffect(() => {
|
|
1770
|
+
const previousRects = columnRowRectsRef.current;
|
|
1771
|
+
if (pendingFrameRef.current !== null) {
|
|
1772
|
+
cancelAnimationFrame(pendingFrameRef.current);
|
|
1773
|
+
pendingFrameRef.current = null;
|
|
1774
|
+
}
|
|
1775
|
+
columnRowRefs.current.forEach((element) => {
|
|
1776
|
+
element.style.transition = "none";
|
|
1777
|
+
element.style.transform = "";
|
|
1778
|
+
});
|
|
1779
|
+
const nextRects = /* @__PURE__ */ new Map();
|
|
1780
|
+
columnRowRefs.current.forEach((element, id) => {
|
|
1781
|
+
nextRects.set(id, element.getBoundingClientRect());
|
|
1782
|
+
});
|
|
1783
|
+
const toAnimate = [];
|
|
1784
|
+
columnRowRefs.current.forEach((element, id) => {
|
|
1785
|
+
const previous = previousRects.get(id);
|
|
1786
|
+
const next = nextRects.get(id);
|
|
1787
|
+
if (!previous || !next) return;
|
|
1788
|
+
const deltaY = previous.top - next.top;
|
|
1789
|
+
if (Math.abs(deltaY) < .5) return;
|
|
1790
|
+
element.style.transform = `translateY(${deltaY}px)`;
|
|
1791
|
+
toAnimate.push(element);
|
|
1792
|
+
});
|
|
1793
|
+
if (toAnimate.length > 0) {
|
|
1794
|
+
toAnimate.forEach((element) => void element.getBoundingClientRect());
|
|
1795
|
+
pendingFrameRef.current = requestAnimationFrame(() => {
|
|
1796
|
+
toAnimate.forEach((element) => {
|
|
1797
|
+
element.style.transition = "transform 180ms ease";
|
|
1798
|
+
element.style.transform = "";
|
|
1799
|
+
});
|
|
1800
|
+
pendingFrameRef.current = null;
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
columnRowRectsRef.current = nextRects;
|
|
1804
|
+
}, [displayedOrderKey]);
|
|
1805
|
+
const { filterableColumns, showAllFiltersInline, inlineFilterColumns, paneFilterCount } = React.useMemo(() => {
|
|
1806
|
+
const mergedAwayFilterIds = new Set(userColumns.flatMap((col) => {
|
|
1807
|
+
const hostId = getColumnId(col);
|
|
1808
|
+
return !hostId || effectiveColumnVisibility[hostId] !== false ? col.meta?.filterMergeWith ?? [] : [];
|
|
1809
|
+
}));
|
|
1810
|
+
const withFilterOrderPositions = orderedRealColumns.filter((column) => column.getCanFilter() && !mergedAwayFilterIds.has(column.id)).map((column, index) => ({
|
|
1811
|
+
column,
|
|
1812
|
+
order: column.columnDef.meta?.filterOrder ?? index
|
|
1813
|
+
}));
|
|
1814
|
+
withFilterOrderPositions.sort((a, b) => a.order - b.order);
|
|
1815
|
+
const filterableColumns = withFilterOrderPositions.map((entry) => entry.column);
|
|
1816
|
+
const hasFilterOnlyColumn = filterableColumns.some((column) => !!column.columnDef.meta?.filterOnly);
|
|
1817
|
+
const showAllFiltersInline = !hasFilterOnlyColumn && filterableColumns.length <= 3;
|
|
1818
|
+
const inlineFilterColumns = showAllFiltersInline ? filterableColumns : filterableColumns.filter((column) => !column.columnDef.meta?.filterOnly && pinnedFilterIds.includes(column.id));
|
|
1819
|
+
return {
|
|
1820
|
+
filterableColumns,
|
|
1821
|
+
hasFilterOnlyColumn,
|
|
1822
|
+
showAllFiltersInline,
|
|
1823
|
+
inlineFilterColumns,
|
|
1824
|
+
paneFilterCount: showAllFiltersInline ? 0 : filterableColumns.length - inlineFilterColumns.length
|
|
1825
|
+
};
|
|
1826
|
+
}, [
|
|
1827
|
+
orderedRealColumns,
|
|
1828
|
+
userColumns,
|
|
1829
|
+
pinnedFilterIds,
|
|
1830
|
+
effectiveColumnVisibility
|
|
1831
|
+
]);
|
|
1832
|
+
const allSelectedRows = table.getSelectedRowModel().rows;
|
|
1833
|
+
const filteredSelectedRows = table.getFilteredSelectedRowModel().rows;
|
|
1834
|
+
const selectedRows = selectionScope === "all" ? allSelectedRows : filteredSelectedRows;
|
|
1835
|
+
const hiddenByFilterCount = selectionScope === "all" ? 0 : allSelectedRows.length - filteredSelectedRows.length;
|
|
1836
|
+
const hasSelection = selectedRows.length > 0;
|
|
1837
|
+
const isRowSelectableIgnoringCap = (row) => typeof enableRowSelection === "function" ? enableRowSelection(row) : !!enableRowSelection;
|
|
1838
|
+
const selectableFilteredRows = table.getFilteredRowModel().rows.filter(isRowSelectableIgnoringCap);
|
|
1839
|
+
const isAllFilteredSelected = selectableFilteredRows.length > 0 && selectableFilteredRows.every((row) => !!rowSelection[row.id]);
|
|
1840
|
+
const selectAllFiltered = () => {
|
|
1841
|
+
const next = { ...rowSelection };
|
|
1842
|
+
let remaining = maxSelectedRows != null ? Math.max(0, maxSelectedRows - Object.keys(next).length) : Infinity;
|
|
1843
|
+
for (const row of selectableFilteredRows) {
|
|
1844
|
+
if (next[row.id]) continue;
|
|
1845
|
+
if (remaining <= 0) break;
|
|
1846
|
+
next[row.id] = true;
|
|
1847
|
+
remaining -= 1;
|
|
1848
|
+
}
|
|
1849
|
+
setRowSelection(next);
|
|
1850
|
+
};
|
|
1851
|
+
const effectiveSelectionBarPlacement = onLoadMore ? "floating" : selectionBarPlacement ?? "toolbar";
|
|
1852
|
+
const bulkActionsBarMounted = hasSelection && effectiveSelectionBarPlacement === "floating";
|
|
1853
|
+
React.useLayoutEffect(() => {
|
|
1854
|
+
const el = bulkActionsBarRef.current;
|
|
1855
|
+
if (!el) {
|
|
1856
|
+
setBulkActionsBarHeight(0);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
const observer = new ResizeObserver(([entry]) => setBulkActionsBarHeight(entry.contentRect.height));
|
|
1860
|
+
observer.observe(el);
|
|
1861
|
+
return () => observer.disconnect();
|
|
1862
|
+
}, [bulkActionsBarMounted]);
|
|
1863
|
+
const [canScrollHorizontally, setCanScrollHorizontally] = React.useState(false);
|
|
1864
|
+
React.useLayoutEffect(() => {
|
|
1865
|
+
const container = tableContainerRef.current;
|
|
1866
|
+
const tableEl = container?.querySelector("table");
|
|
1867
|
+
if (!container || !tableEl) return;
|
|
1868
|
+
function measure() {
|
|
1869
|
+
if (!container) return;
|
|
1870
|
+
setCanScrollHorizontally(container.scrollWidth > container.clientWidth);
|
|
1871
|
+
}
|
|
1872
|
+
measure();
|
|
1873
|
+
const observer = new ResizeObserver(measure);
|
|
1874
|
+
observer.observe(tableEl);
|
|
1875
|
+
observer.observe(container);
|
|
1876
|
+
return () => observer.disconnect();
|
|
1877
|
+
}, []);
|
|
1878
|
+
const rowClickHintId = React.useId();
|
|
1879
|
+
const selectedIdsKey = selectedRows.map((row) => row.id).join("\0");
|
|
1880
|
+
React.useEffect(() => {
|
|
1881
|
+
onRowSelectionChange?.(selectedRows.map((row) => row.original), {
|
|
1882
|
+
allSelectedRows: allSelectedRows.map((row) => row.original),
|
|
1883
|
+
hiddenByFilterCount
|
|
1884
|
+
});
|
|
1885
|
+
}, [
|
|
1886
|
+
rowSelection,
|
|
1887
|
+
columnFilters,
|
|
1888
|
+
globalFilter,
|
|
1889
|
+
selectionScope,
|
|
1890
|
+
selectedIdsKey
|
|
1891
|
+
]);
|
|
1892
|
+
const rows = table.getRowModel().rows;
|
|
1893
|
+
const visibleLeafColumns = table.getVisibleLeafColumns();
|
|
1894
|
+
const resizableColumnIds = visibleLeafColumns.filter((column) => column.getCanResize()).map((column) => column.id);
|
|
1895
|
+
const [activeResizeColumnId, setActiveResizeColumnId] = React.useState(null);
|
|
1896
|
+
const resizeHandleRefs = React.useRef(/* @__PURE__ */ new Map());
|
|
1897
|
+
const showFooter = !loading && !error && !onLoadMore;
|
|
1898
|
+
const headerRowHeightPx = size === "xs" ? 32 : size === "sm" ? 36 : 40;
|
|
1899
|
+
const headerGroupsForRender = table.getHeaderGroups();
|
|
1900
|
+
const passthroughLeafIds = new Set(headerGroupsForRender[0]?.headers.filter((header) => header.subHeaders.length > 0 && !Array.isArray(header.column.columnDef.columns)).map((header) => header.column.id) ?? []);
|
|
1901
|
+
const groupBoundaryColumnIds = React.useMemo(() => {
|
|
1902
|
+
const starts = /* @__PURE__ */ new Set();
|
|
1903
|
+
const ends = /* @__PURE__ */ new Set();
|
|
1904
|
+
for (const header of headerGroupsForRender[0]?.headers ?? []) {
|
|
1905
|
+
if (header.subHeaders.length === 0) continue;
|
|
1906
|
+
if (passthroughLeafIds.has(header.column.id)) continue;
|
|
1907
|
+
const leaves = header.getLeafHeaders().filter((leaf) => leaf.subHeaders.length === 0);
|
|
1908
|
+
if (leaves.length === 0) continue;
|
|
1909
|
+
starts.add(leaves[0].column.id);
|
|
1910
|
+
ends.add(leaves[leaves.length - 1].column.id);
|
|
1911
|
+
}
|
|
1912
|
+
return {
|
|
1913
|
+
starts,
|
|
1914
|
+
ends
|
|
1915
|
+
};
|
|
1916
|
+
}, [headerGroupsForRender]);
|
|
1917
|
+
const mergedCellRowSpans = React.useMemo(() => {
|
|
1918
|
+
const spans = /* @__PURE__ */ new Map();
|
|
1919
|
+
for (const column of visibleLeafColumns) {
|
|
1920
|
+
if (!column.columnDef.meta?.mergeRepeatedValues) continue;
|
|
1921
|
+
const columnSpans = /* @__PURE__ */ new Map();
|
|
1922
|
+
let runStart = 0;
|
|
1923
|
+
for (let i = 1; i <= rows.length; i++) {
|
|
1924
|
+
if (i < rows.length && rows[i].getValue(column.id) === rows[runStart].getValue(column.id)) continue;
|
|
1925
|
+
columnSpans.set(rows[runStart].id, i - runStart);
|
|
1926
|
+
for (let covered = runStart + 1; covered < i; covered++) columnSpans.set(rows[covered].id, 0);
|
|
1927
|
+
runStart = i;
|
|
1928
|
+
}
|
|
1929
|
+
spans.set(column.id, columnSpans);
|
|
1930
|
+
}
|
|
1931
|
+
return spans;
|
|
1932
|
+
}, [visibleLeafColumns, rows]);
|
|
1933
|
+
const [rowsAreaHeight, setRowsAreaHeight] = React.useState(null);
|
|
1934
|
+
React.useLayoutEffect(() => {
|
|
1935
|
+
const mainColumn = mainColumnRef.current;
|
|
1936
|
+
const container = tableContainerRef.current;
|
|
1937
|
+
if (!mainColumn || !container) return;
|
|
1938
|
+
function measure() {
|
|
1939
|
+
if (!mainColumn || !container) return;
|
|
1940
|
+
const mainRect = mainColumn.getBoundingClientRect();
|
|
1941
|
+
const containerRect = container.getBoundingClientRect();
|
|
1942
|
+
const footerEl = footerRef.current;
|
|
1943
|
+
const footerHeight = footerEl ? footerEl.getBoundingClientRect().height : 0;
|
|
1944
|
+
const available = mainRect.bottom - containerRect.top - footerHeight;
|
|
1945
|
+
setRowsAreaHeight(Math.max(available, 0));
|
|
1946
|
+
}
|
|
1947
|
+
measure();
|
|
1948
|
+
const observer = new ResizeObserver(measure);
|
|
1949
|
+
observer.observe(mainColumn);
|
|
1950
|
+
if (footerRef.current) observer.observe(footerRef.current);
|
|
1951
|
+
return () => observer.disconnect();
|
|
1952
|
+
}, [showFooter]);
|
|
1953
|
+
const pageIndex = table.getState().pagination.pageIndex;
|
|
1954
|
+
const filteredRowCount = table.getFilteredRowModel().rows.length;
|
|
1955
|
+
const currentPageSize = table.getState().pagination.pageSize;
|
|
1956
|
+
const rangeStart = filteredRowCount === 0 ? 0 : pageIndex * currentPageSize + 1;
|
|
1957
|
+
const rangeEnd = Math.min((pageIndex + 1) * currentPageSize, filteredRowCount);
|
|
1958
|
+
const [liveAnnouncement, setLiveAnnouncement] = React.useState("");
|
|
1959
|
+
React.useEffect(() => {
|
|
1960
|
+
const timeout = setTimeout(() => {
|
|
1961
|
+
setLiveAnnouncement(!hasSelection ? "" : isAllFilteredSelected ? `All ${filteredRowCount} rows selected` : `${selectedRows.length} of ${filteredRowCount} rows selected`);
|
|
1962
|
+
}, 400);
|
|
1963
|
+
return () => clearTimeout(timeout);
|
|
1964
|
+
}, [
|
|
1965
|
+
hasSelection,
|
|
1966
|
+
isAllFilteredSelected,
|
|
1967
|
+
selectedRows.length,
|
|
1968
|
+
filteredRowCount
|
|
1969
|
+
]);
|
|
1970
|
+
React.useEffect(() => {
|
|
1971
|
+
const timeout = setTimeout(() => {
|
|
1972
|
+
setLiveAnnouncement(filteredRowCount === 0 ? "No results" : `Showing ${rangeStart}–${rangeEnd} of ${filteredRowCount} results`);
|
|
1973
|
+
}, 500);
|
|
1974
|
+
return () => clearTimeout(timeout);
|
|
1975
|
+
}, [
|
|
1976
|
+
columnFilters,
|
|
1977
|
+
globalFilter,
|
|
1978
|
+
filteredRowCount,
|
|
1979
|
+
rangeStart,
|
|
1980
|
+
rangeEnd
|
|
1981
|
+
]);
|
|
1982
|
+
const sortingMountedRef = React.useRef(false);
|
|
1983
|
+
React.useEffect(() => {
|
|
1984
|
+
if (!sortingMountedRef.current) {
|
|
1985
|
+
sortingMountedRef.current = true;
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
if (sorting.length === 0) {
|
|
1989
|
+
setLiveAnnouncement("Sort cleared");
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
const labels = sorting.map((sort) => `${columnLabel(table.getColumn(sort.id))}, ${sort.desc ? "descending" : "ascending"}`);
|
|
1993
|
+
setLiveAnnouncement(`Sorted by ${labels.join(", then ")}`);
|
|
1994
|
+
}, [sorting]);
|
|
1995
|
+
const previousColumnPinningRef = React.useRef(columnPinning);
|
|
1996
|
+
React.useEffect(() => {
|
|
1997
|
+
const previous = previousColumnPinningRef.current;
|
|
1998
|
+
previousColumnPinningRef.current = columnPinning;
|
|
1999
|
+
const newlyPinned = (side) => (columnPinning[side] ?? []).find((id) => !(previous[side] ?? []).includes(id));
|
|
2000
|
+
const newlyUnpinned = [...previous.left ?? [], ...previous.right ?? []].find((id) => !(columnPinning.left ?? []).includes(id) && !(columnPinning.right ?? []).includes(id));
|
|
2001
|
+
const pinnedLeftId = newlyPinned("left");
|
|
2002
|
+
const pinnedRightId = newlyPinned("right");
|
|
2003
|
+
const columnId = pinnedLeftId ?? pinnedRightId ?? newlyUnpinned;
|
|
2004
|
+
if (!columnId) return;
|
|
2005
|
+
const column = table.getColumn(columnId);
|
|
2006
|
+
if (!column) return;
|
|
2007
|
+
setLiveAnnouncement(pinnedLeftId ? `${columnLabel(column)} pinned left` : pinnedRightId ? `${columnLabel(column)} pinned right` : `${columnLabel(column)} unpinned`);
|
|
2008
|
+
}, [columnPinning]);
|
|
2009
|
+
const loadMoreSentinelRef = React.useRef(null);
|
|
2010
|
+
React.useEffect(() => {
|
|
2011
|
+
if (!onLoadMore || !hasMore) return;
|
|
2012
|
+
const sentinel = loadMoreSentinelRef.current;
|
|
2013
|
+
if (!sentinel) return;
|
|
2014
|
+
const observer = new IntersectionObserver((entries) => {
|
|
2015
|
+
if (entries[0]?.isIntersecting) onLoadMore();
|
|
2016
|
+
}, { rootMargin: `${loadMoreOffset}px` });
|
|
2017
|
+
observer.observe(sentinel);
|
|
2018
|
+
return () => observer.disconnect();
|
|
2019
|
+
}, [
|
|
2020
|
+
onLoadMore,
|
|
2021
|
+
hasMore,
|
|
2022
|
+
loadMoreOffset
|
|
2023
|
+
]);
|
|
2024
|
+
const controlSize = size === "default" ? "sm" : "xs";
|
|
2025
|
+
const controlIconSize = size === "default" ? "sm" : "xs";
|
|
2026
|
+
const hoverRevealClass = revealColumnControlsOnHover ? "opacity-0 transition-opacity group-hover/header-actions:opacity-100 group-focus-within/header-actions:opacity-100 [@media(hover:none)]:opacity-100" : "";
|
|
2027
|
+
const hoverRevealRowClass = revealColumnControlsOnHover ? "opacity-0 transition-opacity group-hover/column-row:opacity-100 group-focus-within/column-row:opacity-100 [@media(hover:none)]:opacity-100" : "";
|
|
2028
|
+
const hoverRevealFilterRowClass = revealColumnControlsOnHover ? "opacity-0 transition-opacity group-hover/filter-row:opacity-100 group-focus-within/filter-row:opacity-100 [@media(hover:none)]:opacity-100" : "";
|
|
2029
|
+
const [openHeaderColumnId, setOpenHeaderColumnId] = React.useState(null);
|
|
2030
|
+
function renderColumnMenuRow(column) {
|
|
2031
|
+
const locked = lockedIds.has(column.id);
|
|
2032
|
+
const canHide = column.getCanHide();
|
|
2033
|
+
const label = columnLabel(column);
|
|
2034
|
+
const isDragging = draggedColumnId === column.id;
|
|
2035
|
+
const isDropTarget = dragOverColumnId === column.id && draggedColumnId !== null && !isDragging;
|
|
2036
|
+
const dropSide = isDropTarget && draggedColumnId ? dropInsertSide(draggedColumnId, column.id) : null;
|
|
2037
|
+
const pins = pinActions(column);
|
|
2038
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2039
|
+
ref: getColumnRowRef(column.id),
|
|
2040
|
+
draggable: !locked,
|
|
2041
|
+
onDragStart: () => {
|
|
2042
|
+
if (locked) return;
|
|
2043
|
+
setDraggedColumnId(column.id);
|
|
2044
|
+
},
|
|
2045
|
+
onDragEnd: () => {
|
|
2046
|
+
if (dragOverDebounceRef.current) {
|
|
2047
|
+
clearTimeout(dragOverDebounceRef.current);
|
|
2048
|
+
dragOverDebounceRef.current = null;
|
|
2049
|
+
}
|
|
2050
|
+
setDraggedColumnId(null);
|
|
2051
|
+
setDragOverColumnId(null);
|
|
2052
|
+
},
|
|
2053
|
+
onDragEnter: (event) => {
|
|
2054
|
+
if (locked || !draggedColumnId) return;
|
|
2055
|
+
event.preventDefault();
|
|
2056
|
+
},
|
|
2057
|
+
onDragOver: (event) => {
|
|
2058
|
+
if (locked || !draggedColumnId) return;
|
|
2059
|
+
event.preventDefault();
|
|
2060
|
+
if (dragOverColumnId === column.id) return;
|
|
2061
|
+
if (dragOverDebounceRef.current) clearTimeout(dragOverDebounceRef.current);
|
|
2062
|
+
dragOverDebounceRef.current = setTimeout(() => {
|
|
2063
|
+
setDragOverColumnId(column.id);
|
|
2064
|
+
dragOverDebounceRef.current = null;
|
|
2065
|
+
}, 80);
|
|
2066
|
+
},
|
|
2067
|
+
onDrop: (event) => {
|
|
2068
|
+
event.preventDefault();
|
|
2069
|
+
if (dragOverDebounceRef.current) {
|
|
2070
|
+
clearTimeout(dragOverDebounceRef.current);
|
|
2071
|
+
dragOverDebounceRef.current = null;
|
|
2072
|
+
}
|
|
2073
|
+
if (draggedColumnId) {
|
|
2074
|
+
const finalReorder = computeReorderedIds(draggedColumnId, column.id);
|
|
2075
|
+
if (finalReorder) commitSectionOrder(finalReorder.section, finalReorder.order);
|
|
2076
|
+
}
|
|
2077
|
+
setDraggedColumnId(null);
|
|
2078
|
+
setDragOverColumnId(null);
|
|
2079
|
+
},
|
|
2080
|
+
className: cn("group/column-row relative flex items-center gap-1 rounded-md px-1.5 py-1 hover:bg-accent/50", !locked && "cursor-grab active:cursor-grabbing", isDragging && "opacity-40", isDropTarget && "bg-accent"),
|
|
2081
|
+
children: [
|
|
2082
|
+
dropSide && /* @__PURE__ */ jsx("div", {
|
|
2083
|
+
"aria-hidden": "true",
|
|
2084
|
+
className: cn("absolute inset-x-1.5 z-10 h-0.5 bg-primary", dropSide === "before" ? "top-0" : "bottom-0")
|
|
2085
|
+
}),
|
|
2086
|
+
/* @__PURE__ */ jsxs(Label, {
|
|
2087
|
+
className: "flex flex-1 items-center gap-2 truncate font-normal",
|
|
2088
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
2089
|
+
checked: canHide ? column.getIsVisible() : true,
|
|
2090
|
+
disabled: !canHide,
|
|
2091
|
+
onCheckedChange: (value) => column.toggleVisibility(!!value)
|
|
2092
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2093
|
+
className: "truncate",
|
|
2094
|
+
children: label
|
|
2095
|
+
})]
|
|
2096
|
+
}),
|
|
2097
|
+
pins.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
2098
|
+
className: cn("flex shrink-0 items-center", hoverRevealRowClass),
|
|
2099
|
+
children: pins.map((action) => /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
2100
|
+
asChild: true,
|
|
2101
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
2102
|
+
type: "button",
|
|
2103
|
+
variant: "ghost",
|
|
2104
|
+
size: "xs",
|
|
2105
|
+
iconOnly: true,
|
|
2106
|
+
"aria-label": action.label,
|
|
2107
|
+
onClick: action.onClick,
|
|
2108
|
+
children: action.icon ? /* @__PURE__ */ jsx(action.icon, { className: "size-3.5" }) : null
|
|
2109
|
+
})
|
|
2110
|
+
}), /* @__PURE__ */ jsx(TooltipContent, { children: action.label })] }, action.label))
|
|
2111
|
+
}),
|
|
2112
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2113
|
+
className: cn("flex shrink-0 items-center", hoverRevealRowClass),
|
|
2114
|
+
children: [/* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
2115
|
+
asChild: true,
|
|
2116
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
2117
|
+
type: "button",
|
|
2118
|
+
variant: "ghost",
|
|
2119
|
+
size: "xs",
|
|
2120
|
+
iconOnly: true,
|
|
2121
|
+
disabled: locked || !canMoveColumn(column.id, "up"),
|
|
2122
|
+
"aria-label": `Move ${label} left`,
|
|
2123
|
+
onClick: () => moveColumn(column.id, "up"),
|
|
2124
|
+
children: /* @__PURE__ */ jsx(ArrowLeftIcon, { className: "size-3.5" })
|
|
2125
|
+
})
|
|
2126
|
+
}), /* @__PURE__ */ jsx(TooltipContent, { children: "Move left" })] }), /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
2127
|
+
asChild: true,
|
|
2128
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
2129
|
+
type: "button",
|
|
2130
|
+
variant: "ghost",
|
|
2131
|
+
size: "xs",
|
|
2132
|
+
iconOnly: true,
|
|
2133
|
+
disabled: locked || !canMoveColumn(column.id, "down"),
|
|
2134
|
+
"aria-label": `Move ${label} right`,
|
|
2135
|
+
onClick: () => moveColumn(column.id, "down"),
|
|
2136
|
+
children: /* @__PURE__ */ jsx(ArrowRightIcon, { className: "size-3.5" })
|
|
2137
|
+
})
|
|
2138
|
+
}), /* @__PURE__ */ jsx(TooltipContent, { children: "Move right" })] })]
|
|
2139
|
+
})
|
|
2140
|
+
]
|
|
2141
|
+
}, column.id);
|
|
2142
|
+
}
|
|
2143
|
+
function commitPageJump(value) {
|
|
2144
|
+
if (value.trim() === "") return;
|
|
2145
|
+
const page = Number(value);
|
|
2146
|
+
if (!Number.isFinite(page)) return;
|
|
2147
|
+
const clamped = Math.min(table.getPageCount(), Math.max(1, Math.round(page)));
|
|
2148
|
+
table.setPageIndex(clamped - 1);
|
|
2149
|
+
}
|
|
2150
|
+
const renderPageSelect = (triggerClassName) => table.getPageCount() > PAGE_SELECT_THRESHOLD ? /* @__PURE__ */ jsx(Input, {
|
|
2151
|
+
type: "number",
|
|
2152
|
+
min: 1,
|
|
2153
|
+
max: table.getPageCount(),
|
|
2154
|
+
defaultValue: pageIndex + 1,
|
|
2155
|
+
"aria-label": "Current page",
|
|
2156
|
+
className: cn("w-14 text-center", triggerClassName),
|
|
2157
|
+
onKeyDown: (event) => {
|
|
2158
|
+
if (event.key !== "Enter") return;
|
|
2159
|
+
commitPageJump(event.currentTarget.value);
|
|
2160
|
+
},
|
|
2161
|
+
onBlur: (event) => commitPageJump(event.currentTarget.value)
|
|
2162
|
+
}, pageIndex) : /* @__PURE__ */ jsxs(Select, {
|
|
2163
|
+
value: String(pageIndex + 1),
|
|
2164
|
+
onValueChange: (value) => table.setPageIndex(Number(value) - 1),
|
|
2165
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
2166
|
+
size: controlSize,
|
|
2167
|
+
"aria-label": "Current page",
|
|
2168
|
+
className: triggerClassName,
|
|
2169
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
2170
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children: Array.from({ length: table.getPageCount() }, (_, index) => /* @__PURE__ */ jsx(SelectItem, {
|
|
2171
|
+
value: String(index + 1),
|
|
2172
|
+
children: index + 1
|
|
2173
|
+
}, index)) })]
|
|
2174
|
+
});
|
|
2175
|
+
const renderNumberedPagination = () => {
|
|
2176
|
+
const pageCount = table.getPageCount();
|
|
2177
|
+
const currentPage = pageIndex + 1;
|
|
2178
|
+
return /* @__PURE__ */ jsx(Pagination, {
|
|
2179
|
+
className: "mx-0 w-auto justify-start",
|
|
2180
|
+
children: /* @__PURE__ */ jsxs(PaginationContent, { children: [
|
|
2181
|
+
/* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationPrevious, {
|
|
2182
|
+
size: controlIconSize,
|
|
2183
|
+
disabled: !table.getCanPreviousPage(),
|
|
2184
|
+
onClick: () => table.previousPage()
|
|
2185
|
+
}) }),
|
|
2186
|
+
getPaginationWindow(currentPage, pageCount).map((page, index) => page === "ellipsis" ? /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationEllipsis, {}) }, `ellipsis-${index}`) : /* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationLink, {
|
|
2187
|
+
size: controlIconSize,
|
|
2188
|
+
isActive: page === currentPage,
|
|
2189
|
+
onClick: () => table.setPageIndex(page - 1),
|
|
2190
|
+
children: page
|
|
2191
|
+
}) }, page)),
|
|
2192
|
+
/* @__PURE__ */ jsx(PaginationItem, { children: /* @__PURE__ */ jsx(PaginationNext, {
|
|
2193
|
+
size: controlIconSize,
|
|
2194
|
+
disabled: !table.getCanNextPage(),
|
|
2195
|
+
onClick: () => table.nextPage()
|
|
2196
|
+
}) })
|
|
2197
|
+
] })
|
|
2198
|
+
});
|
|
2199
|
+
};
|
|
2200
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2201
|
+
"data-slot": "data-table",
|
|
2202
|
+
className: cn("flex h-full", className),
|
|
2203
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2204
|
+
ref: mainColumnRef,
|
|
2205
|
+
className: "flex min-h-0 min-w-0 flex-1 flex-col",
|
|
2206
|
+
children: [
|
|
2207
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2208
|
+
className: cn("@container/data-table-toolbar mb-[calc(var(--card-spacing,1rem)*0.75)] flex flex-wrap items-center justify-between gap-2 px-(--card-spacing,1rem) @max-sm/data-table-toolbar:flex-col @max-sm/data-table-toolbar:items-stretch", filtersPaneOpen ? "-ml-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px))]" : "-mx-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px)*2)]"),
|
|
2209
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2210
|
+
className: cn("flex min-w-0 flex-1 flex-wrap items-center gap-2", size === "default" && "min-h-8", size === "sm" && "min-h-7", size === "xs" && "min-h-6"),
|
|
2211
|
+
children: hasSelection && effectiveSelectionBarPlacement === "toolbar" ? /* @__PURE__ */ jsxs("div", {
|
|
2212
|
+
className: "flex min-w-0 flex-1 items-center gap-1.5",
|
|
2213
|
+
children: [
|
|
2214
|
+
isAllFilteredSelected ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
2215
|
+
className: "px-1 text-sm whitespace-nowrap text-muted-foreground",
|
|
2216
|
+
children: [
|
|
2217
|
+
"All ",
|
|
2218
|
+
table.getFilteredRowModel().rows.length,
|
|
2219
|
+
" selected"
|
|
2220
|
+
]
|
|
2221
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2222
|
+
type: "button",
|
|
2223
|
+
variant: "ghost",
|
|
2224
|
+
size: controlSize,
|
|
2225
|
+
onClick: () => setRowSelection({}),
|
|
2226
|
+
children: "Clear selection"
|
|
2227
|
+
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
2228
|
+
className: "px-1 text-sm whitespace-nowrap text-muted-foreground",
|
|
2229
|
+
children: [maxSelectedRows != null ? `${selectedRows.length} of ${maxSelectedRows} max selected` : `${selectedRows.length} selected`, hiddenByFilterCount > 0 && ` (${hiddenByFilterCount} hidden by filters)`]
|
|
2230
|
+
}), /* @__PURE__ */ jsxs(Button, {
|
|
2231
|
+
type: "button",
|
|
2232
|
+
variant: "ghost",
|
|
2233
|
+
size: controlSize,
|
|
2234
|
+
disabled: atSelectionCap,
|
|
2235
|
+
title: atSelectionCap ? `Up to ${maxSelectedRows} rows can be selected` : void 0,
|
|
2236
|
+
onClick: selectAllFiltered,
|
|
2237
|
+
children: [
|
|
2238
|
+
/* @__PURE__ */ jsx(CheckCheckIcon, {}),
|
|
2239
|
+
"Select all ",
|
|
2240
|
+
selectableFilteredRows.length
|
|
2241
|
+
]
|
|
2242
|
+
})] }),
|
|
2243
|
+
renderBulkActions && /* @__PURE__ */ jsx("div", {
|
|
2244
|
+
className: "flex items-center gap-1",
|
|
2245
|
+
children: renderBulkActions(selectedRows.map((row) => row.original), () => setRowSelection({}))
|
|
2246
|
+
}),
|
|
2247
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2248
|
+
type: "button",
|
|
2249
|
+
variant: "ghost",
|
|
2250
|
+
size: controlIconSize,
|
|
2251
|
+
iconOnly: true,
|
|
2252
|
+
"aria-label": "Clear selection",
|
|
2253
|
+
onClick: () => setRowSelection({}),
|
|
2254
|
+
children: /* @__PURE__ */ jsx(XIcon, {})
|
|
2255
|
+
})
|
|
2256
|
+
]
|
|
2257
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(InputGroup, {
|
|
2258
|
+
className: cn("max-w-64", controlSize === "sm" && "h-7", controlSize === "xs" && "h-6"),
|
|
2259
|
+
children: [/* @__PURE__ */ jsx(InputGroupAddon, { children: /* @__PURE__ */ jsx(SearchIcon, {}) }), /* @__PURE__ */ jsx(InputGroupInput, {
|
|
2260
|
+
"aria-label": searchPlaceholder,
|
|
2261
|
+
placeholder: searchPlaceholder,
|
|
2262
|
+
value: searchInput,
|
|
2263
|
+
onChange: (event) => {
|
|
2264
|
+
setSearchInput(event.target.value);
|
|
2265
|
+
debouncedSetGlobalFilter(event.target.value);
|
|
2266
|
+
},
|
|
2267
|
+
onBlur: () => setGlobalFilter(searchInput),
|
|
2268
|
+
onKeyDown: (event) => {
|
|
2269
|
+
if (event.key === "Enter") setGlobalFilter(searchInput);
|
|
2270
|
+
}
|
|
2271
|
+
})]
|
|
2272
|
+
}), (inlineFilterColumns.length > 0 || !showAllFiltersInline && paneFilterCount > 0) && /* @__PURE__ */ jsxs("div", {
|
|
2273
|
+
className: "flex flex-wrap items-center gap-1",
|
|
2274
|
+
children: [inlineFilterColumns.map((column) => /* @__PURE__ */ jsx("div", {
|
|
2275
|
+
className: cn(filtersPaneOpen && "hidden"),
|
|
2276
|
+
children: /* @__PURE__ */ jsx(InlineColumnFilter, {
|
|
2277
|
+
column,
|
|
2278
|
+
size: controlSize,
|
|
2279
|
+
tableDeferred: filterApplyMode === "deferred",
|
|
2280
|
+
showFilterCounts,
|
|
2281
|
+
stickyFilterSelectAll
|
|
2282
|
+
})
|
|
2283
|
+
}, column.id)), /* @__PURE__ */ jsxs("div", {
|
|
2284
|
+
className: "flex items-center gap-4",
|
|
2285
|
+
children: [!showAllFiltersInline && /* @__PURE__ */ jsx(Button, {
|
|
2286
|
+
ref: filtersPaneTriggerRef,
|
|
2287
|
+
type: "button",
|
|
2288
|
+
variant: "outline",
|
|
2289
|
+
size: controlSize,
|
|
2290
|
+
"aria-expanded": filtersPaneOpen,
|
|
2291
|
+
"aria-controls": filtersPaneId,
|
|
2292
|
+
onClick: () => setFiltersPaneOpen((open) => !open),
|
|
2293
|
+
children: "All filters"
|
|
2294
|
+
}), !showAllFiltersInline && !filtersPaneOpen && columnFilters.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
2295
|
+
className: "flex items-center gap-2",
|
|
2296
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
2297
|
+
className: "text-sm text-muted-foreground",
|
|
2298
|
+
children: [
|
|
2299
|
+
columnFilters.length,
|
|
2300
|
+
" filter",
|
|
2301
|
+
columnFilters.length === 1 ? "" : "s",
|
|
2302
|
+
" applied"
|
|
2303
|
+
]
|
|
2304
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2305
|
+
type: "button",
|
|
2306
|
+
variant: "ghost",
|
|
2307
|
+
size: controlSize,
|
|
2308
|
+
onClick: () => setColumnFilters([]),
|
|
2309
|
+
children: "Clear filters"
|
|
2310
|
+
})]
|
|
2311
|
+
})]
|
|
2312
|
+
})]
|
|
2313
|
+
})] })
|
|
2314
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2315
|
+
className: "flex items-center gap-2",
|
|
2316
|
+
children: [
|
|
2317
|
+
aiAction && /* @__PURE__ */ jsxs(Button, {
|
|
2318
|
+
type: "button",
|
|
2319
|
+
variant: "ai",
|
|
2320
|
+
size,
|
|
2321
|
+
onClick: aiAction.onClick,
|
|
2322
|
+
children: [/* @__PURE__ */ jsx(AISparkle, {
|
|
2323
|
+
size: "sm",
|
|
2324
|
+
"data-icon": "inline-start"
|
|
2325
|
+
}), aiAction.label]
|
|
2326
|
+
}),
|
|
2327
|
+
actionButtons?.slice(0, 3).map((action, index) => action.variant === "icon" ? /* @__PURE__ */ jsx(Button, {
|
|
2328
|
+
type: "button",
|
|
2329
|
+
variant: "outline",
|
|
2330
|
+
size: controlIconSize,
|
|
2331
|
+
iconOnly: true,
|
|
2332
|
+
disabled: action.disabled,
|
|
2333
|
+
"aria-label": action.label,
|
|
2334
|
+
onClick: action.onClick,
|
|
2335
|
+
children: action.icon
|
|
2336
|
+
}, index) : /* @__PURE__ */ jsxs(Button, {
|
|
2337
|
+
type: "button",
|
|
2338
|
+
variant: action.variant === "secondary" ? "outline" : "default",
|
|
2339
|
+
size: controlSize,
|
|
2340
|
+
disabled: action.disabled,
|
|
2341
|
+
onClick: action.onClick,
|
|
2342
|
+
children: [action.icon && /* @__PURE__ */ jsx("span", {
|
|
2343
|
+
"data-icon": "inline-start",
|
|
2344
|
+
children: action.icon
|
|
2345
|
+
}), action.label]
|
|
2346
|
+
}, index)),
|
|
2347
|
+
showColumnToggle && manageableColumns.length > 0 && /* @__PURE__ */ jsxs(Popover, {
|
|
2348
|
+
open: columnsMenuOpen,
|
|
2349
|
+
onOpenChange: handleColumnsMenuOpenChange,
|
|
2350
|
+
children: [/* @__PURE__ */ jsx(PopoverTrigger, {
|
|
2351
|
+
asChild: true,
|
|
2352
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
2353
|
+
type: "button",
|
|
2354
|
+
variant: "outline",
|
|
2355
|
+
size: controlIconSize,
|
|
2356
|
+
iconOnly: true,
|
|
2357
|
+
"aria-label": "Columns",
|
|
2358
|
+
children: /* @__PURE__ */ jsx(Columns3Icon, {})
|
|
2359
|
+
})
|
|
2360
|
+
}), /* @__PURE__ */ jsxs(PopoverContent, {
|
|
2361
|
+
align: "end",
|
|
2362
|
+
className: "w-72 gap-0 p-1",
|
|
2363
|
+
children: [
|
|
2364
|
+
manageableColumns.length > 10 && /* @__PURE__ */ jsx("div", {
|
|
2365
|
+
className: "px-1.5 pb-1",
|
|
2366
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
2367
|
+
"aria-label": "Search columns",
|
|
2368
|
+
placeholder: "Search columns...",
|
|
2369
|
+
value: columnSearchQuery,
|
|
2370
|
+
onChange: (event) => setColumnSearchQuery(event.target.value),
|
|
2371
|
+
className: "h-7",
|
|
2372
|
+
onKeyDown: (event) => event.stopPropagation()
|
|
2373
|
+
})
|
|
2374
|
+
}),
|
|
2375
|
+
/* @__PURE__ */ jsx("div", {
|
|
2376
|
+
className: "max-h-72 overflow-y-auto",
|
|
2377
|
+
children: (() => {
|
|
2378
|
+
const matchesSearch = (column) => columnLabel(column).toLowerCase().includes(columnSearchQuery.toLowerCase());
|
|
2379
|
+
const filteredLeft = displayedLeft.filter(matchesSearch);
|
|
2380
|
+
const filteredCenter = displayedCenter.filter(matchesSearch);
|
|
2381
|
+
const filteredRight = displayedRight.filter(matchesSearch);
|
|
2382
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2383
|
+
filteredLeft.map(renderColumnMenuRow),
|
|
2384
|
+
filteredLeft.length > 0 && (filteredCenter.length > 0 || filteredRight.length > 0) && /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border" }),
|
|
2385
|
+
filteredCenter.map(renderColumnMenuRow),
|
|
2386
|
+
filteredRight.length > 0 && (filteredLeft.length > 0 || filteredCenter.length > 0) && /* @__PURE__ */ jsx("div", { className: "my-1 border-t border-border" }),
|
|
2387
|
+
filteredRight.map(renderColumnMenuRow)
|
|
2388
|
+
] });
|
|
2389
|
+
})()
|
|
2390
|
+
}),
|
|
2391
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2392
|
+
className: "-mx-1 flex w-[calc(100%+--spacing(2))] items-center justify-between gap-2 border-t border-border px-1.5 pt-2 pb-1.5",
|
|
2393
|
+
children: [hasColumnCustomization ? /* @__PURE__ */ jsxs(Button, {
|
|
2394
|
+
type: "button",
|
|
2395
|
+
variant: "ghost",
|
|
2396
|
+
size: "xs",
|
|
2397
|
+
onClick: resetColumns,
|
|
2398
|
+
children: [/* @__PURE__ */ jsx(RotateCcwIcon, { "data-icon": "inline-start" }), "Reset to defaults"]
|
|
2399
|
+
}) : /* @__PURE__ */ jsx("span", {}), /* @__PURE__ */ jsxs("div", {
|
|
2400
|
+
className: "flex items-center gap-1",
|
|
2401
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
2402
|
+
type: "button",
|
|
2403
|
+
variant: "ghost",
|
|
2404
|
+
size: "xs",
|
|
2405
|
+
onClick: () => handleColumnsMenuOpenChange(false),
|
|
2406
|
+
children: "Cancel"
|
|
2407
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2408
|
+
type: "button",
|
|
2409
|
+
size: "xs",
|
|
2410
|
+
disabled: !hasPendingColumnsMenuChanges,
|
|
2411
|
+
onClick: () => {
|
|
2412
|
+
columnsMenuAppliedRef.current = true;
|
|
2413
|
+
handleColumnsMenuOpenChange(false);
|
|
2414
|
+
onColumnLayoutChange?.({
|
|
2415
|
+
visibility: columnVisibility,
|
|
2416
|
+
order: columnOrder,
|
|
2417
|
+
pinning: columnPinning,
|
|
2418
|
+
sizing: columnSizing
|
|
2419
|
+
});
|
|
2420
|
+
},
|
|
2421
|
+
children: "Apply"
|
|
2422
|
+
})]
|
|
2423
|
+
})]
|
|
2424
|
+
})
|
|
2425
|
+
]
|
|
2426
|
+
})]
|
|
2427
|
+
})
|
|
2428
|
+
]
|
|
2429
|
+
})]
|
|
2430
|
+
}),
|
|
2431
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2432
|
+
ref: tableContainerRef,
|
|
2433
|
+
tabIndex: canScrollHorizontally ? 0 : void 0,
|
|
2434
|
+
"aria-label": canScrollHorizontally ? `${ariaLabel ?? "Table"} scroll area` : void 0,
|
|
2435
|
+
className: cn(canScrollHorizontally && "focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none", "relative min-h-0", "-mx-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px)*2)] [scrollbar-width:thin] [scrollbar-color:var(--color-border)_transparent] overflow-auto", "[&_[data-slot=table-container]]:mx-0 [&_[data-slot=table-container]]:w-full [&_[data-slot=table-container]]:overflow-visible", size === "xs" ? "[&_tbody_:focus-visible]:scroll-mt-8" : size === "sm" ? "[&_tbody_:focus-visible]:scroll-mt-9" : "[&_tbody_:focus-visible]:scroll-mt-10", filtersPaneOpen && "mr-0 w-[calc(100%+var(--card-spacing,0px))]"),
|
|
2436
|
+
style: onLoadMore ? { maxHeight: maxHeight ?? "32rem" } : {
|
|
2437
|
+
...rowsAreaHeight === null ? void 0 : { height: rowsAreaHeight },
|
|
2438
|
+
...maxHeight ? { maxHeight } : void 0
|
|
2439
|
+
},
|
|
2440
|
+
children: [
|
|
2441
|
+
onRowClick && /* @__PURE__ */ jsx("span", {
|
|
2442
|
+
id: rowClickHintId,
|
|
2443
|
+
className: "sr-only",
|
|
2444
|
+
children: "Press Enter to view details"
|
|
2445
|
+
}),
|
|
2446
|
+
(loading || loadingMore) && /* @__PURE__ */ jsx("span", {
|
|
2447
|
+
role: "status",
|
|
2448
|
+
className: "sr-only",
|
|
2449
|
+
children: "Loading…"
|
|
2450
|
+
}),
|
|
2451
|
+
/* @__PURE__ */ jsx("span", {
|
|
2452
|
+
role: "status",
|
|
2453
|
+
className: "sr-only",
|
|
2454
|
+
children: liveAnnouncement
|
|
2455
|
+
}),
|
|
2456
|
+
/* @__PURE__ */ jsxs(Table, {
|
|
2457
|
+
size,
|
|
2458
|
+
"aria-label": ariaLabel,
|
|
2459
|
+
"aria-rowcount": filteredRowCount + headerGroupsForRender.length,
|
|
2460
|
+
"aria-colcount": visibleLeafColumns.length,
|
|
2461
|
+
className: cn("table-fixed border-separate border-spacing-0", striped && "[&_tbody_tr:nth-child(even)]:bg-muted/20", striped && "[&_tbody_tr:hover]:bg-muted/50!", cellDividers && "[&_td]:border-r [&_td:last-child]:border-r-0 [&_th]:border-r [&_th:last-child]:border-r-0"),
|
|
2462
|
+
children: [
|
|
2463
|
+
caption != null && /* @__PURE__ */ jsx(TableCaption, {
|
|
2464
|
+
className: cn(captionHidden && "sr-only"),
|
|
2465
|
+
children: caption
|
|
2466
|
+
}),
|
|
2467
|
+
/* @__PURE__ */ jsx("colgroup", { children: table.getHeaderGroups().at(-1)?.headers.filter((header) => !header.column.columnDef.meta?.filterOnly).map((header) => /* @__PURE__ */ jsx("col", { style: { width: header.column.getSize() } }, header.column.id)) }),
|
|
2468
|
+
/* @__PURE__ */ jsx(TableHeader, { children: headerGroupsForRender.map((headerGroup, headerGroupIndex) => /* @__PURE__ */ jsx(TableRow, { children: headerGroup.headers.filter((header) => !header.column.columnDef.meta?.filterOnly && !(headerGroupIndex > 0 && passthroughLeafIds.has(header.column.id))).map((header) => {
|
|
2469
|
+
const isPassthroughLeaf = passthroughLeafIds.has(header.column.id);
|
|
2470
|
+
const isRealGroupHeader = header.subHeaders.length > 0 && !isPassthroughLeaf;
|
|
2471
|
+
const isLeafHeader = !isRealGroupHeader && (!header.isPlaceholder || isPassthroughLeaf);
|
|
2472
|
+
const canSort = isLeafHeader && header.column.getCanSort();
|
|
2473
|
+
const sortDirection = header.column.getIsSorted();
|
|
2474
|
+
const canResize = isLeafHeader && header.column.getCanResize();
|
|
2475
|
+
const align = header.column.columnDef.meta?.align ?? "left";
|
|
2476
|
+
const headerControlsReveal = openHeaderColumnId === header.column.id ? "opacity-100" : hoverRevealClass;
|
|
2477
|
+
const setHeaderOpen = (open) => setOpenHeaderColumnId(open ? header.column.id : null);
|
|
2478
|
+
const headerActions = !isLeafHeader ? [] : header.column.columnDef.meta?.headerActions?.(header.column, table) ?? [];
|
|
2479
|
+
return /* @__PURE__ */ jsxs(TableHead, {
|
|
2480
|
+
"data-column-id": header.column.id,
|
|
2481
|
+
colSpan: header.colSpan,
|
|
2482
|
+
rowSpan: isPassthroughLeaf && headerGroupIndex === 0 ? headerGroupsForRender.length : void 0,
|
|
2483
|
+
"aria-sort": !canSort ? void 0 : sortDirection === "asc" ? "ascending" : sortDirection === "desc" ? "descending" : "none",
|
|
2484
|
+
style: {
|
|
2485
|
+
...stickyHeader && {
|
|
2486
|
+
position: "sticky",
|
|
2487
|
+
top: headerGroupIndex * headerRowHeightPx
|
|
2488
|
+
},
|
|
2489
|
+
...getPinnedStyle(header.column)
|
|
2490
|
+
},
|
|
2491
|
+
className: cn("group/header-actions relative", isRealGroupHeader && "border-x border-b border-border text-center", groupBoundaryColumnIds.starts.has(header.column.id) && "border-l border-border", groupBoundaryColumnIds.ends.has(header.column.id) && "border-r border-border", header.column.columnDef.meta?.mergeRepeatedValues && "border-x border-border", getPinnedClassName(header.column, table), getScrollFadeClassName(header.column, table, canScrollHorizontally), stickyHeader && "z-20 bg-background", stickyHeader && header.column.getIsPinned() && "z-30", isLeafHeader && aiColumnIds?.includes(header.column.id) && "border-t-2 border-t-ai-wash bg-ai-glow"),
|
|
2492
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2493
|
+
className: cn("flex items-center gap-0.5", align === "right" && "flex-row-reverse"),
|
|
2494
|
+
children: [
|
|
2495
|
+
/* @__PURE__ */ jsx("div", {
|
|
2496
|
+
className: cn("min-w-0 flex-1", align === "right" && "flex justify-end"),
|
|
2497
|
+
children: isRealGroupHeader ? /* @__PURE__ */ jsx("div", {
|
|
2498
|
+
className: "min-w-0 truncate text-center text-xs font-medium",
|
|
2499
|
+
children: flexRender(header.column.columnDef.header, header.getContext())
|
|
2500
|
+
}) : header.isPlaceholder && !isPassthroughLeaf ? null : canSort ? /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
2501
|
+
asChild: true,
|
|
2502
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
2503
|
+
type: "button",
|
|
2504
|
+
variant: "ghost",
|
|
2505
|
+
size: "xs",
|
|
2506
|
+
className: cn("h-7 max-w-full min-w-0 shrink! gap-1 px-2 font-medium", align === "right" ? "-mr-2" : "-ml-2"),
|
|
2507
|
+
onClick: header.column.getToggleSortingHandler(),
|
|
2508
|
+
children: [
|
|
2509
|
+
/* @__PURE__ */ jsx("span", {
|
|
2510
|
+
className: "min-w-0 truncate",
|
|
2511
|
+
children: flexRender(header.column.columnDef.header, header.getContext())
|
|
2512
|
+
}),
|
|
2513
|
+
enableMultiSort && sorting.length > 1 && sortDirection && (() => {
|
|
2514
|
+
const sortRank = sorting.findIndex((sort) => sort.id === header.column.id) + 1;
|
|
2515
|
+
return /* @__PURE__ */ jsx("span", {
|
|
2516
|
+
"aria-hidden": "true",
|
|
2517
|
+
className: "flex size-3.5 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-medium text-muted-foreground",
|
|
2518
|
+
children: sortRank
|
|
2519
|
+
});
|
|
2520
|
+
})(),
|
|
2521
|
+
sortDirection === "asc" ? /* @__PURE__ */ jsx(ArrowUpIcon, { className: "size-3.5 shrink-0" }) : sortDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { className: "size-3.5 shrink-0" }) : /* @__PURE__ */ jsx(ChevronsUpDownIcon, { className: "size-3.5 shrink-0 opacity-50" })
|
|
2522
|
+
]
|
|
2523
|
+
})
|
|
2524
|
+
}), /* @__PURE__ */ jsxs(TooltipContent, { children: [sortDirection === "asc" ? "Sort descending" : sortDirection === "desc" ? "Clear sort" : "Sort ascending", enableMultiSort && " — Shift+click to sort by multiple columns"] })] }) : /* @__PURE__ */ jsx("div", {
|
|
2525
|
+
className: "min-w-0 truncate text-xs",
|
|
2526
|
+
children: flexRender(header.column.columnDef.header, header.getContext())
|
|
2527
|
+
})
|
|
2528
|
+
}),
|
|
2529
|
+
isLeafHeader && mergedFilterColumns(header.column, table).some((c) => c.getCanFilter()) && /* @__PURE__ */ jsx(ColumnFilterTrigger, {
|
|
2530
|
+
column: header.column,
|
|
2531
|
+
table,
|
|
2532
|
+
revealClassName: headerControlsReveal,
|
|
2533
|
+
onOpenChange: setHeaderOpen,
|
|
2534
|
+
tableDeferred: filterApplyMode === "deferred",
|
|
2535
|
+
showFilterCounts,
|
|
2536
|
+
stickyFilterSelectAll
|
|
2537
|
+
}),
|
|
2538
|
+
headerActions.length > 0 && /* @__PURE__ */ jsx(RowActions, {
|
|
2539
|
+
actions: headerActions,
|
|
2540
|
+
revealClassName: headerControlsReveal,
|
|
2541
|
+
onOpenChange: setHeaderOpen
|
|
2542
|
+
}),
|
|
2543
|
+
isLeafHeader && aiColumnIds?.includes(header.column.id) && /* @__PURE__ */ jsxs(AILabel, { children: [/* @__PURE__ */ jsx(AIMarkerTrigger, {
|
|
2544
|
+
size,
|
|
2545
|
+
ariaLabel: `AI-generated column: ${columnLabel(header.column)}`
|
|
2546
|
+
}), /* @__PURE__ */ jsxs(AILabelContent, { children: [/* @__PURE__ */ jsx(PopoverHeader, { children: /* @__PURE__ */ jsx(PopoverTitle, { children: "Why you're seeing this" }) }), /* @__PURE__ */ jsx(PopoverDescription, { children: "This column's values are generated or suggested by AI." })] })] })
|
|
2547
|
+
]
|
|
2548
|
+
}), canResize && /* @__PURE__ */ jsx("div", {
|
|
2549
|
+
ref: (element) => {
|
|
2550
|
+
if (element) resizeHandleRefs.current.set(header.column.id, element);
|
|
2551
|
+
else resizeHandleRefs.current.delete(header.column.id);
|
|
2552
|
+
},
|
|
2553
|
+
onMouseDown: header.getResizeHandler(),
|
|
2554
|
+
onTouchStart: header.getResizeHandler(),
|
|
2555
|
+
onDoubleClick: () => fitColumnToContent(header.column, table),
|
|
2556
|
+
tabIndex: (activeResizeColumnId ?? resizableColumnIds[0]) === header.column.id ? 0 : -1,
|
|
2557
|
+
onFocus: () => setActiveResizeColumnId(header.column.id),
|
|
2558
|
+
onKeyDown: (event) => {
|
|
2559
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
2560
|
+
event.preventDefault();
|
|
2561
|
+
const index = resizableColumnIds.indexOf(header.column.id);
|
|
2562
|
+
const nextIndex = event.key === "ArrowDown" ? index + 1 : index - 1;
|
|
2563
|
+
const nextId = resizableColumnIds[nextIndex];
|
|
2564
|
+
if (!nextId) return;
|
|
2565
|
+
resizeHandleRefs.current.get(nextId)?.focus();
|
|
2566
|
+
setActiveResizeColumnId(nextId);
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
const minSize = header.column.columnDef.minSize ?? 20;
|
|
2570
|
+
const maxSize = header.column.columnDef.maxSize ?? Number.MAX_SAFE_INTEGER;
|
|
2571
|
+
const clamp = (size) => Math.min(maxSize, Math.max(minSize, size));
|
|
2572
|
+
let nextSize;
|
|
2573
|
+
if (event.key === "Home") nextSize = minSize;
|
|
2574
|
+
else if (event.key === "End") nextSize = maxSize < 1e6 ? maxSize : void 0;
|
|
2575
|
+
else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
2576
|
+
const step = event.shiftKey ? 50 : 10;
|
|
2577
|
+
const delta = event.key === "ArrowRight" ? step : -step;
|
|
2578
|
+
nextSize = clamp(header.getSize() + delta);
|
|
2579
|
+
}
|
|
2580
|
+
if (nextSize === void 0) return;
|
|
2581
|
+
event.preventDefault();
|
|
2582
|
+
setColumnSizing((prev) => ({
|
|
2583
|
+
...prev,
|
|
2584
|
+
[header.column.id]: nextSize
|
|
2585
|
+
}));
|
|
2586
|
+
},
|
|
2587
|
+
role: "separator",
|
|
2588
|
+
"aria-orientation": "vertical",
|
|
2589
|
+
"aria-label": `Resize ${columnLabel(header.column)}`,
|
|
2590
|
+
"aria-valuenow": header.getSize(),
|
|
2591
|
+
"aria-valuemin": header.column.columnDef.minSize ?? 20,
|
|
2592
|
+
"aria-valuemax": typeof header.column.columnDef.maxSize === "number" && header.column.columnDef.maxSize < 1e6 ? header.column.columnDef.maxSize : void 0,
|
|
2593
|
+
className: cn("absolute top-0 right-0 h-full w-1 cursor-col-resize touch-none select-none hover:bg-primary/50 focus-visible:bg-primary focus-visible:outline-none", header.column.getIsResizing() && "bg-primary")
|
|
2594
|
+
})]
|
|
2595
|
+
}, header.id);
|
|
2596
|
+
}) }, headerGroup.id)) }),
|
|
2597
|
+
/* @__PURE__ */ jsxs(TableBody, { children: [loading ? Array.from({ length: table.getState().pagination.pageSize }).map((_, rowIndex) => /* @__PURE__ */ jsx(TableRow, { children: visibleLeafColumns.map((_, columnIndex) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, columnIndex)) }, rowIndex)) : error ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
|
|
2598
|
+
colSpan: visibleLeafColumns.length,
|
|
2599
|
+
className: "h-56 text-center",
|
|
2600
|
+
children: /* @__PURE__ */ jsx(Empty, {
|
|
2601
|
+
role: "alert",
|
|
2602
|
+
children: /* @__PURE__ */ jsxs(EmptyHeader, { children: [
|
|
2603
|
+
/* @__PURE__ */ jsx(EmptyMedia, {
|
|
2604
|
+
variant: "icon",
|
|
2605
|
+
children: /* @__PURE__ */ jsx(CircleAlertIcon, {})
|
|
2606
|
+
}),
|
|
2607
|
+
/* @__PURE__ */ jsx(EmptyTitle, { children: "Failed to load data" }),
|
|
2608
|
+
/* @__PURE__ */ jsx(EmptyDescription, { children: errorDescription ?? (typeof error === "boolean" ? "Something went wrong. Try again." : error) })
|
|
2609
|
+
] })
|
|
2610
|
+
})
|
|
2611
|
+
}) }) : rows.length === 0 ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
|
|
2612
|
+
colSpan: visibleLeafColumns.length,
|
|
2613
|
+
className: "h-56 text-center",
|
|
2614
|
+
children: /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsxs(EmptyHeader, { children: [
|
|
2615
|
+
/* @__PURE__ */ jsx(EmptyMedia, {
|
|
2616
|
+
variant: "icon",
|
|
2617
|
+
children: /* @__PURE__ */ jsx(SearchXIcon, {})
|
|
2618
|
+
}),
|
|
2619
|
+
/* @__PURE__ */ jsx(EmptyTitle, { children: emptyTitle }),
|
|
2620
|
+
/* @__PURE__ */ jsx(EmptyDescription, { children: emptyDescription })
|
|
2621
|
+
] }) })
|
|
2622
|
+
}) }) : rows.map((row, rowIndex) => /* @__PURE__ */ jsxs(React.Fragment, { children: [/* @__PURE__ */ jsx(TableRow, {
|
|
2623
|
+
"aria-rowindex": (onLoadMore ? 0 : pageIndex * currentPageSize) + rowIndex + headerGroupsForRender.length + 1,
|
|
2624
|
+
"data-state": row.getIsSelected() ? "selected" : void 0,
|
|
2625
|
+
onClick: onRowClick ? () => onRowClick(row.original) : void 0,
|
|
2626
|
+
onKeyDown: onRowClick ? (event) => {
|
|
2627
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
2628
|
+
event.preventDefault();
|
|
2629
|
+
onRowClick(row.original);
|
|
2630
|
+
} : void 0,
|
|
2631
|
+
tabIndex: onRowClick ? 0 : void 0,
|
|
2632
|
+
"aria-describedby": onRowClick ? rowClickHintId : void 0,
|
|
2633
|
+
className: cn(onRowClick && "cursor-pointer outline-none focus-visible:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset", aiRowPredicate?.(row.original) && "bg-ai-glow", renderExpandedRow && row.getIsExpanded() && "border-b-0"),
|
|
2634
|
+
children: row.getVisibleCells().filter((cell) => !cell.column.columnDef.meta?.filterOnly && mergedCellRowSpans.get(cell.column.id)?.get(row.id) !== 0).map((cell, cellIndex) => {
|
|
2635
|
+
const alignRight = cell.column.columnDef.meta?.align === "right";
|
|
2636
|
+
const mergeRowSpan = mergedCellRowSpans.get(cell.column.id)?.get(row.id);
|
|
2637
|
+
const cellContent = aiCellPredicate?.(row.original, cell.column.id) ? /* @__PURE__ */ jsxs("span", {
|
|
2638
|
+
className: "inline-flex items-center gap-1",
|
|
2639
|
+
children: [flexRender(cell.column.columnDef.cell, cell.getContext()), /* @__PURE__ */ jsxs(AILabel, { children: [/* @__PURE__ */ jsx(AIMarkerTrigger, {
|
|
2640
|
+
size,
|
|
2641
|
+
ariaLabel: "AI-suggested value"
|
|
2642
|
+
}), /* @__PURE__ */ jsxs(AILabelContent, { children: [/* @__PURE__ */ jsx(PopoverHeader, { children: /* @__PURE__ */ jsx(PopoverTitle, { children: "Why you're seeing this" }) }), /* @__PURE__ */ jsx(PopoverDescription, { children: "This value is generated or suggested by AI." })] })] })]
|
|
2643
|
+
}) : flexRender(cell.column.columnDef.cell, cell.getContext());
|
|
2644
|
+
const needsPinnedAiGlow = cell.column.getIsPinned() === "left" && aiRowPredicate?.(row.original);
|
|
2645
|
+
return /* @__PURE__ */ jsx(TableCell, {
|
|
2646
|
+
"data-column-id": cell.column.id,
|
|
2647
|
+
rowSpan: mergeRowSpan && mergeRowSpan > 1 ? mergeRowSpan : void 0,
|
|
2648
|
+
style: {
|
|
2649
|
+
...getPinnedStyle(cell.column),
|
|
2650
|
+
...needsPinnedAiGlow && {
|
|
2651
|
+
backgroundSize: `${table.getTotalSize()}px 100%`,
|
|
2652
|
+
backgroundPositionX: `-${cell.column.getStart("left")}px`
|
|
2653
|
+
}
|
|
2654
|
+
},
|
|
2655
|
+
className: cn("overflow-hidden text-ellipsis", mergeRowSpan != null && "align-top", mergeRowSpan != null && "border-x border-border", groupBoundaryColumnIds.starts.has(cell.column.id) && "border-l border-border", groupBoundaryColumnIds.ends.has(cell.column.id) && "border-r border-border", getPinnedClassName(cell.column, table), getScrollFadeClassName(cell.column, table, canScrollHorizontally), aiColumnIds?.includes(cell.column.id) && "bg-ai-glow", cellIndex === 0 && aiRowPredicate?.(row.original) && "shadow-[inset_2px_0_0_0_var(--color-ai-wash)]", needsPinnedAiGlow && "bg-ai-glow"),
|
|
2656
|
+
children: alignRight ? /* @__PURE__ */ jsx("div", {
|
|
2657
|
+
className: "flex items-center justify-end",
|
|
2658
|
+
children: cellContent
|
|
2659
|
+
}) : cellContent
|
|
2660
|
+
}, cell.id);
|
|
2661
|
+
})
|
|
2662
|
+
}), renderExpandedRow && row.getIsExpanded() && /* @__PURE__ */ jsx(TableRow, {
|
|
2663
|
+
"data-slot": "data-table-expanded-row",
|
|
2664
|
+
id: `data-table-expanded-${row.id}`,
|
|
2665
|
+
children: /* @__PURE__ */ jsx(TableCell, {
|
|
2666
|
+
colSpan: visibleLeafColumns.length,
|
|
2667
|
+
className: "bg-muted/20 p-0",
|
|
2668
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2669
|
+
className: "ml-8 border-t border-border p-4",
|
|
2670
|
+
children: renderExpandedRow(row.original)
|
|
2671
|
+
})
|
|
2672
|
+
})
|
|
2673
|
+
})] }, row.id)), onLoadMore && hasMore && rows.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(TableRow, {
|
|
2674
|
+
ref: loadMoreSentinelRef,
|
|
2675
|
+
"aria-hidden": "true",
|
|
2676
|
+
"data-slot": "data-table-load-more-sentinel",
|
|
2677
|
+
className: "border-none",
|
|
2678
|
+
children: /* @__PURE__ */ jsx(TableCell, {
|
|
2679
|
+
colSpan: visibleLeafColumns.length,
|
|
2680
|
+
className: "h-px p-0"
|
|
2681
|
+
})
|
|
2682
|
+
}), loadingMore && Array.from({ length: 3 }).map((_, rowIndex) => /* @__PURE__ */ jsx(TableRow, { children: visibleLeafColumns.map((_, columnIndex) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-full" }) }, columnIndex)) }, `load-more-skeleton-${rowIndex}`))] })] })
|
|
2683
|
+
]
|
|
2684
|
+
}),
|
|
2685
|
+
bulkActionsBarMounted && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
2686
|
+
"aria-hidden": "true",
|
|
2687
|
+
style: { height: bulkActionsBarHeight + 8 }
|
|
2688
|
+
}), /* @__PURE__ */ jsx(BulkActionsBar, {
|
|
2689
|
+
ref: bulkActionsBarRef,
|
|
2690
|
+
count: selectedRows.length,
|
|
2691
|
+
hiddenByFilterCount,
|
|
2692
|
+
totalCount: selectableFilteredRows.length,
|
|
2693
|
+
isAllSelected: isAllFilteredSelected,
|
|
2694
|
+
maxSelectedRows,
|
|
2695
|
+
atSelectionCap,
|
|
2696
|
+
onSelectAll: selectAllFiltered,
|
|
2697
|
+
onClear: () => setRowSelection({}),
|
|
2698
|
+
controlSize,
|
|
2699
|
+
controlIconSize,
|
|
2700
|
+
children: renderBulkActions?.(selectedRows.map((row) => row.original), () => setRowSelection({}))
|
|
2701
|
+
})] })
|
|
2702
|
+
]
|
|
2703
|
+
}),
|
|
2704
|
+
showFooter && /* @__PURE__ */ jsxs("div", {
|
|
2705
|
+
ref: footerRef,
|
|
2706
|
+
"data-slot": "card-footer",
|
|
2707
|
+
className: cn("flex flex-wrap items-center gap-4 border-t border-border bg-muted/50 px-(--card-spacing,1rem) py-1.5", filtersPaneOpen ? "-ml-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px))] rounded-bl-xl" : "-mx-(--card-spacing,0px) w-[calc(100%+var(--card-spacing,0px)*2)] rounded-b-xl"),
|
|
2708
|
+
children: [
|
|
2709
|
+
/* @__PURE__ */ jsx("p", {
|
|
2710
|
+
role: "status",
|
|
2711
|
+
className: "text-sm whitespace-nowrap text-muted-foreground",
|
|
2712
|
+
children: filteredRowCount === 0 ? "0 items" : `${rangeStart}–${rangeEnd} of ${filteredRowCount} items`
|
|
2713
|
+
}),
|
|
2714
|
+
showPageSizeSelector && /* @__PURE__ */ jsxs("div", {
|
|
2715
|
+
className: "flex items-center gap-1.5 text-sm text-muted-foreground",
|
|
2716
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2717
|
+
className: "whitespace-nowrap",
|
|
2718
|
+
children: "Items per page:"
|
|
2719
|
+
}), /* @__PURE__ */ jsxs(Select, {
|
|
2720
|
+
value: String(currentPageSize),
|
|
2721
|
+
onValueChange: (value) => table.setPageSize(Number(value)),
|
|
2722
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
2723
|
+
size: controlSize,
|
|
2724
|
+
"aria-label": "Items per page",
|
|
2725
|
+
className: "h-auto border-none bg-transparent px-1 font-medium text-foreground",
|
|
2726
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
2727
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children: pageSizeOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, {
|
|
2728
|
+
value: String(option),
|
|
2729
|
+
children: option
|
|
2730
|
+
}, option)) })]
|
|
2731
|
+
})]
|
|
2732
|
+
}),
|
|
2733
|
+
table.getPageCount() > 1 && (paginationStyle === "numbered" ? /* @__PURE__ */ jsx("div", {
|
|
2734
|
+
className: "ml-auto border-l border-border pl-4",
|
|
2735
|
+
children: renderNumberedPagination()
|
|
2736
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
2737
|
+
className: "ml-auto flex items-center gap-3 border-l border-border pl-4",
|
|
2738
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2739
|
+
className: "flex items-center gap-1.5 text-sm text-muted-foreground",
|
|
2740
|
+
children: [renderPageSelect("h-auto border-none bg-transparent px-1 font-medium text-foreground"), /* @__PURE__ */ jsxs("span", {
|
|
2741
|
+
className: "whitespace-nowrap",
|
|
2742
|
+
children: [
|
|
2743
|
+
"of ",
|
|
2744
|
+
table.getPageCount(),
|
|
2745
|
+
" pages"
|
|
2746
|
+
]
|
|
2747
|
+
})]
|
|
2748
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2749
|
+
className: "flex items-center gap-0.5",
|
|
2750
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
2751
|
+
type: "button",
|
|
2752
|
+
variant: "ghost",
|
|
2753
|
+
size: controlIconSize,
|
|
2754
|
+
iconOnly: true,
|
|
2755
|
+
"aria-label": "Previous page",
|
|
2756
|
+
disabled: !table.getCanPreviousPage(),
|
|
2757
|
+
onClick: () => table.previousPage(),
|
|
2758
|
+
children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
|
|
2759
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2760
|
+
type: "button",
|
|
2761
|
+
variant: "ghost",
|
|
2762
|
+
size: controlIconSize,
|
|
2763
|
+
iconOnly: true,
|
|
2764
|
+
"aria-label": "Next page",
|
|
2765
|
+
disabled: !table.getCanNextPage(),
|
|
2766
|
+
onClick: () => table.nextPage(),
|
|
2767
|
+
children: /* @__PURE__ */ jsx(ChevronRightIcon, {})
|
|
2768
|
+
})]
|
|
2769
|
+
})]
|
|
2770
|
+
}))
|
|
2771
|
+
]
|
|
2772
|
+
})
|
|
2773
|
+
]
|
|
2774
|
+
}), filtersPaneOpen && /* @__PURE__ */ jsxs("div", {
|
|
2775
|
+
ref: filtersPaneRef,
|
|
2776
|
+
id: filtersPaneId,
|
|
2777
|
+
role: "region",
|
|
2778
|
+
"aria-labelledby": filtersPaneHeadingId,
|
|
2779
|
+
onKeyDown: (event) => {
|
|
2780
|
+
if (event.key !== "Escape") return;
|
|
2781
|
+
if (event.target.closest("[data-slot=popover-content], [role=listbox]")) return;
|
|
2782
|
+
setFiltersPaneOpen(false);
|
|
2783
|
+
},
|
|
2784
|
+
style: filtersPaneEdgeAdjust ? {
|
|
2785
|
+
marginTop: filtersPaneEdgeAdjust.marginTop,
|
|
2786
|
+
height: filtersPaneEdgeAdjust.height
|
|
2787
|
+
} : void 0,
|
|
2788
|
+
className: "-mr-(--card-spacing,0px) flex w-72 shrink-0 flex-col gap-4 border-l border-border bg-card p-(--card-spacing,1rem)",
|
|
2789
|
+
children: [
|
|
2790
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2791
|
+
className: "flex items-center justify-between gap-2",
|
|
2792
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
2793
|
+
id: filtersPaneHeadingId,
|
|
2794
|
+
className: "text-base font-medium text-foreground",
|
|
2795
|
+
children: "All filters"
|
|
2796
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2797
|
+
ref: filtersPaneCloseButtonRef,
|
|
2798
|
+
type: "button",
|
|
2799
|
+
variant: "ghost",
|
|
2800
|
+
size: "xs",
|
|
2801
|
+
iconOnly: true,
|
|
2802
|
+
"aria-label": "Close filters",
|
|
2803
|
+
onClick: () => setFiltersPaneOpen(false),
|
|
2804
|
+
children: /* @__PURE__ */ jsx(XIcon, {})
|
|
2805
|
+
})]
|
|
2806
|
+
}),
|
|
2807
|
+
/* @__PURE__ */ jsx("div", {
|
|
2808
|
+
className: "-m-1 flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-1",
|
|
2809
|
+
children: filterableColumns.map((column) => {
|
|
2810
|
+
const pinned = pinnedFilterIds.includes(column.id);
|
|
2811
|
+
const label = columnLabel(column);
|
|
2812
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2813
|
+
className: "group/filter-row flex flex-col gap-1.5",
|
|
2814
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2815
|
+
className: "flex items-center justify-between gap-2",
|
|
2816
|
+
children: [/* @__PURE__ */ jsxs(Label, { children: [label, !column.getIsVisible() && !column.columnDef.meta?.filterOnly && /* @__PURE__ */ jsx("span", {
|
|
2817
|
+
className: "ml-1 font-normal text-muted-foreground",
|
|
2818
|
+
children: "(hidden)"
|
|
2819
|
+
})] }), allowFilterPinning && !column.columnDef.meta?.filterOnly && /* @__PURE__ */ jsx(Button, {
|
|
2820
|
+
type: "button",
|
|
2821
|
+
variant: "ghost",
|
|
2822
|
+
size: "xs",
|
|
2823
|
+
iconOnly: true,
|
|
2824
|
+
"aria-label": pinned ? `Unpin ${label}` : `Pin ${label}`,
|
|
2825
|
+
className: hoverRevealFilterRowClass,
|
|
2826
|
+
onClick: () => toggleFilterPin(column.id),
|
|
2827
|
+
children: pinned ? /* @__PURE__ */ jsx(PinOffIcon, { className: "size-3.5" }) : /* @__PURE__ */ jsx(PinIcon, { className: "size-3.5" })
|
|
2828
|
+
})]
|
|
2829
|
+
}), /* @__PURE__ */ jsx(PaneColumnFilterField, {
|
|
2830
|
+
column,
|
|
2831
|
+
showFilterCounts,
|
|
2832
|
+
tableDeferred: filterApplyMode === "deferred",
|
|
2833
|
+
stickyFilterSelectAll
|
|
2834
|
+
})]
|
|
2835
|
+
}, column.id);
|
|
2836
|
+
})
|
|
2837
|
+
}),
|
|
2838
|
+
/* @__PURE__ */ jsx("div", {
|
|
2839
|
+
className: "-mx-(--card-spacing,1rem) -mb-(--card-spacing,1rem) flex w-[calc(100%+var(--card-spacing,1rem)*2)] items-center justify-center border-t border-border px-(--card-spacing,1rem) py-1.5",
|
|
2840
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
2841
|
+
type: "button",
|
|
2842
|
+
variant: "ghost",
|
|
2843
|
+
size: controlSize,
|
|
2844
|
+
className: "w-full",
|
|
2845
|
+
disabled: columnFilters.length === 0,
|
|
2846
|
+
onClick: () => setColumnFilters([]),
|
|
2847
|
+
children: "Clear filters"
|
|
2848
|
+
})
|
|
2849
|
+
})
|
|
2850
|
+
]
|
|
2851
|
+
})]
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
/**
|
|
2855
|
+
* WHEN TO USE
|
|
2856
|
+
* - Tabular data that needs sorting, search, column filters, selectable
|
|
2857
|
+
* rows with bulk actions, row actions, expandable detail rows, row-click
|
|
2858
|
+
* navigation, AI affordances, column management, or pagination handled
|
|
2859
|
+
* for you — an admin list, a resource picker, anything past "just render
|
|
2860
|
+
* some rows."
|
|
2861
|
+
*
|
|
2862
|
+
* NOT WHEN
|
|
2863
|
+
* - A fixed, small table with no interaction beyond display — use Table
|
|
2864
|
+
* directly, it's lighter and this adds nothing over it.
|
|
2865
|
+
* - You need virtualization or server-driven async fetching — neither is
|
|
2866
|
+
* built in this pass. Virtualization has no existing MMDS primitive to
|
|
2867
|
+
* lean on and would need a further headless dependency (e.g.
|
|
2868
|
+
* `@tanstack/react-virtual`) — out of scope here. (Column resize is
|
|
2869
|
+
* already built — `enableColumnResizing` — this isn't that.)
|
|
2870
|
+
*
|
|
2871
|
+
* KEY RULES
|
|
2872
|
+
* - `columns`/`data` follow `@tanstack/react-table`'s `ColumnDef<TData>[]`
|
|
2873
|
+
* shape directly — this component doesn't wrap or reinvent that API, it
|
|
2874
|
+
* renders it. Read TanStack Table's own docs for `accessorKey`, custom
|
|
2875
|
+
* `cell` renderers, `enableSorting: false` per column, etc.
|
|
2876
|
+
* - Sorting, filtering, search, selection, expansion, column order/
|
|
2877
|
+
* visibility, and pagination state all live inside DataTable (via
|
|
2878
|
+
* `useReactTable`) — an exception to MMDS's usual fully-uncontrolled
|
|
2879
|
+
* convention, because the whole point of this component is to own that
|
|
2880
|
+
* state so you don't have to. `onRowSelectionChange` is one escape
|
|
2881
|
+
* hatch, for wiring selection to something outside (a bulk delete call, a
|
|
2882
|
+
* parent form); `onStateChange` is another, firing sorting/columnFilters/
|
|
2883
|
+
* globalFilter live for the same reason (a URL param, analytics, a
|
|
2884
|
+
* server-driven refetch) — read-only, no live two-way binding, but
|
|
2885
|
+
* paired with `defaultSorting`/`defaultColumnFilters`/`defaultGlobalFilter`
|
|
2886
|
+
* (seeding each on first render) it's enough to round-trip state out to
|
|
2887
|
+
* a URL and back in on next mount. `defaultPinnedFilterIds` seeds the
|
|
2888
|
+
* toolbar's inline-filter set the same way — all four still fully
|
|
2889
|
+
* uncontrolled after first render, same as everything else here.
|
|
2890
|
+
* - Columns render at a fixed width (`table-fixed` + a `<colgroup>` driven by
|
|
2891
|
+
* each column's `ColumnDef.size`, default 150) instead of auto-fitting to
|
|
2892
|
+
* content — set `size` per column for anything narrower/wider than that.
|
|
2893
|
+
* This trades content-driven auto-sizing for widths that stay put across
|
|
2894
|
+
* sorting, filtering, and pagination.
|
|
2895
|
+
* - The toolbar carries the same edge-to-edge treatment as `Table` itself
|
|
2896
|
+
* (see Table's own KEY RULES) — its outer box matches the table's wider
|
|
2897
|
+
* footprint instead of looking narrower next to it, and its own padding
|
|
2898
|
+
* keeps search/filter/Columns content at the same inset as the table's
|
|
2899
|
+
* cell content. Automatic, no prop for it.
|
|
2900
|
+
* - Toolbar controls (search, inline/"All filters" filters, Columns) and
|
|
2901
|
+
* footer controls (items-per-page, page-jump, Previous/Next) render one
|
|
2902
|
+
* size notch below `size` itself — "default" gets "sm" controls, "sm"
|
|
2903
|
+
* gets "xs", "xs" stays "xs" (already the floor). They're always-visible,
|
|
2904
|
+
* repetitive chrome, not row content competing for the same emphasis —
|
|
2905
|
+
* automatic, not a prop, and it scales with `size` rather than being
|
|
2906
|
+
* fixed at one density. The `aiAction` button is the deliberate exception
|
|
2907
|
+
* — it's meant to stand out, so it stays at `size` itself.
|
|
2908
|
+
* - `loading` swaps every cell for a `Skeleton` shaped to the real columns —
|
|
2909
|
+
* don't also filter `data` to empty while loading, pass the real (stale
|
|
2910
|
+
* or previous) data and let `loading` cover it, or column widths jump.
|
|
2911
|
+
* - `error` (a boolean) swaps the whole body for an Empty-based failure
|
|
2912
|
+
* state — same composition the error-state pattern uses elsewhere
|
|
2913
|
+
* (Patterns/UI States), just pre-wired here — and `errorDescription`
|
|
2914
|
+
* supplies its copy (default "Something went wrong. Try again."). Two
|
|
2915
|
+
* props on purpose: whether it failed and what to say are different
|
|
2916
|
+
* things; passing the copy as `error` itself still works but is
|
|
2917
|
+
* deprecated. A plain empty `data` array
|
|
2918
|
+
* (not loading, no error) renders the "no results" Empty state instead.
|
|
2919
|
+
* - `enableRowSelection` injects a checkbox column automatically — don't
|
|
2920
|
+
* add your own selection column to `columns`.
|
|
2921
|
+
* - Row actions are just a normal column whose `cell` renders the exported
|
|
2922
|
+
* `RowActions` helper (inline icon buttons for ≤2 actions, an overflow
|
|
2923
|
+
* menu past that) — there's no dedicated `actions` prop, this is the same
|
|
2924
|
+
* "put a Button in a cell" technique any custom `cell` already uses.
|
|
2925
|
+
* `RowActions` stops its own clicks from bubbling, so it's safe inside a
|
|
2926
|
+
* row that also sets `onRowClick`.
|
|
2927
|
+
* - `renderExpandedRow` adds a leading expand/collapse toggle to every row;
|
|
2928
|
+
* expanding inserts a full-width row below it with whatever you return —
|
|
2929
|
+
* a detail card, or a nested table of its own. The toggle stops its click
|
|
2930
|
+
* from bubbling, so it composes with `onRowClick` too.
|
|
2931
|
+
* - `onRowClick` makes the whole row clickable and keyboard-activatable
|
|
2932
|
+
* (Enter/Space), for row→detail navigation. It composes with
|
|
2933
|
+
* `enableRowSelection` — the selection checkbox stops its own click from
|
|
2934
|
+
* bubbling up, so checking a row never also triggers `onRowClick`. Any
|
|
2935
|
+
* other interactive element you put inside a cell (a button, a link)
|
|
2936
|
+
* needs its own `stopPropagation` the same way if it shouldn't also
|
|
2937
|
+
* trigger the row click.
|
|
2938
|
+
* - `striped` and `cellDividers` are independent visual modifiers — zebra
|
|
2939
|
+
* row tinting and vertical column rules, respectively — for tables where
|
|
2940
|
+
* plain row/column boundaries are hard to scan.
|
|
2941
|
+
* - AI presence has a distinct answer at each level, but DataTable's own
|
|
2942
|
+
* outer surface never takes a border/shadow of its own — there's no `ai`
|
|
2943
|
+
* boolean prop. For whole-table AI presence, the table just sits inside
|
|
2944
|
+
* an AI Card — the exact same recipe AI/Card's "BoundaryTreatments" story uses (a
|
|
2945
|
+
* `bg-gradient-to-r from-ai-wash to-ai-grad-from p-px` wrapper div, not
|
|
2946
|
+
* a one-off DataTable-specific border) — plus a soft inward glow rising
|
|
2947
|
+
* from the Card's own bottom edge (`bg-ai-glow-bottom`, fading out at 15%
|
|
2948
|
+
* of the Card's own height) as a plain Card className — a background-image,
|
|
2949
|
+
* so it layers over Card's own `bg-card` (`cn` registers the glow classes
|
|
2950
|
+
* as background-image utilities so tailwind-merge keeps both).
|
|
2951
|
+
* `AILabel` sits in the
|
|
2952
|
+
* Card's top corner (a `CardAction`) — see the "WithAIWholeTable" story.
|
|
2953
|
+
* The whole-table treatment is a boundary (gradient outline), an inward
|
|
2954
|
+
* bottom glow and a corner marker — not the row/column glow used below.
|
|
2955
|
+
* Below that: `aiAction` (table level)
|
|
2956
|
+
* renders a `variant="ai"` toolbar button; `aiRowPredicate` (row level)
|
|
2957
|
+
* puts a leading `AILabel` as the very first thing in the row — ahead
|
|
2958
|
+
* of the expand toggle and selection checkbox, its own leading slot —
|
|
2959
|
+
* plus a colored left edge (an inset box-shadow on the row's first
|
|
2960
|
+
* cell, not `border-l` on the `<tr>` — the table is `border-separate`
|
|
2961
|
+
* for pinned columns, and in that model a `<tr>` never paints borders)
|
|
2962
|
+
* and a soft inward glow (`bg-ai-glow`, fading out at 20% of the row's own
|
|
2963
|
+
* width, a background-image, not a solid fill) bleeding in from that
|
|
2964
|
+
* same left edge — plus a second copy of that same glow painted directly
|
|
2965
|
+
* on every left-pinned cell (however many — the injected AI column and a
|
|
2966
|
+
* genuinely pinned column can both land in that same sticky group, see
|
|
2967
|
+
* `effectiveColumnPinning` above), needed because pinning makes each
|
|
2968
|
+
* `position: sticky` with its own opaque background, so the `<tr>`'s copy
|
|
2969
|
+
* (anchored to the row's original, unscrolled left edge) would otherwise
|
|
2970
|
+
* end up hidden behind them once the table scrolls horizontally. Each
|
|
2971
|
+
* copy is sized via an explicit `backgroundSize` (the row's real total
|
|
2972
|
+
* width, from `table.getTotalSize()`, not that cell's own narrower one)
|
|
2973
|
+
* and shifted by `backgroundPositionX: -column.getStart("left")` so it
|
|
2974
|
+
* shows the exact slice of that same gradient that falls at its actual
|
|
2975
|
+
* position in the row — the whole pinned group reads as one continuous
|
|
2976
|
+
* glow, pixel-identical to the unpinned case, rather than a smaller
|
|
2977
|
+
* version of it; `aiColumnIds` (column level)
|
|
2978
|
+
* adds the same left-edge glow down the whole column, header cell
|
|
2979
|
+
* included (every cell in a column shares the same width, so 20% of it
|
|
2980
|
+
* resolves to the same px value down the whole column, tiling into one
|
|
2981
|
+
* continuous glow with no visible seam), a colored top rule on that
|
|
2982
|
+
* column's header cell
|
|
2983
|
+
* (mirrored by the row's own left edge above), and a leading
|
|
2984
|
+
* `AILabel` (generic copy) automatically in that header;
|
|
2985
|
+
* `aiCellPredicate` (cell level) adds just a trailing `AILabel` inline
|
|
2986
|
+
* after the value — no glow, no border, since a single AI-touched value
|
|
2987
|
+
* doesn't need its own boundary treatment, only the explainability
|
|
2988
|
+
* entry point. Every `AILabelTrigger` here follows DataTable's own
|
|
2989
|
+
* density prop — "default" at default density, "sm" at both sm and xs
|
|
2990
|
+
* — never AILabel's `iconOnly`, which is opt-in only for when a
|
|
2991
|
+
* designer explicitly asks for it. Give an AI-marked column enough
|
|
2992
|
+
* width for its title plus the visible "AI" marker (the header keeps
|
|
2993
|
+
* both; the title truncates first). Row/column backgrounds use
|
|
2994
|
+
* `background-image` rather than `box-shadow` specifically for the row
|
|
2995
|
+
* case — `box-shadow` doesn't paint reliably on `<tr>` in every
|
|
2996
|
+
* browser, `background-image` does. Compose `AILabel` directly into a
|
|
2997
|
+
* column's own `header`/`cell` instead of `aiColumnIds` when a column
|
|
2998
|
+
* needs its own specific explanation text.
|
|
2999
|
+
* - `meta: { align: "left" | "right" }` (default `"left"`) aligns both a
|
|
3000
|
+
* column's header and its cell content — the guideline is quantitative
|
|
3001
|
+
* content (amounts, counts, percentages, anything meant to be compared
|
|
3002
|
+
* or summed down the column) gets `"right"`, text stays `"left"`. Cell
|
|
3003
|
+
* alignment uses `flex justify-end`, not just `text-align`, so it also
|
|
3004
|
+
* right-aligns a custom `cell` that renders its own Badge/icon cluster,
|
|
3005
|
+
* not only plain text. `"right"` mirrors the header too — the sort
|
|
3006
|
+
* button's label moves to that side and the filter icon/`headerActions`
|
|
3007
|
+
* cluster moves to the *other* side (`flex-row-reverse` on the same row,
|
|
3008
|
+
* not a second layout to maintain), so a numeric column's controls sit
|
|
3009
|
+
* off to the side its digits don't occupy.
|
|
3010
|
+
* - Search matches only `searchableColumnIds` (default: the first two
|
|
3011
|
+
* entries in `columns`) — set it explicitly to widen/narrow the scope.
|
|
3012
|
+
* Matches highlight automatically for plain columns (no custom `cell`);
|
|
3013
|
+
* for a column with a custom `cell`, highlight it yourself with the
|
|
3014
|
+
* exported `HighlightMatch`, reading the query off
|
|
3015
|
+
* `info.table.getState().globalFilter`.
|
|
3016
|
+
* - Every column gets a filter icon in its header cell. In the toolbar,
|
|
3017
|
+
* filters show inline right after search — all of them if there are 3 or
|
|
3018
|
+
* fewer filterable columns, otherwise just the pinned ones (the first two
|
|
3019
|
+
* by default, and however many the user pins from the pane after that —
|
|
3020
|
+
* uncapped, the row wraps onto more lines rather than overflowing) plus
|
|
3021
|
+
* an "All filters" button, shown whenever there are more than 3
|
|
3022
|
+
* filterable columns at all — regardless of how many are currently
|
|
3023
|
+
* pinned inline, even every one of them, since the pane is still the
|
|
3024
|
+
* only place to *unpin* one. That button opens a docked pane, not a
|
|
3025
|
+
* floating overlay — the table area (`flex-1`) shrinks to make room for
|
|
3026
|
+
* it on the surface DataTable sits on, instead of the pane covering the
|
|
3027
|
+
* table, and reaches that surface's true top/right/bottom edge (measured
|
|
3028
|
+
* in JS against the nearest Card, since a header/footer's height isn't a
|
|
3029
|
+
* fixed token) while never growing taller than that surface — its own
|
|
3030
|
+
* filter-list area is a `min-h-0` flex child so a long list scrolls
|
|
3031
|
+
* internally instead of pushing the pane past the surface's edge. While
|
|
3032
|
+
* the pane is open, the inline pinned filter buttons and the "All
|
|
3033
|
+
* filters" trigger both hide — every filterable column and its current
|
|
3034
|
+
* value already lives in the pane itself, each with a pin/unpin toggle
|
|
3035
|
+
* (hover/keyboard-focus-revealed by default, like the header cell's own
|
|
3036
|
+
* controls — see `revealColumnControlsOnHover`), so there's one place to
|
|
3037
|
+
* look, not two copies of the same control. `allowFilterPinning` (default
|
|
3038
|
+
* `true`) turns that toggle off entirely for a designer-fixed toolbar —
|
|
3039
|
+
* `defaultPinnedFilterIds` (or its first-two default) becomes the
|
|
3040
|
+
* permanent inline set, with no user-facing way to change which columns
|
|
3041
|
+
* those are; every other filterable column still lives in the pane, just
|
|
3042
|
+
* with nothing to move it out. Both the inline pinned chips and the pane
|
|
3043
|
+
* list order their filterable columns by the table's own column order by
|
|
3044
|
+
* default; set `meta: { filterOrder: N }` on a `ColumnDef` to decouple a
|
|
3045
|
+
* filter's display position from where its column currently sits — handy
|
|
3046
|
+
* when drag-and-drop/the Columns menu can reorder columns but a filter
|
|
3047
|
+
* should stay put regardless. Once
|
|
3048
|
+
* any filter is set, the toolbar shows "N filters applied" (every
|
|
3049
|
+
* applied filter, inline pinned ones included, not just pane-only ones)
|
|
3050
|
+
* next to a "Clear filters" button that clears all of them the same way
|
|
3051
|
+
* — the pane's own footer "Clear filters" does the same, just from
|
|
3052
|
+
* inside the pane. Default filter is text-contains; set
|
|
3053
|
+
* `meta: { filterVariant: "select" }` on a `ColumnDef` for a
|
|
3054
|
+
* multi-select filter instead — a checklist in the header icon's own
|
|
3055
|
+
* popover; a `SelectMultiple` for the inline pinned button (its
|
|
3056
|
+
* trigger's placeholder is the column label, its value only ever the
|
|
3057
|
+
* first selected value's label, `maxLabelChars={12}` ellipsis-truncating
|
|
3058
|
+
* that one past 12 characters even with just one selected, plus "+N"
|
|
3059
|
+
* once more than one is — so neither a single long value nor a long list
|
|
3060
|
+
* can run the toolbar out of room) and for the pane (which holds every
|
|
3061
|
+
* filterable column at once). A multiselect filter (`"select"` or
|
|
3062
|
+
* `"range"`) always requires a deliberate Apply — in every surface,
|
|
3063
|
+
* regardless of `filterApplyMode` — since checking several boxes is a
|
|
3064
|
+
* set to review before committing, not a single complete thought; picks
|
|
3065
|
+
* still show as checked/selected immediately, on the trigger and in the
|
|
3066
|
+
* list both — only the real filtering of the table's rows waits for
|
|
3067
|
+
* Apply, not the visible selection itself. Closed (nothing open to
|
|
3068
|
+
* preview into), the trigger always reflects whatever's actually
|
|
3069
|
+
* committed. Its Cancel/Apply footer always lives inside that one
|
|
3070
|
+
* field's own popover — the header icon's, the inline pinned button's,
|
|
3071
|
+
* or (in the pane) that field's own, not a pane-wide batch, even though
|
|
3072
|
+
* the pane holds every filterable column open at once; each field there
|
|
3073
|
+
* resolves independently, exactly like the toolbar's. `filterVariant:
|
|
3074
|
+
* "singleSelect"` picks exactly one value from that same kind of list —
|
|
3075
|
+
* `SelectMultiple` capped at one value (`closeOnSelect`), in every
|
|
3076
|
+
* surface including the pane, not a native-feeling dropdown — and,
|
|
3077
|
+
* since it closes the instant a pick is made, is never itself deferred
|
|
3078
|
+
* regardless of `filterApplyMode` (no open popover left by the time a
|
|
3079
|
+
* footer could matter). `meta: { filterRender: (props) => node }` is
|
|
3080
|
+
* the escape hatch past those four built-in shapes — a date-range
|
|
3081
|
+
* picker, a slider, a radio group, colored selection badges, anything a
|
|
3082
|
+
* designer's own filter needs — given the same `{ column, value,
|
|
3083
|
+
* onValueChange }` every built-in variant already receives. Deferred
|
|
3084
|
+
* under `filterApplyMode="deferred"` in every surface, not just the
|
|
3085
|
+
* header-icon popover: the inline toolbar button wraps it in a Popover +
|
|
3086
|
+
* the same shared Cancel/Apply footer text filters get there; the pane —
|
|
3087
|
+
* which has no popover for any variant — adds an inline Cancel/Apply row
|
|
3088
|
+
* directly under the rendered content instead. Immediate, every surface
|
|
3089
|
+
* renders it with no extra chrome, same as always.
|
|
3090
|
+
* - `meta: { filterOnly: true }` defines a column purely to filter a data
|
|
3091
|
+
* point that isn't shown anywhere in the table — its `header`/`cell`
|
|
3092
|
+
* never render, and unlike a plain hidden column it has no checkbox
|
|
3093
|
+
* anywhere to show it (excluded from the Columns menu entirely, not just
|
|
3094
|
+
* defaulted off), while still showing up as a normal filterable column in
|
|
3095
|
+
* the "All filters" pane — never inline in the toolbar, and never
|
|
3096
|
+
* pinnable there either (no header cell for either to point back at).
|
|
3097
|
+
* Its presence alone forces the pane to exist even when 3 or fewer
|
|
3098
|
+
* *other* columns are filterable, since the pane is its only access
|
|
3099
|
+
* point.
|
|
3100
|
+
* - `meta: { filterMergeWith: [...ids] }` folds other columns' filter
|
|
3101
|
+
* fields into *this* column's own header-icon popover instead of each
|
|
3102
|
+
* getting a separate toolbar/pane entry — for a cell whose custom `cell`
|
|
3103
|
+
* already displays more than one filterable value at once (e.g. a name
|
|
3104
|
+
* plus an MRN below it), so filtering either reads as one decision from
|
|
3105
|
+
* one icon. Pairs naturally with `filterOnly` on the merged column(s),
|
|
3106
|
+
* since the value being merged in is usually already folded into this
|
|
3107
|
+
* column's own `cell` rather than shown in a column of its own — but
|
|
3108
|
+
* that's a convention, not a requirement. Each merged field renders
|
|
3109
|
+
* below this column's own (always first, unlabeled), under a divider,
|
|
3110
|
+
* labeled with its own `header`. The icon fills, and the
|
|
3111
|
+
* `filterApplyMode="deferred"` footer commits/discards, based on any of
|
|
3112
|
+
* them having an active filter — not just this column's own.
|
|
3113
|
+
* - Column reordering lives only in the Columns menu, not the header
|
|
3114
|
+
* cells themselves — drag a row there (a thin primary-colored line
|
|
3115
|
+
* marks exactly where it'll land) or use its Move left/Move right
|
|
3116
|
+
* buttons — real, individually focusable/clickable buttons, so no
|
|
3117
|
+
* custom keyboard handling is needed for them to be keyboard-operable,
|
|
3118
|
+
* unlike the mouse-only drag. They say left/right, not up/down, even
|
|
3119
|
+
* though the list itself is vertical — that's what the button actually
|
|
3120
|
+
* does to the real table, which is what the label should describe, not
|
|
3121
|
+
* the popover's own incidental layout. Every one of these buttons —
|
|
3122
|
+
* Move left/right, and (once a column can pin) Pin left/Pin right or
|
|
3123
|
+
* "Unpin" using the same icons `columnMenuActions` renders in the
|
|
3124
|
+
* header — is hover/keyboard-focus-revealed by default (see
|
|
3125
|
+
* `revealColumnControlsOnHover`) and carries its own Tooltip labelling
|
|
3126
|
+
* it. Once any column is pinned, the list splits into up to three
|
|
3127
|
+
* sections — pinned left, unpinned/center, pinned right, each separated
|
|
3128
|
+
* by its own divider — mirroring the same grouping the real header/body
|
|
3129
|
+
* cells render in; reordering (drag or the Move buttons) stays confined
|
|
3130
|
+
* to one section at a time, matching what a plain visual drag could
|
|
3131
|
+
* mean anyway. Moving a column *into* or *out of* a pinned section is
|
|
3132
|
+
* what the Pin left/Pin right/Unpin buttons are for instead. Past 10
|
|
3133
|
+
* manageable columns the list grows a search box and scrolls instead of
|
|
3134
|
+
* pushing the menu taller. Visibility, order, and pinning changes apply
|
|
3135
|
+
* live as you make them (so the menu's own rows and the real table
|
|
3136
|
+
* never disagree), but the menu's footer still offers Cancel (reverts
|
|
3137
|
+
* all three to whatever was committed when the menu opened, including
|
|
3138
|
+
* if you dismiss it by clicking outside or Escape) and Apply (disabled
|
|
3139
|
+
* until at least one of the three has actually changed since the menu
|
|
3140
|
+
* opened) — so the whole session reads as one reviewable batch of
|
|
3141
|
+
* changes, not silent live edits. "Reset to defaults" sits in that same
|
|
3142
|
+
* footer, only rendered once order, hidden columns, or pinning have
|
|
3143
|
+
* actually changed from default — a different comparison than Apply's
|
|
3144
|
+
* (against the dev-configured default, not against this session's own
|
|
3145
|
+
* starting point).
|
|
3146
|
+
* - `lockedColumnIds` marks a column as fully static: the user can't
|
|
3147
|
+
* reorder it (shows a lock glyph instead of a drag handle, and isn't a
|
|
3148
|
+
* valid drop target) or hide it (the show/hide checkbox doesn't even
|
|
3149
|
+
* render — `enableHiding: false` is forced regardless of what the
|
|
3150
|
+
* column's own `ColumnDef` says). Every injected column (selection,
|
|
3151
|
+
* expand, AI) is always locked. Column-visibility control (the checkbox
|
|
3152
|
+
* on each row) also only lists columns that don't set
|
|
3153
|
+
* `enableHiding: false` themselves — non-hideable columns still show up
|
|
3154
|
+
* for reordering.
|
|
3155
|
+
* - Column pinning: set `meta: { pin: "left" | "right" }` on a `ColumnDef`
|
|
3156
|
+
* to seed a column's *initial* pinned side. From there it's live,
|
|
3157
|
+
* user-togglable `@tanstack/react-table` state — wire the exported
|
|
3158
|
+
* `columnMenuActions` into `meta.headerActions` for a ready-made Pin
|
|
3159
|
+
* left/Pin right toggle (see below), or call `column.pin(...)` from any
|
|
3160
|
+
* custom action of your own. Any number of columns can pin to the same
|
|
3161
|
+
* side at once — they render as a contiguous group at that edge, in the
|
|
3162
|
+
* order they were pinned (reorderable afterward from the Columns menu,
|
|
3163
|
+
* confined to that group — see above), with a divider towards whichever
|
|
3164
|
+
* edge actually borders the scrollable/center columns (the left group's
|
|
3165
|
+
* right edge, the right group's left edge) on top of the solid
|
|
3166
|
+
* background every pinned cell already gets so scrolled content doesn't
|
|
3167
|
+
* show through it. Set `meta: { pinLocked: true }` to keep a column's
|
|
3168
|
+
* pin fully dev-controlled with no user-facing toggle (`columnMenuActions`
|
|
3169
|
+
* omits both Pin actions for it — a custom `headerActions` entry could
|
|
3170
|
+
* still call `column.pin()` on it directly, since this only suppresses
|
|
3171
|
+
* the built-in affordance, not the underlying state). A pinned column
|
|
3172
|
+
* renders `position: sticky` at the correct cumulative offset, so it
|
|
3173
|
+
* stays in view while the rest of the table scrolls horizontally —
|
|
3174
|
+
* hover/selected/striped row tinting won't show through a pinned cell
|
|
3175
|
+
* either, a known trade-off of frozen columns. Pinning a real column left
|
|
3176
|
+
* also pulls the injected AI/expand/selection columns (whichever are
|
|
3177
|
+
* present) into that same left-pinned group ahead of it, live — so they
|
|
3178
|
+
* stay genuinely first and sticky too, matching their own "leading slot"
|
|
3179
|
+
* documentation, instead of being pushed behind a real pinned column the
|
|
3180
|
+
* moment one exists. Only kicks in once something's actually pinned left;
|
|
3181
|
+
* otherwise these render exactly as before (unpinned, first in the
|
|
3182
|
+
* unpinned/center group by definition order — no sticky, no divider).
|
|
3183
|
+
* Nothing to configure — this is automatic and not itself user-togglable.
|
|
3184
|
+
* - "Sticky first column" isn't its own prop — it's this same pinning
|
|
3185
|
+
* recipe, combined with `lockedColumnIds` so the user can't unpin or
|
|
3186
|
+
* reorder it away from that edge: `defaultColumnPinning={{ left: ["id"] }}`
|
|
3187
|
+
* plus `lockedColumnIds={["id"]}` (see `ColumnManagement`).
|
|
3188
|
+
* - `meta.headerActions` adds custom actions to a column's header cell,
|
|
3189
|
+
* next to its sort button and filter icon — hidden until that header
|
|
3190
|
+
* cell is hovered or keyboard-focused (see `revealColumnControlsOnHover`
|
|
3191
|
+
* below), and rendered with the same exported `RowActions` helper row
|
|
3192
|
+
* actions use (inline for ≤2 actions, an overflow menu past that),
|
|
3193
|
+
* uniformly for every column including pinned ones. It's a function of
|
|
3194
|
+
* `(column, table)`, not a static array, so it can reflect live
|
|
3195
|
+
* per-column state. The exported `columnMenuActions` is the ready-made
|
|
3196
|
+
* bundle for this slot — Pin left/Pin right (each becomes "Unpin" once
|
|
3197
|
+
* pinned to that side, and both are omitted for a `pinLocked` column),
|
|
3198
|
+
* Hide column, and Fit to content, each omitted when the column can't
|
|
3199
|
+
* do it. Being 3-4 actions, it renders as a single overflow "more" menu
|
|
3200
|
+
* in practice — the same shape as a plain custom `headerActions`
|
|
3201
|
+
* function returning your own `RowAction[]` would, if you need
|
|
3202
|
+
* something `columnMenuActions` doesn't cover.
|
|
3203
|
+
* - `revealColumnControlsOnHover` (default `true`) is the one knob for
|
|
3204
|
+
* whether the filter icon, `headerActions`/`columnMenuActions`, and the
|
|
3205
|
+
* Columns menu's own pin/move controls stay hidden until hovered or
|
|
3206
|
+
* keyboard-focused, or render always-visible — set `false` for a
|
|
3207
|
+
* consumer whose final UX calls for every control visible up front.
|
|
3208
|
+
* - `enableColumnResizing` adds a drag handle to every resizable column's
|
|
3209
|
+
* right border (`ColumnDef.enableResizing: false` exempts one; injected
|
|
3210
|
+
* columns already are). It's also what unlocks `columnMenuActions`' "Fit
|
|
3211
|
+
* to content" — that action measures every currently-rendered cell in
|
|
3212
|
+
* the column (this page's rows, not the whole dataset) via `scrollWidth`
|
|
3213
|
+
* and sets the column to the widest one found.
|
|
3214
|
+
* - Bulk actions while any row is selected land in one of two places,
|
|
3215
|
+
* never both:
|
|
3216
|
+
* - `onLoadMore` (infinite scroll, no footer): the toolbar stays
|
|
3217
|
+
* mounted and interactive (search, filters) — searching/filtering
|
|
3218
|
+
* the rows loaded so far while some of them are selected is exactly
|
|
3219
|
+
* the "load more, then narrow down what's loaded" flow this mode is
|
|
3220
|
+
* for. Instead, a floating bar hovers over the table's own bottom
|
|
3221
|
+
* edge (never the raw viewport). Three sections divided by a line:
|
|
3222
|
+
* count + "Select all" (every filtered row across everything loaded
|
|
3223
|
+
* so far, not just what's rendered), `renderBulkActions`'s own
|
|
3224
|
+
* product-configured actions in the middle, and a close button that
|
|
3225
|
+
* clears the selection. Scrolling to the last row while any row is
|
|
3226
|
+
* selected reveals extra bottom padding sized to the bar's own
|
|
3227
|
+
* height — otherwise the last row(s) would stay hidden behind it
|
|
3228
|
+
* permanently.
|
|
3229
|
+
* - Paginated (a real footer): no floating bar, and the toolbar's own
|
|
3230
|
+
* search/filter controls are swapped out in place for the selection
|
|
3231
|
+
* itself — count, "Select all" (every filtered row across every
|
|
3232
|
+
* page, not just the current one — same as infinite scroll's own),
|
|
3233
|
+
* `renderBulkActions`'s actions, then a close button. Search/filters
|
|
3234
|
+
* act on rows that may not even be the ones selected once paging is
|
|
3235
|
+
* involved, so showing both the selection and a still-live search
|
|
3236
|
+
* box read as two competing toolbars — this is the table's original
|
|
3237
|
+
* selection behavior, kept for exactly that reason.
|
|
3238
|
+
* - `getRowId` (e.g. `(row) => row.id`) gives each row a stable identity
|
|
3239
|
+
* across a sort/filter/pagination change, passed straight through to
|
|
3240
|
+
* `@tanstack/react-table` — without it, a row's identity defaults to its
|
|
3241
|
+
* index, which is meaningless the moment the row set changes underneath
|
|
3242
|
+
* it (filtering 100 rows down to 30 shouldn't treat "row 3" before and
|
|
3243
|
+
* after as the same thing unless it actually is).
|
|
3244
|
+
* - Footer is a full-width bar with the current range/total first, then
|
|
3245
|
+
* "Items per page" (`pageSizeOptions`, default `[10, 20, 30]`; set
|
|
3246
|
+
* `showPageSizeSelector={false}` to drop it — a design call for tables
|
|
3247
|
+
* where the consumer shouldn't be choosing page size, not a user
|
|
3248
|
+
* preference), and — past one page — a jump-to-page select plus
|
|
3249
|
+
* Previous/Next. It carries the same
|
|
3250
|
+
* edge-to-edge treatment as the toolbar and `Table` itself, but on all
|
|
3251
|
+
* three sides it borders — left, right, *and* bottom — so on a Card it
|
|
3252
|
+
* reads as that Card's own footer (same border-t/bg-muted/rounded-b-xl
|
|
3253
|
+
* look as a real CardFooter, and it borrows CardFooter's own
|
|
3254
|
+
* `data-slot` so a surrounding Card zeroes its bottom padding for it
|
|
3255
|
+
* too).
|
|
3256
|
+
* - `onLoadMore` switches to infinite scroll instead of pagination: every
|
|
3257
|
+
* row in `data` renders (no client-side page slicing), there's no footer
|
|
3258
|
+
* at all, and the table scrolls inside its own `maxHeight`-bounded
|
|
3259
|
+
* container (default `32rem`) instead of the page — the header (and any
|
|
3260
|
+
* pinned columns) stay locked in place via `position: sticky` while the
|
|
3261
|
+
* body scrolls. `onLoadMore` fires once an invisible sentinel below the
|
|
3262
|
+
* last row scrolls near-into view. Fetch the next page and append it to
|
|
3263
|
+
* `data` yourself; `hasMore` stops rendering the sentinel once there's
|
|
3264
|
+
* nothing left, and `loadingMore` renders 3 skeleton rows below the last
|
|
3265
|
+
* real one — same shape as `loading`, not a spinner — while a fetch is
|
|
3266
|
+
* in flight. Sorting/filtering still run client-side over whatever's
|
|
3267
|
+
* currently loaded — an inherent limitation with partial data, not
|
|
3268
|
+
* something this mode solves.
|
|
3269
|
+
*
|
|
3270
|
+
* USAGE
|
|
3271
|
+
* const columns: ColumnDef<Invoice>[] = [
|
|
3272
|
+
* { accessorKey: "id", header: "Invoice" },
|
|
3273
|
+
* { accessorKey: "amount", header: "Amount" },
|
|
3274
|
+
* { accessorKey: "status", header: "Status", meta: { filterVariant: "select" } },
|
|
3275
|
+
* ]
|
|
3276
|
+
*
|
|
3277
|
+
* <DataTable columns={columns} data={invoices} aria-label="Invoices" />
|
|
3278
|
+
*
|
|
3279
|
+
* // Selectable rows with a bulk-delete action — the count (swapped
|
|
3280
|
+
* // into the toolbar here, since this table paginates), "Select all",
|
|
3281
|
+
* // and the close/clear button are all built in; this only fills the
|
|
3282
|
+
* // actions themselves
|
|
3283
|
+
* <DataTable
|
|
3284
|
+
* columns={columns}
|
|
3285
|
+
* data={invoices}
|
|
3286
|
+
* enableRowSelection
|
|
3287
|
+
* renderBulkActions={(selected, clear) => (
|
|
3288
|
+
* <Button size="sm" variant="ghost" onClick={() => { onDelete(selected); clear() }}>
|
|
3289
|
+
* <TrashIcon />
|
|
3290
|
+
* Delete
|
|
3291
|
+
* </Button>
|
|
3292
|
+
* )}
|
|
3293
|
+
* />
|
|
3294
|
+
*
|
|
3295
|
+
* // Row actions (inline for ≤2, overflow menu for >2)
|
|
3296
|
+
* const withActions: ColumnDef<Invoice>[] = [
|
|
3297
|
+
* ...columns,
|
|
3298
|
+
* {
|
|
3299
|
+
* id: "actions",
|
|
3300
|
+
* header: "",
|
|
3301
|
+
* size: 72,
|
|
3302
|
+
* enableSorting: false,
|
|
3303
|
+
* enableHiding: false,
|
|
3304
|
+
* enableColumnFilter: false,
|
|
3305
|
+
* cell: ({ row }) => (
|
|
3306
|
+
* <RowActions
|
|
3307
|
+
* actions={[
|
|
3308
|
+
* { label: "Edit", icon: PencilIcon, onClick: () => onEdit(row.original) },
|
|
3309
|
+
* { label: "Delete", icon: TrashIcon, variant: "destructive", onClick: () => onDelete(row.original) },
|
|
3310
|
+
* ]}
|
|
3311
|
+
* />
|
|
3312
|
+
* ),
|
|
3313
|
+
* },
|
|
3314
|
+
* ]
|
|
3315
|
+
*
|
|
3316
|
+
* // Header-cell actions: a fully custom one, for anything columnMenuActions
|
|
3317
|
+
* // doesn't cover
|
|
3318
|
+
* const withHeaderActions: ColumnDef<Invoice>[] = columns.map((col) =>
|
|
3319
|
+
* col.id === "status"
|
|
3320
|
+
* ? {
|
|
3321
|
+
* ...col,
|
|
3322
|
+
* meta: {
|
|
3323
|
+
* ...col.meta,
|
|
3324
|
+
* headerActions: () => [
|
|
3325
|
+
* { label: "Search", icon: SearchIcon, onClick: () => openColumnSearch("status") },
|
|
3326
|
+
* ],
|
|
3327
|
+
* },
|
|
3328
|
+
* }
|
|
3329
|
+
* : col
|
|
3330
|
+
* )
|
|
3331
|
+
*
|
|
3332
|
+
* // Column resizing (drag handles) plus the ready-made Pin left/Pin
|
|
3333
|
+
* // right/Hide/Fit-to-content menu on every column
|
|
3334
|
+
* const withColumnMenu: ColumnDef<Invoice>[] = columns.map((col) => ({
|
|
3335
|
+
* ...col,
|
|
3336
|
+
* meta: { ...col.meta, headerActions: columnMenuActions },
|
|
3337
|
+
* }))
|
|
3338
|
+
* <DataTable columns={withColumnMenu} data={invoices} enableColumnResizing />
|
|
3339
|
+
*
|
|
3340
|
+
* // Right-align a quantitative column — header and cells both flip
|
|
3341
|
+
* const withAlignedAmount: ColumnDef<Invoice>[] = columns.map((col) =>
|
|
3342
|
+
* col.id === "amount"
|
|
3343
|
+
* ? { ...col, meta: { ...col.meta, align: "right" } }
|
|
3344
|
+
* : col
|
|
3345
|
+
* )
|
|
3346
|
+
*
|
|
3347
|
+
* // Expandable detail row, striping, and column dividers
|
|
3348
|
+
* <DataTable
|
|
3349
|
+
* columns={columns}
|
|
3350
|
+
* data={invoices}
|
|
3351
|
+
* renderExpandedRow={(invoice) => <InvoiceDetailCard invoice={invoice} />}
|
|
3352
|
+
* striped
|
|
3353
|
+
* cellDividers
|
|
3354
|
+
* />
|
|
3355
|
+
*
|
|
3356
|
+
* // AI presence at every level (except whole-table — wrap in a Card for that, see below)
|
|
3357
|
+
* <DataTable
|
|
3358
|
+
* columns={columns}
|
|
3359
|
+
* data={invoices}
|
|
3360
|
+
* aiAction={{ label: "Summarize", onClick: () => runAISummary(invoices) }}
|
|
3361
|
+
* aiRowPredicate={(invoice) => invoice.flaggedByAI}
|
|
3362
|
+
* aiColumnIds={["amount"]}
|
|
3363
|
+
* aiCellPredicate={(invoice, columnId) => columnId === "customer" && invoice.customerSuggestedByAI}
|
|
3364
|
+
* />
|
|
3365
|
+
*
|
|
3366
|
+
* // Whole-table AI presence: the table just sits inside an AI Card (the
|
|
3367
|
+
* // same recipe AI/Card's "BoundaryTreatments" story uses) plus a soft glow rising from
|
|
3368
|
+
* // the Card's own bottom edge (bg-ai-glow-bottom, a plain Card className) —
|
|
3369
|
+
* // AILabel in its top corner, never on DataTable's own surface
|
|
3370
|
+
* <div className="rounded-xl bg-gradient-to-r from-ai-wash to-ai-grad-from p-px">
|
|
3371
|
+
* <Card className="w-full bg-ai-glow-bottom">
|
|
3372
|
+
* <CardHeader>
|
|
3373
|
+
* <CardTitle>Invoices</CardTitle>
|
|
3374
|
+
* <CardAction>
|
|
3375
|
+
* <AILabel>
|
|
3376
|
+
* <AILabelTrigger />
|
|
3377
|
+
* <AILabelContent>...</AILabelContent>
|
|
3378
|
+
* </AILabel>
|
|
3379
|
+
* </CardAction>
|
|
3380
|
+
* </CardHeader>
|
|
3381
|
+
* <CardContent>
|
|
3382
|
+
* <DataTable columns={columns} data={invoices} />
|
|
3383
|
+
* </CardContent>
|
|
3384
|
+
* </Card>
|
|
3385
|
+
* </div>
|
|
3386
|
+
*
|
|
3387
|
+
* // Loading and error states
|
|
3388
|
+
* <DataTable columns={columns} data={[]} loading />
|
|
3389
|
+
* <DataTable columns={columns} data={[]} error errorDescription="Couldn't reach the server." />
|
|
3390
|
+
*
|
|
3391
|
+
* // Clickable rows (e.g. navigate to a detail view), a locked first column,
|
|
3392
|
+
* // and a wider items-per-page range
|
|
3393
|
+
* <DataTable
|
|
3394
|
+
* columns={columns}
|
|
3395
|
+
* data={invoices}
|
|
3396
|
+
* onRowClick={(invoice) => router.push(`/invoices/${invoice.id}`)}
|
|
3397
|
+
* lockedColumnIds={["id"]}
|
|
3398
|
+
* pageSizeOptions={[10, 25, 50]}
|
|
3399
|
+
* />
|
|
3400
|
+
*
|
|
3401
|
+
* // Drop the "Items per page" control from the static footer — page size
|
|
3402
|
+
* // stays fixed at whatever `pageSize` is, not something the user picks
|
|
3403
|
+
* <DataTable columns={columns} data={invoices} showPageSizeSelector={false} />
|
|
3404
|
+
*
|
|
3405
|
+
* // Infinite scroll instead of pagination — no footer
|
|
3406
|
+
* const [invoices, setInvoices] = useState(firstPage)
|
|
3407
|
+
* const [loadingMore, setLoadingMore] = useState(false)
|
|
3408
|
+
* <DataTable
|
|
3409
|
+
* columns={columns}
|
|
3410
|
+
* data={invoices}
|
|
3411
|
+
* hasMore={invoices.length < total}
|
|
3412
|
+
* loadingMore={loadingMore}
|
|
3413
|
+
* onLoadMore={async () => {
|
|
3414
|
+
* setLoadingMore(true)
|
|
3415
|
+
* setInvoices((current) => [...current, ...await fetchNextPage(current.length)])
|
|
3416
|
+
* setLoadingMore(false)
|
|
3417
|
+
* }}
|
|
3418
|
+
* />
|
|
3419
|
+
*/
|
|
3420
|
+
//#endregion
|
|
3421
|
+
export { DataTable, HighlightMatch, RowActions, columnMenuActions };
|
|
3422
|
+
|
|
3423
|
+
//# sourceMappingURL=data-table.js.map
|