@handled-ai/design-system 0.13.2 → 0.14.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/dist/components/data-table-filter.d.ts +2 -1
- package/dist/components/data-table-filter.js +7 -2
- package/dist/components/data-table-filter.js.map +1 -1
- package/dist/components/empty-state.d.ts +3 -1
- package/dist/components/empty-state.js +15 -6
- package/dist/components/empty-state.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/data-table-filter.test.tsx +41 -0
- package/src/components/__tests__/empty-state.test.tsx +74 -0
- package/src/components/data-table-filter.tsx +7 -1
- package/src/components/empty-state.tsx +22 -5
|
@@ -12,7 +12,8 @@ interface DataTableFilterProps {
|
|
|
12
12
|
categories: DataTableFilterCategory[];
|
|
13
13
|
selectedFilters: Record<string, string[]>;
|
|
14
14
|
onToggleFilter: (categoryId: string, option: string) => void;
|
|
15
|
+
className?: string;
|
|
15
16
|
}
|
|
16
|
-
declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, }: DataTableFilterProps): React.JSX.Element;
|
|
17
|
+
declare function DataTableFilter({ categories, selectedFilters, onToggleFilter, className, }: DataTableFilterProps): React.JSX.Element;
|
|
17
18
|
|
|
18
19
|
export { DataTableFilter, type DataTableFilterCategory };
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
import { ListFilter, Search } from "lucide-react";
|
|
7
|
+
import { cn } from "../lib/utils.js";
|
|
7
8
|
import { Button } from "./button.js";
|
|
8
9
|
import {
|
|
9
10
|
DropdownMenu,
|
|
@@ -17,7 +18,8 @@ import {
|
|
|
17
18
|
function DataTableFilter({
|
|
18
19
|
categories,
|
|
19
20
|
selectedFilters,
|
|
20
|
-
onToggleFilter
|
|
21
|
+
onToggleFilter,
|
|
22
|
+
className
|
|
21
23
|
}) {
|
|
22
24
|
const [query, setQuery] = React.useState("");
|
|
23
25
|
const visibleCategories = React.useMemo(() => {
|
|
@@ -47,7 +49,10 @@ function DataTableFilter({
|
|
|
47
49
|
{
|
|
48
50
|
variant: "outline",
|
|
49
51
|
size: "sm",
|
|
50
|
-
className:
|
|
52
|
+
className: cn(
|
|
53
|
+
"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50",
|
|
54
|
+
className
|
|
55
|
+
),
|
|
51
56
|
children: [
|
|
52
57
|
/* @__PURE__ */ jsx(ListFilter, { className: "h-3.5 w-3.5" }),
|
|
53
58
|
"Filter",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ListFilter, Search } from \"lucide-react\"\n\nimport { Button } from \"./button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: string[]\n}\n\ninterface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n option.toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n const activeCount = React.useMemo(\n () =>\n Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n ),\n [selectedFilters]\n )\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className
|
|
1
|
+
{"version":3,"sources":["../../src/components/data-table-filter.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { ListFilter, Search } from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport { Button } from \"./button\"\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSub,\n DropdownMenuSubContent,\n DropdownMenuSubTrigger,\n DropdownMenuTrigger,\n} from \"./dropdown-menu\"\n\nexport interface DataTableFilterCategory {\n id: string\n label: string\n icon: React.ComponentType<{ className?: string }>\n options: string[]\n}\n\ninterface DataTableFilterProps {\n categories: DataTableFilterCategory[]\n selectedFilters: Record<string, string[]>\n onToggleFilter: (categoryId: string, option: string) => void\n className?: string\n}\n\nexport function DataTableFilter({\n categories,\n selectedFilters,\n onToggleFilter,\n className,\n}: DataTableFilterProps) {\n const [query, setQuery] = React.useState(\"\")\n\n const visibleCategories = React.useMemo(() => {\n const normalized = query.trim().toLowerCase()\n if (!normalized) {\n return categories\n }\n\n return categories.filter((category) => {\n if (category.label.toLowerCase().includes(normalized)) {\n return true\n }\n\n return category.options.some((option) =>\n option.toLowerCase().includes(normalized)\n )\n })\n }, [categories, query])\n\n const activeCount = React.useMemo(\n () =>\n Object.values(selectedFilters).reduce(\n (count, selected) => count + selected.length,\n 0\n ),\n [selectedFilters]\n )\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"sm\"\n className={cn(\n \"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50\",\n className\n )}\n >\n <ListFilter className=\"h-3.5 w-3.5\" />\n Filter\n {activeCount > 0 ? (\n <span className=\"rounded bg-muted px-1.5 py-0 text-[10px] font-semibold\">\n {activeCount}\n </span>\n ) : null}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"w-[240px] p-0\">\n <div className=\"sticky top-0 z-10 border-b border-border bg-popover p-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n className=\"h-8 w-full rounded-md bg-muted/50 py-1 pr-2 pl-7 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted\"\n placeholder=\"Search filters...\"\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onClick={(event) => event.stopPropagation()}\n onKeyDown={(event) => event.stopPropagation()}\n />\n </div>\n </div>\n\n <div className=\"max-h-[320px] overflow-y-auto p-1\">\n {visibleCategories.map((category) => (\n <DropdownMenuSub key={category.id}>\n <DropdownMenuSubTrigger className=\"cursor-pointer py-1.5 text-xs\">\n <category.icon className=\"mr-2 h-3.5 w-3.5 text-muted-foreground\" />\n {category.label}\n </DropdownMenuSubTrigger>\n <DropdownMenuSubContent className=\"w-52 p-1\">\n {category.options.map((option) => {\n const selected =\n selectedFilters[category.id]?.includes(option) ?? false\n\n return (\n <DropdownMenuItem\n key={option}\n className=\"cursor-pointer justify-between text-xs\"\n onSelect={(event) => {\n event.preventDefault()\n onToggleFilter(category.id, option)\n }}\n >\n {option}\n {selected ? (\n <span className=\"text-[10px] font-semibold text-brand-purple\">\n Applied\n </span>\n ) : null}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuSubContent>\n </DropdownMenuSub>\n ))}\n\n {visibleCategories.length === 0 ? (\n <div className=\"p-2 text-center text-xs text-muted-foreground\">\n No filters found\n </div>\n ) : null}\n </div>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -5,7 +5,9 @@ interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
5
5
|
title?: string;
|
|
6
6
|
description: string;
|
|
7
7
|
action?: React.ReactNode;
|
|
8
|
+
secondaryAction?: React.ReactNode;
|
|
9
|
+
size?: 'sm' | 'lg';
|
|
8
10
|
}
|
|
9
|
-
declare function EmptyState({ icon, title, description, action, className, ...rest }: EmptyStateProps): React.JSX.Element;
|
|
11
|
+
declare function EmptyState({ icon, title, description, action, secondaryAction, size, className, ...rest }: EmptyStateProps): React.JSX.Element;
|
|
10
12
|
|
|
11
13
|
export { EmptyState, type EmptyStateProps };
|
|
@@ -32,12 +32,21 @@ var __objRest = (source, exclude) => {
|
|
|
32
32
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
33
33
|
import { cn } from "../lib/utils.js";
|
|
34
34
|
function EmptyState(_a) {
|
|
35
|
-
var _b = _a, { icon, title, description, action, className } = _b, rest = __objRest(_b, ["icon", "title", "description", "action", "className"]);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
var _b = _a, { icon, title, description, action, secondaryAction, size = "sm", className } = _b, rest = __objRest(_b, ["icon", "title", "description", "action", "secondaryAction", "size", "className"]);
|
|
36
|
+
const isLg = size === "lg";
|
|
37
|
+
return /* @__PURE__ */ jsxs("div", __spreadProps(__spreadValues({ "data-slot": "empty-state", className: cn("flex flex-col items-center justify-center py-24 gap-3 text-center", className) }, rest), { children: [
|
|
38
|
+
icon && /* @__PURE__ */ jsx("div", { "data-slot": "empty-state-icon", className: cn(
|
|
39
|
+
"text-muted-foreground/30",
|
|
40
|
+
isLg ? "[&>svg]:w-16 [&>svg]:h-16 [&>img]:h-[140px] [&>img]:w-auto [&>img]:object-contain" : "[&>svg]:w-12 [&>svg]:h-12"
|
|
41
|
+
), children: icon }),
|
|
42
|
+
title && /* @__PURE__ */ jsx("p", { "data-slot": "empty-state-title", className: cn(
|
|
43
|
+
isLg ? "text-base font-semibold text-foreground" : "text-sm font-medium text-muted-foreground"
|
|
44
|
+
), children: title }),
|
|
45
|
+
/* @__PURE__ */ jsx("p", { "data-slot": "empty-state-description", className: cn(
|
|
46
|
+
isLg ? "text-sm text-muted-foreground" : "text-xs text-muted-foreground"
|
|
47
|
+
), children: description }),
|
|
48
|
+
action && /* @__PURE__ */ jsx("div", { "data-slot": "empty-state-action", className: "mt-3", children: action }),
|
|
49
|
+
secondaryAction && /* @__PURE__ */ jsx("div", { "data-slot": "empty-state-secondary-action", className: action ? "mt-1" : "mt-3", children: secondaryAction })
|
|
41
50
|
] }));
|
|
42
51
|
}
|
|
43
52
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/empty-state.tsx"],"sourcesContent":["import * as React from \"react\"\n\nimport { cn } from \"../lib/utils\"\n\ninterface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {\n icon?: React.ReactNode\n title?: string\n description: string\n action?: React.ReactNode\n}\n\nfunction EmptyState({ icon, title, description, action, className, ...rest }: EmptyStateProps) {\n return (\n <div data-slot=\"empty-state\" className={cn(\"flex flex-col items-center justify-center py-24 gap-3\", className)} {...rest}>\n {icon && (\n <div data-slot=\"empty-state-icon\" className
|
|
1
|
+
{"version":3,"sources":["../../src/components/empty-state.tsx"],"sourcesContent":["import * as React from \"react\"\n\nimport { cn } from \"../lib/utils\"\n\ninterface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {\n icon?: React.ReactNode\n title?: string\n description: string\n action?: React.ReactNode\n secondaryAction?: React.ReactNode\n size?: 'sm' | 'lg'\n}\n\nfunction EmptyState({ icon, title, description, action, secondaryAction, size = 'sm', className, ...rest }: EmptyStateProps) {\n const isLg = size === 'lg'\n return (\n <div data-slot=\"empty-state\" className={cn(\"flex flex-col items-center justify-center py-24 gap-3 text-center\", className)} {...rest}>\n {icon && (\n <div data-slot=\"empty-state-icon\" className={cn(\n \"text-muted-foreground/30\",\n isLg\n ? \"[&>svg]:w-16 [&>svg]:h-16 [&>img]:h-[140px] [&>img]:w-auto [&>img]:object-contain\"\n : \"[&>svg]:w-12 [&>svg]:h-12\"\n )}>\n {icon}\n </div>\n )}\n {title && (\n <p data-slot=\"empty-state-title\" className={cn(\n isLg ? \"text-base font-semibold text-foreground\" : \"text-sm font-medium text-muted-foreground\"\n )}>\n {title}\n </p>\n )}\n <p data-slot=\"empty-state-description\" className={cn(\n isLg ? \"text-sm text-muted-foreground\" : \"text-xs text-muted-foreground\"\n )}>\n {description}\n </p>\n {action && (\n <div data-slot=\"empty-state-action\" className=\"mt-3\">\n {action}\n </div>\n )}\n {secondaryAction && (\n <div data-slot=\"empty-state-secondary-action\" className={action ? \"mt-1\" : \"mt-3\"}>\n {secondaryAction}\n </div>\n )}\n </div>\n )\n}\n\nexport { EmptyState, type EmptyStateProps }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgBI,SAEI,KAFJ;AAdJ,SAAS,UAAU;AAWnB,SAAS,WAAW,IAAyG;AAAzG,eAAE,QAAM,OAAO,aAAa,QAAQ,iBAAiB,OAAO,MAAM,UAbtF,IAaoB,IAAgF,iBAAhF,IAAgF,CAA9E,QAAM,SAAO,eAAa,UAAQ,mBAAiB,QAAa;AACpF,QAAM,OAAO,SAAS;AACtB,SACE,qBAAC,sCAAI,aAAU,eAAc,WAAW,GAAG,qEAAqE,SAAS,KAAO,OAA/H,EACE;AAAA,YACC,oBAAC,SAAI,aAAU,oBAAmB,WAAW;AAAA,MAC3C;AAAA,MACA,OACI,sFACA;AAAA,IACN,GACG,gBACH;AAAA,IAED,SACC,oBAAC,OAAE,aAAU,qBAAoB,WAAW;AAAA,MAC1C,OAAO,4CAA4C;AAAA,IACrD,GACG,iBACH;AAAA,IAEF,oBAAC,OAAE,aAAU,2BAA0B,WAAW;AAAA,MAChD,OAAO,kCAAkC;AAAA,IAC3C,GACG,uBACH;AAAA,IACC,UACC,oBAAC,SAAI,aAAU,sBAAqB,WAAU,QAC3C,kBACH;AAAA,IAED,mBACC,oBAAC,SAAI,aAAU,gCAA+B,WAAW,SAAS,SAAS,QACxE,2BACH;AAAA,MAEJ;AAEJ;","names":[]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { DataTableFilter } from "../data-table-filter";
|
|
5
|
+
import { ListFilter } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
const defaultProps = {
|
|
8
|
+
categories: [
|
|
9
|
+
{
|
|
10
|
+
id: "status",
|
|
11
|
+
label: "Status",
|
|
12
|
+
icon: ListFilter,
|
|
13
|
+
options: ["Open", "Closed"],
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
selectedFilters: {} as Record<string, string[]>,
|
|
17
|
+
onToggleFilter: () => {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe("DataTableFilter", () => {
|
|
21
|
+
it("renders trigger button with default classes", () => {
|
|
22
|
+
const { container } = render(<DataTableFilter {...defaultProps} />);
|
|
23
|
+
const button = container.querySelector("button");
|
|
24
|
+
expect(button).not.toBeNull();
|
|
25
|
+
expect(button!.className).toContain("h-8");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("merges custom className into trigger button", () => {
|
|
29
|
+
const { container } = render(
|
|
30
|
+
<DataTableFilter
|
|
31
|
+
{...defaultProps}
|
|
32
|
+
className="bg-primary text-primary-foreground"
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
const button = container.querySelector("button");
|
|
36
|
+
expect(button).not.toBeNull();
|
|
37
|
+
expect(button!.className).toContain("bg-primary");
|
|
38
|
+
expect(button!.className).toContain("text-primary-foreground");
|
|
39
|
+
expect(button!.className).toContain("h-8");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -73,4 +73,78 @@ describe("EmptyState", () => {
|
|
|
73
73
|
const el = container.querySelector('[data-slot="empty-state"]')!;
|
|
74
74
|
expect(el.classList.contains("my-custom-class")).toBe(true);
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it("default size is 'sm' with small typography", () => {
|
|
78
|
+
const { container } = render(
|
|
79
|
+
<EmptyState description="No items" title="Empty" />
|
|
80
|
+
);
|
|
81
|
+
const titleEl = container.querySelector('[data-slot="empty-state-title"]')!;
|
|
82
|
+
const descEl = container.querySelector('[data-slot="empty-state-description"]')!;
|
|
83
|
+
expect(titleEl.className).toContain("text-sm");
|
|
84
|
+
expect(descEl.className).toContain("text-xs");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("size 'lg' applies larger typography", () => {
|
|
88
|
+
const { container } = render(
|
|
89
|
+
<EmptyState description="No items" title="Empty" size="lg" />
|
|
90
|
+
);
|
|
91
|
+
const titleEl = container.querySelector('[data-slot="empty-state-title"]')!;
|
|
92
|
+
const descEl = container.querySelector('[data-slot="empty-state-description"]')!;
|
|
93
|
+
expect(titleEl.className).toContain("text-base");
|
|
94
|
+
expect(titleEl.className).toContain("font-semibold");
|
|
95
|
+
expect(descEl.className).toContain("text-sm");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("size 'lg' uses larger icon sizing", () => {
|
|
99
|
+
const { container } = render(
|
|
100
|
+
<EmptyState
|
|
101
|
+
description="No items"
|
|
102
|
+
size="lg"
|
|
103
|
+
icon={<svg data-testid="test-svg"><rect /></svg>}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
const iconWrapper = container.querySelector('[data-slot="empty-state-icon"]')!;
|
|
107
|
+
expect(iconWrapper.className).toContain("[&>svg]:w-16");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("renders secondaryAction when provided", () => {
|
|
111
|
+
const { container } = render(
|
|
112
|
+
<EmptyState
|
|
113
|
+
description="No items"
|
|
114
|
+
secondaryAction={<a href="/help">Help link</a>}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
const secondaryWrapper = container.querySelector('[data-slot="empty-state-secondary-action"]');
|
|
118
|
+
expect(secondaryWrapper).not.toBeNull();
|
|
119
|
+
expect(screen.getByText("Help link")).not.toBeNull();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("omits secondaryAction wrapper when not provided", () => {
|
|
123
|
+
const { container } = render(<EmptyState description="No items" />);
|
|
124
|
+
const secondaryWrapper = container.querySelector('[data-slot="empty-state-secondary-action"]');
|
|
125
|
+
expect(secondaryWrapper).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("secondaryAction uses mt-1 when action is also present", () => {
|
|
129
|
+
const { container } = render(
|
|
130
|
+
<EmptyState
|
|
131
|
+
description="No items"
|
|
132
|
+
action={<button type="button">Primary</button>}
|
|
133
|
+
secondaryAction={<a href="/help">Secondary</a>}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
const secondaryWrapper = container.querySelector('[data-slot="empty-state-secondary-action"]')!;
|
|
137
|
+
expect(secondaryWrapper.className).toContain("mt-1");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("secondaryAction uses mt-3 when action is not present", () => {
|
|
141
|
+
const { container } = render(
|
|
142
|
+
<EmptyState
|
|
143
|
+
description="No items"
|
|
144
|
+
secondaryAction={<a href="/help">Secondary</a>}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
const secondaryWrapper = container.querySelector('[data-slot="empty-state-secondary-action"]')!;
|
|
148
|
+
expect(secondaryWrapper.className).toContain("mt-3");
|
|
149
|
+
});
|
|
76
150
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from "react"
|
|
4
4
|
import { ListFilter, Search } from "lucide-react"
|
|
5
5
|
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
6
7
|
import { Button } from "./button"
|
|
7
8
|
import {
|
|
8
9
|
DropdownMenu,
|
|
@@ -25,12 +26,14 @@ interface DataTableFilterProps {
|
|
|
25
26
|
categories: DataTableFilterCategory[]
|
|
26
27
|
selectedFilters: Record<string, string[]>
|
|
27
28
|
onToggleFilter: (categoryId: string, option: string) => void
|
|
29
|
+
className?: string
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export function DataTableFilter({
|
|
31
33
|
categories,
|
|
32
34
|
selectedFilters,
|
|
33
35
|
onToggleFilter,
|
|
36
|
+
className,
|
|
34
37
|
}: DataTableFilterProps) {
|
|
35
38
|
const [query, setQuery] = React.useState("")
|
|
36
39
|
|
|
@@ -66,7 +69,10 @@ export function DataTableFilter({
|
|
|
66
69
|
<Button
|
|
67
70
|
variant="outline"
|
|
68
71
|
size="sm"
|
|
69
|
-
className=
|
|
72
|
+
className={cn(
|
|
73
|
+
"h-8 gap-2 rounded-md border-border/60 bg-background text-xs font-normal shadow-none hover:bg-muted/50",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
70
76
|
>
|
|
71
77
|
<ListFilter className="h-3.5 w-3.5" />
|
|
72
78
|
Filter
|
|
@@ -7,22 +7,34 @@ interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
7
7
|
title?: string
|
|
8
8
|
description: string
|
|
9
9
|
action?: React.ReactNode
|
|
10
|
+
secondaryAction?: React.ReactNode
|
|
11
|
+
size?: 'sm' | 'lg'
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
function EmptyState({ icon, title, description, action, className, ...rest }: EmptyStateProps) {
|
|
14
|
+
function EmptyState({ icon, title, description, action, secondaryAction, size = 'sm', className, ...rest }: EmptyStateProps) {
|
|
15
|
+
const isLg = size === 'lg'
|
|
13
16
|
return (
|
|
14
|
-
<div data-slot="empty-state" className={cn("flex flex-col items-center justify-center py-24 gap-3", className)} {...rest}>
|
|
17
|
+
<div data-slot="empty-state" className={cn("flex flex-col items-center justify-center py-24 gap-3 text-center", className)} {...rest}>
|
|
15
18
|
{icon && (
|
|
16
|
-
<div data-slot="empty-state-icon" className=
|
|
19
|
+
<div data-slot="empty-state-icon" className={cn(
|
|
20
|
+
"text-muted-foreground/30",
|
|
21
|
+
isLg
|
|
22
|
+
? "[&>svg]:w-16 [&>svg]:h-16 [&>img]:h-[140px] [&>img]:w-auto [&>img]:object-contain"
|
|
23
|
+
: "[&>svg]:w-12 [&>svg]:h-12"
|
|
24
|
+
)}>
|
|
17
25
|
{icon}
|
|
18
26
|
</div>
|
|
19
27
|
)}
|
|
20
28
|
{title && (
|
|
21
|
-
<p data-slot="empty-state-title" className=
|
|
29
|
+
<p data-slot="empty-state-title" className={cn(
|
|
30
|
+
isLg ? "text-base font-semibold text-foreground" : "text-sm font-medium text-muted-foreground"
|
|
31
|
+
)}>
|
|
22
32
|
{title}
|
|
23
33
|
</p>
|
|
24
34
|
)}
|
|
25
|
-
<p data-slot="empty-state-description" className=
|
|
35
|
+
<p data-slot="empty-state-description" className={cn(
|
|
36
|
+
isLg ? "text-sm text-muted-foreground" : "text-xs text-muted-foreground"
|
|
37
|
+
)}>
|
|
26
38
|
{description}
|
|
27
39
|
</p>
|
|
28
40
|
{action && (
|
|
@@ -30,6 +42,11 @@ function EmptyState({ icon, title, description, action, className, ...rest }: Em
|
|
|
30
42
|
{action}
|
|
31
43
|
</div>
|
|
32
44
|
)}
|
|
45
|
+
{secondaryAction && (
|
|
46
|
+
<div data-slot="empty-state-secondary-action" className={action ? "mt-1" : "mt-3"}>
|
|
47
|
+
{secondaryAction}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
33
50
|
</div>
|
|
34
51
|
)
|
|
35
52
|
}
|