aplance-ui 1.5.1 → 1.6.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 +1 -1
- package/dist/api.d.ts +2 -1
- package/dist/api.js +103 -68
- package/dist/api.js.map +1 -1
- package/dist/frameworks/vite-react/vite.config.ts.template +1 -1
- package/dist/index.js +103 -68
- package/dist/index.js.map +1 -1
- package/package.json +12 -6
- package/registry/components/primitives/modal/modal.module.css +9 -3
- package/registry/components/primitives/popover/popover-context.ts +2 -0
- package/registry/components/primitives/popover/popover.tsx +47 -2
- package/registry/components/ui/chart/chart.tsx +51 -30
- package/registry/components/ui/chart/types.ts +5 -1
- package/registry/components/ui/data-table/checkbox-column.tsx +12 -8
- package/registry/components/ui/data-table/data-table.module.css +41 -19
- package/registry/components/ui/data-table/data-table.tsx +1285 -1238
- package/registry/components/ui/data-table/filter-bar.tsx +56 -13
- package/registry/components/ui/data-table/filter-modal.tsx +16 -82
- package/registry/components/ui/data-table/filter-sheet.tsx +97 -0
- package/registry/components/ui/data-table/filters.tsx +21 -21
- package/registry/components/ui/data-table/pagination-bar.tsx +4 -6
- package/registry/components/ui/data-table/types.ts +149 -80
- package/registry/components/ui/data-table/useDataTable.tsx +35 -16
- package/registry/components/ui/data-table/utils.ts +691 -471
- package/registry/components/ui/input-fields/date-picker/calendar.tsx +7 -15
- package/registry/components/ui/input-fields/date-picker/date-picker.tsx +1 -1
- package/registry/components/ui/input-fields/date-picker/types.ts +3 -3
- package/registry/components/ui/input-fields/emoji-picker/emoji-palette.tsx +1 -1
- package/registry/registry.json +17 -12
- package/registry/styles/media.css +1 -1
|
@@ -1,18 +1,44 @@
|
|
|
1
1
|
import { Fragment } from "react";
|
|
2
|
-
import
|
|
2
|
+
import { FilterSheet } from "./filter-sheet";
|
|
3
|
+
import type { FilterBarProps, FilterField } from "./types";
|
|
4
|
+
import { placementOf, renderFilter } from "./utils";
|
|
3
5
|
import s from "./data-table.module.css";
|
|
4
6
|
|
|
7
|
+
interface FilterRowProps extends FilterBarProps {
|
|
8
|
+
/** The filters kept behind the "Filters" button at the row's end. */
|
|
9
|
+
kept: FilterField[];
|
|
10
|
+
/** The button shows only its icon, beside the row's own fields. */
|
|
11
|
+
iconOnly?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
/**
|
|
6
|
-
*
|
|
15
|
+
* A single row of filters, then the "Filters" button for those kept off it.
|
|
7
16
|
* Each field owns its rendering via a render-prop and sets its own grid
|
|
8
|
-
* column width. The
|
|
17
|
+
* column width. The row never wraps - consumers pick widths that fit.
|
|
18
|
+
* With no field of its own, the row is the button alone, with its label; with
|
|
19
|
+
* no filter at all, it renders nothing.
|
|
9
20
|
*
|
|
10
21
|
* Width defaults: input fields -> `1fr`, actions -> `auto`.
|
|
11
22
|
*/
|
|
12
|
-
export const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
export const FilterRow = ({
|
|
24
|
+
fields,
|
|
25
|
+
kept,
|
|
26
|
+
values,
|
|
27
|
+
onChange,
|
|
28
|
+
iconOnly = false,
|
|
29
|
+
}: FilterRowProps) => {
|
|
30
|
+
if (fields.length === 0 && kept.length === 0) return null;
|
|
31
|
+
if (fields.length === 0) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={s.filter_modal_bar}>
|
|
34
|
+
<FilterSheet fields={kept} values={values} onChange={onChange} />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const gridCols = [
|
|
39
|
+
...fields.map((f) => f.width ?? (f.type === "action" ? "auto" : "1fr")),
|
|
40
|
+
...(kept.length > 0 ? ["auto"] : []),
|
|
41
|
+
].join(" ");
|
|
16
42
|
|
|
17
43
|
return (
|
|
18
44
|
<div className={s.filter_bar} style={{ gridTemplateColumns: gridCols }}>
|
|
@@ -20,14 +46,31 @@ export const FilterBar = ({ fields, values, onChange }: FilterBarProps) => {
|
|
|
20
46
|
<Fragment
|
|
21
47
|
key={field.type === "action" ? `act:${i}` : `k:${field.key}`}
|
|
22
48
|
>
|
|
23
|
-
{field
|
|
24
|
-
? field.render()
|
|
25
|
-
: field.render({
|
|
26
|
-
value: values[field.key] ?? "",
|
|
27
|
-
onChange: (v) => onChange(field.key, v),
|
|
28
|
-
})}
|
|
49
|
+
{renderFilter(field, values, onChange)}
|
|
29
50
|
</Fragment>
|
|
30
51
|
))}
|
|
52
|
+
{kept.length > 0 && (
|
|
53
|
+
<FilterSheet
|
|
54
|
+
fields={kept}
|
|
55
|
+
values={values}
|
|
56
|
+
onChange={onChange}
|
|
57
|
+
iconOnly={iconOnly}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
31
60
|
</div>
|
|
32
61
|
);
|
|
33
62
|
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Headless filter bar for a screen wide enough: every filter in one row,
|
|
66
|
+
* but those placed in the modal, which sit behind a "Filters" button at its
|
|
67
|
+
* end.
|
|
68
|
+
*/
|
|
69
|
+
export const FilterBar = ({ fields, values, onChange }: FilterBarProps) => (
|
|
70
|
+
<FilterRow
|
|
71
|
+
fields={fields.filter((f) => placementOf(f) !== "modal")}
|
|
72
|
+
kept={fields.filter((f) => placementOf(f) === "modal")}
|
|
73
|
+
values={values}
|
|
74
|
+
onChange={onChange}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
@@ -1,86 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { IconFilter } from "@tabler/icons-react";
|
|
3
|
-
import {
|
|
4
|
-
Modal,
|
|
5
|
-
ModalBody,
|
|
6
|
-
ModalFooter,
|
|
7
|
-
ModalHeader,
|
|
8
|
-
ModalTitle,
|
|
9
|
-
} from "../../primitives/modal/modal";
|
|
10
|
-
import { Icon } from "../../primitives/icon/icon";
|
|
11
|
-
import { Button } from "../button/button";
|
|
1
|
+
import { FilterRow } from "./filter-bar";
|
|
12
2
|
import type { FilterBarProps } from "./types";
|
|
13
|
-
import
|
|
3
|
+
import { placementOf } from "./utils";
|
|
14
4
|
|
|
15
5
|
/**
|
|
16
|
-
* Small-screen counterpart to <FilterBar>.
|
|
17
|
-
* the
|
|
6
|
+
* Small-screen counterpart to <FilterBar>. The filters placed inline, as a
|
|
7
|
+
* search, keep their place in the row with an icon "Filters" button beside
|
|
8
|
+
* them; every other filter moves into its modal, a bottom sheet on phones,
|
|
9
|
+
* still filtering live. With no inline filter, the button stands alone with
|
|
10
|
+
* its label.
|
|
18
11
|
*/
|
|
19
|
-
export const FilterModal = ({ fields, values, onChange }: FilterBarProps) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const clearAll = () => {
|
|
29
|
-
inputFields.forEach((field) => {
|
|
30
|
-
if ((values[field.key] ?? "") !== "") onChange(field.key, "");
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<div className={s.filter_modal_bar}>
|
|
36
|
-
<Button
|
|
37
|
-
type="button"
|
|
38
|
-
variant="bordered"
|
|
39
|
-
className={s.filter_trigger}
|
|
40
|
-
onClick={() => setIsOpen(true)}
|
|
41
|
-
>
|
|
42
|
-
<Icon>
|
|
43
|
-
<IconFilter />
|
|
44
|
-
</Icon>
|
|
45
|
-
Filters
|
|
46
|
-
{activeCount > 0 && <span className={s.filter_count}>{activeCount}</span>}
|
|
47
|
-
</Button>
|
|
48
|
-
|
|
49
|
-
<Modal isOpen={isOpen} onOpenChange={setIsOpen} placement="bottom" size="md">
|
|
50
|
-
<ModalHeader>
|
|
51
|
-
<ModalTitle>Filters</ModalTitle>
|
|
52
|
-
</ModalHeader>
|
|
53
|
-
<ModalBody>
|
|
54
|
-
<div className={s.filter_modal_fields}>
|
|
55
|
-
{fields.map((field, i) => (
|
|
56
|
-
<div
|
|
57
|
-
className={s.filter_modal_field}
|
|
58
|
-
key={field.type === "action" ? `act:${i}` : `k:${field.key}`}
|
|
59
|
-
>
|
|
60
|
-
{field.type === "action"
|
|
61
|
-
? field.render()
|
|
62
|
-
: field.render({
|
|
63
|
-
value: values[field.key] ?? "",
|
|
64
|
-
onChange: (v) => onChange(field.key, v),
|
|
65
|
-
})}
|
|
66
|
-
</div>
|
|
67
|
-
))}
|
|
68
|
-
</div>
|
|
69
|
-
</ModalBody>
|
|
70
|
-
<ModalFooter>
|
|
71
|
-
<Button
|
|
72
|
-
type="button"
|
|
73
|
-
variant="ghost"
|
|
74
|
-
onClick={clearAll}
|
|
75
|
-
disabled={activeCount === 0}
|
|
76
|
-
>
|
|
77
|
-
Clear all
|
|
78
|
-
</Button>
|
|
79
|
-
<Button type="button" variant="primary" onClick={() => setIsOpen(false)}>
|
|
80
|
-
Done
|
|
81
|
-
</Button>
|
|
82
|
-
</ModalFooter>
|
|
83
|
-
</Modal>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
};
|
|
12
|
+
export const FilterModal = ({ fields, values, onChange }: FilterBarProps) => (
|
|
13
|
+
<FilterRow
|
|
14
|
+
fields={fields.filter((f) => placementOf(f) === "inline")}
|
|
15
|
+
kept={fields.filter((f) => placementOf(f) !== "inline")}
|
|
16
|
+
values={values}
|
|
17
|
+
onChange={onChange}
|
|
18
|
+
iconOnly
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { IconFilter } from "@tabler/icons-react";
|
|
3
|
+
import {
|
|
4
|
+
Modal,
|
|
5
|
+
ModalBody,
|
|
6
|
+
ModalFooter,
|
|
7
|
+
ModalHeader,
|
|
8
|
+
ModalTitle,
|
|
9
|
+
} from "../../primitives/modal/modal";
|
|
10
|
+
import { Icon } from "../../primitives/icon/icon";
|
|
11
|
+
import { Button } from "../button/button";
|
|
12
|
+
import type { FilterSheetProps } from "./types";
|
|
13
|
+
import { isFilterSet, renderFilter } from "./utils";
|
|
14
|
+
import s from "./data-table.module.css";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The "Filters" button and the modal it opens: the filters kept off a row,
|
|
18
|
+
* stacked, with Clear all and Done. The button counts the filters in use,
|
|
19
|
+
* each against its own default; the modal is a bottom sheet on phones.
|
|
20
|
+
*/
|
|
21
|
+
export const FilterSheet = ({
|
|
22
|
+
fields,
|
|
23
|
+
values,
|
|
24
|
+
onChange,
|
|
25
|
+
iconOnly = false,
|
|
26
|
+
}: FilterSheetProps) => {
|
|
27
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
28
|
+
|
|
29
|
+
// Action slots (buttons/menus) render alongside the inputs; only input fields
|
|
30
|
+
// carry a value, so only they count toward the badge and Clear all.
|
|
31
|
+
const inUse = fields.filter(
|
|
32
|
+
(field) => field.type === "field" && isFilterSet(field, values),
|
|
33
|
+
);
|
|
34
|
+
const clearAll = () => {
|
|
35
|
+
inUse.forEach((field) => {
|
|
36
|
+
if (field.type === "field") onChange(field.key, field.defaultValue ?? "");
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
// One name for every layout: the icon-only button has no visible text, and
|
|
40
|
+
// the count badge alone reads as a bare number.
|
|
41
|
+
const label =
|
|
42
|
+
inUse.length > 0 ? `Filters, ${inUse.length} in use` : "Filters";
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<Button
|
|
47
|
+
type="button"
|
|
48
|
+
variant="bordered"
|
|
49
|
+
layout={iconOnly && inUse.length === 0 ? "box" : "default"}
|
|
50
|
+
className={s.filter_trigger}
|
|
51
|
+
aria-label={label}
|
|
52
|
+
onClick={() => setIsOpen(true)}
|
|
53
|
+
>
|
|
54
|
+
<Icon>
|
|
55
|
+
<IconFilter />
|
|
56
|
+
</Icon>
|
|
57
|
+
{iconOnly ? null : "Filters"}
|
|
58
|
+
{inUse.length > 0 && (
|
|
59
|
+
<span className={s.filter_count} aria-hidden="true">
|
|
60
|
+
{inUse.length}
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
63
|
+
</Button>
|
|
64
|
+
|
|
65
|
+
<Modal isOpen={isOpen} onOpenChange={setIsOpen} size="md">
|
|
66
|
+
<ModalHeader>
|
|
67
|
+
<ModalTitle>Filters</ModalTitle>
|
|
68
|
+
</ModalHeader>
|
|
69
|
+
<ModalBody>
|
|
70
|
+
<div className={s.filter_modal_fields}>
|
|
71
|
+
{fields.map((field, i) => (
|
|
72
|
+
<div
|
|
73
|
+
className={s.filter_modal_field}
|
|
74
|
+
key={field.type === "action" ? `act:${i}` : `k:${field.key}`}
|
|
75
|
+
>
|
|
76
|
+
{renderFilter(field, values, onChange)}
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
</ModalBody>
|
|
81
|
+
<ModalFooter>
|
|
82
|
+
<Button
|
|
83
|
+
type="button"
|
|
84
|
+
variant="ghost"
|
|
85
|
+
onClick={clearAll}
|
|
86
|
+
disabled={inUse.length === 0}
|
|
87
|
+
>
|
|
88
|
+
Clear all
|
|
89
|
+
</Button>
|
|
90
|
+
<Button type="button" variant="primary" onClick={() => setIsOpen(false)}>
|
|
91
|
+
Done
|
|
92
|
+
</Button>
|
|
93
|
+
</ModalFooter>
|
|
94
|
+
</Modal>
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { useMediaQuery } from "../../../hooks/useMediaQuery";
|
|
2
|
-
import { FilterBar } from "./filter-bar";
|
|
3
|
-
import { FilterModal } from "./filter-modal";
|
|
4
|
-
import type { FilterBarProps } from "./types";
|
|
5
|
-
|
|
6
|
-
// Below this width an inline filter row can't fit its fields (matches the
|
|
7
|
-
// `--md` custom-media token in styles/media.css): the filters collapse into a
|
|
8
|
-
// single "Filters" button and a modal instead of stacking full-height.
|
|
9
|
-
const FILTER_COMPACT_QUERY = "(max-width: 900px)";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A list's filters: the inline <FilterBar> while it fits, the <FilterModal>
|
|
13
|
-
* on a narrower screen. DataTable shows its filters through it; a list built
|
|
14
|
-
* on its own uses it the same way.
|
|
15
|
-
*/
|
|
16
|
-
export const Filters = (props: FilterBarProps) =>
|
|
17
|
-
useMediaQuery(FILTER_COMPACT_QUERY) ? (
|
|
18
|
-
<FilterModal {...props} />
|
|
19
|
-
) : (
|
|
20
|
-
<FilterBar {...props} />
|
|
21
|
-
);
|
|
1
|
+
import { useMediaQuery } from "../../../hooks/useMediaQuery";
|
|
2
|
+
import { FilterBar } from "./filter-bar";
|
|
3
|
+
import { FilterModal } from "./filter-modal";
|
|
4
|
+
import type { FilterBarProps } from "./types";
|
|
5
|
+
|
|
6
|
+
// Below this width an inline filter row can't fit its fields (matches the
|
|
7
|
+
// `--md` custom-media token in styles/media.css): the filters collapse into a
|
|
8
|
+
// single "Filters" button and a modal instead of stacking full-height.
|
|
9
|
+
const FILTER_COMPACT_QUERY = "(max-width: 900px)";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A list's filters: the inline <FilterBar> while it fits, the <FilterModal>
|
|
13
|
+
* on a narrower screen. DataTable shows its filters through it; a list built
|
|
14
|
+
* on its own uses it the same way.
|
|
15
|
+
*/
|
|
16
|
+
export const Filters = (props: FilterBarProps) =>
|
|
17
|
+
useMediaQuery(FILTER_COMPACT_QUERY) ? (
|
|
18
|
+
<FilterModal {...props} />
|
|
19
|
+
) : (
|
|
20
|
+
<FilterBar {...props} />
|
|
21
|
+
);
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
IconChevronsLeft,
|
|
5
5
|
IconChevronsRight,
|
|
6
6
|
} from "@tabler/icons-react";
|
|
7
|
+
import type { RowData } from "@tanstack/react-table";
|
|
7
8
|
import clsx from "clsx";
|
|
8
9
|
import { Button } from "../button/button";
|
|
9
10
|
import { CustomSelect } from "../input-fields/custom-select/custom-select";
|
|
@@ -18,7 +19,7 @@ const getSkeletonWidth = (
|
|
|
18
19
|
maxCharacters: number,
|
|
19
20
|
) => `${Math.min(maxCharacters, Math.max(minCharacters, characters))}ch`;
|
|
20
21
|
|
|
21
|
-
export const PaginationBar = <TData
|
|
22
|
+
export const PaginationBar = <TData extends RowData>({
|
|
22
23
|
table,
|
|
23
24
|
pagination,
|
|
24
25
|
onPaginationChange,
|
|
@@ -32,6 +33,7 @@ export const PaginationBar = <TData,>({
|
|
|
32
33
|
label,
|
|
33
34
|
extra,
|
|
34
35
|
}: PaginationBarProps<TData>) => {
|
|
36
|
+
// -1 while the total is unknown (server-side, nothing counted yet).
|
|
35
37
|
const pageCount = table.getPageCount();
|
|
36
38
|
const total = totalCount ?? table.getRowCount();
|
|
37
39
|
const canPrev = table.getCanPreviousPage();
|
|
@@ -82,11 +84,7 @@ export const PaginationBar = <TData,>({
|
|
|
82
84
|
.map((n) => ({ value: String(n), label: String(n) }));
|
|
83
85
|
|
|
84
86
|
const resolvedPageCount =
|
|
85
|
-
pageCount > 0
|
|
86
|
-
? pageCount
|
|
87
|
-
: totalCount === undefined
|
|
88
|
-
? Math.max(1, pagination.pageIndex + 1)
|
|
89
|
-
: Math.max(1, Math.ceil(totalCount / pagination.pageSize));
|
|
87
|
+
pageCount > 0 ? pageCount : Math.max(1, pagination.pageIndex + 1);
|
|
90
88
|
const countSkeletonWidth = getSkeletonWidth(
|
|
91
89
|
String(total).length +
|
|
92
90
|
rowLabel.length +
|