@neasg/design-system 0.4.18 → 0.4.19

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.
@@ -72,7 +72,7 @@ const FilterIcon = forwardRef(({ onMouseEnter, onMouseLeave, className, size = 2
72
72
  controls.start("normal");
73
73
  }
74
74
  }, [controls, onMouseLeave, isParentHovered]);
75
- return (_jsx("div", { ref: divRef, className: cn(className), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...props, children: _jsx(motion.svg, { animate: controls, initial: "normal", fill: "none", height: size, stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", transition: DEFAULT_TRANSITION, variants: SVG_VARIANTS, viewBox: "0 0 24 24", width: size, xmlns: "http://www.w3.org/2000/svg", children: _jsx("polygon", { points: "22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" }) }) }));
75
+ return (_jsx("div", { ref: divRef, "data-slot": "filter-icon", className: cn(className), onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, ...props, children: _jsx(motion.svg, { animate: controls, initial: "normal", fill: "none", height: size, stroke: "currentColor", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", transition: DEFAULT_TRANSITION, variants: SVG_VARIANTS, viewBox: "0 0 24 24", width: size, xmlns: "http://www.w3.org/2000/svg", children: _jsx("polygon", { points: "22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" }) }) }));
76
76
  });
77
77
  FilterIcon.displayName = "FilterIcon";
78
78
  export { FilterIcon };
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { ArrowRight, ArrowUpRight, SlidersHorizontal } from "lucide-react";
4
+ import { ArrowRight, ArrowUpRight } from "lucide-react";
5
+ import { FilterIcon } from "./animated-icons";
5
6
  import { Badge } from "./badge";
6
7
  import { Button } from "./button";
7
8
  import { Command, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "./command";
@@ -211,7 +212,7 @@ function CommandSearch({ value, onValueChange, sections, children, className, is
211
212
  ? "h-control w-full min-w-[14rem] justify-start gap-[var(--space-2)] rounded-control border border-[hsl(var(--layout-sidebar-border))] bg-card px-[var(--space-2)] !font-normal text-muted-foreground shadow-none hover:bg-card hover:text-foreground focus-visible:border-ring focus-visible:bg-card focus-visible:ring-ring/30"
212
213
  : isCollapsed
213
214
  ? "h-control min-h-control w-control min-w-control max-w-control shrink-0 rounded-control p-[var(--space-0)] !font-normal text-muted-foreground hover:bg-card hover:text-foreground"
214
- : "h-control w-full min-w-[14rem] justify-start gap-[var(--space-2)] rounded-control border border-border bg-card px-[var(--space-2)] !font-normal text-muted-foreground hover:bg-card hover:text-foreground", triggerClassName), children: [_jsx(SearchIcon, { className: "shrink-0", size: 16 }), !isCollapsed ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 text-left text-sm", children: triggerPlaceholder }), _jsx("kbd", { className: "hidden h-[var(--space-5)] items-center gap-[var(--space-1)] rounded border border-border bg-muted px-[var(--space-1)] font-mono text-[11px] text-muted-foreground sm:inline-flex", children: resolvedLabel })] })) : null] }), _jsx(DialogRoot, { open: isOpen, onOpenChange: setIsOpen, children: _jsxs(DialogSurface, { className: cn("w-[calc(100vw-var(--space-8))] max-w-[60rem] gap-[var(--space-0)] overflow-hidden p-[var(--space-0)] sm:w-[min(72vw,60rem)]", dialogClassName), "aria-describedby": undefined, hideCloseButton: true, children: [_jsx(DialogTitle, { className: "sr-only", children: inputPlaceholder }), _jsxs(Command, { shouldFilter: false, className: "rounded-none border-0", children: [_jsx(CommandInput, { ref: inputRef, value: value, onValueChange: onValueChange, placeholder: inputPlaceholder, wrapperClassName: "border-b px-[var(--space-3)] py-[var(--space-2)]", iconClassName: "hidden", className: "h-control border-0 bg-transparent px-[var(--space-3)] py-[var(--space-0)] pr-[calc(var(--space-12)+var(--space-6))] text-sm shadow-none focus-visible:border-transparent focus-visible:ring-0", endAdornment: _jsxs(_Fragment, { children: [value ? (_jsx("button", { type: "button", "aria-label": "Clear search", onMouseDown: preserveInputFocus, onClick: clearSearch, className: "flex size-[var(--space-6)] items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(XIcon, { size: 14, "aria-hidden": "true" }) })) : null, _jsx("kbd", { className: "inline-flex h-[var(--space-5)] items-center rounded border border-border bg-muted px-[var(--space-1)] font-mono text-[11px] leading-none text-muted-foreground", "aria-hidden": "true", children: "Esc" })] }) }), allFilter || filters.length ? (_jsxs("div", { role: "group", "aria-label": "Search filters", "data-multiselectable": allowMultiSelect ? "true" : "false", className: "flex flex-wrap items-center gap-[var(--space-1)] border-b px-[var(--space-3)] py-[var(--space-2)]", children: [allFilter ? (_jsxs(FilterToggle, { selected: allFilter.selected, "aria-pressed": Boolean(allFilter.selected), onMouseDown: preserveInputFocus, onClick: () => selectFilter(allFilter), children: [allFilter.label, typeof allFilter.count === "number" ? (_jsx(CountBadge, { count: allFilter.count, hideWhenZero: true, size: "sm", variant: allFilter.selected ? "default" : "muted" })) : null] })) : null, filters.map((filter) => (_jsxs(FilterToggle, { selected: filter.selected, "aria-pressed": Boolean(filter.selected), onMouseDown: preserveInputFocus, onClick: () => selectFilter(filter), children: [filter.label, typeof filter.count === "number" ? (_jsx(CountBadge, { count: filter.count, hideWhenZero: true, size: "sm", variant: filter.selected ? "default" : "muted" })) : null] }, filter.value)))] })) : null, subFilters.length ? (_jsxs("div", { role: "group", "aria-label": "Search sub-filters", "data-multiselectable": allowMultiSelect ? "true" : "false", className: "flex flex-wrap items-center gap-[var(--space-1)] border-b bg-muted/30 px-[var(--space-3)] py-[var(--space-2)]", children: [_jsx(SlidersHorizontal, { "data-slot": "command-search-sub-filter-icon", className: "mr-[var(--space-1)] h-3.5 w-3.5 shrink-0 text-muted-foreground", "aria-hidden": "true" }), subFilters.map((filter) => (_jsxs(FilterToggle, { selected: filter.selected, "aria-pressed": Boolean(filter.selected), onMouseDown: preserveInputFocus, onClick: () => selectFilter(filter), children: [filter.label, typeof filter.count === "number" ? (_jsx(CountBadge, { count: filter.count, hideWhenZero: true, size: "sm", variant: filter.selected ? "default" : "muted" })) : null] }, filter.value)))] })) : null, _jsx(CommandList, { "aria-busy": loading || undefined, className: cn("p-[var(--space-1)]", resultsClassName), style: {
215
+ : "h-control w-full min-w-[14rem] justify-start gap-[var(--space-2)] rounded-control border border-border bg-card px-[var(--space-2)] !font-normal text-muted-foreground hover:bg-card hover:text-foreground", triggerClassName), children: [_jsx(SearchIcon, { className: "shrink-0", size: 16 }), !isCollapsed ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "flex-1 text-left text-sm", children: triggerPlaceholder }), _jsx("kbd", { className: "hidden h-[var(--space-5)] items-center gap-[var(--space-1)] rounded border border-border bg-muted px-[var(--space-1)] font-mono text-[11px] text-muted-foreground sm:inline-flex", children: resolvedLabel })] })) : null] }), _jsx(DialogRoot, { open: isOpen, onOpenChange: setIsOpen, children: _jsxs(DialogSurface, { className: cn("w-[calc(100vw-var(--space-8))] max-w-[60rem] gap-[var(--space-0)] overflow-hidden p-[var(--space-0)] sm:w-[min(72vw,60rem)]", dialogClassName), "aria-describedby": undefined, hideCloseButton: true, children: [_jsx(DialogTitle, { className: "sr-only", children: inputPlaceholder }), _jsxs(Command, { shouldFilter: false, className: "rounded-none border-0", children: [_jsx(CommandInput, { ref: inputRef, value: value, onValueChange: onValueChange, placeholder: inputPlaceholder, wrapperClassName: "border-b px-[var(--space-3)] py-[var(--space-2)]", iconClassName: "hidden", className: "h-control border-0 bg-transparent px-[var(--space-3)] py-[var(--space-0)] pr-[calc(var(--space-12)+var(--space-6))] text-sm shadow-none focus-visible:border-transparent focus-visible:ring-0", endAdornment: _jsxs(_Fragment, { children: [value ? (_jsx("button", { type: "button", "aria-label": "Clear search", onMouseDown: preserveInputFocus, onClick: clearSearch, className: "flex size-[var(--space-6)] items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(XIcon, { size: 14, "aria-hidden": "true" }) })) : null, _jsx("kbd", { className: "inline-flex h-[var(--space-5)] items-center rounded border border-border bg-muted px-[var(--space-1)] font-mono text-[11px] leading-none text-muted-foreground", "aria-hidden": "true", children: "Esc" })] }) }), allFilter || filters.length ? (_jsxs("div", { role: "group", "aria-label": "Search filters", "data-multiselectable": allowMultiSelect ? "true" : "false", className: "flex flex-wrap items-center gap-[var(--space-1)] border-b px-[var(--space-3)] py-[var(--space-2)]", children: [allFilter ? (_jsxs(FilterToggle, { selected: allFilter.selected, "aria-pressed": Boolean(allFilter.selected), onMouseDown: preserveInputFocus, onClick: () => selectFilter(allFilter), children: [allFilter.label, typeof allFilter.count === "number" ? (_jsx(CountBadge, { count: allFilter.count, hideWhenZero: true, size: "sm", variant: allFilter.selected ? "default" : "muted" })) : null] })) : null, filters.map((filter) => (_jsxs(FilterToggle, { selected: filter.selected, "aria-pressed": Boolean(filter.selected), onMouseDown: preserveInputFocus, onClick: () => selectFilter(filter), children: [filter.label, typeof filter.count === "number" ? (_jsx(CountBadge, { count: filter.count, hideWhenZero: true, size: "sm", variant: filter.selected ? "default" : "muted" })) : null] }, filter.value)))] })) : null, subFilters.length ? (_jsxs("div", { role: "group", "aria-label": "Search sub-filters", "data-multiselectable": allowMultiSelect ? "true" : "false", className: "flex flex-wrap items-center gap-[var(--space-1)] border-b bg-muted/30 px-[var(--space-3)] py-[var(--space-2)]", children: [_jsx(FilterIcon, { "data-slot": "command-search-sub-filter-icon", className: "mr-[var(--space-1)] shrink-0 text-muted-foreground", size: 14, "aria-hidden": "true" }), subFilters.map((filter) => (_jsxs(FilterToggle, { selected: filter.selected, "aria-pressed": Boolean(filter.selected), onMouseDown: preserveInputFocus, onClick: () => selectFilter(filter), children: [filter.label, typeof filter.count === "number" ? (_jsx(CountBadge, { count: filter.count, hideWhenZero: true, size: "sm", variant: filter.selected ? "default" : "muted" })) : null] }, filter.value)))] })) : null, _jsx(CommandList, { "aria-busy": loading || undefined, className: cn("p-[var(--space-1)]", resultsClassName), style: {
215
216
  minHeight: resultsMinHeight,
216
217
  maxHeight: resultsMaxHeight,
217
218
  }, children: loading ? (_jsx(CommandSearchSkeleton, { rows: loadingRows })) : children !== undefined ? children : sections ? (visibleSections.length ? (visibleSections.map((section, index) => {
@@ -5,6 +5,8 @@ export interface DateRangeInputValue {
5
5
  }
6
6
  interface DateInputSharedProps {
7
7
  label: React.ReactNode;
8
+ /** Visible heading for the calendar popover. */
9
+ popoverTitle?: React.ReactNode;
8
10
  appearance?: "field" | "inline";
9
11
  description?: React.ReactNode;
10
12
  error?: React.ReactNode;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { format, parse } from "date-fns";
5
5
  import { CalendarIcon } from "lucide-react";
@@ -7,8 +7,8 @@ import { XIcon } from "./animated-icons/x";
7
7
  import { Calendar } from "./calendar";
8
8
  import { inputControlClassName } from "./input-control";
9
9
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
10
- import { PopoverContent, PopoverRoot, PopoverTrigger } from "./popover-primitive";
11
10
  import { cn } from "./lib/utils";
11
+ import { Popover } from "./popover";
12
12
  function createLocalDate(year, monthIndex, day) {
13
13
  const date = new Date(year, monthIndex, day);
14
14
  if (date.getFullYear() !== year ||
@@ -93,7 +93,7 @@ function formatDisplayRange(range) {
93
93
  }
94
94
  const DateInput = React.forwardRef((props, ref) => {
95
95
  var _a, _b, _c;
96
- const { label, appearance = "field", description, error, invalid = false, hideLabel = false, disabled = false, clearable = false, clearLabel, minDate, maxDate, className, triggerClassName, labelClassName, descriptionClassName, errorClassName, id, } = props;
96
+ const { label, popoverTitle, appearance = "field", description, error, invalid = false, hideLabel = false, disabled = false, clearable = false, clearLabel, minDate, maxDate, className, triggerClassName, labelClassName, descriptionClassName, errorClassName, id, } = props;
97
97
  const [open, setOpen] = React.useState(false);
98
98
  const [isSelectingRangeEnd, setIsSelectingRangeEnd] = React.useState(false);
99
99
  const generatedId = React.useId();
@@ -164,11 +164,11 @@ const DateInput = React.forwardRef((props, ref) => {
164
164
  }
165
165
  };
166
166
  const disabledDays = (date) => isDateOutsideBounds(date, minDate, maxDate);
167
- const calendarContent = (_jsx(PopoverContent, { className: "w-auto p-0", align: "start", children: props.mode === "range" ? (_jsx(Calendar, { mode: "range", selected: selectedRange, onSelect: handleRangeSelect, disabled: disabledDays, defaultMonth: (_b = selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.from) !== null && _b !== void 0 ? _b : selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.to, numberOfMonths: (_c = props.numberOfMonths) !== null && _c !== void 0 ? _c : 2, showOutsideDays: false, autoFocus: true })) : (_jsx(Calendar, { mode: "single", selected: selectedDate, onSelect: handleSelect, disabled: disabledDays, defaultMonth: selectedDate, autoFocus: true })) }));
167
+ const calendarContent = (_jsx(_Fragment, { children: props.mode === "range" ? (_jsx(Calendar, { mode: "range", selected: selectedRange, onSelect: handleRangeSelect, disabled: disabledDays, defaultMonth: (_b = selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.from) !== null && _b !== void 0 ? _b : selectedRange === null || selectedRange === void 0 ? void 0 : selectedRange.to, numberOfMonths: (_c = props.numberOfMonths) !== null && _c !== void 0 ? _c : 2, showOutsideDays: false, autoFocus: true })) : (_jsx(Calendar, { mode: "single", selected: selectedDate, onSelect: handleSelect, disabled: disabledDays, defaultMonth: selectedDate, autoFocus: true })) }));
168
168
  if (appearance === "inline") {
169
- return (_jsxs("span", { className: cn("inline-flex", className), "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsxs(PopoverRoot, { open: open, onOpenChange: handleOpenChange, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { ref: ref, type: "button", id: inputId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("inline-flex cursor-pointer items-baseline rounded-sm border-0 bg-transparent p-0 font-medium text-primary underline decoration-primary/40 underline-offset-4 transition-colors hover:decoration-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", !displayValue && "text-muted-foreground decoration-border", isInvalid && "text-destructive decoration-destructive", triggerClassName), children: [_jsxs("span", { className: "sr-only", children: [label, ": "] }), _jsx("span", { children: displayValue !== null && displayValue !== void 0 ? displayValue : placeholder })] }) }), calendarContent] }), showDescription ? (_jsx("span", { id: descriptionId, className: "sr-only", children: description })) : null, error ? (_jsx("span", { id: errorId, role: "alert", className: "sr-only", children: error })) : null] }));
169
+ return (_jsxs("span", { className: cn("inline-flex", className), "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(Popover, { open: open, onOpenChange: handleOpenChange, align: "start", title: popoverTitle, surface: popoverTitle ? "menu" : "panel", contentClassName: "w-auto p-0", trigger: _jsxs("button", { ref: ref, type: "button", id: inputId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("inline-flex cursor-pointer items-baseline rounded-sm border-0 bg-transparent p-0 font-medium text-primary underline decoration-primary/40 underline-offset-4 transition-colors hover:decoration-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", !displayValue && "text-muted-foreground decoration-border", isInvalid && "text-destructive decoration-destructive", triggerClassName), children: [_jsxs("span", { className: "sr-only", children: [label, ": "] }), _jsx("span", { children: displayValue !== null && displayValue !== void 0 ? displayValue : placeholder })] }), children: calendarContent }), showDescription ? (_jsx("span", { id: descriptionId, className: "sr-only", children: description })) : null, error ? (_jsx("span", { id: errorId, role: "alert", className: "sr-only", children: error })) : null] }));
170
170
  }
171
- return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs(PopoverRoot, { open: open, onOpenChange: handleOpenChange, children: [_jsxs("div", { className: "relative", children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { ref: ref, type: "button", id: inputId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn(inputControlClassName, "relative cursor-pointer pr-9 text-left", showClearButton && "pr-16", !displayValue && "text-muted-foreground", isInvalid && "border-destructive focus-visible:ring-destructive", triggerClassName), children: _jsx("span", { className: "block truncate", children: displayValue !== null && displayValue !== void 0 ? displayValue : placeholder }) }) }), _jsxs("span", { "aria-hidden": showClearButton ? undefined : true, className: "pointer-events-none absolute inset-y-0 right-3 flex items-center gap-2 text-muted-foreground", children: [showClearButton ? (_jsx("button", { type: "button", "aria-label": resolvedClearLabel, onClick: handleClear, className: "pointer-events-auto inline-flex h-4 w-4 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed", children: _jsx(XIcon, { size: 14, className: "flex items-center justify-center" }) })) : null, _jsx(CalendarIcon, { "aria-hidden": "true", className: "h-4 w-4 shrink-0", strokeWidth: 2 })] })] }), calendarContent] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
171
+ return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: inputId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs("div", { className: "relative", children: [_jsx(Popover, { open: open, onOpenChange: handleOpenChange, align: "start", title: popoverTitle, surface: popoverTitle ? "menu" : "panel", contentClassName: "w-auto p-0", trigger: _jsx("button", { ref: ref, type: "button", id: inputId, disabled: disabled, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn(inputControlClassName, "relative cursor-pointer pr-9 text-left", showClearButton && "pr-16", !displayValue && "text-muted-foreground", isInvalid && "border-destructive focus-visible:ring-destructive", triggerClassName), children: _jsx("span", { className: "block truncate", children: displayValue !== null && displayValue !== void 0 ? displayValue : placeholder }) }), children: calendarContent }), _jsxs("span", { "aria-hidden": showClearButton ? undefined : true, className: "pointer-events-none absolute inset-y-0 right-3 flex items-center gap-2 text-muted-foreground", children: [showClearButton ? (_jsx("button", { type: "button", "aria-label": resolvedClearLabel, onClick: handleClear, className: "pointer-events-auto inline-flex h-4 w-4 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed", children: _jsx(XIcon, { size: 14, className: "flex items-center justify-center" }) })) : null, _jsx(CalendarIcon, { "aria-hidden": "true", className: "h-4 w-4 shrink-0", strokeWidth: 2 })] })] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
172
172
  });
173
173
  DateInput.displayName = "DateInput";
174
174
  export { DateInput };
@@ -25,14 +25,18 @@ export interface FilterPopoverProps extends Pick<PopoverProps, "align" | "defaul
25
25
  selectedCount?: number;
26
26
  trigger?: React.ReactElement;
27
27
  triggerLabel?: React.ReactNode;
28
+ title?: React.ReactNode;
28
29
  groupListLabel?: React.ReactNode;
29
30
  resetLabel?: React.ReactNode;
30
31
  emptyLabel?: React.ReactNode;
32
+ ariaLabel?: string;
31
33
  onReset?: () => void;
32
34
  contentClassName?: string;
33
35
  viewClassName?: string;
34
36
  maxHeight?: string;
37
+ /** Opens the only group directly, without the grouped-filter navigation screen. */
38
+ directSingleGroup?: boolean;
35
39
  }
36
40
  declare function getFilterSelectionLabel(count: number): string;
37
- declare function FilterPopover({ align, contentClassName, defaultOpen, emptyLabel, groupListLabel, groups, maxHeight, modal, onOpenChange, onReset, open, resetLabel, selectedCount, side, trigger, triggerLabel, viewClassName, }: FilterPopoverProps): import("react/jsx-runtime").JSX.Element;
41
+ declare function FilterPopover({ align, ariaLabel, contentClassName, defaultOpen, directSingleGroup, emptyLabel, groupListLabel, groups, maxHeight, modal, onOpenChange, onReset, open, resetLabel, selectedCount, side, trigger, triggerLabel, title, viewClassName, }: FilterPopoverProps): import("react/jsx-runtime").JSX.Element;
38
42
  export { FilterPopover, getFilterSelectionLabel };
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { ChevronLeft, ChevronRight, SlidersHorizontal } from "lucide-react";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import { FilterIcon } from "./animated-icons";
5
6
  import { Button } from "./button";
6
7
  import { CountBadge } from "./count-badge";
7
8
  import { cn } from "./lib/utils";
@@ -32,12 +33,13 @@ function getFilterSelectionLabel(count) {
32
33
  }
33
34
  return count === 1 ? "1 selected" : `${count} selected`;
34
35
  }
35
- function FilterPopover({ align = "start", contentClassName, defaultOpen, emptyLabel = "No options found.", groupListLabel = "Filter by", groups, maxHeight = "max-h-80", modal, onOpenChange, onReset, open, resetLabel = "Reset filters", selectedCount, side, trigger, triggerLabel = "Filters", viewClassName, }) {
36
- var _a, _b, _c, _d, _e;
36
+ function FilterPopover({ align = "start", ariaLabel, contentClassName, defaultOpen, directSingleGroup = false, emptyLabel = "No options found.", groupListLabel = "Filter by", groups, maxHeight = "max-h-80", modal, onOpenChange, onReset, open, resetLabel = "Reset filters", selectedCount, side, trigger, triggerLabel = "Filters", title, viewClassName, }) {
37
+ var _a, _b, _c, _d, _e, _f, _g, _h;
37
38
  const [activeGroupId, setActiveGroupId] = React.useState(null);
38
39
  const [transitionDirection, setTransitionDirection] = React.useState("forward");
39
40
  const [query, setQuery] = React.useState("");
40
- const activeGroup = (_a = groups.find((group) => group.id === activeGroupId)) !== null && _a !== void 0 ? _a : null;
41
+ const directGroup = directSingleGroup && groups.length === 1 ? groups[0] : null;
42
+ const activeGroup = (_a = directGroup !== null && directGroup !== void 0 ? directGroup : groups.find((group) => group.id === activeGroupId)) !== null && _a !== void 0 ? _a : null;
41
43
  const resolvedSelectedCount = selectedCount !== null && selectedCount !== void 0 ? selectedCount : groups.reduce((total, group) => total + getGroupSelectedCount(group), 0);
42
44
  const normalizedQuery = query.trim().toLowerCase();
43
45
  const filteredOptions = activeGroup && normalizedQuery
@@ -62,8 +64,15 @@ function FilterPopover({ align = "start", contentClassName, defaultOpen, emptyLa
62
64
  setTransitionDirection("backward");
63
65
  setActiveGroupId(null);
64
66
  };
65
- const defaultTrigger = (_jsxs(Button, { type: "button", variant: "outline", children: [_jsx(SlidersHorizontal, { className: "h-4 w-4" }), triggerLabel, resolvedSelectedCount > 0 ? (_jsx(CountBadge, { count: resolvedSelectedCount, size: "sm" })) : null] }));
66
- return (_jsxs(Popover, { align: align, contentClassName: cn("w-[360px]", contentClassName), defaultOpen: defaultOpen, modal: modal, onOpenChange: onOpenChange, open: open, side: side, surface: "menu", trigger: trigger !== null && trigger !== void 0 ? trigger : defaultTrigger, children: [_jsx(PopoverMenuView, { className: cn("-mx-1 -mt-1 px-1 pt-1", viewClassName), direction: transitionDirection, viewKey: activeGroup ? `group-${activeGroup.id}` : "filter-groups", children: activeGroup ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "-mx-1 -mt-1 border-b p-1", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "w-full justify-start", onClick: showFilterGroups, children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), activeGroup.label] }) }), activeGroup.searchable ? (_jsx(PopoverMenuSearch, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: (_c = activeGroup.searchPlaceholder) !== null && _c !== void 0 ? _c : `Search ${getGroupLabel(activeGroup).toLowerCase()}...`, "aria-label": (_d = activeGroup.searchPlaceholder) !== null && _d !== void 0 ? _d : `Search ${getGroupLabel(activeGroup).toLowerCase()}` })) : null, _jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsx(PopoverMenuSection, { children: filteredOptions.length ? (filteredOptions.map((option) => (_jsx(PopoverMenuItem, { active: option.selected, "aria-label": option.ariaLabel, "aria-pressed": option.selected, disabled: option.disabled, onClick: option.onSelect, children: option.label }, option.id)))) : (_jsx(Typography, { as: "p", variant: "caption", className: "px-2 py-2", children: (_e = activeGroup.emptyLabel) !== null && _e !== void 0 ? _e : emptyLabel })) }) })] })) : (_jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsxs(PopoverMenuGroup, { children: [_jsx(PopoverMenuGroupLabel, { children: groupListLabel }), _jsx(PopoverMenuSection, { children: groups.map((group) => {
67
+ const defaultTrigger = (_jsxs(Button, { type: "button", variant: "outline", children: [_jsx(FilterIcon, { size: 16 }), triggerLabel, resolvedSelectedCount > 0 ? (_jsx(CountBadge, { count: resolvedSelectedCount, size: "sm" })) : null] }));
68
+ const handleOpenChange = (nextOpen) => {
69
+ if (!nextOpen && directGroup) {
70
+ setQuery("");
71
+ }
72
+ onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(nextOpen);
73
+ };
74
+ const resolvedTitle = (_c = title !== null && title !== void 0 ? title : directGroup === null || directGroup === void 0 ? void 0 : directGroup.label) !== null && _c !== void 0 ? _c : (!activeGroup ? triggerLabel : undefined);
75
+ return (_jsxs(Popover, { align: align, contentClassName: cn("w-[360px]", contentClassName), defaultOpen: defaultOpen, modal: modal, onOpenChange: handleOpenChange, open: open, side: side, surface: "menu", trigger: trigger !== null && trigger !== void 0 ? trigger : defaultTrigger, title: resolvedTitle, "aria-label": (_e = (_d = ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : getText(resolvedTitle)) !== null && _d !== void 0 ? _d : getText(triggerLabel)) !== null && _e !== void 0 ? _e : "Filters", children: [_jsx(PopoverMenuView, { className: cn("-mx-1 -mt-1 px-1 pt-1", viewClassName), direction: transitionDirection, viewKey: activeGroup ? `group-${activeGroup.id}` : "filter-groups", children: activeGroup ? (_jsxs(_Fragment, { children: [!directGroup ? (_jsx("div", { className: "-mx-1 -mt-1 border-b p-1", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "w-full justify-start", onClick: showFilterGroups, children: [_jsx(ChevronLeft, { className: "h-4 w-4" }), activeGroup.label] }) })) : null, activeGroup.searchable ? (_jsx(PopoverMenuSearch, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: (_f = activeGroup.searchPlaceholder) !== null && _f !== void 0 ? _f : `Search ${getGroupLabel(activeGroup).toLowerCase()}...`, "aria-label": (_g = activeGroup.searchPlaceholder) !== null && _g !== void 0 ? _g : `Search ${getGroupLabel(activeGroup).toLowerCase()}` })) : null, _jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsx(PopoverMenuSection, { children: filteredOptions.length ? (filteredOptions.map((option) => (_jsx(PopoverMenuItem, { active: option.selected, "aria-label": option.ariaLabel, "aria-pressed": option.selected, disabled: option.disabled, onClick: option.onSelect, children: option.label }, option.id)))) : (_jsx(Typography, { as: "p", variant: "caption", className: "px-2 py-2", children: (_h = activeGroup.emptyLabel) !== null && _h !== void 0 ? _h : emptyLabel })) }) })] })) : (_jsx(PopoverMenuScrollArea, { maxHeight: maxHeight, children: _jsxs(PopoverMenuGroup, { children: [_jsx(PopoverMenuGroupLabel, { children: groupListLabel }), _jsx(PopoverMenuSection, { children: groups.map((group) => {
67
76
  var _a;
68
77
  const groupSelectedCount = getGroupSelectedCount(group);
69
78
  const selectionLabel = (_a = group.selectionLabel) !== null && _a !== void 0 ? _a : getFilterSelectionLabel(groupSelectedCount);
package/dist/index.d.ts CHANGED
@@ -82,8 +82,8 @@ export { TaskModeShell, TASK_MODE_SHELL_ANIMATION_DURATION_MS, } from "./task-mo
82
82
  export type { TaskModeShellProps } from "./task-mode-shell";
83
83
  export { useFileUpload } from "./use-file-upload";
84
84
  export type { FileUploadProgress, FileUploadResult, FileUploadStatus, UseFileUploadOptions, UseFileUploadReturn, } from "./use-file-upload";
85
- export { Popover } from "./popover";
86
- export type { PopoverProps } from "./popover";
85
+ export { Popover, PopoverTitle } from "./popover";
86
+ export type { PopoverProps, PopoverTitleProps } from "./popover";
87
87
  export { PopoverMenuContent, PopoverMenuCheckboxItem, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, } from "./popover-menu";
88
88
  export type { PopoverMenuContentProps, PopoverMenuCheckboxItemProps, PopoverMenuHeaderProps, PopoverMenuSectionProps, PopoverMenuItemProps, PopoverMenuDividerProps, PopoverMenuFooterActionProps, PopoverMenuFooterMetaProps, PopoverMenuFooterProps, PopoverMenuGroupProps, PopoverMenuGroupLabelProps, PopoverMenuSearchProps, PopoverMenuScrollAreaProps, PopoverMenuViewProps, } from "./popover-menu";
89
89
  export { SearchInput } from "./search-input";
@@ -133,6 +133,7 @@ export { useTableSort } from "./use-table-sort";
133
133
  export type { UseTableSortOptions, UseTableSortReturn, } from "./use-table-sort";
134
134
  export { Table, TableRoot, TableContainer, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, TableRowSkeleton, TABLE_SKELETON_PRESETS, } from "./table";
135
135
  export type { TableColumn, TableColumnWidths, TableProps, TableResizeMode, TableRowExpansionProps, TableRowTone, TableSortDirection, TableSortingProps, TableSkeletonConfig, TableRowSkeletonProps, } from "./table";
136
+ export type { TableColumnFilter, TableColumnFilterOption, TableDateColumnFilter, TableOptionsColumnFilter, TableTextColumnFilter, } from "./table-column-filter";
136
137
  export { Tabs, TabsRoot, TabWithDropdown, type TabsItem, type TabsItemLabel, type TabsItemLabelProps, type TabsProps, TabsList, TabsTrigger, TabsContent, type TabWithDropdownProps, useTabsContext, type TabDropdownSection, } from "./tabs";
137
138
  export { Tooltip, TooltipRoot, TooltipTrigger, TooltipContent, TooltipProvider, type TooltipProps, } from "./tooltip";
138
139
  export { Toaster, toast } from "./toaster";
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ export { RadioGroup } from "./radio-group";
42
42
  export { ScrollHint } from "./scroll-hint";
43
43
  export { TaskModeShell, TASK_MODE_SHELL_ANIMATION_DURATION_MS, } from "./task-mode-shell";
44
44
  export { useFileUpload } from "./use-file-upload";
45
- export { Popover } from "./popover";
45
+ export { Popover, PopoverTitle } from "./popover";
46
46
  export { PopoverMenuContent, PopoverMenuCheckboxItem, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, } from "./popover-menu";
47
47
  export { SearchInput } from "./search-input";
48
48
  export { SectionNav } from "./section-nav";
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type ButtonProps } from "./button";
3
3
  import { type CheckboxProps } from "./checkbox";
4
- import { InputControl } from "./input-control";
4
+ import { type InputProps } from "./input";
5
5
  import { PopoverContent } from "./popover-primitive";
6
6
  import { Typography } from "./typography";
7
7
  export interface PopoverMenuHeaderProps extends Omit<React.ComponentPropsWithoutRef<typeof Typography>, "as" | "asChild" | "variant" | "color"> {
@@ -41,10 +41,12 @@ declare function PopoverMenuGroup({ className, children, ...props }: PopoverMenu
41
41
  export interface PopoverMenuGroupLabelProps extends Omit<React.ComponentPropsWithoutRef<typeof Typography>, "as" | "asChild" | "variant" | "color"> {
42
42
  }
43
43
  declare function PopoverMenuGroupLabel({ className, children, ...props }: PopoverMenuGroupLabelProps): import("react/jsx-runtime").JSX.Element;
44
- export interface PopoverMenuSearchProps extends React.ComponentPropsWithoutRef<typeof InputControl> {
44
+ export interface PopoverMenuSearchProps extends Omit<InputProps, "className" | "hideLabel" | "inputClassName" | "label"> {
45
+ label?: React.ReactNode;
46
+ className?: string;
45
47
  containerClassName?: string;
46
48
  }
47
- declare const PopoverMenuSearch: React.ForwardRefExoticComponent<PopoverMenuSearchProps & React.RefAttributes<HTMLInputElement>>;
49
+ declare const PopoverMenuSearch: React.ForwardRefExoticComponent<Omit<PopoverMenuSearchProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
48
50
  export interface PopoverMenuScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
49
51
  maxHeight?: string;
50
52
  }
@@ -4,8 +4,8 @@ import * as React from "react";
4
4
  import { Check } from "lucide-react";
5
5
  import { Button } from "./button";
6
6
  import { Checkbox } from "./checkbox";
7
+ import { Input } from "./input";
7
8
  import { cn } from "./lib/utils";
8
- import { InputControl } from "./input-control";
9
9
  import { PopoverContent } from "./popover-primitive";
10
10
  import { Typography } from "./typography";
11
11
  const POPOVER_MENU_VIEW_TRANSITION_MS = 300;
@@ -44,7 +44,10 @@ function PopoverMenuGroup({ className, children, ...props }) {
44
44
  function PopoverMenuGroupLabel({ className, children, ...props }) {
45
45
  return (_jsx(Typography, { as: "p", variant: "caption", color: "muted", className: cn("px-2 py-1.5", className), ...props, children: children }));
46
46
  }
47
- const PopoverMenuSearch = React.forwardRef(({ className, containerClassName, ...props }, ref) => (_jsx("div", { className: cn("relative -mx-1 -mt-1 mb-1 px-2 py-1 after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-border after:content-['']", containerClassName), children: _jsx(InputControl, { ref: ref, className: cn("h-8 border-0 bg-transparent px-2 py-0 text-sm shadow-none focus-visible:ring-0", className), ...props }) })));
47
+ const PopoverMenuSearch = React.forwardRef(({ "aria-label": ariaLabel, className, containerClassName, label, placeholder, ...props }, ref) => {
48
+ var _a, _b;
49
+ return (_jsx("div", { className: cn("relative -mx-1 -mt-1 mb-1 px-2 py-1 after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-border after:content-['']", containerClassName), children: _jsx(Input, { ref: ref, label: (_b = (_a = label !== null && label !== void 0 ? label : ariaLabel) !== null && _a !== void 0 ? _a : placeholder) !== null && _b !== void 0 ? _b : "Search", hideLabel: true, className: "gap-0", inputClassName: className, "aria-label": ariaLabel, placeholder: placeholder, ...props }) }));
50
+ });
48
51
  PopoverMenuSearch.displayName = "PopoverMenuSearch";
49
52
  function PopoverMenuScrollArea({ className, children, maxHeight = "max-h-64", ...props }) {
50
53
  return (_jsx("div", { className: cn(maxHeight, "space-y-1 overflow-y-auto overflow-x-hidden", className), ...props, children: children }));
package/dist/popover.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import * as React from "react";
2
2
  import * as PopoverPrimitive from "@radix-ui/react-popover";
3
3
  import { type TooltipProps } from "./tooltip";
4
+ import { Typography } from "./typography";
4
5
  type PopoverRootProps = React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Root>;
5
6
  type PopoverContentProps = React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>;
6
- export interface PopoverProps extends Pick<PopoverRootProps, "defaultOpen" | "modal" | "onOpenChange" | "open">, Omit<PopoverContentProps, "children" | "className"> {
7
+ export interface PopoverProps extends Pick<PopoverRootProps, "defaultOpen" | "modal" | "onOpenChange" | "open">, Omit<PopoverContentProps, "children" | "className" | "title"> {
7
8
  trigger: React.ReactElement;
8
9
  children: React.ReactNode;
10
+ /** Visible popover heading. Also labels the popover for assistive technology. */
11
+ title?: React.ReactNode;
9
12
  surface?: "panel" | "menu";
10
13
  contentClassName?: string;
11
14
  triggerTooltip?: React.ReactNode;
@@ -13,5 +16,8 @@ export interface PopoverProps extends Pick<PopoverRootProps, "defaultOpen" | "mo
13
16
  triggerTooltipSideOffset?: TooltipProps["sideOffset"];
14
17
  triggerTooltipClassName?: TooltipProps["contentClassName"];
15
18
  }
16
- declare function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, surface, contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align, sideOffset, ...contentProps }: PopoverProps): import("react/jsx-runtime").JSX.Element;
17
- export { Popover };
19
+ declare function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, title, surface, contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align, sideOffset, ...contentProps }: PopoverProps): import("react/jsx-runtime").JSX.Element;
20
+ export interface PopoverTitleProps extends Omit<React.ComponentPropsWithoutRef<typeof Typography>, "as" | "asChild" | "variant" | "color"> {
21
+ }
22
+ declare function PopoverTitle({ className, children, ...props }: PopoverTitleProps): import("react/jsx-runtime").JSX.Element;
23
+ export { Popover, PopoverTitle };
package/dist/popover.js CHANGED
@@ -1,10 +1,19 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
3
4
  import { cn } from "./lib/utils";
4
5
  import { PopoverContent, PopoverRoot, PopoverTrigger, } from "./popover-primitive";
5
6
  import { Tooltip } from "./tooltip";
6
- function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, surface = "panel", contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align = "center", sideOffset = 4, ...contentProps }) {
7
+ import { Typography } from "./typography";
8
+ function Popover({ defaultOpen, modal, onOpenChange, open, trigger, children, title, surface = "panel", contentClassName, triggerTooltip, triggerTooltipSide, triggerTooltipSideOffset, triggerTooltipClassName, align = "center", sideOffset = 4, ...contentProps }) {
9
+ const generatedTitleId = React.useId();
10
+ const titleId = title ? `popover-title-${generatedTitleId}` : undefined;
7
11
  const triggerElement = _jsx(PopoverTrigger, { asChild: true, children: trigger });
8
- return (_jsxs(PopoverRoot, { defaultOpen: defaultOpen, modal: modal, onOpenChange: onOpenChange, open: open, children: [triggerTooltip ? (_jsx(Tooltip, { content: triggerTooltip, side: triggerTooltipSide, sideOffset: triggerTooltipSideOffset, contentClassName: triggerTooltipClassName, children: triggerElement })) : (triggerElement), _jsx(PopoverContent, { align: align, sideOffset: sideOffset, className: cn(surface === "menu" && "overflow-hidden p-1", contentClassName), ...contentProps, children: children })] }));
12
+ return (_jsxs(PopoverRoot, { defaultOpen: defaultOpen, modal: modal, onOpenChange: onOpenChange, open: open, children: [triggerTooltip ? (_jsx(Tooltip, { content: triggerTooltip, side: triggerTooltipSide, sideOffset: triggerTooltipSideOffset, contentClassName: triggerTooltipClassName, children: triggerElement })) : (triggerElement), _jsxs(PopoverContent, { align: align, sideOffset: sideOffset, "aria-labelledby": titleId !== null && titleId !== void 0 ? titleId : contentProps["aria-labelledby"], className: cn(surface === "menu" && "overflow-hidden p-1", contentClassName), ...contentProps, children: [title ? (_jsx(PopoverTitle, { id: titleId, className: cn(surface === "menu"
13
+ ? "px-2 py-1.5"
14
+ : "-mx-4 -mt-4 mb-4 border-b px-4 py-3"), children: title })) : null, children] })] }));
9
15
  }
10
- export { Popover };
16
+ function PopoverTitle({ className, children, ...props }) {
17
+ return (_jsx(Typography, { as: "p", variant: "label", "data-slot": "popover-title", className: className, ...props, children: children }));
18
+ }
19
+ export { Popover, PopoverTitle };
@@ -0,0 +1,42 @@
1
+ import * as React from "react";
2
+ interface TableColumnFilterBase {
3
+ /** Overrides the column header text in accessible filter labels. */
4
+ label?: string;
5
+ /** Short text shown in the filter row before a value is selected. */
6
+ placeholder?: string;
7
+ disabled?: boolean;
8
+ }
9
+ export interface TableTextColumnFilter extends TableColumnFilterBase {
10
+ type: "text";
11
+ value: string;
12
+ onValueChange: (value: string) => void;
13
+ }
14
+ export interface TableColumnFilterOption {
15
+ label: React.ReactNode;
16
+ value: string;
17
+ disabled?: boolean;
18
+ }
19
+ export interface TableOptionsColumnFilter extends TableColumnFilterBase {
20
+ type: "options";
21
+ values: readonly string[];
22
+ onValuesChange: (values: string[]) => void;
23
+ options: readonly TableColumnFilterOption[];
24
+ /** Adds a compact option search field. Defaults to true for more than seven options. */
25
+ searchable?: boolean;
26
+ searchPlaceholder?: string;
27
+ emptyMessage?: string;
28
+ }
29
+ export interface TableDateColumnFilter extends TableColumnFilterBase {
30
+ type: "date";
31
+ value: string;
32
+ onValueChange: (value: string) => void;
33
+ minDate?: Date;
34
+ maxDate?: Date;
35
+ }
36
+ export type TableColumnFilter = TableTextColumnFilter | TableOptionsColumnFilter | TableDateColumnFilter;
37
+ interface TableColumnFilterControlProps {
38
+ filter: TableColumnFilter;
39
+ label: string;
40
+ }
41
+ declare function TableColumnFilterControl({ filter, label, }: TableColumnFilterControlProps): import("react/jsx-runtime").JSX.Element;
42
+ export { TableColumnFilterControl };
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { FilterIcon } from "./animated-icons";
5
+ import { DateInput } from "./date-input";
6
+ import { FilterPopover } from "./filter-popover";
7
+ import { FilterToggle, filterToggleVariants, } from "./filter-toggle";
8
+ import { cn } from "./lib/utils";
9
+ import { Popover } from "./popover";
10
+ import { PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuSearch, } from "./popover-menu";
11
+ function isTableColumnFilterActive(filter) {
12
+ return filter.type === "options"
13
+ ? filter.values.length > 0
14
+ : filter.value.trim().length > 0;
15
+ }
16
+ function getOptionsFilterSummary(filter) {
17
+ var _a, _b, _c;
18
+ if (filter.values.length === 0) {
19
+ return (_a = filter.placeholder) !== null && _a !== void 0 ? _a : "Filter";
20
+ }
21
+ if (filter.values.length > 1) {
22
+ return `${filter.values.length} selected`;
23
+ }
24
+ return ((_c = (_b = filter.options.find((option) => option.value === filter.values[0])) === null || _b === void 0 ? void 0 : _b.label) !== null && _c !== void 0 ? _c : filter.values[0]);
25
+ }
26
+ const TableFilterTrigger = React.forwardRef(({ active, disabled, label, children, ...props }, ref) => (_jsx(FilterToggle, { ref: ref, type: "button", disabled: disabled, selected: active, "aria-label": `Filter by ${label}`, "aria-pressed": active, className: cn("h-9 w-full min-w-0 justify-start overflow-hidden px-2 text-sm font-normal", !active &&
27
+ "bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground"), icon: _jsx(FilterIcon, { "aria-hidden": "true", className: "shrink-0", size: 16 }), ...props, children: _jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: children }) })));
28
+ TableFilterTrigger.displayName = "TableFilterTrigger";
29
+ function TextColumnFilter({ filter, label, }) {
30
+ var _a, _b, _c;
31
+ const active = isTableColumnFilterActive(filter);
32
+ const accessibleLabel = (_a = filter.label) !== null && _a !== void 0 ? _a : label;
33
+ return (_jsxs(Popover, { align: "start", surface: "menu", "aria-label": `${accessibleLabel} filter`, title: `Filter by ${accessibleLabel}`, trigger: _jsx(TableFilterTrigger, { active: active, disabled: filter.disabled, label: accessibleLabel, children: active ? filter.value : (_b = filter.placeholder) !== null && _b !== void 0 ? _b : "Filter" }), children: [_jsx(PopoverMenuSearch, { autoFocus: true, value: filter.value, onChange: (event) => filter.onValueChange(event.target.value), placeholder: (_c = filter.placeholder) !== null && _c !== void 0 ? _c : `Search ${accessibleLabel}`, "aria-label": `Filter ${accessibleLabel}`, disabled: filter.disabled }), active ? (_jsx(PopoverMenuFooter, { children: _jsx(PopoverMenuFooterAction, { onClick: () => filter.onValueChange(""), children: "Clear filter" }) })) : null] }));
34
+ }
35
+ function OptionsColumnFilter({ filter, label, }) {
36
+ var _a, _b, _c, _d;
37
+ const active = isTableColumnFilterActive(filter);
38
+ const accessibleLabel = (_a = filter.label) !== null && _a !== void 0 ? _a : label;
39
+ const searchable = (_b = filter.searchable) !== null && _b !== void 0 ? _b : filter.options.length > 7;
40
+ const toggleOption = (optionValue) => {
41
+ const nextValues = filter.values.includes(optionValue)
42
+ ? filter.values.filter((value) => value !== optionValue)
43
+ : [...filter.values, optionValue];
44
+ filter.onValuesChange(nextValues);
45
+ };
46
+ return (_jsx(FilterPopover, { directSingleGroup: true, ariaLabel: `${accessibleLabel} filter`, title: `Filter by ${accessibleLabel}`, trigger: _jsx(TableFilterTrigger, { active: active, disabled: filter.disabled, label: accessibleLabel, children: getOptionsFilterSummary(filter) }), groups: [
47
+ {
48
+ id: `table-column-${accessibleLabel}`,
49
+ label: `Filter by ${accessibleLabel}`,
50
+ searchable,
51
+ searchPlaceholder: (_c = filter.searchPlaceholder) !== null && _c !== void 0 ? _c : "Search options",
52
+ emptyLabel: (_d = filter.emptyMessage) !== null && _d !== void 0 ? _d : "No options found",
53
+ selectedCount: filter.values.length,
54
+ options: filter.options.map((option) => ({
55
+ id: option.value,
56
+ label: option.label,
57
+ selected: filter.values.includes(option.value),
58
+ disabled: filter.disabled || option.disabled,
59
+ onSelect: () => toggleOption(option.value),
60
+ })),
61
+ },
62
+ ], selectedCount: filter.values.length, onReset: active ? () => filter.onValuesChange([]) : undefined, resetLabel: "Clear filter" }));
63
+ }
64
+ function DateColumnFilter({ filter, label, }) {
65
+ var _a, _b;
66
+ const active = isTableColumnFilterActive(filter);
67
+ const accessibleLabel = (_a = filter.label) !== null && _a !== void 0 ? _a : label;
68
+ return (_jsx(DateInput, { label: `Filter by ${accessibleLabel}`, hideLabel: true, value: filter.value, onValueChange: filter.onValueChange, placeholder: (_b = filter.placeholder) !== null && _b !== void 0 ? _b : "Pick a date", disabled: filter.disabled, minDate: filter.minDate, maxDate: filter.maxDate, clearable: true, clearLabel: `Clear ${accessibleLabel} filter`, className: "gap-0", triggerClassName: cn(filterToggleVariants({ selected: active }), "h-9 w-full min-w-0 justify-start text-sm font-normal shadow-none", active ? "pl-2 pr-16" : "pl-2 pr-9", !active &&
69
+ "border-0 bg-transparent text-muted-foreground hover:bg-accent hover:text-accent-foreground") }));
70
+ }
71
+ function TableColumnFilterControl({ filter, label, }) {
72
+ if (filter.type === "text") {
73
+ return _jsx(TextColumnFilter, { filter: filter, label: label });
74
+ }
75
+ if (filter.type === "options") {
76
+ return _jsx(OptionsColumnFilter, { filter: filter, label: label });
77
+ }
78
+ return _jsx(DateColumnFilter, { filter: filter, label: label });
79
+ }
80
+ export { TableColumnFilterControl };
package/dist/table.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { type PaginationProps } from "./pagination";
3
+ import { type TableColumnFilter } from "./table-column-filter";
3
4
  interface TableRootProps extends React.HTMLAttributes<HTMLTableElement> {
4
5
  wrapperClassName?: string;
5
6
  }
@@ -73,6 +74,10 @@ export interface TableColumn<Row> {
73
74
  sortable?: boolean;
74
75
  sortKey?: string;
75
76
  sortLabel?: string;
77
+ /** Controlled filter rendered in a second header row. Filter the supplied `rows` in the screen or data hook. */
78
+ filter?: TableColumnFilter;
79
+ /** Optional class name for this column's filter cell. */
80
+ filterClassName?: string;
76
81
  /** When false, restore-defaults helpers can omit the column from the visible set. */
77
82
  defaultVisible?: boolean;
78
83
  /** When false, the column visibility menu keeps the column out of its checklist. */
package/dist/table.js CHANGED
@@ -6,6 +6,7 @@ import { ArrowUpIcon } from "./animated-icons/arrow-up";
6
6
  import { EmptyState } from "./empty-state";
7
7
  import { Pagination } from "./pagination";
8
8
  import { Skeleton } from "./skeleton";
9
+ import { TableColumnFilterControl, } from "./table-column-filter";
9
10
  import { cn } from "./lib/utils";
10
11
  const TableRoot = React.forwardRef(({ className, wrapperClassName: _wrapperClassName, ...props }, ref) => (_jsx("table", { ref: ref, className: cn("w-full caption-bottom text-sm", className), ...props })));
11
12
  TableRoot.displayName = "TableRoot";
@@ -110,7 +111,7 @@ function hasColumnWidthOverrides(columnWidths) {
110
111
  }
111
112
  function getMeasuredColumnWidths(resizeHandle, columns, fallbackColumnWidths) {
112
113
  const table = resizeHandle.closest("table");
113
- const headerCells = table === null || table === void 0 ? void 0 : table.querySelectorAll('thead th:not([data-slot="table-expansion-head"])');
114
+ const headerCells = table === null || table === void 0 ? void 0 : table.querySelectorAll('thead th[data-slot="table-column-head"]');
114
115
  if (!(headerCells === null || headerCells === void 0 ? void 0 : headerCells.length)) {
115
116
  return fallbackColumnWidths;
116
117
  }
@@ -177,8 +178,8 @@ function stickyClassName(sticky, kind, showShadow = false) {
177
178
  : "shadow-[inset_1px_0_0_0_hsl(var(--table-border)),_-8px_0_12px_-10px_hsl(var(--foreground)_/_0.35)]"
178
179
  : undefined;
179
180
  const background = kind === "head" ? "bg-[hsl(var(--table-header-background))]" : "bg-card";
180
- // z-20 on header cells so they layer above sticky body cells on the other axis.
181
- const z = kind === "head" ? "z-20" : "z-10";
181
+ // Header and filter cells layer above sticky body cells on the other axis.
182
+ const z = kind === "cell" ? "z-10" : "z-20";
182
183
  return cn("sticky", background, side, z, shadow);
183
184
  }
184
185
  function getStickyShadowVisibility(sticky, scrollState) {
@@ -245,7 +246,7 @@ export const TABLE_SKELETON_PRESETS = {
245
246
  };
246
247
  function SortableTableHead({ field, children, currentSortField, currentSortDirection, onSort, className, allowWrap = false, width, minWidth, maxWidth, align, sortLabel, resizeHandle, treatWidthAsMinWidth, containOverflow, }) {
247
248
  const isSorted = currentSortField === field;
248
- return (_jsxs(TableHead, { className: cn(resizeHandle && "relative", alignClassName(align), className), wrap: allowWrap, width: width, minWidth: minWidth, maxWidth: maxWidth, treatWidthAsMinWidth: treatWidthAsMinWidth, containOverflow: containOverflow, children: [_jsxs("button", { type: "button", onClick: () => onSort(field), className: cn("-ml-1.5 flex h-7 cursor-pointer items-center gap-1 rounded px-1.5 py-0 font-medium transition-colors hover:text-foreground", containOverflow && "max-w-full overflow-hidden", allowWrap
249
+ return (_jsxs(TableHead, { "data-slot": "table-column-head", className: cn(resizeHandle && "relative", alignClassName(align), className), wrap: allowWrap, width: width, minWidth: minWidth, maxWidth: maxWidth, treatWidthAsMinWidth: treatWidthAsMinWidth, containOverflow: containOverflow, children: [_jsxs("button", { type: "button", onClick: () => onSort(field), className: cn("-ml-1.5 flex h-7 cursor-pointer items-center gap-1 rounded px-1.5 py-0 font-medium transition-colors hover:text-foreground", containOverflow && "max-w-full overflow-hidden", allowWrap
249
250
  ? "max-w-full whitespace-normal text-left"
250
251
  : "whitespace-nowrap", align === "right" && "ml-auto justify-end", align === "center" && "mx-auto justify-center"), style: { font: "inherit", color: "inherit", fontWeight: 500 }, "aria-label": `Sort by ${sortLabel !== null && sortLabel !== void 0 ? sortLabel : field}`, children: [_jsx("span", { className: cn("min-w-0", containOverflow && !allowWrap && "truncate", containOverflow && allowWrap && "break-words [overflow-wrap:anywhere]"), children: children }), _jsx("span", { className: "inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center", children: isSorted ? (currentSortDirection === "asc" ? (_jsx(ArrowUpIcon, { className: "shrink-0", size: 14 })) : (_jsx(ArrowDownIcon, { className: "shrink-0", size: 14 }))) : (_jsx(ArrowUpDown, { className: "h-3.5 w-3.5 shrink-0 opacity-50" })) })] }), resizeHandle] }));
251
252
  }
@@ -331,6 +332,7 @@ function isStickyResizeBoundary(column, nextColumn) {
331
332
  function Table({ columns, rows, getRowKey, emptyState = (_jsx(EmptyState, { message: "No data available", description: "There are no rows to display in this table yet.", className: "border-0 bg-transparent px-0 py-6" })), toolbar, caption, className, containerClassName, tableClassName, rowClassName, rowTone, maxHeight, minHeight, sorting, pagination, loading = false, loadingRows = 5, visibleColumnKeys, resizableColumns = false, columnResizeMode = "pair", columnWidths, defaultColumnWidths, onColumnWidthsChange, renderColumnResizeHandle, tableLayout, centerEmptyState = false, emptyStateMinHeight = DEFAULT_CENTERED_EMPTY_STATE_MIN_HEIGHT, striped = false, rowExpansion, onRowClick, onScroll, style, ...props }) {
332
333
  const visibleColumns = getVisibleColumns(columns, visibleColumnKeys);
333
334
  const hasExpansionColumn = Boolean(rowExpansion);
335
+ const hasColumnFilters = visibleColumns.some((column) => column.filter);
334
336
  const isGrowResizeMode = columnResizeMode === "grow";
335
337
  const isColumnResizingEnabled = resizableColumns || visibleColumns.some((column) => column.resizable);
336
338
  // Use auto layout by default so the browser can grow a column to contain its
@@ -561,18 +563,25 @@ function Table({ columns, rows, getRowKey, emptyState = (_jsx(EmptyState, { mess
561
563
  minWidth: getRenderedColumnMinWidth(column, isColumnResizingEnabled),
562
564
  maxWidth: getRenderedColumnMaxWidth(column, isColumnResizingEnabled),
563
565
  treatWidthAsMinWidth: !isColumnResizingEnabled,
564
- }) }, column.key)))] }), _jsx(TableHeader, { children: _jsxs(TableRow, { children: [hasExpansionColumn ? (_jsx(TableHead, { "data-slot": "table-expansion-head", "aria-label": "Expand rows", className: "w-12 px-2" })) : null, visibleColumns.map((column, columnIndex) => {
565
- var _a;
566
- const sortField = (_a = column.sortKey) !== null && _a !== void 0 ? _a : column.key;
567
- const isSortable = column.sortable && sorting;
568
- const columnWidth = getRenderedColumnWidth({
569
- column,
570
- columnWidths: resolvedColumnWidths,
571
- resizing: isColumnResizingEnabled,
572
- });
573
- const resizeHandle = renderResizeHandle(column, columnIndex);
574
- return (isSortable ? (_jsx(SortableTableHead, { field: sortField, currentSortField: sorting.field, currentSortDirection: sorting.direction, onSort: sorting.onSort, className: cn(stickyClassName(column.sticky, "head", getStickyShadowVisibility(column.sticky, stickyScrollState)), column.headerClassName), allowWrap: column.wrap, width: columnWidth, minWidth: getRenderedColumnMinWidth(column, isColumnResizingEnabled), maxWidth: getRenderedColumnMaxWidth(column, isColumnResizingEnabled), align: column.align, sortLabel: column.sortLabel, resizeHandle: resizeHandle, treatWidthAsMinWidth: !isColumnResizingEnabled, containOverflow: isColumnResizingEnabled, children: column.header }, column.key)) : (_jsxs(TableHead, { width: columnWidth, minWidth: getRenderedColumnMinWidth(column, isColumnResizingEnabled), maxWidth: getRenderedColumnMaxWidth(column, isColumnResizingEnabled), wrap: column.wrap, treatWidthAsMinWidth: !isColumnResizingEnabled, containOverflow: isColumnResizingEnabled, className: cn(resizeHandle && "relative", alignClassName(column.align), stickyClassName(column.sticky, "head", getStickyShadowVisibility(column.sticky, stickyScrollState)), column.headerClassName), children: [column.header, resizeHandle] }, column.key)));
575
- })] }) }), _jsx(TableBody, { children: loading ? (_jsx(TableRowSkeleton, { rows: loadingRows, columns: [
566
+ }) }, column.key)))] }), _jsxs(TableHeader, { children: [_jsxs(TableRow, { children: [hasExpansionColumn ? (_jsx(TableHead, { "data-slot": "table-expansion-head", "aria-label": "Expand rows", className: "w-12 px-2" })) : null, visibleColumns.map((column, columnIndex) => {
567
+ var _a;
568
+ const sortField = (_a = column.sortKey) !== null && _a !== void 0 ? _a : column.key;
569
+ const isSortable = column.sortable && sorting;
570
+ const columnWidth = getRenderedColumnWidth({
571
+ column,
572
+ columnWidths: resolvedColumnWidths,
573
+ resizing: isColumnResizingEnabled,
574
+ });
575
+ const resizeHandle = renderResizeHandle(column, columnIndex);
576
+ return (isSortable ? (_jsx(SortableTableHead, { field: sortField, currentSortField: sorting.field, currentSortDirection: sorting.direction, onSort: sorting.onSort, className: cn(stickyClassName(column.sticky, "head", getStickyShadowVisibility(column.sticky, stickyScrollState)), column.headerClassName), allowWrap: column.wrap, width: columnWidth, minWidth: getRenderedColumnMinWidth(column, isColumnResizingEnabled), maxWidth: getRenderedColumnMaxWidth(column, isColumnResizingEnabled), align: column.align, sortLabel: column.sortLabel, resizeHandle: resizeHandle, treatWidthAsMinWidth: !isColumnResizingEnabled, containOverflow: isColumnResizingEnabled, children: column.header }, column.key)) : (_jsxs(TableHead, { "data-slot": "table-column-head", width: columnWidth, minWidth: getRenderedColumnMinWidth(column, isColumnResizingEnabled), maxWidth: getRenderedColumnMaxWidth(column, isColumnResizingEnabled), wrap: column.wrap, treatWidthAsMinWidth: !isColumnResizingEnabled, containOverflow: isColumnResizingEnabled, className: cn(resizeHandle && "relative", alignClassName(column.align), stickyClassName(column.sticky, "head", getStickyShadowVisibility(column.sticky, stickyScrollState)), column.headerClassName), children: [column.header, resizeHandle] }, column.key)));
577
+ })] }), hasColumnFilters ? (_jsxs(TableRow, { "data-slot": "table-filter-row", className: "!bg-card", children: [hasExpansionColumn ? (_jsx(TableHead, { "data-slot": "table-expansion-filter-cell", "aria-hidden": "true", className: "w-12 px-2" })) : null, visibleColumns.map((column) => {
578
+ const columnWidth = getRenderedColumnWidth({
579
+ column,
580
+ columnWidths: resolvedColumnWidths,
581
+ resizing: isColumnResizingEnabled,
582
+ });
583
+ return (_jsx(TableHead, { "data-slot": "table-filter-cell", width: columnWidth, minWidth: getRenderedColumnMinWidth(column, isColumnResizingEnabled), maxWidth: getRenderedColumnMaxWidth(column, isColumnResizingEnabled), treatWidthAsMinWidth: !isColumnResizingEnabled, containOverflow: isColumnResizingEnabled, className: cn("relative h-11 px-2 py-1 font-normal", stickyClassName(column.sticky, "filter", getStickyShadowVisibility(column.sticky, stickyScrollState)), column.filterClassName), children: column.filter ? (_jsx("div", { "data-slot": "table-filter-control", className: "absolute inset-x-2 inset-y-1 min-w-0", children: _jsx(TableColumnFilterControl, { filter: column.filter, label: getColumnResizeLabel(column) }) })) : null }, column.key));
584
+ })] })) : null] }), _jsx(TableBody, { children: loading ? (_jsx(TableRowSkeleton, { rows: loadingRows, columns: [
576
585
  ...(hasExpansionColumn
577
586
  ? [{ width: "1rem", height: "1rem" }]
578
587
  : []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neasg/design-system",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",