@neasg/design-system 0.4.18 → 0.4.20

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 };
@@ -4,6 +4,7 @@ import { AvatarProfile } from "./avatar";
4
4
  import { Button } from "./button";
5
5
  import { cn } from "./lib/utils";
6
6
  import { Popover } from "./popover";
7
+ import { popoverMenuItemFocusClassName } from "./popover-menu";
7
8
  import { Typography } from "./typography";
8
9
  function AvatarProfilePopover({ trigger, children, contentClassName, align = "start", sideOffset = 4, ...props }) {
9
10
  return (_jsx(Popover, { ...props, trigger: trigger, surface: "menu", align: align, sideOffset: sideOffset, contentClassName: cn("max-h-[calc(100vh-1rem)] w-80 max-w-[calc(100vw-1rem)] overflow-visible", contentClassName), children: children }));
@@ -21,7 +22,7 @@ function AvatarProfilePopoverFooter({ className, ...props }) {
21
22
  return (_jsx("div", { "data-slot": "avatar-profile-popover-footer", className: cn("-mx-1 -mb-1 border-t p-1.5", className), ...props }));
22
23
  }
23
24
  function AvatarProfilePopoverFooterAction({ className, tone = "default", type = "button", ...props }) {
24
- return (_jsx(Button, { type: type, variant: "ghost", size: "sm", className: cn("h-auto min-h-8 w-full justify-start px-2 py-1.5 text-left whitespace-normal focus-visible:ring-inset focus-visible:ring-1 focus-visible:ring-offset-0", tone === "destructive" &&
25
+ return (_jsx(Button, { type: type, variant: "ghost", size: "sm", className: cn("h-auto min-h-8 w-full justify-start px-2 py-1.5 text-left whitespace-normal", popoverMenuItemFocusClassName, tone === "destructive" &&
25
26
  "text-destructive hover:bg-destructive/10 hover:text-destructive", className), ...props }));
26
27
  }
27
28
  export { AvatarProfilePopover, AvatarProfilePopoverContent, AvatarProfilePopoverHeader, AvatarProfilePopoverSection, AvatarProfilePopoverFooter, AvatarProfilePopoverFooterAction, };
@@ -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,12 +1,13 @@
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";
8
9
  import { Popover } from "./popover";
9
- import { PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuItem, PopoverMenuScrollArea, PopoverMenuSearch, PopoverMenuSection, PopoverMenuView, } from "./popover-menu";
10
+ import { PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuItem, PopoverMenuScrollArea, PopoverMenuSearch, PopoverMenuSection, PopoverMenuView, popoverMenuItemFocusClassName, } from "./popover-menu";
10
11
  import { Typography } from "./typography";
11
12
  function getText(value) {
12
13
  if (typeof value === "string" || typeof value === "number") {
@@ -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: cn("w-full justify-start", popoverMenuItemFocusClassName), 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: [!resolvedTitle ? (_jsx(PopoverMenuGroupLabel, { children: groupListLabel })) : null, _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);
@@ -9,7 +9,7 @@ const filterToggleVariants = cva("inline-flex shrink-0 cursor-pointer select-non
9
9
  muted: "",
10
10
  },
11
11
  selected: {
12
- true: "border-transparent bg-primary/10 text-primary",
12
+ true: "border-transparent bg-primary/10 text-primary hover:bg-primary/20 focus-visible:bg-primary/20",
13
13
  false: "border-transparent bg-muted text-muted-foreground hover:bg-muted/80",
14
14
  },
15
15
  },
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,6 +1,7 @@
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 { ChevronDownIcon } from "./animated-icons/chevron-down";
4
5
  import { XIcon } from "./animated-icons/x";
5
6
  import { Badge } from "./badge";
6
7
  import { Field, FieldDescription, FieldError, FieldLabel } from "./field";
@@ -22,6 +23,8 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
22
23
  const containerRef = React.useRef(null);
23
24
  const triggerRef = React.useRef(null);
24
25
  const searchInputRef = React.useRef(null);
26
+ const listboxRef = React.useRef(null);
27
+ const openFocusTargetRef = React.useRef(searchable ? "search" : "first");
25
28
  const normalizedQuery = searchQuery.trim().toLowerCase();
26
29
  const filteredOptions = searchable && normalizedQuery
27
30
  ? options.filter((opt) => {
@@ -33,6 +36,28 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
33
36
  const selectedLabels = value
34
37
  .map((v) => options.find((opt) => opt.value === v))
35
38
  .filter(Boolean);
39
+ const getEnabledOptionElements = React.useCallback(() => {
40
+ var _a, _b;
41
+ return Array.from((_b = (_a = listboxRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('[role="option"]:not(:disabled)')) !== null && _b !== void 0 ? _b : []);
42
+ }, []);
43
+ const focusBoundaryOption = React.useCallback((boundary) => {
44
+ const enabledOptions = getEnabledOptionElements();
45
+ const option = boundary === "first"
46
+ ? enabledOptions[0]
47
+ : enabledOptions[enabledOptions.length - 1];
48
+ option === null || option === void 0 ? void 0 : option.focus({ preventScroll: true });
49
+ }, [getEnabledOptionElements]);
50
+ const openWithFocus = React.useCallback((target) => {
51
+ openFocusTargetRef.current = target;
52
+ setOpen(true);
53
+ }, []);
54
+ const closeAndFocusTrigger = React.useCallback(() => {
55
+ setOpen(false);
56
+ window.requestAnimationFrame(() => {
57
+ var _a;
58
+ (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true });
59
+ });
60
+ }, []);
36
61
  const toggleOption = React.useCallback((optionValue) => {
37
62
  const next = value.includes(optionValue)
38
63
  ? value.filter((v) => v !== optionValue)
@@ -47,14 +72,16 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
47
72
  setSearchQuery("");
48
73
  return;
49
74
  }
50
- if (searchable) {
51
- const frame = window.requestAnimationFrame(() => {
52
- var _a;
75
+ const frame = window.requestAnimationFrame(() => {
76
+ var _a;
77
+ if (openFocusTargetRef.current === "search" && searchable) {
53
78
  (_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
54
- });
55
- return () => window.cancelAnimationFrame(frame);
56
- }
57
- }, [open, searchable]);
79
+ return;
80
+ }
81
+ focusBoundaryOption(openFocusTargetRef.current === "last" ? "last" : "first");
82
+ });
83
+ return () => window.cancelAnimationFrame(frame);
84
+ }, [focusBoundaryOption, open, searchable]);
58
85
  React.useEffect(() => {
59
86
  const handleClickOutside = (event) => {
60
87
  if (containerRef.current &&
@@ -72,33 +99,91 @@ function MultiSelect({ label, options, value, onValueChange, placeholder = "Sele
72
99
  return;
73
100
  if (event.key === "Enter" || event.key === " ") {
74
101
  event.preventDefault();
75
- setOpen((prev) => !prev);
102
+ if (open) {
103
+ setOpen(false);
104
+ }
105
+ else {
106
+ openWithFocus(searchable ? "search" : "first");
107
+ }
76
108
  }
77
109
  else if (event.key === "ArrowDown") {
78
110
  event.preventDefault();
79
- setOpen(true);
111
+ openWithFocus("first");
112
+ }
113
+ else if (event.key === "ArrowUp") {
114
+ event.preventDefault();
115
+ openWithFocus("last");
80
116
  }
81
117
  else if (event.key === "Escape") {
82
- setOpen(false);
118
+ event.preventDefault();
119
+ closeAndFocusTrigger();
120
+ }
121
+ }, [closeAndFocusTrigger, disabled, open, openWithFocus, searchable]);
122
+ const handleSearchKeyDown = React.useCallback((event) => {
123
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
124
+ event.preventDefault();
125
+ focusBoundaryOption(event.key === "ArrowDown" ? "first" : "last");
126
+ return;
127
+ }
128
+ if (event.key === "Escape") {
129
+ event.preventDefault();
130
+ closeAndFocusTrigger();
131
+ }
132
+ }, [closeAndFocusTrigger, focusBoundaryOption]);
133
+ const handleListboxKeyDown = React.useCallback((event) => {
134
+ var _a, _b;
135
+ const enabledOptions = getEnabledOptionElements();
136
+ const currentIndex = enabledOptions.findIndex((option) => option === document.activeElement);
137
+ if (event.key === "Escape") {
138
+ event.preventDefault();
139
+ closeAndFocusTrigger();
140
+ return;
141
+ }
142
+ if (event.key === "Home" || event.key === "End") {
143
+ event.preventDefault();
144
+ focusBoundaryOption(event.key === "Home" ? "first" : "last");
145
+ return;
146
+ }
147
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") {
148
+ return;
149
+ }
150
+ event.preventDefault();
151
+ if (event.key === "ArrowUp" && currentIndex <= 0 && searchable) {
152
+ (_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true });
153
+ return;
83
154
  }
84
- }, [disabled]);
155
+ const offset = event.key === "ArrowDown" ? 1 : -1;
156
+ const nextIndex = Math.min(Math.max(currentIndex + offset, 0), enabledOptions.length - 1);
157
+ (_b = enabledOptions[nextIndex]) === null || _b === void 0 ? void 0 : _b.focus({ preventScroll: true });
158
+ }, [
159
+ closeAndFocusTrigger,
160
+ focusBoundaryOption,
161
+ getEnabledOptionElements,
162
+ searchable,
163
+ ]);
85
164
  return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { id: labelId, htmlFor: selectId, className: cn(hideLabel && "sr-only", labelClassName), onClick: (event) => {
86
165
  var _a;
87
166
  event.preventDefault();
88
167
  (_a = triggerRef.current) === null || _a === void 0 ? void 0 : _a.focus();
89
- }, children: label }), _jsxs("div", { ref: containerRef, className: "relative", children: [_jsx("div", { ref: triggerRef, role: "combobox", id: selectId, tabIndex: disabled ? -1 : 0, onClick: () => {
168
+ }, children: label }), _jsxs("div", { ref: containerRef, className: "relative", children: [_jsxs("div", { ref: triggerRef, role: "combobox", id: selectId, tabIndex: disabled ? -1 : 0, onClick: () => {
90
169
  if (!disabled) {
91
- setOpen((prev) => !prev);
170
+ if (open) {
171
+ setOpen(false);
172
+ }
173
+ else {
174
+ openWithFocus(searchable ? "search" : "first");
175
+ }
92
176
  }
93
- }, onKeyDown: handleTriggerKeyDown, "aria-expanded": open, "aria-controls": listboxId, "aria-haspopup": "listbox", "aria-invalid": isInvalid ? "true" : undefined, "aria-disabled": disabled ? "true" : undefined, "aria-labelledby": labelId, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("flex h-control w-full cursor-pointer items-center gap-1.5 overflow-hidden rounded-control border border-input bg-background px-2.5 py-1.5 text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", isInvalid ? "border-destructive" : "border-input", disabled && "cursor-not-allowed opacity-50", triggerClassName), children: selectedLabels.length > 0 ? (_jsxs(_Fragment, { children: [selectedLabels.slice(0, maxVisibleTags).map((opt) => (_jsxs(Badge, { variant: "secondary", className: "max-w-[10rem] shrink-0 justify-start gap-1 overflow-visible text-sm font-medium", children: [_jsx("span", { className: "min-w-0 truncate", children: opt.label }), _jsx("button", { type: "button", className: "inline-flex cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: (e) => {
94
- e.stopPropagation();
95
- removeOption(opt.value);
96
- }, "aria-label": `Remove ${opt.label}`, children: _jsx(XIcon, { size: 12 }) })] }, opt.value))), selectedLabels.length > maxVisibleTags ? (_jsxs(Badge, { variant: "secondary", className: "shrink-0 text-sm font-medium", title: selectedLabels
97
- .slice(maxVisibleTags)
98
- .map((o) => o.label)
99
- .join(", "), children: ["+", selectedLabels.length - maxVisibleTags] })) : null] })) : (_jsx("span", { className: "text-muted-foreground", children: placeholder })) }), open ? (_jsxs("div", { className: "absolute z-50 mt-1 w-full rounded-overlay border bg-popover shadow-md", children: [searchable ? (_jsx("div", { className: "border-b px-2 py-1", children: _jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: searchPlaceholder, className: "h-8 border-0 bg-transparent px-2 py-0 text-sm shadow-none focus-visible:ring-0", onKeyDown: (e) => e.stopPropagation() }) })) : null, _jsx("div", { id: listboxId, role: "listbox", "aria-label": typeof label === "string" ? label : "Options", "aria-multiselectable": "true", className: "max-h-64 overflow-y-auto p-1", children: filteredOptions.length > 0 ? (filteredOptions.map((option) => {
177
+ }, onKeyDown: handleTriggerKeyDown, "aria-expanded": open, "aria-controls": listboxId, "aria-haspopup": "listbox", "aria-invalid": isInvalid ? "true" : undefined, "aria-disabled": disabled ? "true" : undefined, "aria-labelledby": labelId, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, className: cn("relative flex h-control w-full cursor-pointer items-center gap-1.5 overflow-hidden rounded-control border border-input bg-background px-2.5 py-1.5 pr-8 text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2", isInvalid ? "border-destructive" : "border-input", disabled && "cursor-not-allowed opacity-50", triggerClassName), children: [selectedLabels.length > 0 ? (_jsxs(_Fragment, { children: [selectedLabels.slice(0, maxVisibleTags).map((opt) => (_jsxs(Badge, { variant: "secondary", className: "max-w-[10rem] shrink-0 justify-start gap-1 overflow-visible text-sm font-medium", children: [_jsx("span", { className: "min-w-0 truncate", children: opt.label }), _jsx("button", { type: "button", className: "inline-flex cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: (e) => {
178
+ e.stopPropagation();
179
+ removeOption(opt.value);
180
+ }, "aria-label": `Remove ${opt.label}`, children: _jsx(XIcon, { size: 12 }) })] }, opt.value))), selectedLabels.length > maxVisibleTags ? (_jsxs(Badge, { variant: "secondary", className: "shrink-0 text-sm font-medium", title: selectedLabels
181
+ .slice(maxVisibleTags)
182
+ .map((o) => o.label)
183
+ .join(", "), children: ["+", selectedLabels.length - maxVisibleTags] })) : null] })) : (_jsx("span", { className: "text-muted-foreground", children: placeholder })), _jsx(ChevronDownIcon, { "aria-hidden": "true", size: 16, className: cn("pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 shrink-0 text-muted-foreground transition-transform duration-150", open && "rotate-180") })] }), open ? (_jsxs("div", { className: "absolute z-50 mt-1 w-full rounded-overlay border bg-popover shadow-md", children: [searchable ? (_jsx("div", { className: "border-b p-1", children: _jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: searchPlaceholder, className: "h-control border-0 bg-transparent px-2 py-0 text-sm shadow-none", onKeyDown: handleSearchKeyDown }) })) : null, _jsx("div", { ref: listboxRef, id: listboxId, role: "listbox", "aria-label": typeof label === "string" ? label : "Options", "aria-multiselectable": "true", className: "max-h-64 overflow-y-auto p-1", onKeyDown: handleListboxKeyDown, children: filteredOptions.length > 0 ? (filteredOptions.map((option) => {
100
184
  const isSelected = value.includes(option.value);
101
- return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSelected, disabled: option.disabled, onClick: () => toggleOption(option.value), className: cn("flex min-h-8 w-full cursor-pointer items-center gap-1.5 rounded-sm px-2 py-1.5 text-left text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50", isSelected && "bg-accent"), children: [_jsx(Checkbox, { checked: isSelected, disabled: option.disabled, tabIndex: -1, readOnly: true, "aria-hidden": "true", className: "mt-0.5" }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: option.label }), option.description ? (_jsx(Typography, { as: "span", variant: "caption", children: option.description })) : null] })] }, option.value));
185
+ return (_jsxs("button", { type: "button", role: "option", "aria-selected": isSelected, disabled: option.disabled, tabIndex: -1, onClick: () => toggleOption(option.value), className: cn("flex min-h-8 w-full cursor-pointer items-center gap-1.5 rounded-sm px-2 py-1.5 text-left text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50", isSelected &&
186
+ "bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary focus:bg-primary/20 focus:text-primary"), children: [_jsx(Checkbox, { checked: isSelected, disabled: option.disabled, tabIndex: -1, readOnly: true, "aria-hidden": "true", className: "mt-0.5" }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { children: option.label }), option.description ? (_jsx(Typography, { as: "span", variant: "caption", children: option.description })) : null] })] }, option.value));
102
187
  })) : (_jsx("div", { className: "px-2.5 py-1.5 text-center text-sm text-muted-foreground", children: "No options found." })) })] })) : null] }), showDescription ? (_jsx(FieldDescription, { id: descriptionId, className: descriptionClassName, children: description })) : null, _jsx(FieldError, { id: errorId, className: errorClassName, children: error })] }));
103
188
  }
104
189
  export { MultiSelect };
@@ -1,9 +1,11 @@
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
+ declare const popoverMenuItemFocusClassName = "rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-0";
8
+ declare const popoverMenuItemFocusWithinClassName = "rounded-sm focus-within:ring-2 focus-within:ring-inset focus-within:ring-ring";
7
9
  export interface PopoverMenuHeaderProps extends Omit<React.ComponentPropsWithoutRef<typeof Typography>, "as" | "asChild" | "variant" | "color"> {
8
10
  }
9
11
  export interface PopoverMenuContentProps extends React.ComponentPropsWithoutRef<typeof PopoverContent> {
@@ -41,10 +43,12 @@ declare function PopoverMenuGroup({ className, children, ...props }: PopoverMenu
41
43
  export interface PopoverMenuGroupLabelProps extends Omit<React.ComponentPropsWithoutRef<typeof Typography>, "as" | "asChild" | "variant" | "color"> {
42
44
  }
43
45
  declare function PopoverMenuGroupLabel({ className, children, ...props }: PopoverMenuGroupLabelProps): import("react/jsx-runtime").JSX.Element;
44
- export interface PopoverMenuSearchProps extends React.ComponentPropsWithoutRef<typeof InputControl> {
46
+ export interface PopoverMenuSearchProps extends Omit<InputProps, "className" | "hideLabel" | "inputClassName" | "label"> {
47
+ label?: React.ReactNode;
48
+ className?: string;
45
49
  containerClassName?: string;
46
50
  }
47
- declare const PopoverMenuSearch: React.ForwardRefExoticComponent<PopoverMenuSearchProps & React.RefAttributes<HTMLInputElement>>;
51
+ declare const PopoverMenuSearch: React.ForwardRefExoticComponent<Omit<PopoverMenuSearchProps, "ref"> & React.RefAttributes<HTMLInputElement>>;
48
52
  export interface PopoverMenuScrollAreaProps extends React.HTMLAttributes<HTMLDivElement> {
49
53
  maxHeight?: string;
50
54
  }
@@ -54,4 +58,4 @@ export interface PopoverMenuViewProps extends React.HTMLAttributes<HTMLDivElemen
54
58
  viewKey?: React.Key;
55
59
  }
56
60
  declare function PopoverMenuView({ children, className, direction, viewKey, ...props }: PopoverMenuViewProps): import("react/jsx-runtime").JSX.Element;
57
- export { PopoverMenuContent, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuCheckboxItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, };
61
+ export { PopoverMenuContent, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuCheckboxItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, popoverMenuItemFocusClassName, popoverMenuItemFocusWithinClassName, };
@@ -4,12 +4,14 @@ 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;
12
12
  const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
13
+ const popoverMenuItemFocusClassName = "rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-0";
14
+ const popoverMenuItemFocusWithinClassName = "rounded-sm focus-within:ring-2 focus-within:ring-inset focus-within:ring-ring";
13
15
  const PopoverMenuContent = React.forwardRef(({ className, ...props }, ref) => (_jsx(PopoverContent, { ref: ref, className: cn("overflow-hidden p-1", className), ...props })));
14
16
  PopoverMenuContent.displayName = "PopoverMenuContent";
15
17
  function PopoverMenuHeader({ className, children, ...props }) {
@@ -19,11 +21,12 @@ function PopoverMenuSection({ className, children, ...props }) {
19
21
  return (_jsx("div", { className: cn("space-y-0", className), ...props, children: children }));
20
22
  }
21
23
  function PopoverMenuItem({ className, children, active, type = "button", ...props }) {
22
- return (_jsxs(Button, { type: type, variant: "ghost", className: cn("h-auto min-h-8 w-full justify-start gap-2 rounded-sm px-2 py-2 text-left font-normal whitespace-normal focus-visible:ring-inset focus-visible:ring-1 focus-visible:ring-offset-0", active &&
23
- "bg-accent text-accent-foreground hover:bg-accent hover:text-accent-foreground", className), ...props, children: [children, active ? _jsx(Check, { "aria-hidden": "true", className: "ml-auto h-4 w-4 shrink-0" }) : null] }));
24
+ return (_jsxs(Button, { type: type, variant: "ghost", "data-slot": "popover-menu-item", className: cn("h-auto min-h-8 w-full justify-start gap-2 px-2 py-2 text-left font-normal whitespace-normal", popoverMenuItemFocusClassName, active &&
25
+ "bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary focus-visible:bg-primary/20 focus-visible:text-primary", className), ...props, children: [children, active ? _jsx(Check, { "aria-hidden": "true", className: "ml-auto h-4 w-4 shrink-0" }) : null] }));
24
26
  }
25
27
  function PopoverMenuCheckboxItem({ className, children, checked = false, description, disabled, onChange, readOnly, ...props }) {
26
- return (_jsxs("label", { className: cn("flex h-auto min-h-8 w-full cursor-pointer items-start gap-1.5 rounded-sm px-2 py-2 text-left text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-within:bg-accent focus-within:text-accent-foreground", checked && "bg-accent text-accent-foreground", disabled &&
28
+ return (_jsxs("label", { className: cn("flex h-auto min-h-8 w-full cursor-pointer items-start gap-1.5 rounded-sm px-2 py-2 text-left text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-within:bg-accent focus-within:text-accent-foreground", popoverMenuItemFocusWithinClassName, checked &&
29
+ "bg-primary/10 text-primary hover:bg-primary/20 hover:text-primary focus-within:bg-primary/20 focus-within:text-primary", disabled &&
27
30
  "cursor-not-allowed opacity-50 hover:bg-transparent hover:text-inherit", className), children: [_jsx(Checkbox, { ...props, checked: checked, disabled: disabled, readOnly: readOnly !== null && readOnly !== void 0 ? readOnly : !onChange, onChange: onChange, className: "mt-0.5" }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx(Typography, { as: "span", variant: "bodySm", className: "block", children: children }), description ? (_jsx(Typography, { as: "span", variant: "caption", className: "mt-0.5 block", children: description })) : null] })] }));
28
31
  }
29
32
  function PopoverMenuDivider({ className, ...props }) {
@@ -33,7 +36,7 @@ function PopoverMenuFooter({ className, children, inset = "menu", ...props }) {
33
36
  return (_jsx("div", { className: cn("flex items-center gap-2 border-t p-1.5", inset === "menu" && "-mx-1 -mb-1", inset === "flush" && "m-0", className), ...props, children: children }));
34
37
  }
35
38
  function PopoverMenuFooterAction({ className, type = "button", ...props }) {
36
- return (_jsx(Button, { type: type, variant: "ghost", size: "sm", className: cn("h-auto min-h-8 w-full justify-start px-2 py-1.5 text-left whitespace-normal focus-visible:ring-inset focus-visible:ring-1 focus-visible:ring-offset-0", className), ...props }));
39
+ return (_jsx(Button, { type: type, variant: "ghost", size: "sm", className: cn("h-auto min-h-8 w-full justify-start px-2 py-1.5 text-left whitespace-normal", popoverMenuItemFocusClassName, className), ...props }));
37
40
  }
38
41
  function PopoverMenuFooterMeta({ className, children, ...props }) {
39
42
  return (_jsx(Typography, { as: "span", variant: "bodySm", color: "muted", className: cn("shrink-0 px-2 font-medium", className), ...props, children: children }));
@@ -44,7 +47,10 @@ function PopoverMenuGroup({ className, children, ...props }) {
44
47
  function PopoverMenuGroupLabel({ className, children, ...props }) {
45
48
  return (_jsx(Typography, { as: "p", variant: "caption", color: "muted", className: cn("px-2 py-1.5", className), ...props, children: children }));
46
49
  }
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 }) })));
50
+ const PopoverMenuSearch = React.forwardRef(({ "aria-label": ariaLabel, className, containerClassName, label, placeholder, ...props }, ref) => {
51
+ var _a, _b;
52
+ 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, "data-popover-menu-search": "true", 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 }) }));
53
+ });
48
54
  PopoverMenuSearch.displayName = "PopoverMenuSearch";
49
55
  function PopoverMenuScrollArea({ className, children, maxHeight = "max-h-64", ...props }) {
50
56
  return (_jsx("div", { className: cn(maxHeight, "space-y-1 overflow-y-auto overflow-x-hidden", className), ...props, children: children }));
@@ -103,4 +109,4 @@ function PopoverMenuView({ children, className, direction = "forward", viewKey =
103
109
  "motion-safe:slide-in-from-right-full", activeDirection === "backward" &&
104
110
  "motion-safe:slide-in-from-left-full"), children: children }, viewKey)] }));
105
111
  }
106
- export { PopoverMenuContent, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuCheckboxItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, };
112
+ export { PopoverMenuContent, PopoverMenuHeader, PopoverMenuSection, PopoverMenuItem, PopoverMenuCheckboxItem, PopoverMenuDivider, PopoverMenuFooter, PopoverMenuFooterAction, PopoverMenuFooterMeta, PopoverMenuGroup, PopoverMenuGroupLabel, PopoverMenuSearch, PopoverMenuScrollArea, PopoverMenuView, popoverMenuItemFocusClassName, popoverMenuItemFocusWithinClassName, };
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,61 @@
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
+ const contentOnKeyDown = contentProps.onKeyDown;
13
+ const handleContentKeyDown = React.useCallback((event) => {
14
+ contentOnKeyDown === null || contentOnKeyDown === void 0 ? void 0 : contentOnKeyDown(event);
15
+ if (event.defaultPrevented ||
16
+ !["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
17
+ return;
18
+ }
19
+ const menuItems = Array.from(event.currentTarget.querySelectorAll('[data-slot="popover-menu-item"]:not(:disabled)')).filter((item) => !item.closest('[aria-hidden="true"]'));
20
+ if (menuItems.length === 0) {
21
+ return;
22
+ }
23
+ const currentIndex = menuItems.findIndex((item) => item === document.activeElement);
24
+ const searchInput = event.currentTarget.querySelector('[data-popover-menu-search="true"]');
25
+ let focusTarget;
26
+ if (event.key === "Home") {
27
+ focusTarget = menuItems[0];
28
+ }
29
+ else if (event.key === "End") {
30
+ focusTarget = menuItems[menuItems.length - 1];
31
+ }
32
+ else if (document.activeElement === searchInput) {
33
+ focusTarget =
34
+ event.key === "ArrowDown"
35
+ ? menuItems[0]
36
+ : menuItems[menuItems.length - 1];
37
+ }
38
+ else if (currentIndex >= 0) {
39
+ if (event.key === "ArrowUp" && currentIndex === 0 && searchInput) {
40
+ focusTarget = searchInput;
41
+ }
42
+ else {
43
+ const offset = event.key === "ArrowDown" ? 1 : -1;
44
+ const nextIndex = Math.min(Math.max(currentIndex + offset, 0), menuItems.length - 1);
45
+ focusTarget = menuItems[nextIndex];
46
+ }
47
+ }
48
+ if (!focusTarget) {
49
+ return;
50
+ }
51
+ event.preventDefault();
52
+ focusTarget.focus({ preventScroll: true });
53
+ }, [contentOnKeyDown]);
54
+ 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, onKeyDown: handleContentKeyDown, children: [title ? (_jsx(PopoverTitle, { id: titleId, className: cn(surface === "menu"
55
+ ? "px-2 py-1.5"
56
+ : "-mx-4 -mt-4 mb-4 border-b px-4 py-3"), children: title })) : null, children] })] }));
9
57
  }
10
- export { Popover };
58
+ function PopoverTitle({ className, children, ...props }) {
59
+ return (_jsx(Typography, { as: "p", variant: "label", "data-slot": "popover-title", className: className, ...props, children: children }));
60
+ }
61
+ export { Popover, PopoverTitle };
@@ -7,6 +7,7 @@ import { Button } from "./button";
7
7
  import { cn } from "./lib/utils";
8
8
  import { Link } from "./link";
9
9
  import { Popover } from "./popover";
10
+ import { popoverMenuItemFocusClassName } from "./popover-menu";
10
11
  import { Typography } from "./typography";
11
12
  function hasSectionNavChildren(item) {
12
13
  return Array.isArray(item.items) && item.items.length > 0;
@@ -28,7 +29,7 @@ const SectionNavRow = React.forwardRef(({ item, active, surface, hasChildren = f
28
29
  const isLink = Boolean(item.href) && !item.disabled && !hasChildren;
29
30
  const className = cn(surface === "list"
30
31
  ? "w-full justify-start gap-2 rounded-control px-2.5 text-sm font-medium shadow-none"
31
- : "h-auto min-h-8 w-full justify-start gap-2 rounded-sm px-2 py-1.5 text-left text-sm font-normal shadow-none whitespace-normal", active && !item.disabled
32
+ : cn("h-auto min-h-8 w-full justify-start gap-2 px-2 py-1.5 text-left text-sm font-normal shadow-none whitespace-normal", popoverMenuItemFocusClassName), active && !item.disabled
32
33
  ? surface === "list"
33
34
  ? "bg-background hover:bg-background"
34
35
  : "bg-accent text-accent-foreground hover:bg-accent hover:text-accent-foreground"
@@ -42,14 +43,14 @@ const SectionNavRow = React.forwardRef(({ item, active, surface, hasChildren = f
42
43
  hasChildren,
43
44
  });
44
45
  if (isLink) {
45
- return (_jsx(Button, { ref: ref, asChild: true, variant: "ghost", disabled: item.disabled, className: cn(className, triggerClassName), "aria-current": active ? "page" : undefined, ...buttonProps, children: _jsx(Link, { variant: "inherit", href: item.href, target: item.target, rel: item.rel, onClick: (event) => {
46
+ return (_jsx(Button, { ref: ref, asChild: true, variant: "ghost", disabled: item.disabled, "data-slot": surface === "menu" ? "popover-menu-item" : undefined, className: cn(className, triggerClassName), "aria-current": active ? "page" : undefined, ...buttonProps, children: _jsx(Link, { variant: "inherit", href: item.href, target: item.target, rel: item.rel, onClick: (event) => {
46
47
  var _a;
47
48
  externalOnClick === null || externalOnClick === void 0 ? void 0 : externalOnClick(event);
48
49
  onSelect === null || onSelect === void 0 ? void 0 : onSelect();
49
50
  (_a = item.onClick) === null || _a === void 0 ? void 0 : _a.call(item);
50
51
  }, children: content }) }));
51
52
  }
52
- return (_jsx(Button, { ref: ref, type: "button", variant: "ghost", disabled: item.disabled, className: cn(className, triggerClassName), "aria-current": active && !hasChildren ? "page" : undefined, "aria-expanded": hasChildren ? ariaExpanded : undefined, "aria-haspopup": hasChildren ? "menu" : undefined, ...buttonProps, onClick: hasChildren
53
+ return (_jsx(Button, { ref: ref, type: "button", variant: "ghost", disabled: item.disabled, "data-slot": surface === "menu" ? "popover-menu-item" : undefined, className: cn(className, triggerClassName), "aria-current": active && !hasChildren ? "page" : undefined, "aria-expanded": hasChildren ? ariaExpanded : undefined, "aria-haspopup": hasChildren ? "menu" : undefined, ...buttonProps, onClick: hasChildren
53
54
  ? externalOnClick
54
55
  : (event) => {
55
56
  var _a;
@@ -27,7 +27,7 @@ const SelectContent = React.forwardRef(({ className, children, header, position
27
27
  SelectContent.displayName = SelectPrimitive.Content.displayName;
28
28
  const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Label, { ref: ref, "data-slot": "select-label", className: cn("px-2 py-1.5 text-xs text-muted-foreground", className), ...props })));
29
29
  SelectLabel.displayName = SelectPrimitive.Label.displayName;
30
- const SelectItem = React.forwardRef(({ className, children, description, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, "data-slot": "select-item", className: cn("relative flex min-h-8 w-full cursor-pointer select-none items-center gap-1.5 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { "data-slot": "select-item-indicator", className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(SelectPrimitive.ItemText, { className: "break-words font-normal", children: children }), description ? (_jsx(Typography, { as: "span", variant: "caption", children: description })) : null] })] })));
30
+ const SelectItem = React.forwardRef(({ className, children, description, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, "data-slot": "select-item", className: cn("relative flex min-h-8 w-full cursor-pointer select-none items-center gap-1.5 rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary data-[state=checked]:hover:bg-primary/20 data-[state=checked]:hover:text-primary data-[state=checked]:focus:bg-primary/20 data-[state=checked]:focus:text-primary data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { "data-slot": "select-item-indicator", className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx(SelectPrimitive.ItemText, { className: "break-words font-normal", children: children }), description ? (_jsx(Typography, { as: "span", variant: "caption", children: description })) : null] })] })));
31
31
  SelectItem.displayName = SelectPrimitive.Item.displayName;
32
32
  const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx(SelectPrimitive.Separator, { ref: ref, "data-slot": "select-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props })));
33
33
  SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
package/dist/select.js CHANGED
@@ -43,6 +43,7 @@ function Select({ label, options, placeholder = "Select an option", description,
43
43
  const generatedId = React.useId();
44
44
  const selectId = id !== null && id !== void 0 ? id : `select-${generatedId}`;
45
45
  const searchInputRef = React.useRef(null);
46
+ const contentRef = React.useRef(null);
46
47
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
47
48
  const [searchQuery, setSearchQuery] = React.useState("");
48
49
  const errorId = error ? `${selectId}-error` : undefined;
@@ -69,6 +70,31 @@ function Select({ label, options, placeholder = "Select an option", description,
69
70
  }
70
71
  input.focus({ preventScroll: true });
71
72
  }, []);
73
+ const getEnabledOptionElements = React.useCallback(() => {
74
+ var _a, _b;
75
+ return Array.from((_b = (_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.querySelectorAll('[data-slot="select-item"]:not([data-disabled])')) !== null && _b !== void 0 ? _b : []);
76
+ }, []);
77
+ const focusBoundaryOption = React.useCallback((boundary) => {
78
+ const enabledOptions = getEnabledOptionElements();
79
+ const option = boundary === "first"
80
+ ? enabledOptions[0]
81
+ : enabledOptions[enabledOptions.length - 1];
82
+ option === null || option === void 0 ? void 0 : option.focus({ preventScroll: true });
83
+ }, [getEnabledOptionElements]);
84
+ const handleContentKeyDownCapture = React.useCallback((event) => {
85
+ if (event.key !== "ArrowUp") {
86
+ return;
87
+ }
88
+ const firstOption = getEnabledOptionElements()[0];
89
+ if (document.activeElement !== firstOption) {
90
+ return;
91
+ }
92
+ event.preventDefault();
93
+ event.stopPropagation();
94
+ if (searchable) {
95
+ focusSearchInput();
96
+ }
97
+ }, [focusSearchInput, getEnabledOptionElements, searchable]);
72
98
  React.useEffect(() => {
73
99
  if (!isOpen) {
74
100
  setSearchQuery("");
@@ -110,7 +136,17 @@ function Select({ label, options, placeholder = "Select an option", description,
110
136
  }
111
137
  onOpenChange === null || onOpenChange === void 0 ? void 0 : onOpenChange(nextOpen);
112
138
  }, [onOpenChange, open]);
113
- return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: selectId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs(SelectRoot, { disabled: disabled, open: open, onOpenChange: handleOpenChange, ...props, children: [_jsx(SelectTrigger, { id: selectId, className: triggerClassName, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { className: contentClassName, header: searchable ? (_jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), placeholder: searchPlaceholder, "aria-label": searchInputAriaLabel, autoComplete: "off", className: "h-control border-0 bg-transparent px-2 py-0 text-sm shadow-none focus-visible:border-0 focus-visible:ring-0", onKeyDown: (event) => {
139
+ return (_jsxs(Field, { className: className, "data-disabled": disabled ? "true" : undefined, "data-invalid": isInvalid ? "true" : undefined, children: [_jsx(FieldLabel, { htmlFor: selectId, className: cn(hideLabel && "sr-only", labelClassName), children: label }), _jsxs(SelectRoot, { disabled: disabled, open: open, onOpenChange: handleOpenChange, ...props, children: [_jsx(SelectTrigger, { id: selectId, className: triggerClassName, "aria-invalid": isInvalid ? "true" : undefined, "aria-describedby": [descriptionId, errorId].filter(Boolean).join(" ") || undefined, children: _jsx(SelectValue, { placeholder: placeholder }) }), _jsx(SelectContent, { ref: contentRef, className: contentClassName, onKeyDownCapture: handleContentKeyDownCapture, header: searchable ? (_jsx(InputControl, { ref: searchInputRef, value: searchQuery, onChange: (event) => setSearchQuery(event.target.value), placeholder: searchPlaceholder, "aria-label": searchInputAriaLabel, autoComplete: "off", className: "h-control border-0 bg-transparent px-2 py-0 text-sm shadow-none", onKeyDown: (event) => {
140
+ if (event.key === "ArrowDown" ||
141
+ event.key === "ArrowUp") {
142
+ event.preventDefault();
143
+ event.stopPropagation();
144
+ focusBoundaryOption(event.key === "ArrowDown" ? "first" : "last");
145
+ return;
146
+ }
147
+ if (event.key === "Escape" || event.key === "Tab") {
148
+ return;
149
+ }
114
150
  event.stopPropagation();
115
151
  } })) : undefined, viewportClassName: searchable ? "max-h-64" : undefined, children: hasVisibleOptions ? (optionGroups.map((group, groupIndex) => {
116
152
  var _a;
@@ -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}${active ? ", 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, containerClassName: "after:hidden" }), 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: 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}${active ? ", active" : ""}`, 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/dist/tabs.js CHANGED
@@ -7,7 +7,7 @@ import { ChevronDownIcon } from "./animated-icons/chevron-down";
7
7
  import { XIcon } from "./animated-icons/x";
8
8
  import { cn } from "./lib/utils";
9
9
  import { Popover } from "./popover";
10
- import { PopoverMenuSection } from "./popover-menu";
10
+ import { PopoverMenuSection, popoverMenuItemFocusClassName, } from "./popover-menu";
11
11
  const TabsContext = React.createContext(null);
12
12
  const getTabValueSelector = (value) => {
13
13
  const escapedValue = typeof CSS !== "undefined" && typeof CSS.escape === "function"
@@ -82,7 +82,7 @@ function TabTriggerWithDropdown({ item, triggerClassName, contentClassName, }) {
82
82
  setInternalActiveSection((_b = (_a = sections[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null);
83
83
  }
84
84
  }, [isTabSelected, item.activeSection, sections]);
85
- return (_jsx(Popover, { open: open, onOpenChange: setOpen, surface: "menu", align: "start", contentClassName: cn("min-w-[12rem]", contentClassName), trigger: _jsxs(TabsTrigger, { value: item.value, disabled: item.disabled, "data-state": isTabSelected ? "active" : "inactive", "data-tab-value": item.value, className: cn("flex items-center gap-1", triggerClassName), children: [renderTabsItemLabel(item.label, isTabSelected), _jsx(ChevronDownIcon, { size: 12, className: cn("transition-transform duration-200", open && "rotate-180") })] }), children: _jsx(PopoverMenuSection, { className: "space-y-1", children: sections.map((section) => (_jsxs("button", { type: "button", className: cn("flex min-h-fit w-full cursor-pointer items-start gap-2 rounded-sm px-3 py-2 text-left text-sm font-normal leading-5 text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring", isTabSelected &&
85
+ return (_jsx(Popover, { open: open, onOpenChange: setOpen, surface: "menu", align: "start", contentClassName: cn("min-w-[12rem]", contentClassName), trigger: _jsxs(TabsTrigger, { value: item.value, disabled: item.disabled, "data-state": isTabSelected ? "active" : "inactive", "data-tab-value": item.value, className: cn("flex items-center gap-1", triggerClassName), children: [renderTabsItemLabel(item.label, isTabSelected), _jsx(ChevronDownIcon, { size: 12, className: cn("transition-transform duration-200", open && "rotate-180") })] }), children: _jsx(PopoverMenuSection, { className: "space-y-1", children: sections.map((section) => (_jsxs("button", { type: "button", "data-slot": "popover-menu-item", className: cn("flex min-h-fit w-full cursor-pointer items-start gap-2 px-3 py-2 text-left text-sm font-normal leading-5 text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground", popoverMenuItemFocusClassName, isTabSelected &&
86
86
  activeSection === section.id &&
87
87
  "bg-accent text-accent-foreground hover:bg-accent hover:text-accent-foreground"), onClick: () => handleSectionClick(section.id), children: [_jsx("span", { className: "block min-w-0 flex-1 whitespace-normal break-words leading-5", children: section.label }), isTabSelected && activeSection === section.id ? (_jsx(Check, { "aria-hidden": "true", className: "mt-0.5 h-4 w-4 shrink-0" })) : null] }, section.id))) }) }));
88
88
  }
@@ -41,6 +41,6 @@ function useSearchHighlight(text, query) {
41
41
  }
42
42
  function SearchHighlight({ text, query, className, markClassName, ...props }) {
43
43
  const segments = useSearchHighlight(text, query);
44
- return (_jsx("span", { className: className, ...props, children: segments.map((segment, index) => segment.matched ? (_jsx("mark", { className: cn("rounded-[2px] bg-[hsl(var(--status-warning))] px-0.5 font-medium text-[hsl(var(--status-warning-foreground))]", markClassName), children: segment.text }, `${segment.text}-${index}`)) : (_jsx(React.Fragment, { children: segment.text }, `${segment.text}-${index}`))) }));
44
+ return (_jsx("span", { className: className, ...props, children: segments.map((segment, index) => segment.matched ? (_jsx("mark", { className: cn("rounded-[2px] bg-primary/10 px-0.5 font-medium text-primary dark:bg-primary/50 dark:text-foreground dark:ring-1 dark:ring-inset dark:ring-[hsl(var(--status-success-foreground)/0.55)]", markClassName), children: segment.text }, `${segment.text}-${index}`)) : (_jsx(React.Fragment, { children: segment.text }, `${segment.text}-${index}`))) }));
45
45
  }
46
46
  export { SearchHighlight, useSearchHighlight };
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.20",
4
4
  "description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",