@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.
@@ -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(function DateRangePicker2({
55
- value,
56
- onChange,
57
- placeholder: placeholderProp,
58
- disabled = false,
59
- minDate,
60
- maxDate,
61
- className,
62
- id,
63
- "aria-label": ariaLabel,
64
- locale: localeProp
65
- }, ref) {
66
- const [open, setOpen] = React.useState(false);
67
- const messages = useNexubMessages();
68
- const calendar = useCalendarLocale(localeProp);
69
- const placeholder = placeholderProp ?? messages.pickDateRange;
70
- const display = formatRange(value, placeholder, calendar.format);
71
- const label = ariaLabel ? `${ariaLabel}: ${display}` : display;
72
- return /* @__PURE__ */ jsxs(RadixPopover.Root, { open, onOpenChange: setOpen, children: [
73
- /* @__PURE__ */ jsx(RadixPopover.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(
74
- "button",
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
- ref,
77
- id,
78
- disabled,
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
- align: "start",
100
- className: cn(
101
- "z-[var(--z-popover)] outline-none",
102
- floatingSurface,
103
- "nx-motion nx-pop origin-[var(--radix-popover-content-transform-origin)]"
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 type { DateRangePickerProps, DateRange } from './DateRangePicker.js';
2
+ export { todayIn } from './DateRangePicker.js';
3
+ export type { DateRangePickerProps, DateRange, DateRangePreset } from './DateRangePicker.js';
@@ -1,4 +1,6 @@
1
1
  import { DateRangePicker } from "./DateRangePicker.js";
2
+ import { todayIn } from "./DateRangePicker.js";
2
3
  export {
3
- DateRangePicker
4
+ DateRangePicker,
5
+ todayIn
4
6
  };
@@ -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,2 @@
1
+ export { FilterBuilder, filterOperators, isFilterGroup, emptyFilter, countFilters } from './FilterBuilder.js';
2
+ export type { FilterBuilderProps, FilterField, FilterFieldType, FilterOperator, FilterRule, FilterGroup } from './FilterBuilder.js';
@@ -0,0 +1,8 @@
1
+ import { FilterBuilder, filterOperators, isFilterGroup, emptyFilter, countFilters } from "./FilterBuilder.js";
2
+ export {
3
+ FilterBuilder,
4
+ countFilters,
5
+ emptyFilter,
6
+ filterOperators,
7
+ isFilterGroup
8
+ };
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ export { SavedViews } from './SavedViews.js';
2
+ export type { SavedViewsProps, SavedView } from './SavedViews.js';
@@ -0,0 +1,4 @@
1
+ import { SavedViews } from "./SavedViews.js";
2
+ export {
3
+ SavedViews
4
+ };
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 type { DateRangePickerProps, DateRange } from './components/DateRangePicker/index.js';
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,