@nexub/react 5.3.0 → 5.5.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/accessibility.json +21 -1
- package/dist/components/DataTable/DataTable.d.ts +29 -2
- package/dist/components/DataTable/DataTable.js +255 -51
- package/dist/components/DataTable/index.d.ts +1 -1
- package/dist/components/DataTableVirtual/DataTableVirtual.d.ts +19 -0
- package/dist/components/DataTableVirtual/DataTableVirtual.js +37 -0
- package/dist/components/DataTableVirtual/index.d.ts +2 -0
- package/dist/components/DataTableVirtual/index.js +4 -0
- package/dist/components/DateRangePicker/DateRangePicker.d.ts +17 -0
- package/dist/components/DateRangePicker/DateRangePicker.js +123 -71
- package/dist/components/DateRangePicker/index.d.ts +2 -1
- package/dist/components/DateRangePicker/index.js +3 -1
- package/dist/components/FilterBuilder/FilterBuilder.d.ts +50 -0
- package/dist/components/FilterBuilder/FilterBuilder.js +164 -0
- package/dist/components/FilterBuilder/index.d.ts +2 -0
- package/dist/components/FilterBuilder/index.js +8 -0
- package/dist/components/SavedViews/SavedViews.d.ts +26 -0
- package/dist/components/SavedViews/SavedViews.js +65 -0
- package/dist/components/SavedViews/index.d.ts +2 -0
- package/dist/components/SavedViews/index.js +4 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +10 -0
- package/dist/styles.css +1 -1
- package/package.json +13 -6
|
@@ -5,10 +5,22 @@ import * as RadixPopover from "@radix-ui/react-popover";
|
|
|
5
5
|
import { DayPicker } from "react-day-picker";
|
|
6
6
|
import { CalendarDays } from "@nexub/icons";
|
|
7
7
|
import { cn } from "../../utils/cn.js";
|
|
8
|
-
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
8
|
+
import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
9
9
|
import { useCalendarLocale } from "../DatePicker/DatePicker.js";
|
|
10
10
|
import { field, fieldFocusVisible } from "../../utils/control.js";
|
|
11
11
|
import { floatingSurface } from "../../utils/surface.js";
|
|
12
|
+
function todayIn(timeZone, now = /* @__PURE__ */ new Date()) {
|
|
13
|
+
if (!timeZone) return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
14
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
15
|
+
timeZone,
|
|
16
|
+
year: "numeric",
|
|
17
|
+
month: "numeric",
|
|
18
|
+
day: "numeric"
|
|
19
|
+
}).formatToParts(now);
|
|
20
|
+
const get = (t) => Number(parts.find((p) => p.type === t)?.value);
|
|
21
|
+
return new Date(get("year"), get("month") - 1, get("day"));
|
|
22
|
+
}
|
|
23
|
+
const addDays = (d, n) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
|
|
12
24
|
function formatRange(range, placeholder, formatDate) {
|
|
13
25
|
if (!range) return placeholder;
|
|
14
26
|
if (range.from && range.to) {
|
|
@@ -51,79 +63,119 @@ const calendarClassNames = {
|
|
|
51
63
|
disabled: "",
|
|
52
64
|
hidden: "invisible"
|
|
53
65
|
};
|
|
54
|
-
const DateRangePicker = React.forwardRef(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
const DateRangePicker = React.forwardRef(
|
|
67
|
+
function DateRangePicker2({
|
|
68
|
+
value,
|
|
69
|
+
onChange,
|
|
70
|
+
placeholder: placeholderProp,
|
|
71
|
+
disabled = false,
|
|
72
|
+
minDate,
|
|
73
|
+
maxDate,
|
|
74
|
+
className,
|
|
75
|
+
id,
|
|
76
|
+
"aria-label": ariaLabel,
|
|
77
|
+
locale: localeProp,
|
|
78
|
+
presets: presetsProp,
|
|
79
|
+
timeZone
|
|
80
|
+
}, ref) {
|
|
81
|
+
const [open, setOpen] = React.useState(false);
|
|
82
|
+
const messages = useNexubMessages();
|
|
83
|
+
const { locale } = useLocale();
|
|
84
|
+
const presets = presetsProp === true ? [
|
|
85
|
+
{ label: messages.today, range: (t) => ({ from: t, to: t }) },
|
|
86
|
+
{ label: messages.last7Days, range: (t) => ({ from: addDays(t, -6), to: t }) },
|
|
87
|
+
{ label: messages.last30Days, range: (t) => ({ from: addDays(t, -29), to: t }) },
|
|
75
88
|
{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"aria-label": label,
|
|
80
|
-
className: cn(
|
|
81
|
-
"inline-flex h-control-md w-full items-center justify-start gap-2 px-3.5 text-body-md",
|
|
82
|
-
field,
|
|
83
|
-
fieldFocusVisible,
|
|
84
|
-
// Open reads like focus: the 2px primary border (as Select).
|
|
85
|
-
"data-[state=open]:border-[var(--color-primary)] data-[state=open]:shadow-[inset_0_0_0_1px_var(--color-primary)]",
|
|
86
|
-
"disabled:cursor-not-allowed disabled:border-[var(--color-outline-variant)] disabled:bg-[var(--color-surface-container-low)] disabled:text-[var(--color-text-disabled)]",
|
|
87
|
-
!value && "text-[var(--color-on-surface-muted)]",
|
|
88
|
-
className
|
|
89
|
-
),
|
|
90
|
-
children: [
|
|
91
|
-
/* @__PURE__ */ jsx(CalendarDays, { size: "sm", "aria-hidden": "true" }),
|
|
92
|
-
display
|
|
93
|
-
]
|
|
94
|
-
}
|
|
95
|
-
) }),
|
|
96
|
-
/* @__PURE__ */ jsx(RadixPopover.Portal, { children: /* @__PURE__ */ jsx(
|
|
97
|
-
RadixPopover.Content,
|
|
89
|
+
label: messages.monthToDate,
|
|
90
|
+
range: (t) => ({ from: new Date(t.getFullYear(), t.getMonth(), 1), to: t })
|
|
91
|
+
},
|
|
98
92
|
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
),
|
|
105
|
-
children: /* @__PURE__ */ jsx(
|
|
106
|
-
DayPicker,
|
|
107
|
-
{
|
|
108
|
-
mode: "range",
|
|
109
|
-
selected: value,
|
|
110
|
-
defaultMonth: value?.from,
|
|
111
|
-
onSelect: onChange,
|
|
112
|
-
numberOfMonths: 2,
|
|
113
|
-
startMonth: minDate,
|
|
114
|
-
endMonth: maxDate,
|
|
115
|
-
autoFocus: true,
|
|
116
|
-
showOutsideDays: false,
|
|
117
|
-
className: "relative p-3",
|
|
118
|
-
classNames: calendarClassNames,
|
|
119
|
-
...calendar.dayPickerProps
|
|
120
|
-
}
|
|
121
|
-
)
|
|
93
|
+
label: messages.lastMonth,
|
|
94
|
+
range: (t) => ({
|
|
95
|
+
from: new Date(t.getFullYear(), t.getMonth() - 1, 1),
|
|
96
|
+
to: new Date(t.getFullYear(), t.getMonth(), 0)
|
|
97
|
+
})
|
|
122
98
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
99
|
+
] : presetsProp || [];
|
|
100
|
+
const zoneName = timeZone ? new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: "shortOffset" }).formatToParts(/* @__PURE__ */ new Date()).find((p) => p.type === "timeZoneName")?.value ?? timeZone : void 0;
|
|
101
|
+
const calendar = useCalendarLocale(localeProp);
|
|
102
|
+
const placeholder = placeholderProp ?? messages.pickDateRange;
|
|
103
|
+
const display = formatRange(value, placeholder, calendar.format);
|
|
104
|
+
const label = ariaLabel ? `${ariaLabel}: ${display}` : display;
|
|
105
|
+
return /* @__PURE__ */ jsxs(RadixPopover.Root, { open, onOpenChange: setOpen, children: [
|
|
106
|
+
/* @__PURE__ */ jsx(RadixPopover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
107
|
+
"button",
|
|
108
|
+
{
|
|
109
|
+
ref,
|
|
110
|
+
id,
|
|
111
|
+
disabled,
|
|
112
|
+
"aria-label": label,
|
|
113
|
+
className: cn(
|
|
114
|
+
"inline-flex h-control-md w-full items-center justify-start gap-2 px-3.5 text-body-md",
|
|
115
|
+
field,
|
|
116
|
+
fieldFocusVisible,
|
|
117
|
+
// Open reads like focus: the 2px primary border (as Select).
|
|
118
|
+
"data-[state=open]:border-[var(--color-primary)] data-[state=open]:shadow-[inset_0_0_0_1px_var(--color-primary)]",
|
|
119
|
+
"disabled:cursor-not-allowed disabled:border-[var(--color-outline-variant)] disabled:bg-[var(--color-surface-container-low)] disabled:text-[var(--color-text-disabled)]",
|
|
120
|
+
!value && "text-[var(--color-on-surface-muted)]",
|
|
121
|
+
className
|
|
122
|
+
),
|
|
123
|
+
children: [
|
|
124
|
+
/* @__PURE__ */ jsx(CalendarDays, { size: "sm", "aria-hidden": "true" }),
|
|
125
|
+
display
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
) }),
|
|
129
|
+
/* @__PURE__ */ jsx(RadixPopover.Portal, { children: /* @__PURE__ */ jsx(
|
|
130
|
+
RadixPopover.Content,
|
|
131
|
+
{
|
|
132
|
+
align: "start",
|
|
133
|
+
className: cn(
|
|
134
|
+
"z-[var(--z-popover)] outline-none",
|
|
135
|
+
floatingSurface,
|
|
136
|
+
"nx-motion nx-pop origin-[var(--radix-popover-content-transform-origin)]"
|
|
137
|
+
),
|
|
138
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row", children: [
|
|
139
|
+
presets.length > 0 && /* @__PURE__ */ jsx("ul", { className: "flex flex-wrap gap-1 border-b border-[var(--color-outline-variant)] p-2 sm:flex-col sm:flex-nowrap sm:border-b-0 sm:border-e", children: presets.map((p) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
|
|
140
|
+
"button",
|
|
141
|
+
{
|
|
142
|
+
type: "button",
|
|
143
|
+
className: "nx-state-layer w-full rounded-shape-control px-3 py-1.5 text-start text-body-sm text-[var(--color-on-surface)]",
|
|
144
|
+
onClick: () => {
|
|
145
|
+
onChange?.(p.range(todayIn(timeZone)));
|
|
146
|
+
setOpen(false);
|
|
147
|
+
},
|
|
148
|
+
children: p.label
|
|
149
|
+
}
|
|
150
|
+
) }, p.label)) }),
|
|
151
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
152
|
+
/* @__PURE__ */ jsx(
|
|
153
|
+
DayPicker,
|
|
154
|
+
{
|
|
155
|
+
mode: "range",
|
|
156
|
+
selected: value,
|
|
157
|
+
defaultMonth: value?.from,
|
|
158
|
+
onSelect: onChange,
|
|
159
|
+
numberOfMonths: 2,
|
|
160
|
+
startMonth: minDate,
|
|
161
|
+
endMonth: maxDate,
|
|
162
|
+
autoFocus: true,
|
|
163
|
+
showOutsideDays: false,
|
|
164
|
+
className: "relative p-3",
|
|
165
|
+
classNames: calendarClassNames,
|
|
166
|
+
...calendar.dayPickerProps
|
|
167
|
+
}
|
|
168
|
+
),
|
|
169
|
+
zoneName && /* @__PURE__ */ jsx("p", { className: "px-4 pb-3 text-label-sm text-[var(--color-on-surface-muted)]", children: messages.timeZoneLabel(`${zoneName} (${timeZone})`) })
|
|
170
|
+
] })
|
|
171
|
+
] })
|
|
172
|
+
}
|
|
173
|
+
) })
|
|
174
|
+
] });
|
|
175
|
+
}
|
|
176
|
+
);
|
|
126
177
|
DateRangePicker.displayName = "DateRangePicker";
|
|
127
178
|
export {
|
|
128
|
-
DateRangePicker
|
|
179
|
+
DateRangePicker,
|
|
180
|
+
todayIn
|
|
129
181
|
};
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { DateRangePicker } from './DateRangePicker.js';
|
|
2
|
-
export
|
|
2
|
+
export { todayIn } from './DateRangePicker.js';
|
|
3
|
+
export type { DateRangePickerProps, DateRange, DateRangePreset } from './DateRangePicker.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { NexubMessages } from '@nexub/core';
|
|
3
|
+
export type FilterFieldType = 'text' | 'number' | 'select' | 'date';
|
|
4
|
+
export type FilterOperator = keyof NexubMessages['filterOperators'];
|
|
5
|
+
export interface FilterField {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
type: FilterFieldType;
|
|
9
|
+
/** Choices for `type: 'select'`. */
|
|
10
|
+
options?: {
|
|
11
|
+
value: string;
|
|
12
|
+
label: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export interface FilterRule {
|
|
16
|
+
id: string;
|
|
17
|
+
field: string;
|
|
18
|
+
operator: FilterOperator;
|
|
19
|
+
/** Always a string (dates as YYYY-MM-DD), so the query serialises as-is. */
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
export interface FilterGroup {
|
|
23
|
+
id: string;
|
|
24
|
+
combinator: 'and' | 'or';
|
|
25
|
+
rules: Array<FilterRule | FilterGroup>;
|
|
26
|
+
}
|
|
27
|
+
/** Operators offered for each field type, in menu order. */
|
|
28
|
+
export declare const filterOperators: Record<FilterFieldType, FilterOperator[]>;
|
|
29
|
+
export declare const isFilterGroup: (r: FilterRule | FilterGroup) => r is FilterGroup;
|
|
30
|
+
/** An empty top-level group, a starting `value`. */
|
|
31
|
+
export declare const emptyFilter: () => FilterGroup;
|
|
32
|
+
/** Rules with a field, operator and (unless "is empty") a value. Counts nested groups too. */
|
|
33
|
+
export declare function countFilters(group: FilterGroup): number;
|
|
34
|
+
export interface FilterBuilderProps {
|
|
35
|
+
fields: FilterField[];
|
|
36
|
+
value?: FilterGroup;
|
|
37
|
+
defaultValue?: FilterGroup;
|
|
38
|
+
onValueChange?: (value: FilterGroup) => void;
|
|
39
|
+
/** How many levels of nested groups "Add group" may create. Default 1; 0 hides it. */
|
|
40
|
+
maxDepth?: number;
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds an AND/OR filter query as plain JSON. Nexub doesn't run it: send it
|
|
45
|
+
* to your API or evaluate it yourself.
|
|
46
|
+
*/
|
|
47
|
+
export declare function FilterBuilder({ fields, value: valueProp, defaultValue, onValueChange, maxDepth, className }: FilterBuilderProps): React.JSX.Element;
|
|
48
|
+
export declare namespace FilterBuilder {
|
|
49
|
+
var displayName: string;
|
|
50
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Plus, X } from "@nexub/icons";
|
|
5
|
+
import { cn } from "../../utils/cn.js";
|
|
6
|
+
import { useControllableState } from "../../hooks/useControllableState.js";
|
|
7
|
+
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
8
|
+
import { Button } from "../Button/Button.js";
|
|
9
|
+
import { IconButton } from "../IconButton/IconButton.js";
|
|
10
|
+
import { Input } from "../Input/Input.js";
|
|
11
|
+
import { Select } from "../Select/Select.js";
|
|
12
|
+
import { SegmentedControl } from "../SegmentedControl/SegmentedControl.js";
|
|
13
|
+
const filterOperators = {
|
|
14
|
+
text: ["contains", "equals", "notEquals", "startsWith", "isEmpty"],
|
|
15
|
+
number: ["equals", "notEquals", "greaterThan", "lessThan"],
|
|
16
|
+
select: ["equals", "notEquals"],
|
|
17
|
+
date: ["equals", "before", "after"]
|
|
18
|
+
};
|
|
19
|
+
const isFilterGroup = (r) => "rules" in r;
|
|
20
|
+
let seq = 0;
|
|
21
|
+
const uid = () => `f${(++seq).toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
22
|
+
const emptyFilter = () => ({ id: uid(), combinator: "and", rules: [] });
|
|
23
|
+
function countFilters(group) {
|
|
24
|
+
return group.rules.reduce(
|
|
25
|
+
(n, r) => n + (isFilterGroup(r) ? countFilters(r) : r.operator === "isEmpty" || r.value !== "" ? 1 : 0),
|
|
26
|
+
0
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
function FilterBuilder({ fields, value: valueProp, defaultValue, onValueChange, maxDepth = 1, className }) {
|
|
30
|
+
const [value = emptyFilter(), setValue] = useControllableState({
|
|
31
|
+
value: valueProp,
|
|
32
|
+
defaultValue: defaultValue ?? emptyFilter(),
|
|
33
|
+
onChange: onValueChange
|
|
34
|
+
});
|
|
35
|
+
return /* @__PURE__ */ jsx(
|
|
36
|
+
GroupView,
|
|
37
|
+
{
|
|
38
|
+
group: value,
|
|
39
|
+
fields,
|
|
40
|
+
depth: 0,
|
|
41
|
+
maxDepth,
|
|
42
|
+
onChange: setValue,
|
|
43
|
+
className
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
FilterBuilder.displayName = "FilterBuilder";
|
|
48
|
+
function GroupView({ group, fields, depth, maxDepth, onChange, onRemove, className }) {
|
|
49
|
+
const t = useNexubMessages();
|
|
50
|
+
const first = fields[0];
|
|
51
|
+
const setRules = (rules) => onChange({ ...group, rules });
|
|
52
|
+
const newRule = () => ({
|
|
53
|
+
id: uid(),
|
|
54
|
+
field: first?.id ?? "",
|
|
55
|
+
operator: filterOperators[first?.type ?? "text"][0],
|
|
56
|
+
value: ""
|
|
57
|
+
});
|
|
58
|
+
return /* @__PURE__ */ jsxs(
|
|
59
|
+
"div",
|
|
60
|
+
{
|
|
61
|
+
role: "group",
|
|
62
|
+
"aria-label": t.filterGroup(group.combinator),
|
|
63
|
+
className: cn(
|
|
64
|
+
"flex flex-col gap-2",
|
|
65
|
+
depth > 0 && "rounded-shape-container border border-[var(--color-outline-variant)] p-3",
|
|
66
|
+
className
|
|
67
|
+
),
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
70
|
+
/* @__PURE__ */ jsxs(
|
|
71
|
+
SegmentedControl.Root,
|
|
72
|
+
{
|
|
73
|
+
size: "sm",
|
|
74
|
+
"aria-label": t.filterGroup(group.combinator),
|
|
75
|
+
value: group.combinator,
|
|
76
|
+
onValueChange: (v) => v && onChange({ ...group, combinator: v }),
|
|
77
|
+
children: [
|
|
78
|
+
/* @__PURE__ */ jsx(SegmentedControl.Item, { value: "and", children: t.matchAll }),
|
|
79
|
+
/* @__PURE__ */ jsx(SegmentedControl.Item, { value: "or", children: t.matchAny })
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
),
|
|
83
|
+
onRemove && /* @__PURE__ */ jsx(IconButton, { label: t.removeGroup, variant: "ghost", size: "sm", className: "ms-auto", onClick: onRemove, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
|
|
84
|
+
] }),
|
|
85
|
+
group.rules.map((r, i) => {
|
|
86
|
+
const replace = (next) => setRules(group.rules.map((x, j) => j === i ? next : x));
|
|
87
|
+
const remove = () => setRules(group.rules.filter((_, j) => j !== i));
|
|
88
|
+
return isFilterGroup(r) ? /* @__PURE__ */ jsx(GroupView, { group: r, fields, depth: depth + 1, maxDepth, onChange: replace, onRemove: remove }, r.id) : /* @__PURE__ */ jsx(RuleRow, { rule: r, fields, onChange: replace, onRemove: remove }, r.id);
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
91
|
+
/* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: () => setRules([...group.rules, newRule()]), disabled: !first, children: [
|
|
92
|
+
/* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
|
|
93
|
+
t.addFilter
|
|
94
|
+
] }),
|
|
95
|
+
depth < maxDepth && /* @__PURE__ */ jsxs(
|
|
96
|
+
Button,
|
|
97
|
+
{
|
|
98
|
+
variant: "ghost",
|
|
99
|
+
size: "sm",
|
|
100
|
+
onClick: () => setRules([...group.rules, { id: uid(), combinator: "and", rules: [newRule()] }]),
|
|
101
|
+
disabled: !first,
|
|
102
|
+
children: [
|
|
103
|
+
/* @__PURE__ */ jsx(Plus, { size: "sm", "aria-hidden": "true" }),
|
|
104
|
+
t.addGroup
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
] })
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
function RuleRow({
|
|
114
|
+
rule,
|
|
115
|
+
fields,
|
|
116
|
+
onChange,
|
|
117
|
+
onRemove
|
|
118
|
+
}) {
|
|
119
|
+
const t = useNexubMessages();
|
|
120
|
+
const field = fields.find((f) => f.id === rule.field) ?? fields[0];
|
|
121
|
+
const ops = filterOperators[field?.type ?? "text"];
|
|
122
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
123
|
+
/* @__PURE__ */ jsxs(
|
|
124
|
+
Select.Root,
|
|
125
|
+
{
|
|
126
|
+
value: field?.id,
|
|
127
|
+
onValueChange: (id) => {
|
|
128
|
+
const f = fields.find((x) => x.id === id);
|
|
129
|
+
const nextOps = filterOperators[f?.type ?? "text"];
|
|
130
|
+
onChange({ ...rule, field: id, operator: nextOps.includes(rule.operator) ? rule.operator : nextOps[0], value: "" });
|
|
131
|
+
},
|
|
132
|
+
children: [
|
|
133
|
+
/* @__PURE__ */ jsx(Select.Trigger, { "aria-label": t.filterField, className: "w-auto min-w-36", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
|
|
134
|
+
/* @__PURE__ */ jsx(Select.Content, { children: fields.map((f) => /* @__PURE__ */ jsx(Select.Item, { value: f.id, children: f.label }, f.id)) })
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
/* @__PURE__ */ jsxs(Select.Root, { value: rule.operator, onValueChange: (op) => onChange({ ...rule, operator: op }), children: [
|
|
139
|
+
/* @__PURE__ */ jsx(Select.Trigger, { "aria-label": t.filterOperator, className: "w-auto min-w-32", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
|
|
140
|
+
/* @__PURE__ */ jsx(Select.Content, { children: ops.map((op) => /* @__PURE__ */ jsx(Select.Item, { value: op, children: t.filterOperators[op] }, op)) })
|
|
141
|
+
] }),
|
|
142
|
+
rule.operator !== "isEmpty" && (field?.type === "select" ? /* @__PURE__ */ jsxs(Select.Root, { value: rule.value || void 0, onValueChange: (v) => onChange({ ...rule, value: v }), children: [
|
|
143
|
+
/* @__PURE__ */ jsx(Select.Trigger, { "aria-label": t.filterValue, className: "w-auto min-w-36", children: /* @__PURE__ */ jsx(Select.Value, {}) }),
|
|
144
|
+
/* @__PURE__ */ jsx(Select.Content, { children: field.options?.map((o) => /* @__PURE__ */ jsx(Select.Item, { value: o.value, children: o.label }, o.value)) })
|
|
145
|
+
] }) : /* @__PURE__ */ jsx(
|
|
146
|
+
Input,
|
|
147
|
+
{
|
|
148
|
+
"aria-label": t.filterValue,
|
|
149
|
+
type: field?.type === "number" ? "number" : field?.type === "date" ? "date" : "text",
|
|
150
|
+
value: rule.value,
|
|
151
|
+
onChange: (e) => onChange({ ...rule, value: e.target.value }),
|
|
152
|
+
wrapperClassName: "w-auto min-w-36 flex-1"
|
|
153
|
+
}
|
|
154
|
+
)),
|
|
155
|
+
/* @__PURE__ */ jsx(IconButton, { label: t.removeFilter, variant: "ghost", size: "sm", onClick: onRemove, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
|
|
156
|
+
] });
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
FilterBuilder,
|
|
160
|
+
countFilters,
|
|
161
|
+
emptyFilter,
|
|
162
|
+
filterOperators,
|
|
163
|
+
isFilterGroup
|
|
164
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface SavedView {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SavedViewsProps {
|
|
7
|
+
views: SavedView[];
|
|
8
|
+
/** The view in use; its name shows on the button. */
|
|
9
|
+
activeId?: string;
|
|
10
|
+
onSelect: (id: string) => void;
|
|
11
|
+
/** "Save as new view…": asks for a name, then calls this. Your app stores the view. */
|
|
12
|
+
onSave?: (name: string) => void;
|
|
13
|
+
/** "Update ‹view›": overwrite the active view with the current filters. */
|
|
14
|
+
onUpdate?: (id: string) => void;
|
|
15
|
+
/** "Delete ‹view›": remove the active view. */
|
|
16
|
+
onDelete?: (id: string) => void;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A menu of saved filter views. UI only: your app keeps the views and what
|
|
21
|
+
* each one restores (filters, sort, columns).
|
|
22
|
+
*/
|
|
23
|
+
export declare function SavedViews({ views, activeId, onSelect, onSave, onUpdate, onDelete, className }: SavedViewsProps): React.JSX.Element;
|
|
24
|
+
export declare namespace SavedViews {
|
|
25
|
+
var displayName: string;
|
|
26
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Bookmark, ChevronDown } from "@nexub/icons";
|
|
5
|
+
import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
|
|
6
|
+
import { Button } from "../Button/Button.js";
|
|
7
|
+
import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
|
|
8
|
+
import { Dialog } from "../Dialog/Dialog.js";
|
|
9
|
+
import { Input } from "../Input/Input.js";
|
|
10
|
+
function SavedViews({ views, activeId, onSelect, onSave, onUpdate, onDelete, className }) {
|
|
11
|
+
const t = useNexubMessages();
|
|
12
|
+
const [naming, setNaming] = React.useState(false);
|
|
13
|
+
const [name, setName] = React.useState("");
|
|
14
|
+
const active = views.find((v) => v.id === activeId);
|
|
15
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
16
|
+
/* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
|
|
17
|
+
/* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className, children: [
|
|
18
|
+
/* @__PURE__ */ jsx(Bookmark, { size: "sm", "aria-hidden": "true" }),
|
|
19
|
+
active?.name ?? t.views,
|
|
20
|
+
/* @__PURE__ */ jsx(ChevronDown, { size: "sm", "aria-hidden": "true" })
|
|
21
|
+
] }) }),
|
|
22
|
+
/* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "start", children: [
|
|
23
|
+
views.length > 0 && /* @__PURE__ */ jsx(DropdownMenu.RadioGroup, { value: activeId ?? "", onValueChange: onSelect, children: views.map((v) => /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: v.id, children: v.name }, v.id)) }),
|
|
24
|
+
views.length > 0 && (onSave || active && (onUpdate || onDelete)) && /* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
|
|
25
|
+
onSave && /* @__PURE__ */ jsx(
|
|
26
|
+
DropdownMenu.Item,
|
|
27
|
+
{
|
|
28
|
+
onSelect: () => {
|
|
29
|
+
setName("");
|
|
30
|
+
setNaming(true);
|
|
31
|
+
},
|
|
32
|
+
children: t.saveView
|
|
33
|
+
}
|
|
34
|
+
),
|
|
35
|
+
active && onUpdate && /* @__PURE__ */ jsx(DropdownMenu.Item, { onSelect: () => onUpdate(active.id), children: t.updateView(active.name) }),
|
|
36
|
+
active && onDelete && /* @__PURE__ */ jsx(DropdownMenu.Item, { onSelect: () => onDelete(active.id), className: "text-[var(--color-error)]", children: t.deleteView(active.name) })
|
|
37
|
+
] })
|
|
38
|
+
] }),
|
|
39
|
+
onSave && /* @__PURE__ */ jsx(Dialog.Root, { open: naming, onOpenChange: setNaming, children: /* @__PURE__ */ jsx(Dialog.Content, { className: "max-w-sm", children: /* @__PURE__ */ jsxs(
|
|
40
|
+
"form",
|
|
41
|
+
{
|
|
42
|
+
onSubmit: (e) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
const n = name.trim();
|
|
45
|
+
if (!n) return;
|
|
46
|
+
onSave(n);
|
|
47
|
+
setNaming(false);
|
|
48
|
+
},
|
|
49
|
+
className: "flex flex-col gap-4",
|
|
50
|
+
children: [
|
|
51
|
+
/* @__PURE__ */ jsx(Dialog.Header, { children: /* @__PURE__ */ jsx(Dialog.Title, { children: t.saveView.replace(/…$/, "") }) }),
|
|
52
|
+
/* @__PURE__ */ jsx(Input, { "aria-label": t.viewName, placeholder: t.viewName, value: name, onChange: (e) => setName(e.target.value), autoFocus: true }),
|
|
53
|
+
/* @__PURE__ */ jsxs(Dialog.Footer, { children: [
|
|
54
|
+
/* @__PURE__ */ jsx(Dialog.Close, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", children: t.cancel }) }),
|
|
55
|
+
/* @__PURE__ */ jsx(Button, { type: "submit", disabled: !name.trim(), children: t.save })
|
|
56
|
+
] })
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
) }) })
|
|
60
|
+
] });
|
|
61
|
+
}
|
|
62
|
+
SavedViews.displayName = "SavedViews";
|
|
63
|
+
export {
|
|
64
|
+
SavedViews
|
|
65
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -161,6 +161,10 @@ export { Combobox } from './components/Combobox/index.js';
|
|
|
161
161
|
export { Autocomplete } from './components/Autocomplete/index.js';
|
|
162
162
|
export type { AutocompleteOption, AutocompleteProps } from './components/Autocomplete/index.js';
|
|
163
163
|
export { MultiSelect } from './components/MultiSelect/index.js';
|
|
164
|
+
export { FilterBuilder, filterOperators, isFilterGroup, emptyFilter, countFilters } from './components/FilterBuilder/index.js';
|
|
165
|
+
export type { FilterBuilderProps, FilterField, FilterFieldType, FilterOperator, FilterRule, FilterGroup } from './components/FilterBuilder/index.js';
|
|
166
|
+
export { SavedViews } from './components/SavedViews/index.js';
|
|
167
|
+
export type { SavedViewsProps, SavedView } from './components/SavedViews/index.js';
|
|
164
168
|
export type { MultiSelectOption, MultiSelectProps } from './components/MultiSelect/index.js';
|
|
165
169
|
export { Switch } from './components/Switch/index.js';
|
|
166
170
|
export type { SwitchProps } from './components/Switch/index.js';
|
|
@@ -200,7 +204,7 @@ export { CommandPalette } from './components/CommandPalette/index.js';
|
|
|
200
204
|
export type { CommandPaletteRootProps } from './components/CommandPalette/index.js';
|
|
201
205
|
export { Table } from './components/Table/index.js';
|
|
202
206
|
export { DataTable } from './components/DataTable/index.js';
|
|
203
|
-
export type { DataTableProps } from './components/DataTable/index.js';
|
|
207
|
+
export type { DataTableProps, DataTableDensity } from './components/DataTable/index.js';
|
|
204
208
|
export { EmptyState } from './components/EmptyState/index.js';
|
|
205
209
|
export { ErrorState } from './components/ErrorState/index.js';
|
|
206
210
|
export type { ErrorStateProps } from './components/ErrorState/index.js';
|
|
@@ -209,7 +213,8 @@ export type { FileUploadRootProps, FileRejection } from './components/FileUpload
|
|
|
209
213
|
export { DatePicker } from './components/DatePicker/index.js';
|
|
210
214
|
export type { DatePickerProps } from './components/DatePicker/index.js';
|
|
211
215
|
export { DateRangePicker } from './components/DateRangePicker/index.js';
|
|
212
|
-
export
|
|
216
|
+
export { todayIn } from './components/DateRangePicker/index.js';
|
|
217
|
+
export type { DateRangePickerProps, DateRange, DateRangePreset } from './components/DateRangePicker/index.js';
|
|
213
218
|
export { ThemeProvider, ThemeScript, useTheme } from './components/ThemeProvider/index.js';
|
|
214
219
|
export type { ThemeMode, GlassMode, ThemeContextValue, ThemeProviderProps, ThemeScriptProps } from './components/ThemeProvider/index.js';
|
|
215
220
|
export { Fade, Slide, Scale, Collapse, ListTransition, SkeletonSwap, usePrefersReducedMotion, Reveal, Float, Marquee } from './components/Motion/index.js';
|
package/dist/index.js
CHANGED
|
@@ -90,6 +90,8 @@ import { Select } from "./components/Select/index.js";
|
|
|
90
90
|
import { Combobox } from "./components/Combobox/index.js";
|
|
91
91
|
import { Autocomplete } from "./components/Autocomplete/index.js";
|
|
92
92
|
import { MultiSelect } from "./components/MultiSelect/index.js";
|
|
93
|
+
import { FilterBuilder, filterOperators, isFilterGroup, emptyFilter, countFilters } from "./components/FilterBuilder/index.js";
|
|
94
|
+
import { SavedViews } from "./components/SavedViews/index.js";
|
|
93
95
|
import { Switch } from "./components/Switch/index.js";
|
|
94
96
|
import { Checkbox } from "./components/Checkbox/index.js";
|
|
95
97
|
import { RadioGroup } from "./components/RadioGroup/index.js";
|
|
@@ -119,6 +121,7 @@ import { ErrorState } from "./components/ErrorState/index.js";
|
|
|
119
121
|
import { FileUpload, fileAccepted } from "./components/FileUpload/index.js";
|
|
120
122
|
import { DatePicker } from "./components/DatePicker/index.js";
|
|
121
123
|
import { DateRangePicker } from "./components/DateRangePicker/index.js";
|
|
124
|
+
import { todayIn } from "./components/DateRangePicker/index.js";
|
|
122
125
|
import { ThemeProvider, ThemeScript, useTheme } from "./components/ThemeProvider/index.js";
|
|
123
126
|
import { Fade, Slide, Scale, Collapse, ListTransition, SkeletonSwap, usePrefersReducedMotion, Reveal, Float, Marquee } from "./components/Motion/index.js";
|
|
124
127
|
export {
|
|
@@ -171,6 +174,7 @@ export {
|
|
|
171
174
|
EyebrowChip,
|
|
172
175
|
Fade,
|
|
173
176
|
FileUpload,
|
|
177
|
+
FilterBuilder,
|
|
174
178
|
FilterChip,
|
|
175
179
|
Flag,
|
|
176
180
|
Flex,
|
|
@@ -220,6 +224,7 @@ export {
|
|
|
220
224
|
RadioGroup,
|
|
221
225
|
Rating,
|
|
222
226
|
Reveal,
|
|
227
|
+
SavedViews,
|
|
223
228
|
Scale,
|
|
224
229
|
ScrollArea,
|
|
225
230
|
ScrollProgress,
|
|
@@ -271,13 +276,17 @@ export {
|
|
|
271
276
|
chipVariants,
|
|
272
277
|
cn,
|
|
273
278
|
containerVariants,
|
|
279
|
+
countFilters,
|
|
274
280
|
defaultMessageStrings,
|
|
275
281
|
defaultMessages,
|
|
282
|
+
emptyFilter,
|
|
276
283
|
fileAccepted,
|
|
284
|
+
filterOperators,
|
|
277
285
|
headingVariants,
|
|
278
286
|
iconButtonVariants,
|
|
279
287
|
inputVariants,
|
|
280
288
|
inputWrapperVariants,
|
|
289
|
+
isFilterGroup,
|
|
281
290
|
labelVariants,
|
|
282
291
|
linkVariants,
|
|
283
292
|
logoVariants,
|
|
@@ -295,6 +304,7 @@ export {
|
|
|
295
304
|
textVariants,
|
|
296
305
|
textareaVariants,
|
|
297
306
|
toast,
|
|
307
|
+
todayIn,
|
|
298
308
|
toggleVariants,
|
|
299
309
|
transitionName,
|
|
300
310
|
useControllableState,
|