@hilum/ui 3.8.6 → 3.10.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 +32 -1
- package/dist/{chunk-RI6QJR2L.js → chunk-R576DV2A.js} +120 -14
- package/dist/chunk-R576DV2A.js.map +1 -0
- package/dist/create-theme.js +2 -1
- package/dist/create-theme.js.map +1 -1
- package/dist/index.d.ts +733 -68
- package/dist/index.js +3097 -1619
- package/dist/index.js.map +1 -1
- package/dist/tokens.css +163 -19
- package/dist/tokens.d.ts +69 -5
- package/dist/tokens.js +1 -1
- package/llms.txt +11 -4
- package/package.json +11 -10
- package/src/components/accordion.tsx +2 -2
- package/src/components/account-menu.tsx +1 -1
- package/src/components/alert-dialog.tsx +107 -0
- package/src/components/ask-user-questions.tsx +24 -18
- package/src/components/badge.tsx +36 -3
- package/src/components/breadcrumb.tsx +4 -2
- package/src/components/button-group.tsx +1 -1
- package/src/components/button.tsx +31 -23
- package/src/components/calendar.tsx +1 -1
- package/src/components/card.tsx +4 -2
- package/src/components/checkbox-card.tsx +1 -1
- package/src/components/checkbox-group.tsx +2 -1
- package/src/components/code-block.tsx +99 -0
- package/src/components/color-input.tsx +9 -6
- package/src/components/color-picker.tsx +38 -26
- package/src/components/combobox.tsx +59 -12
- package/src/components/command-palette.tsx +150 -42
- package/src/components/command.tsx +5 -3
- package/src/components/context-menu.tsx +18 -15
- package/src/components/contextual-save-bar.tsx +156 -0
- package/src/components/data-table.tsx +34 -5
- package/src/components/date-picker.tsx +275 -28
- package/src/components/date-text.tsx +90 -0
- package/src/components/dropdown-menu.tsx +11 -8
- package/src/components/dropdown.tsx +6 -3
- package/src/components/empty-state.tsx +85 -20
- package/src/components/file-dropzone.tsx +2 -1
- package/src/components/file-thumbnail.tsx +84 -20
- package/src/components/filter-bar.tsx +91 -0
- package/src/components/grid-list.tsx +2 -2
- package/src/components/help-tooltip.tsx +1 -1
- package/src/components/input-copy.tsx +1 -1
- package/src/components/input-group.tsx +1 -1
- package/src/components/input-message.tsx +5 -4
- package/src/components/input-number.tsx +103 -11
- package/src/components/input.tsx +5 -3
- package/src/components/label.tsx +1 -0
- package/src/components/media-asset-card.tsx +1 -1
- package/src/components/menu-item.tsx +37 -3
- package/src/components/menubar.tsx +21 -18
- package/src/components/native-select.tsx +1 -1
- package/src/components/navigation-menu.tsx +2 -2
- package/src/components/notification.tsx +1 -1
- package/src/components/pagination.tsx +97 -3
- package/src/components/popover.tsx +2 -0
- package/src/components/property-row.tsx +7 -4
- package/src/components/radio-card.tsx +1 -1
- package/src/components/radio-group.tsx +2 -1
- package/src/components/rating.tsx +136 -0
- package/src/components/resizable.tsx +1 -1
- package/src/components/resource-item.tsx +133 -0
- package/src/components/rich-text-editor.tsx +1 -0
- package/src/components/search-input.tsx +98 -0
- package/src/components/searchable-table.tsx +10 -11
- package/src/components/select.tsx +28 -9
- package/src/components/sidebar.tsx +4 -4
- package/src/components/slider.tsx +78 -36
- package/src/components/sonner.tsx +18 -4
- package/src/components/stat-card.tsx +121 -37
- package/src/components/status-badge.tsx +142 -47
- package/src/components/status-tile.tsx +9 -0
- package/src/components/switch.tsx +6 -5
- package/src/components/tabs-subtle.tsx +10 -2
- package/src/components/tabs.tsx +23 -3
- package/src/components/textarea.tsx +2 -3
- package/src/components/thinking-steps.tsx +3 -3
- package/src/components/toggle.tsx +14 -13
- package/src/components/tooltip.tsx +11 -8
- package/src/components/url-redirect-prompt.tsx +0 -1
- package/src/index.ts +15 -0
- package/src/lib/density-context.tsx +65 -0
- package/src/lib/format.tsx +301 -0
- package/src/lib/interaction.ts +12 -4
- package/src/lib/scroll-fade.tsx +39 -0
- package/src/tokens/tokens.test.ts +77 -0
- package/src/tokens/tokens.ts +114 -5
- package/dist/chunk-RI6QJR2L.js.map +0 -1
|
@@ -125,6 +125,7 @@ const CommandList = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDi
|
|
|
125
125
|
};
|
|
126
126
|
|
|
127
127
|
return (
|
|
128
|
+
// eslint-disable-next-line jsx-a11y/interactive-supports-focus -- focus lives on the options (roving tabindex); the listbox only delegates Arrow keys
|
|
128
129
|
<div
|
|
129
130
|
ref={combinedRef}
|
|
130
131
|
role="listbox"
|
|
@@ -197,6 +198,7 @@ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
|
|
|
197
198
|
};
|
|
198
199
|
|
|
199
200
|
return (
|
|
201
|
+
// eslint-disable-next-line jsx-a11y/interactive-supports-focus -- enabled options get tabIndex=0; disabled options are intentionally unfocusable
|
|
200
202
|
<div
|
|
201
203
|
ref={ref}
|
|
202
204
|
role="option"
|
|
@@ -205,10 +207,10 @@ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
|
|
|
205
207
|
data-cmd-item
|
|
206
208
|
tabIndex={disabled ? undefined : 0}
|
|
207
209
|
className={cn(
|
|
208
|
-
"relative mx-1 flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2",
|
|
210
|
+
"relative mx-1 flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 compact:min-h-7 compact:px-2 compact:py-1 compact:text-[13px] compact:rounded-[4px]",
|
|
209
211
|
"body text-muted-foreground outline-none transition-colors",
|
|
210
212
|
"hover:bg-muted hover:text-foreground",
|
|
211
|
-
"focus:bg-
|
|
213
|
+
"focus:bg-active focus:text-foreground",
|
|
212
214
|
disabled && "pointer-events-none opacity-40",
|
|
213
215
|
className,
|
|
214
216
|
)}
|
|
@@ -230,7 +232,7 @@ CommandItem.displayName = "CommandItem";
|
|
|
230
232
|
|
|
231
233
|
const CommandSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
232
234
|
({ className, ...props }, ref) => (
|
|
233
|
-
<div ref={ref} className={cn("my-1 h-px bg-
|
|
235
|
+
<div ref={ref} className={cn("my-1 h-px bg-border", className)} {...props} />
|
|
234
236
|
),
|
|
235
237
|
);
|
|
236
238
|
CommandSeparator.displayName = "CommandSeparator";
|
|
@@ -4,6 +4,7 @@ import * as React from "react";
|
|
|
4
4
|
import { ContextMenu } from "radix-ui";
|
|
5
5
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
|
6
6
|
import { cn } from "../lib/utils";
|
|
7
|
+
import { useDensityAttributes } from "../lib/density-context";
|
|
7
8
|
import {
|
|
8
9
|
mobilePopperSheetMotionClassName,
|
|
9
10
|
mobilePopperSheetPositionClassName,
|
|
@@ -26,6 +27,7 @@ const ContextMenuContent = React.forwardRef<
|
|
|
26
27
|
<style>{mobilePopperSheetStyle}</style>
|
|
27
28
|
<ContextMenu.Portal>
|
|
28
29
|
<ContextMenu.Content
|
|
30
|
+
{...useDensityAttributes()}
|
|
29
31
|
ref={ref}
|
|
30
32
|
data-hilum-mobile-sheet="true"
|
|
31
33
|
className={cn(
|
|
@@ -58,13 +60,13 @@ const ContextMenuItem = React.forwardRef<
|
|
|
58
60
|
<ContextMenu.Item
|
|
59
61
|
ref={ref}
|
|
60
62
|
className={cn(
|
|
61
|
-
"relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2",
|
|
63
|
+
"relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 compact:min-h-7 compact:px-2 compact:py-1 compact:text-[13px] compact:rounded-[4px]",
|
|
62
64
|
"body outline-none transition-colors",
|
|
63
65
|
destructive
|
|
64
66
|
? "text-destructive focus:bg-destructive/10 focus:text-destructive"
|
|
65
|
-
: "text-
|
|
67
|
+
: "text-foreground focus:bg-active",
|
|
66
68
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
67
|
-
inset && "pl-8",
|
|
69
|
+
inset && "pl-8 compact:pl-7",
|
|
68
70
|
className,
|
|
69
71
|
)}
|
|
70
72
|
{...props}
|
|
@@ -79,9 +81,9 @@ const ContextMenuCheckboxItem = React.forwardRef<
|
|
|
79
81
|
<ContextMenu.CheckboxItem
|
|
80
82
|
ref={ref}
|
|
81
83
|
className={cn(
|
|
82
|
-
"relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5",
|
|
83
|
-
"body text-
|
|
84
|
-
"focus:bg-
|
|
84
|
+
"relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5 compact:min-h-7 compact:py-1 compact:pl-7 compact:pr-2 compact:text-[13px] compact:rounded-[4px]",
|
|
85
|
+
"body text-foreground outline-none transition-colors",
|
|
86
|
+
"focus:bg-active focus:text-foreground",
|
|
85
87
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
86
88
|
className,
|
|
87
89
|
)}
|
|
@@ -104,9 +106,9 @@ const ContextMenuRadioItem = React.forwardRef<
|
|
|
104
106
|
<ContextMenu.RadioItem
|
|
105
107
|
ref={ref}
|
|
106
108
|
className={cn(
|
|
107
|
-
"relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5",
|
|
108
|
-
"body text-
|
|
109
|
-
"focus:bg-
|
|
109
|
+
"relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md py-2 pl-8 pr-2.5 compact:min-h-7 compact:py-1 compact:pl-7 compact:pr-2 compact:text-[13px] compact:rounded-[4px]",
|
|
110
|
+
"body text-foreground outline-none transition-colors",
|
|
111
|
+
"focus:bg-active focus:text-foreground",
|
|
110
112
|
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
111
113
|
className,
|
|
112
114
|
)}
|
|
@@ -130,7 +132,7 @@ const ContextMenuLabel = React.forwardRef<
|
|
|
130
132
|
>(({ className, inset, ...props }, ref) => (
|
|
131
133
|
<ContextMenu.Label
|
|
132
134
|
ref={ref}
|
|
133
|
-
className={cn("px-2.5 py-1 label text-muted-foreground", inset && "pl-8", className)}
|
|
135
|
+
className={cn("px-2.5 py-1 label text-muted-foreground compact:px-2 compact:text-[11px]", inset && "pl-8 compact:pl-7", className)}
|
|
134
136
|
{...props}
|
|
135
137
|
/>
|
|
136
138
|
));
|
|
@@ -142,7 +144,7 @@ const ContextMenuSeparator = React.forwardRef<
|
|
|
142
144
|
>(({ className, ...props }, ref) => (
|
|
143
145
|
<ContextMenu.Separator
|
|
144
146
|
ref={ref}
|
|
145
|
-
className={cn("mx-1 my-1 h-px bg-
|
|
147
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
146
148
|
{...props}
|
|
147
149
|
/>
|
|
148
150
|
));
|
|
@@ -157,10 +159,10 @@ const ContextMenuSubTrigger = React.forwardRef<
|
|
|
157
159
|
<ContextMenu.SubTrigger
|
|
158
160
|
ref={ref}
|
|
159
161
|
className={cn(
|
|
160
|
-
"relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2",
|
|
161
|
-
"body text-
|
|
162
|
-
"focus:bg-
|
|
163
|
-
inset && "pl-8",
|
|
162
|
+
"relative flex min-h-10 cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 compact:min-h-7 compact:px-2 compact:py-1 compact:text-[13px] compact:rounded-[4px]",
|
|
163
|
+
"body text-foreground outline-none transition-colors",
|
|
164
|
+
"focus:bg-active focus:text-foreground data-[state=open]:bg-active",
|
|
165
|
+
inset && "pl-8 compact:pl-7",
|
|
164
166
|
className,
|
|
165
167
|
)}
|
|
166
168
|
{...props}
|
|
@@ -178,6 +180,7 @@ const ContextMenuSubContent = React.forwardRef<
|
|
|
178
180
|
<>
|
|
179
181
|
<style>{mobilePopperSheetStyle}</style>
|
|
180
182
|
<ContextMenu.SubContent
|
|
183
|
+
{...useDensityAttributes()}
|
|
181
184
|
ref={ref}
|
|
182
185
|
data-hilum-mobile-sheet="true"
|
|
183
186
|
className={cn(
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
|
5
|
+
import { CircleAlert } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import { spring } from "../lib/springs";
|
|
8
|
+
import { Button } from "./button";
|
|
9
|
+
|
|
10
|
+
interface ContextualSaveBarProps {
|
|
11
|
+
/** Show the bar — usually `form.isDirty`. */
|
|
12
|
+
open: boolean;
|
|
13
|
+
/** Message on the leading edge. Default: "Unsaved changes". */
|
|
14
|
+
message?: React.ReactNode;
|
|
15
|
+
/** Primary action. */
|
|
16
|
+
onSave?: () => void;
|
|
17
|
+
/** Secondary action — resets the form to its saved state. */
|
|
18
|
+
onDiscard?: () => void;
|
|
19
|
+
saveLabel?: React.ReactNode;
|
|
20
|
+
discardLabel?: React.ReactNode;
|
|
21
|
+
/** Shows a spinner on Save and disables both actions. */
|
|
22
|
+
saving?: boolean;
|
|
23
|
+
/** Disable Save (e.g. while the form is invalid). */
|
|
24
|
+
saveDisabled?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Wire the Save button to a `<form id>` instead of `onSave` — the button
|
|
27
|
+
* becomes `type="submit" form={formId}` so native validation still runs.
|
|
28
|
+
*/
|
|
29
|
+
formId?: string;
|
|
30
|
+
/**
|
|
31
|
+
* `fixed` pins the bar to the viewport (default: top on desktop,
|
|
32
|
+
* bottom on mobile for thumb reach). `sticky` keeps it inside its scroll
|
|
33
|
+
* container — useful inside a settings pane or a dialog.
|
|
34
|
+
*/
|
|
35
|
+
position?: "fixed" | "sticky";
|
|
36
|
+
/** Extra content between the message and the actions (e.g. a "Preview" link). */
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Contextual save bar — the "you have unsaved changes" strip that replaces
|
|
43
|
+
* per-card Save buttons on edit screens (Shopify's contextual save bar).
|
|
44
|
+
* Announces itself politely to assistive tech and binds ⌘S / Ctrl+S to Save
|
|
45
|
+
* while open.
|
|
46
|
+
*/
|
|
47
|
+
function ContextualSaveBar({
|
|
48
|
+
open,
|
|
49
|
+
message = "Unsaved changes",
|
|
50
|
+
onSave,
|
|
51
|
+
onDiscard,
|
|
52
|
+
saveLabel = "Save",
|
|
53
|
+
discardLabel = "Discard",
|
|
54
|
+
saving = false,
|
|
55
|
+
saveDisabled = false,
|
|
56
|
+
formId,
|
|
57
|
+
position = "fixed",
|
|
58
|
+
children,
|
|
59
|
+
className,
|
|
60
|
+
}: ContextualSaveBarProps) {
|
|
61
|
+
const reduceMotion = useReducedMotion();
|
|
62
|
+
const saveRef = React.useRef<HTMLButtonElement>(null);
|
|
63
|
+
|
|
64
|
+
React.useEffect(() => {
|
|
65
|
+
if (!open) return;
|
|
66
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
67
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
if (!saving && !saveDisabled) saveRef.current?.click();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener("keydown", onKeyDown);
|
|
73
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
74
|
+
}, [open, saving, saveDisabled]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<AnimatePresence initial={false}>
|
|
78
|
+
{open && (
|
|
79
|
+
<motion.div
|
|
80
|
+
key="contextual-save-bar"
|
|
81
|
+
role="region"
|
|
82
|
+
aria-label="Unsaved changes"
|
|
83
|
+
data-slot="contextual-save-bar"
|
|
84
|
+
data-position={position}
|
|
85
|
+
initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: -8 }}
|
|
86
|
+
animate={{ opacity: 1, y: 0 }}
|
|
87
|
+
exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: -8 }}
|
|
88
|
+
transition={spring.fast}
|
|
89
|
+
className={cn(
|
|
90
|
+
"z-(--z-sticky) flex min-w-0 items-center gap-3 border-border bg-foreground px-4 py-2.5 text-background",
|
|
91
|
+
position === "fixed"
|
|
92
|
+
? "fixed inset-x-0 bottom-0 border-t pb-[max(0.625rem,env(safe-area-inset-bottom))] sm:top-0 sm:bottom-auto sm:border-t-0 sm:border-b sm:pb-2.5"
|
|
93
|
+
: "sticky top-0 rounded-xl shadow-elevated",
|
|
94
|
+
className,
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
<p
|
|
98
|
+
className="body-sm flex min-w-0 flex-1 items-center gap-2 font-medium"
|
|
99
|
+
aria-live="polite"
|
|
100
|
+
>
|
|
101
|
+
<CircleAlert className="size-4 shrink-0 opacity-70" aria-hidden="true" />
|
|
102
|
+
<span className="truncate">{message}</span>
|
|
103
|
+
</p>
|
|
104
|
+
{children}
|
|
105
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
106
|
+
{onDiscard && (
|
|
107
|
+
<Button
|
|
108
|
+
type="button"
|
|
109
|
+
size="sm"
|
|
110
|
+
variant="ghost"
|
|
111
|
+
className="text-background/80 hover:bg-background/10 hover:text-background"
|
|
112
|
+
onClick={onDiscard}
|
|
113
|
+
disabled={saving}
|
|
114
|
+
>
|
|
115
|
+
{discardLabel}
|
|
116
|
+
</Button>
|
|
117
|
+
)}
|
|
118
|
+
<Button
|
|
119
|
+
ref={saveRef}
|
|
120
|
+
size="sm"
|
|
121
|
+
variant="brand"
|
|
122
|
+
{...(formId ? { type: "submit" as const, form: formId } : { type: "button" as const })}
|
|
123
|
+
{...(onSave ? { onClick: onSave } : {})}
|
|
124
|
+
loading={saving}
|
|
125
|
+
disabled={saveDisabled}
|
|
126
|
+
>
|
|
127
|
+
{saveLabel}
|
|
128
|
+
</Button>
|
|
129
|
+
</div>
|
|
130
|
+
</motion.div>
|
|
131
|
+
)}
|
|
132
|
+
</AnimatePresence>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
ContextualSaveBar.displayName = "ContextualSaveBar";
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Warn before the tab closes / reloads while `dirty` is true. Pair with
|
|
140
|
+
* ContextualSaveBar; route-level blocking belongs to your router.
|
|
141
|
+
*/
|
|
142
|
+
function useUnsavedChangesWarning(dirty: boolean) {
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
if (!dirty) return;
|
|
145
|
+
const onBeforeUnload = (event: BeforeUnloadEvent) => {
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
// Legacy browsers require returnValue to be set.
|
|
148
|
+
event.returnValue = "";
|
|
149
|
+
};
|
|
150
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
151
|
+
return () => window.removeEventListener("beforeunload", onBeforeUnload);
|
|
152
|
+
}, [dirty]);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export { ContextualSaveBar, useUnsavedChangesWarning };
|
|
156
|
+
export type { ContextualSaveBarProps };
|
|
@@ -17,6 +17,7 @@ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from ".
|
|
|
17
17
|
import { Button } from "./button";
|
|
18
18
|
import { Input } from "./input";
|
|
19
19
|
import { cn } from "../lib/utils";
|
|
20
|
+
import { pluralize, useFormatter } from "../lib/format";
|
|
20
21
|
|
|
21
22
|
interface DataTableProps<TData> {
|
|
22
23
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -26,6 +27,12 @@ interface DataTableProps<TData> {
|
|
|
26
27
|
searchPlaceholder?: string;
|
|
27
28
|
pageSize?: number;
|
|
28
29
|
showPagination?: boolean;
|
|
30
|
+
/** Rendered in the table body when there are no rows (e.g. an `<EmptyState>` with an action). */
|
|
31
|
+
emptyState?: React.ReactNode;
|
|
32
|
+
/** Make rows clickable (e.g. open the detail page). Enter/Space activate focused rows. */
|
|
33
|
+
onRowClick?: (row: TData) => void;
|
|
34
|
+
/** Noun for the result count — "12 orders". Default: "result". */
|
|
35
|
+
itemLabel?: string;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
function DataTable<TData>({
|
|
@@ -35,7 +42,11 @@ function DataTable<TData>({
|
|
|
35
42
|
searchPlaceholder = "Search...",
|
|
36
43
|
pageSize = 10,
|
|
37
44
|
showPagination = true,
|
|
45
|
+
emptyState,
|
|
46
|
+
onRowClick,
|
|
47
|
+
itemLabel = "result",
|
|
38
48
|
}: DataTableProps<TData>) {
|
|
49
|
+
const fmt = useFormatter();
|
|
39
50
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
40
51
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
|
|
41
52
|
|
|
@@ -107,7 +118,23 @@ function DataTable<TData>({
|
|
|
107
118
|
<TableBody>
|
|
108
119
|
{table.getRowModel().rows?.length ? (
|
|
109
120
|
table.getRowModel().rows.map((row) => (
|
|
110
|
-
<TableRow
|
|
121
|
+
<TableRow
|
|
122
|
+
key={row.id}
|
|
123
|
+
{...(onRowClick
|
|
124
|
+
? {
|
|
125
|
+
tabIndex: 0,
|
|
126
|
+
className:
|
|
127
|
+
"cursor-pointer focus-visible:outline-none focus-visible:bg-muted hover:bg-muted",
|
|
128
|
+
onClick: () => onRowClick(row.original),
|
|
129
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLTableRowElement>) => {
|
|
130
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
onRowClick(row.original);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
: {})}
|
|
137
|
+
>
|
|
111
138
|
{row.getVisibleCells().map((cell) => (
|
|
112
139
|
<TableCell key={cell.id}>
|
|
113
140
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
@@ -119,9 +146,9 @@ function DataTable<TData>({
|
|
|
119
146
|
<TableRow>
|
|
120
147
|
<TableCell
|
|
121
148
|
colSpan={columns.length}
|
|
122
|
-
className="h-24 text-center text-muted-foreground"
|
|
149
|
+
className={cn(!emptyState && "h-24 text-center text-muted-foreground")}
|
|
123
150
|
>
|
|
124
|
-
No results.
|
|
151
|
+
{emptyState ?? "No results."}
|
|
125
152
|
</TableCell>
|
|
126
153
|
</TableRow>
|
|
127
154
|
)}
|
|
@@ -130,8 +157,9 @@ function DataTable<TData>({
|
|
|
130
157
|
{showPagination && (
|
|
131
158
|
<div className="flex items-center justify-between">
|
|
132
159
|
<p className="caption tabular-nums text-muted-foreground">
|
|
133
|
-
{table.getFilteredRowModel().rows.length
|
|
134
|
-
|
|
160
|
+
{pluralize(table.getFilteredRowModel().rows.length, itemLabel, {
|
|
161
|
+
...(fmt.locale ? { locale: fmt.locale } : {}),
|
|
162
|
+
})}
|
|
135
163
|
</p>
|
|
136
164
|
<div className="flex items-center gap-2">
|
|
137
165
|
<span className="caption tabular-nums text-muted-foreground">
|
|
@@ -161,5 +189,6 @@ function DataTable<TData>({
|
|
|
161
189
|
}
|
|
162
190
|
|
|
163
191
|
export { DataTable };
|
|
192
|
+
export type { DataTableProps };
|
|
164
193
|
export { createColumnHelper } from "@tanstack/react-table";
|
|
165
194
|
export type { ColumnDef };
|