@nexub/react 5.4.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/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 +6 -1
- package/dist/index.js +10 -0
- package/dist/styles.css +1 -1
- package/package.json +6 -6
package/accessibility.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "5.
|
|
2
|
+
"version": "5.5.0",
|
|
3
3
|
"standard": "WCAG 2.2 Level AA",
|
|
4
4
|
"method": {
|
|
5
5
|
"automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark on every pull request; a new violation fails the build.",
|
|
@@ -399,6 +399,17 @@
|
|
|
399
399
|
"keyboard": [],
|
|
400
400
|
"screenReader": []
|
|
401
401
|
},
|
|
402
|
+
{
|
|
403
|
+
"slug": "filter-builder",
|
|
404
|
+
"name": "Filter Builder",
|
|
405
|
+
"package": "@nexub/react",
|
|
406
|
+
"automated": "pass",
|
|
407
|
+
"exceptions": [],
|
|
408
|
+
"keyboard": [],
|
|
409
|
+
"screenReader": [
|
|
410
|
+
"Field, condition and value controls are labelled; remove buttons say what they remove."
|
|
411
|
+
]
|
|
412
|
+
},
|
|
402
413
|
{
|
|
403
414
|
"slug": "filter-chip",
|
|
404
415
|
"name": "Filter Chip",
|
|
@@ -839,6 +850,15 @@
|
|
|
839
850
|
"keyboard": [],
|
|
840
851
|
"screenReader": []
|
|
841
852
|
},
|
|
853
|
+
{
|
|
854
|
+
"slug": "saved-views",
|
|
855
|
+
"name": "Saved Views",
|
|
856
|
+
"package": "@nexub/react",
|
|
857
|
+
"automated": "pass",
|
|
858
|
+
"exceptions": [],
|
|
859
|
+
"keyboard": [],
|
|
860
|
+
"screenReader": []
|
|
861
|
+
},
|
|
842
862
|
{
|
|
843
863
|
"slug": "scroll-area",
|
|
844
864
|
"name": "Scroll Area",
|
|
@@ -2,6 +2,13 @@ import * as React from 'react';
|
|
|
2
2
|
import { type DateRange } from 'react-day-picker';
|
|
3
3
|
import type { Locale } from 'date-fns';
|
|
4
4
|
export type { DateRange };
|
|
5
|
+
export interface DateRangePreset {
|
|
6
|
+
label: string;
|
|
7
|
+
/** `today` is the current date in the picker's time zone (midnight, local Date). */
|
|
8
|
+
range: (today: Date) => DateRange;
|
|
9
|
+
}
|
|
10
|
+
/** Today's calendar date in `timeZone`, as a local midnight Date. */
|
|
11
|
+
export declare function todayIn(timeZone?: string, now?: Date): Date;
|
|
5
12
|
export interface DateRangePickerProps {
|
|
6
13
|
value?: DateRange;
|
|
7
14
|
onChange?: (range: DateRange | undefined) => void;
|
|
@@ -16,5 +23,15 @@ export interface DateRangePickerProps {
|
|
|
16
23
|
'aria-label'?: string;
|
|
17
24
|
/** date-fns Locale for the calendar and trigger text; wins over NexubProvider's `dateLocale`. */
|
|
18
25
|
locale?: Locale;
|
|
26
|
+
/**
|
|
27
|
+
* Quick ranges beside the calendar. `true` shows Today, Last 7 days,
|
|
28
|
+
* Last 30 days, Month to date and Last month; or pass your own.
|
|
29
|
+
*/
|
|
30
|
+
presets?: boolean | DateRangePreset[];
|
|
31
|
+
/**
|
|
32
|
+
* IANA time zone the dates mean, e.g. 'Europe/London'. Shown under the
|
|
33
|
+
* calendar, and presets count "today" there.
|
|
34
|
+
*/
|
|
35
|
+
timeZone?: string;
|
|
19
36
|
}
|
|
20
37
|
export declare const DateRangePicker: React.ForwardRefExoticComponent<DateRangePickerProps & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -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';
|
|
@@ -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';
|