@neasg/design-system 0.4.10 → 0.4.12

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.
Files changed (45) hide show
  1. package/README.md +2 -2
  2. package/dist/avatar-profile-popover.d.ts +34 -0
  3. package/dist/avatar-profile-popover.js +27 -0
  4. package/dist/badge.d.ts +1 -1
  5. package/dist/button.d.ts +1 -1
  6. package/dist/card.d.ts +2 -0
  7. package/dist/card.js +72 -3
  8. package/dist/command-search.d.ts +47 -2
  9. package/dist/command-search.js +173 -24
  10. package/dist/command.d.ts +2 -0
  11. package/dist/command.js +2 -2
  12. package/dist/date-input.d.ts +2 -0
  13. package/dist/date-input.js +44 -4
  14. package/dist/dialog-primitive.js +1 -1
  15. package/dist/draggable-tabs.d.ts +3 -0
  16. package/dist/draggable-tabs.js +4 -0
  17. package/dist/editable-table.js +2 -2
  18. package/dist/filter-popover.d.ts +2 -1
  19. package/dist/filter-popover.js +2 -2
  20. package/dist/guidance-tip.d.ts +26 -0
  21. package/dist/guidance-tip.js +162 -0
  22. package/dist/index.d.ts +13 -5
  23. package/dist/index.js +5 -1
  24. package/dist/jump-target-highlight.d.ts +13 -0
  25. package/dist/jump-target-highlight.js +95 -0
  26. package/dist/layout.d.ts +27 -5
  27. package/dist/layout.js +128 -34
  28. package/dist/message-item.js +9 -10
  29. package/dist/notification.js +1 -1
  30. package/dist/page-layout.d.ts +22 -0
  31. package/dist/page-layout.js +38 -0
  32. package/dist/page-section.js +1 -1
  33. package/dist/popover-menu.js +2 -2
  34. package/dist/routing-timeline.js +1 -1
  35. package/dist/table-column-visibility.d.ts +3 -1
  36. package/dist/table-column-visibility.js +74 -27
  37. package/dist/table-toolbar.d.ts +2 -1
  38. package/dist/table-toolbar.js +7 -2
  39. package/dist/table.d.ts +32 -3
  40. package/dist/table.js +318 -29
  41. package/dist/tabs.d.ts +2 -1
  42. package/dist/tabs.js +8 -5
  43. package/dist/theme-switcher.d.ts +1 -1
  44. package/dist/theme-switcher.js +4 -6
  45. package/package.json +2 -2
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # NEA Design System
2
2
 
3
- Shared NEA UI primitives, theme tokens, and Storybook documentation.
3
+ [![npm version](https://img.shields.io/npm/v/%40neasg%2Fdesign-system?style=flat-square&label=npm)](https://www.npmjs.com/package/@neasg/design-system)
4
4
 
5
- Package: [@neasg/design-system on npm](https://www.npmjs.com/package/@neasg/design-system)
5
+ Shared NEA UI primitives, theme tokens, and Storybook documentation.
6
6
 
7
7
  ## Local Development
8
8
 
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import { type AvatarProfileProps } from "./avatar";
3
+ import { type ButtonProps } from "./button";
4
+ import { type PopoverProps } from "./popover";
5
+ export interface AvatarProfilePopoverProps extends Omit<PopoverProps, "trigger" | "children" | "surface" | "contentClassName"> {
6
+ trigger: React.ReactElement;
7
+ children: React.ReactNode;
8
+ contentClassName?: string;
9
+ }
10
+ declare function AvatarProfilePopover({ trigger, children, contentClassName, align, sideOffset, ...props }: AvatarProfilePopoverProps): import("react/jsx-runtime").JSX.Element;
11
+ export interface AvatarProfilePopoverContentProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ }
13
+ declare function AvatarProfilePopoverContent({ className, ...props }: AvatarProfilePopoverContentProps): import("react/jsx-runtime").JSX.Element;
14
+ export interface AvatarProfilePopoverHeaderProps extends Omit<AvatarProfileProps, "className"> {
15
+ className?: string;
16
+ profileClassName?: string;
17
+ }
18
+ declare function AvatarProfilePopoverHeader({ className, profileClassName, size, avatarClassName, nameClassName, roleClassName, ...props }: AvatarProfilePopoverHeaderProps): import("react/jsx-runtime").JSX.Element;
19
+ export interface AvatarProfilePopoverSectionProps extends React.HTMLAttributes<HTMLElement> {
20
+ label: React.ReactNode;
21
+ description?: React.ReactNode;
22
+ contentClassName?: string;
23
+ labelClassName?: string;
24
+ descriptionClassName?: string;
25
+ }
26
+ declare function AvatarProfilePopoverSection({ className, label, description, children, contentClassName, labelClassName, descriptionClassName, ...props }: AvatarProfilePopoverSectionProps): import("react/jsx-runtime").JSX.Element;
27
+ export interface AvatarProfilePopoverFooterProps extends React.HTMLAttributes<HTMLDivElement> {
28
+ }
29
+ declare function AvatarProfilePopoverFooter({ className, ...props }: AvatarProfilePopoverFooterProps): import("react/jsx-runtime").JSX.Element;
30
+ export interface AvatarProfilePopoverFooterActionProps extends Omit<ButtonProps, "variant" | "size"> {
31
+ tone?: "default" | "destructive";
32
+ }
33
+ declare function AvatarProfilePopoverFooterAction({ className, tone, type, ...props }: AvatarProfilePopoverFooterActionProps): import("react/jsx-runtime").JSX.Element;
34
+ export { AvatarProfilePopover, AvatarProfilePopoverContent, AvatarProfilePopoverHeader, AvatarProfilePopoverSection, AvatarProfilePopoverFooter, AvatarProfilePopoverFooterAction, };
@@ -0,0 +1,27 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AvatarProfile } from "./avatar";
4
+ import { Button } from "./button";
5
+ import { cn } from "./lib/utils";
6
+ import { Popover } from "./popover";
7
+ import { Typography } from "./typography";
8
+ function AvatarProfilePopover({ trigger, children, contentClassName, align = "start", sideOffset = 4, ...props }) {
9
+ 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 }));
10
+ }
11
+ function AvatarProfilePopoverContent({ className, ...props }) {
12
+ return (_jsx("div", { "data-slot": "avatar-profile-popover-content", className: cn("min-w-0 bg-popover text-popover-foreground", className), ...props }));
13
+ }
14
+ function AvatarProfilePopoverHeader({ className, profileClassName, size = "sm", avatarClassName, nameClassName, roleClassName, ...props }) {
15
+ return (_jsx("div", { "data-slot": "avatar-profile-popover-header", className: cn("-mx-1 -mt-1 border-b px-3 py-2.5", className), children: _jsx(AvatarProfile, { ...props, size: size, className: profileClassName, avatarClassName: cn("h-9 w-9", avatarClassName), nameClassName: cn("text-sm leading-5", nameClassName), roleClassName: cn("text-xs leading-4", roleClassName) }) }));
16
+ }
17
+ function AvatarProfilePopoverSection({ className, label, description, children, contentClassName, labelClassName, descriptionClassName, ...props }) {
18
+ return (_jsxs("section", { "data-slot": "avatar-profile-popover-section", className: cn("space-y-1.5 px-2 py-2", className), ...props, children: [_jsx(Typography, { as: "h3", variant: "label", className: labelClassName, children: label }), _jsx("div", { className: cn("min-w-0", contentClassName), children: children }), description ? (_jsx(Typography, { as: "p", variant: "bodySm", color: "muted", className: descriptionClassName, children: description })) : null] }));
19
+ }
20
+ function AvatarProfilePopoverFooter({ className, ...props }) {
21
+ return (_jsx("div", { "data-slot": "avatar-profile-popover-footer", className: cn("-mx-1 -mb-1 border-t p-1.5", className), ...props }));
22
+ }
23
+ 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
+ "text-destructive hover:bg-destructive/10 hover:text-destructive", className), ...props }));
26
+ }
27
+ export { AvatarProfilePopover, AvatarProfilePopoverContent, AvatarProfilePopoverHeader, AvatarProfilePopoverSection, AvatarProfilePopoverFooter, AvatarProfilePopoverFooterAction, };
package/dist/badge.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const badgeVariants: (props?: ({
4
- variant?: "accent" | "muted" | "outline" | "destructive" | "secondary" | "info" | "success" | "warning" | null | undefined;
4
+ variant?: "accent" | "muted" | "destructive" | "outline" | "secondary" | "info" | "success" | "warning" | null | undefined;
5
5
  size?: "default" | "icon" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  /**
package/dist/button.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "link" | "default" | "outline" | "destructive" | "secondary" | "ghost" | null | undefined;
4
+ variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
5
5
  size?: "default" | "sm" | "compact" | "icon" | "compactIcon" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
package/dist/card.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface CardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "t
11
11
  footer?: React.ReactNode;
12
12
  active?: boolean;
13
13
  accent?: "primary";
14
+ /** Optional visual affordance shown in the top-right corner, such as a filter icon. */
15
+ icon?: React.ReactNode;
14
16
  /** Replaces title/stat/subMetric with skeleton placeholders while data is loading. */
15
17
  loading?: boolean;
16
18
  }
package/dist/card.js CHANGED
@@ -22,9 +22,45 @@ const cardSplitGridColumns = {
22
22
  lg: "xl:grid-cols-[minmax(0,1fr)_22rem]",
23
23
  },
24
24
  };
25
- const Card = React.forwardRef(({ title, description, variant = "default", size = "default", stat, subMetric, footer, active, accent, loading = false, children, className, onClick, onKeyDown, role, tabIndex, ...props }, ref) => {
25
+ function mergeRefs(...refs) {
26
+ return (value) => {
27
+ refs.forEach((ref) => {
28
+ if (!ref)
29
+ return;
30
+ if (typeof ref === "function") {
31
+ ref(value);
32
+ return;
33
+ }
34
+ ref.current = value;
35
+ });
36
+ };
37
+ }
38
+ function isAnimatedIconElement(node) {
39
+ var _a, _b;
40
+ if (typeof node.type === "string" || node.type === React.Fragment) {
41
+ return false;
42
+ }
43
+ const component = node.type;
44
+ const componentName = (_b = (_a = component.displayName) !== null && _a !== void 0 ? _a : component.name) !== null && _b !== void 0 ? _b : "";
45
+ return componentName.endsWith("Icon");
46
+ }
47
+ function attachCardIconRef(node, ref) {
48
+ if (!React.isValidElement(node) || !isAnimatedIconElement(node)) {
49
+ return node;
50
+ }
51
+ const refCallback = (value) => {
52
+ ref.current = value;
53
+ };
54
+ return React.cloneElement(node, {
55
+ ref: mergeRefs(node.ref, refCallback),
56
+ });
57
+ }
58
+ const Card = React.forwardRef(({ title, description, variant = "default", size = "default", stat, subMetric, footer, active, accent, icon, loading = false, children, className, onClick, onKeyDown, onMouseEnter, onMouseLeave, onFocus, onBlur, role, tabIndex, "aria-pressed": ariaPressed, ...props }, ref) => {
26
59
  const interactive = Boolean(onClick);
27
60
  const hasStat = stat !== undefined;
61
+ const iconRef = React.useRef(null);
62
+ const showIcon = icon != null;
63
+ const renderedIcon = showIcon ? attachCardIconRef(icon, iconRef) : null;
28
64
  const handleKeyDown = (event) => {
29
65
  onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
30
66
  if (event.defaultPrevented ||
@@ -36,12 +72,45 @@ const Card = React.forwardRef(({ title, description, variant = "default", size =
36
72
  event.preventDefault();
37
73
  onClick(event);
38
74
  };
75
+ const startIconAnimation = () => {
76
+ var _a, _b;
77
+ (_b = (_a = iconRef.current) === null || _a === void 0 ? void 0 : _a.startAnimation) === null || _b === void 0 ? void 0 : _b.call(_a);
78
+ };
79
+ const stopIconAnimation = () => {
80
+ var _a, _b;
81
+ (_b = (_a = iconRef.current) === null || _a === void 0 ? void 0 : _a.stopAnimation) === null || _b === void 0 ? void 0 : _b.call(_a);
82
+ };
83
+ const handleMouseEnter = (event) => {
84
+ if (showIcon) {
85
+ startIconAnimation();
86
+ }
87
+ onMouseEnter === null || onMouseEnter === void 0 ? void 0 : onMouseEnter(event);
88
+ };
89
+ const handleMouseLeave = (event) => {
90
+ if (showIcon) {
91
+ stopIconAnimation();
92
+ }
93
+ onMouseLeave === null || onMouseLeave === void 0 ? void 0 : onMouseLeave(event);
94
+ };
95
+ const handleFocus = (event) => {
96
+ if (showIcon) {
97
+ startIconAnimation();
98
+ }
99
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(event);
100
+ };
101
+ const handleBlur = (event) => {
102
+ if (showIcon) {
103
+ stopIconAnimation();
104
+ }
105
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur(event);
106
+ };
39
107
  const renderedTitle = React.isValidElement(title) ? (title) : (_jsx(Typography, { as: "p", variant: hasStat ? "caption" : "body", color: variant === "gradient" ? "inherit" : hasStat ? "muted" : "foreground", className: cn(hasStat && "font-medium", !hasStat && "font-semibold", variant === "gradient" && "opacity-80"), children: title }));
40
108
  const renderedDescription = description == null ? null : React.isValidElement(description) ? (description) : (_jsx(Typography, { as: "p", variant: "caption", color: variant === "gradient" ? "inherit" : undefined, className: variant === "gradient" ? "opacity-70" : undefined, children: description }));
41
- return (_jsxs("div", { ref: ref, onClick: onClick, onKeyDown: handleKeyDown, role: role !== null && role !== void 0 ? role : (interactive ? "button" : undefined), tabIndex: tabIndex !== null && tabIndex !== void 0 ? tabIndex : (interactive ? 0 : undefined), className: cn("rounded-surface border border-[hsl(var(--card-border))] text-card-foreground", variant === "default" && "bg-card", variant === "elevated" && "bg-card shadow-sm", variant === "gradient" &&
109
+ return (_jsxs("div", { ref: ref, onClick: onClick, onKeyDown: handleKeyDown, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onFocus: handleFocus, onBlur: handleBlur, role: role !== null && role !== void 0 ? role : (interactive ? "button" : undefined), tabIndex: tabIndex !== null && tabIndex !== void 0 ? tabIndex : (interactive ? 0 : undefined), "aria-pressed": ariaPressed !== null && ariaPressed !== void 0 ? ariaPressed : (interactive && active !== undefined ? Boolean(active) : undefined), className: cn("group/card rounded-surface border border-[hsl(var(--card-border))] text-card-foreground", variant === "default" && "bg-card", variant === "elevated" && "bg-card shadow-sm", variant === "gradient" &&
42
110
  "border-[hsl(var(--card-gradient-border))]/30 bg-gradient-to-br from-[hsl(var(--card-gradient-from))] to-[hsl(var(--card-gradient-to))] text-[hsl(var(--card-gradient-foreground))]", active && "border-primary bg-primary/[0.04] ring-1 ring-primary/20", accent === "primary" &&
43
111
  "border-[hsl(var(--card-border))] shadow-[inset_4px_0_0_0_hsl(var(--primary))]", interactive &&
44
- "cursor-pointer transition-[border-color,background-color,box-shadow] hover:border-primary/50 hover:bg-primary/5 hover:shadow-md focus-visible:border-primary/50 focus-visible:bg-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", className), ...props, children: [_jsxs("div", { className: cn("flex flex-col gap-2", size === "default" && "p-4", size === "compact" && "p-3"), children: [loading ? (_jsx(Skeleton, { className: "h-3.5 w-24" })) : (renderedTitle), loading
112
+ "cursor-pointer transition-[border-color,background-color,box-shadow,transform] duration-150 ease-out hover:border-primary/50 hover:bg-primary/5 hover:shadow-md motion-safe:hover:-translate-y-0.5 motion-safe:active:translate-y-0 motion-safe:active:scale-[0.99] focus-visible:border-primary/50 focus-visible:bg-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", className), ...props, children: [_jsxs("div", { className: cn("relative flex flex-col gap-2", size === "default" && "p-4", size === "compact" && "p-3", showIcon && size === "default" && "pr-12", showIcon && size === "compact" && "pr-10"), children: [showIcon ? (_jsx("span", { "aria-hidden": "true", "data-slot": "card-icon", className: cn("pointer-events-none absolute right-3 top-3 inline-flex size-7 items-center justify-center rounded-control border border-[hsl(var(--card-border))] bg-background/80 text-muted-foreground shadow-sm transition-[background-color,color,transform] duration-150 ease-out group-hover/card:scale-105 group-hover/card:bg-primary/10 group-hover/card:text-primary group-focus-visible/card:bg-primary/10 group-focus-visible/card:text-primary motion-reduce:transition-colors [&_svg]:size-3.5", size === "compact" && "right-2.5 top-2.5 size-6 [&_svg]:size-3", active && "border-primary/30 bg-primary/10 text-primary", variant === "gradient" &&
113
+ "border-white/25 bg-white/10 text-current group-hover/card:bg-white/20 group-hover/card:text-current group-focus-visible/card:bg-white/20 group-focus-visible/card:text-current"), children: renderedIcon })) : null, loading ? (_jsx(Skeleton, { className: "h-3.5 w-24" })) : (renderedTitle), loading
45
114
  ? description != null && _jsx(Skeleton, { className: "h-5 w-40" })
46
115
  : renderedDescription, loading ? (stat !== undefined && _jsx(Skeleton, { className: "h-8 w-20" })) : stat ? (_jsx(Typography, { as: "p", variant: "title", color: variant === "gradient" ? "inherit" : undefined, children: stat })) : null, loading ? (subMetric !== undefined && _jsx(Skeleton, { className: "h-5 w-28" })) : subMetric ? (_jsx(Typography, { as: "p", variant: "caption", color: variant === "gradient" ? "inherit" : undefined, className: variant === "gradient" ? "opacity-80" : undefined, children: subMetric })) : null] }), children ? (_jsx("div", { className: cn(size === "default" && "p-4 pt-0", size === "compact" && "p-3 pt-0"), children: children })) : null, footer ? (_jsx("div", { className: cn("flex items-center", size === "default" && "p-4 pt-0", size === "compact" && "p-3 pt-0"), children: footer })) : null] }));
47
116
  });
@@ -1,26 +1,62 @@
1
1
  import * as React from "react";
2
+ import { type BadgeProps } from "./badge";
2
3
  export interface CommandSearchFilter {
3
4
  value: string;
4
5
  label: string;
6
+ count?: number;
5
7
  selected?: boolean;
6
- onSelect?: () => void;
8
+ onSelect?: (details: CommandSearchFilterSelectDetails) => void;
9
+ }
10
+ export interface CommandSearchFilterSelectDetails {
11
+ value: string;
12
+ selected: boolean;
13
+ allowMultiSelect: boolean;
14
+ }
15
+ export type CommandSearchIcon = React.ComponentType<{
16
+ className?: string;
17
+ }>;
18
+ export type CommandSearchItemTone = "accent" | "destructive" | "info" | "muted" | "success" | "warning";
19
+ export interface CommandSearchItemDetail {
20
+ label: string;
21
+ value: string;
22
+ /** Groups related metadata badges together while preserving the default Badge styling. */
23
+ group?: string;
24
+ variant?: BadgeProps["variant"];
25
+ bordered?: boolean;
26
+ showLabel?: boolean;
7
27
  }
8
28
  export interface CommandSearchItem {
9
29
  id: string;
10
30
  title: string;
11
31
  subtitle?: string;
32
+ description?: string;
33
+ details?: CommandSearchItemDetail[];
34
+ icon?: CommandSearchIcon;
35
+ tone?: CommandSearchItemTone;
36
+ affordance?: React.ReactNode;
37
+ affordanceLabel?: string;
12
38
  value?: string;
13
39
  keywords?: string[];
14
40
  disabled?: boolean;
15
41
  closeOnSelect?: boolean;
16
42
  onSelect?: () => void;
17
43
  }
44
+ export interface CommandSearchSectionAction {
45
+ label: React.ReactNode;
46
+ value?: string;
47
+ affordance?: React.ReactNode;
48
+ affordanceLabel?: string;
49
+ closeOnSelect?: boolean;
50
+ onSelect?: () => void;
51
+ }
18
52
  export interface CommandSearchSection {
19
53
  key: string;
20
54
  heading: string;
21
55
  items: CommandSearchItem[];
56
+ action?: CommandSearchSectionAction;
22
57
  }
23
58
  export type CommandSearchShowAllResultsLabel = React.ReactNode | ((query: string, hasResults: boolean) => React.ReactNode);
59
+ export type CommandSearchShortcutPlatform = "mac" | "windows";
24
60
  export interface CommandSearchProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
25
61
  value: string;
26
62
  onValueChange: (value: string) => void;
@@ -35,14 +71,23 @@ export interface CommandSearchProps extends Omit<React.HTMLAttributes<HTMLDivEle
35
71
  inputPlaceholder?: string;
36
72
  /** The key that opens the dialog when pressed with Cmd/Ctrl (e.g. "k", "j"). */
37
73
  shortcutKey?: string;
74
+ /** Controls the default shortcut label shown on the trigger. */
75
+ shortcutPlatform?: CommandSearchShortcutPlatform;
38
76
  /** Override the label shown on the trigger. Defaults to "⌘{shortcutKey.toUpperCase()}". */
39
77
  shortcutLabel?: string;
40
78
  allFilter?: CommandSearchFilter;
41
79
  filters?: CommandSearchFilter[];
80
+ subFilters?: CommandSearchFilter[];
81
+ /** Whether multiple filters can be selected at once. Selection state remains controlled by the passed filters. */
82
+ allowMultiSelect?: boolean;
42
83
  /** Shows command-list skeleton rows while results are being fetched. */
43
84
  loading?: boolean;
44
85
  /** Number of skeleton rows shown while `loading` is true. */
45
86
  loadingRows?: number;
87
+ /** Minimum height for the results pane to keep the dialog from resizing between states. */
88
+ resultsMinHeight?: React.CSSProperties["minHeight"];
89
+ /** Maximum height for the scrollable results pane. */
90
+ resultsMaxHeight?: React.CSSProperties["maxHeight"];
46
91
  /**
47
92
  * Renders a footer action for routing to a full search results page.
48
93
  * The action is shown only when the query is not empty and results are not loading.
@@ -55,5 +100,5 @@ export interface CommandSearchProps extends Omit<React.HTMLAttributes<HTMLDivEle
55
100
  dialogClassName?: string;
56
101
  resultsClassName?: string;
57
102
  }
58
- declare function CommandSearch({ value, onValueChange, sections, children, className, isCollapsed, open, defaultOpen, onOpenChange, triggerVariant, triggerPlaceholder, inputPlaceholder, shortcutKey, shortcutLabel, allFilter, filters, loading, loadingRows, onShowAllResults, showAllResultsLabel, triggerClassName, dialogClassName, resultsClassName, ...props }: CommandSearchProps): import("react/jsx-runtime").JSX.Element;
103
+ declare function CommandSearch({ value, onValueChange, sections, children, className, isCollapsed, open, defaultOpen, onOpenChange, triggerVariant, triggerPlaceholder, inputPlaceholder, shortcutKey, shortcutPlatform, shortcutLabel, allFilter, filters, subFilters, allowMultiSelect, loading, loadingRows, resultsMinHeight, resultsMaxHeight, onShowAllResults, showAllResultsLabel, triggerClassName, dialogClassName, resultsClassName, ...props }: CommandSearchProps): import("react/jsx-runtime").JSX.Element;
59
104
  export { CommandSearch };
@@ -1,10 +1,14 @@
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";
5
+ import { Badge } from "./badge";
4
6
  import { Button } from "./button";
5
7
  import { Command, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "./command";
6
8
  import { EmptyState } from "./empty-state";
7
9
  import { SearchIcon } from "./animated-icons/search";
10
+ import { XIcon } from "./animated-icons/x";
11
+ import { CountBadge } from "./count-badge";
8
12
  import { DialogRoot, DialogSurface, DialogTitle } from "./dialog-primitive";
9
13
  import { FilterToggle } from "./filter-toggle";
10
14
  import { cn } from "./lib/utils";
@@ -15,15 +19,128 @@ import { SearchHighlight } from "./use-search-highlight";
15
19
  function CommandSearchEmptyIcon({ className }) {
16
20
  return _jsx(SearchIcon, { className: className, size: 20 });
17
21
  }
22
+ const commandSearchItemToneClasses = {
23
+ accent: "border-status-accent-foreground/20 bg-status-accent text-status-accent-foreground",
24
+ destructive: "border-status-destructive-foreground/20 bg-status-destructive text-status-destructive-foreground",
25
+ info: "border-status-info-foreground/20 bg-status-info text-status-info-foreground",
26
+ muted: "border-border bg-muted text-muted-foreground",
27
+ success: "border-status-success-foreground/20 bg-status-success text-status-success-foreground",
28
+ warning: "border-status-warning-foreground/20 bg-status-warning text-status-warning-foreground",
29
+ };
18
30
  function getCommandSearchItemValue(item) {
19
- var _a, _b;
20
- const fallbackValue = [item.title, item.subtitle, ...((_a = item.keywords) !== null && _a !== void 0 ? _a : [])]
31
+ var _a, _b, _c, _d;
32
+ const detailValues = (_b = (_a = item.details) === null || _a === void 0 ? void 0 : _a.flatMap((detail) => [detail.label, detail.value])) !== null && _b !== void 0 ? _b : [];
33
+ const fallbackValue = [
34
+ item.title,
35
+ item.subtitle,
36
+ item.description,
37
+ ...detailValues,
38
+ ...((_c = item.keywords) !== null && _c !== void 0 ? _c : []),
39
+ ]
21
40
  .filter(Boolean)
22
41
  .join(" ");
23
- return (_b = item.value) !== null && _b !== void 0 ? _b : fallbackValue;
42
+ return (_d = item.value) !== null && _d !== void 0 ? _d : fallbackValue;
43
+ }
44
+ function getCommandSearchSectionActionValue(section) {
45
+ if (!section.action) {
46
+ return section.heading;
47
+ }
48
+ if (section.action.value) {
49
+ return section.action.value;
50
+ }
51
+ const label = typeof section.action.label === "string"
52
+ ? section.action.label
53
+ : "Show all results";
54
+ return `${section.heading} ${label}`;
55
+ }
56
+ function handleCommandSearchItemKeyDown(event, onSelect) {
57
+ if (event.key !== "Enter" && event.key !== " ") {
58
+ return;
59
+ }
60
+ event.preventDefault();
61
+ event.stopPropagation();
62
+ onSelect();
63
+ }
64
+ function isCommandSearchTitleDetail(detail) {
65
+ return detail.showLabel === false && Boolean(detail.variant);
66
+ }
67
+ function getNormalizedCommandSearchDetailLabel(detail) {
68
+ return detail.label.toLowerCase().replace(/\.$/, "");
69
+ }
70
+ function isCommandSearchCompanyDetail(detail) {
71
+ const label = getNormalizedCommandSearchDetailLabel(detail);
72
+ return (label === "company name" ||
73
+ label === "uen" ||
74
+ label === "postal code");
75
+ }
76
+ const commandSearchDetailGroupPriority = {
77
+ identity: 0,
78
+ summary: 1,
79
+ };
80
+ function getCommandSearchDetailGroup(detail) {
81
+ if (detail.group)
82
+ return detail.group;
83
+ const label = getNormalizedCommandSearchDetailLabel(detail);
84
+ if (label === "company name" ||
85
+ label === "case number" ||
86
+ label === "licence no" ||
87
+ label === "inspection no" ||
88
+ label === "uen" ||
89
+ label === "postal code") {
90
+ return "identity";
91
+ }
92
+ if (label === "active licences" || label === "in progress cases") {
93
+ return "summary";
94
+ }
95
+ return "metadata";
96
+ }
97
+ function getGroupedCommandSearchDetails(details) {
98
+ const groups = new Map();
99
+ details.forEach((detail, index) => {
100
+ const key = getCommandSearchDetailGroup(detail);
101
+ const group = groups.get(key);
102
+ if (group) {
103
+ group.details.push(detail);
104
+ return;
105
+ }
106
+ groups.set(key, { key, details: [detail], order: index });
107
+ });
108
+ return Array.from(groups.values()).sort((a, b) => {
109
+ var _a, _b;
110
+ const priorityA = (_a = commandSearchDetailGroupPriority[a.key]) !== null && _a !== void 0 ? _a : 2;
111
+ const priorityB = (_b = commandSearchDetailGroupPriority[b.key]) !== null && _b !== void 0 ? _b : 2;
112
+ return priorityA - priorityB || a.order - b.order;
113
+ });
114
+ }
115
+ function CommandSearchDetailBadge({ detail, query, className, }) {
116
+ var _a;
117
+ return (_jsxs(Badge, { variant: (_a = detail.variant) !== null && _a !== void 0 ? _a : "muted", bordered: detail.bordered, "data-slot": "command-search-detail", className: cn("max-w-full justify-start text-left", className), title: detail.showLabel === false
118
+ ? detail.value
119
+ : `${detail.label}: ${detail.value}`, children: [detail.showLabel === false ? null : (_jsx("span", { className: "shrink-0", children: detail.label })), _jsx("span", { className: cn("min-w-0 truncate", !detail.variant && "text-foreground"), children: _jsx(SearchHighlight, { text: detail.value, query: query }) })] }));
120
+ }
121
+ function CommandSearchResultItem({ item, query, }) {
122
+ var _a, _b, _c, _d;
123
+ const Icon = item.icon;
124
+ const tone = (_a = item.tone) !== null && _a !== void 0 ? _a : "muted";
125
+ const details = (_b = item.details) !== null && _b !== void 0 ? _b : [];
126
+ const titleDetails = details.filter(isCommandSearchTitleDetail);
127
+ const metadataDetails = details.filter((detail) => !isCommandSearchTitleDetail(detail));
128
+ const companyDetails = metadataDetails.filter(isCommandSearchCompanyDetail);
129
+ const recordDetails = metadataDetails.filter((detail) => !isCommandSearchCompanyDetail(detail));
130
+ const recordDetailGroups = getGroupedCommandSearchDetails(recordDetails);
131
+ const hasDetails = metadataDetails.length > 0;
132
+ const hasRecordDetails = recordDetails.length > 0;
133
+ const hasCompanyDetails = companyDetails.length > 0;
134
+ return (_jsxs(_Fragment, { children: [Icon ? (_jsx("span", { className: cn("mt-[calc(var(--space-1)/2)] flex size-[var(--space-8)] shrink-0 items-center justify-center rounded-control border", commandSearchItemToneClasses[tone]), "aria-hidden": "true", children: _jsx(Icon, { className: "h-3.5 w-3.5" }) })) : null, _jsxs("span", { className: "min-w-0 flex-1 space-y-[var(--space-2)]", children: [_jsxs("span", { "data-slot": "command-search-title-line", className: "flex min-w-0 items-center gap-[var(--space-2)]", children: [_jsx(Typography, { as: "span", variant: "label", className: "min-w-0 truncate", children: _jsx(SearchHighlight, { text: item.title, query: query }) }), titleDetails.map((detail) => (_jsx(CommandSearchDetailBadge, { detail: detail, query: query, className: "shrink-0" }, `${detail.label}-${detail.value}`)))] }), !hasDetails && item.subtitle ? (_jsx(Typography, { as: "span", variant: "caption", className: "block truncate text-muted-foreground", children: _jsx(SearchHighlight, { text: item.subtitle, query: query }) })) : null, !hasDetails && item.description ? (_jsx(Typography, { as: "span", variant: "caption", className: "block line-clamp-2 text-muted-foreground", children: _jsx(SearchHighlight, { text: item.description, query: query }) })) : null, hasDetails ? (_jsxs("span", { "data-slot": "command-search-details-lines", className: "flex min-w-0 flex-col gap-[calc(var(--space-1)+0.125rem)] overflow-hidden", children: [hasRecordDetails ? (_jsx("span", { "data-slot": "command-search-record-details-line", className: "flex min-w-0 items-center gap-[var(--space-1)] overflow-hidden", children: recordDetailGroups.map((group) => (_jsx("span", { "data-slot": "command-search-detail-group", "data-group": group.key, className: "flex min-w-0 shrink items-center gap-[var(--space-1)]", children: group.details.map((detail) => (_jsx(CommandSearchDetailBadge, { detail: detail, query: query, className: "min-w-0 shrink" }, `${detail.label}-${detail.value}`))) }, group.key))) })) : null, hasCompanyDetails ? (_jsx("span", { "data-slot": "command-search-company-details-line", "data-group": "company", className: "flex min-w-0 items-center gap-[var(--space-1)] overflow-hidden", children: companyDetails.map((detail) => (_jsx(CommandSearchDetailBadge, { detail: detail, query: query, className: "min-w-0 shrink" }, `${detail.label}-${detail.value}`))) })) : null] })) : null] }), _jsxs("span", { "data-slot": "command-search-result-affordance", className: "ml-auto flex size-[var(--space-8)] shrink-0 self-center items-center justify-center rounded-control text-muted-foreground opacity-0 transition-opacity group-data-[selected=true]:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100", children: [(_c = item.affordance) !== null && _c !== void 0 ? _c : (_jsx(ArrowUpRight, { className: "size-[var(--space-4)]", "aria-hidden": "true" })), _jsx("span", { className: "sr-only", children: (_d = item.affordanceLabel) !== null && _d !== void 0 ? _d : "Open result" })] })] }));
24
135
  }
25
136
  function CommandSearchSkeleton({ rows = 4 }) {
26
- return (_jsx("div", { className: "space-y-[var(--space-1)] p-[var(--space-1)]", children: Array.from({ length: rows }).map((_, index) => (_jsxs("div", { className: "space-y-[var(--space-2)] rounded-sm px-[var(--space-2)] py-[var(--space-2)]", children: [_jsx(Skeleton, { className: "h-4 w-40" }), _jsx(Skeleton, { className: "h-3 w-56 max-w-full" })] }, index))) }));
137
+ return (_jsx("div", { "data-slot": "command-search-skeleton", className: "space-y-[var(--space-1)] p-[var(--space-1)]", children: Array.from({ length: rows }).map((_, index) => (_jsxs("div", { "data-slot": "command-search-skeleton-row", className: "flex min-h-[var(--space-8)] items-start gap-[var(--space-2)] rounded-sm px-[var(--space-2)] py-[var(--space-3)]", children: [_jsx(Skeleton, { "data-slot": "command-search-skeleton-icon", className: "mt-[calc(var(--space-1)/2)] size-[var(--space-8)] shrink-0 rounded-control" }), _jsxs("div", { className: "min-w-0 flex-1 space-y-[var(--space-2)]", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-[var(--space-2)]", children: [_jsx(Skeleton, { className: "h-[var(--space-4)] w-36 max-w-[55%]" }), _jsx(Skeleton, { className: "h-[var(--space-5)] w-16 shrink-0 rounded-md" })] }), _jsx("div", { className: "flex min-w-0 items-center gap-[var(--space-1)] overflow-hidden", children: _jsx(Skeleton, { className: "h-[var(--space-5)] w-28 shrink rounded-md" }) }), _jsxs("div", { className: "flex min-w-0 items-center gap-[var(--space-1)] overflow-hidden", children: [_jsx(Skeleton, { className: "h-[var(--space-5)] w-40 max-w-[45%] shrink rounded-md" }), _jsx(Skeleton, { className: "h-[var(--space-5)] w-24 shrink rounded-md" }), _jsx(Skeleton, { className: "h-[var(--space-5)] w-24 shrink rounded-md" })] })] }), _jsx(Skeleton, { "data-slot": "command-search-skeleton-affordance", className: "ml-auto size-[var(--space-8)] shrink-0 self-center rounded-control opacity-60" })] }, index))) }));
138
+ }
139
+ function CommandSearchEmptyState({ query, resultsMinHeight, }) {
140
+ const normalizedQuery = query.trim();
141
+ return (_jsx(EmptyState, { "data-slot": "command-search-empty-state", icon: CommandSearchEmptyIcon, style: { minHeight: resultsMinHeight }, message: normalizedQuery ? "No results found" : "Start typing to search", description: normalizedQuery
142
+ ? `No results found for "${query}".`
143
+ : "Results will appear here as you type." }));
27
144
  }
28
145
  function useControllableOpen({ open, defaultOpen, onOpenChange, }) {
29
146
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen !== null && defaultOpen !== void 0 ? defaultOpen : false);
@@ -37,7 +154,7 @@ function useControllableOpen({ open, defaultOpen, onOpenChange, }) {
37
154
  }, [isControlled, onOpenChange]);
38
155
  return [currentOpen, setOpen];
39
156
  }
40
- function CommandSearch({ value, onValueChange, sections, children, className, isCollapsed = false, open, defaultOpen, onOpenChange, triggerVariant = "default", triggerPlaceholder = "Search...", inputPlaceholder = "Search...", shortcutKey = "k", shortcutLabel, allFilter, filters = [], loading = false, loadingRows = 4, onShowAllResults, showAllResultsLabel, triggerClassName, dialogClassName, resultsClassName, ...props }) {
157
+ function CommandSearch({ value, onValueChange, sections, children, className, isCollapsed = false, open, defaultOpen, onOpenChange, triggerVariant = "default", triggerPlaceholder = "Search...", inputPlaceholder = "Search...", shortcutKey = "k", shortcutPlatform = "mac", shortcutLabel, allFilter, filters = [], subFilters = [], allowMultiSelect = true, loading = false, loadingRows = 4, resultsMinHeight = 384, resultsMaxHeight = 560, onShowAllResults, showAllResultsLabel, triggerClassName, dialogClassName, resultsClassName, ...props }) {
41
158
  const [isOpen, setIsOpen] = useControllableOpen({
42
159
  open,
43
160
  defaultOpen,
@@ -53,7 +170,10 @@ function CommandSearch({ value, onValueChange, sections, children, className, is
53
170
  : showAllResultsLabel !== null && showAllResultsLabel !== void 0 ? showAllResultsLabel : (hasResults
54
171
  ? `Show all results for "${normalizedValue}"`
55
172
  : `Search all records for "${normalizedValue}"`);
56
- const resolvedLabel = shortcutLabel !== null && shortcutLabel !== void 0 ? shortcutLabel : `⌘${shortcutKey.toUpperCase()}`;
173
+ const shortcutKeyLabel = shortcutKey.toUpperCase();
174
+ const resolvedLabel = shortcutLabel !== null && shortcutLabel !== void 0 ? shortcutLabel : (shortcutPlatform === "windows"
175
+ ? `Ctrl ${shortcutKeyLabel}`
176
+ : `⌘${shortcutKeyLabel}`);
57
177
  React.useEffect(() => {
58
178
  const handleKeyDown = (event) => {
59
179
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === shortcutKey.toLowerCase()) {
@@ -72,29 +192,58 @@ function CommandSearch({ value, onValueChange, sections, children, className, is
72
192
  const preserveInputFocus = React.useCallback((event) => {
73
193
  event.preventDefault();
74
194
  }, []);
75
- const selectFilter = React.useCallback((onSelect) => {
76
- onSelect === null || onSelect === void 0 ? void 0 : onSelect();
195
+ const selectFilter = React.useCallback((filter) => {
196
+ var _a;
197
+ (_a = filter.onSelect) === null || _a === void 0 ? void 0 : _a.call(filter, {
198
+ value: filter.value,
199
+ selected: Boolean(filter.selected),
200
+ allowMultiSelect,
201
+ });
77
202
  window.setTimeout(() => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, 0);
78
- }, []);
203
+ }, [allowMultiSelect]);
204
+ const clearSearch = React.useCallback(() => {
205
+ onValueChange("");
206
+ window.setTimeout(() => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, 0);
207
+ }, [onValueChange]);
79
208
  return (_jsxs("div", { className: className, ...props, children: [_jsxs(Button, { type: "button", onClick: () => setIsOpen(true), variant: "ghost", size: isCollapsed ? "icon" : "default", className: cn(triggerVariant === "sidebar" && isCollapsed
80
- ? "h-control min-h-control w-control min-w-control max-w-control shrink-0 rounded-control border-none bg-transparent p-0 !font-normal text-muted-foreground hover:bg-card hover:text-foreground focus-visible:bg-card focus-visible:ring-ring/30"
209
+ ? "mx-auto flex h-control min-h-control w-control min-w-control max-w-control shrink-0 gap-0 rounded-control border-none bg-transparent p-[var(--space-0)] !font-normal text-muted-foreground hover:bg-card hover:text-foreground focus-visible:bg-card focus-visible:ring-ring/30"
81
210
  : triggerVariant === "sidebar"
82
- ? "h-control w-full min-w-[14rem] justify-start gap-2 rounded-control border border-[hsl(var(--layout-sidebar-border))] bg-card px-2.5 !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"
211
+ ? "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"
83
212
  : isCollapsed
84
- ? "h-control min-h-control w-control min-w-control max-w-control shrink-0 rounded-control p-0 !font-normal text-muted-foreground hover:bg-card hover:text-foreground"
85
- : "h-control w-full min-w-[14rem] justify-start gap-2 rounded-control border border-border bg-card px-2.5 !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-[20px] items-center gap-1 rounded border border-border bg-muted px-1.5 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-2rem)] max-w-[52rem] gap-0 overflow-hidden p-0 sm:w-[min(64vw,52rem)]", 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, className: "text-sm" }), allFilter || filters.length ? (_jsxs("div", { className: "flex flex-wrap items-center gap-1 border-b px-2.5 py-1.5", children: [allFilter ? (_jsx(FilterToggle, { selected: allFilter.selected, onMouseDown: preserveInputFocus, onClick: () => selectFilter(allFilter.onSelect), children: allFilter.label })) : null, filters.map((filter) => (_jsx(FilterToggle, { selected: filter.selected, onMouseDown: preserveInputFocus, onClick: () => selectFilter(filter.onSelect), children: filter.label }, filter.value)))] })) : null, _jsx(CommandList, { "aria-busy": loading || undefined, className: cn("max-h-[400px] p-1", resultsClassName), children: loading ? (_jsx(CommandSearchSkeleton, { rows: loadingRows })) : children !== undefined ? children : sections ? (visibleSections.length ? (visibleSections.map((section, index) => (_jsxs(React.Fragment, { children: [index > 0 ? _jsx(CommandSeparator, { alwaysRender: true }) : null, _jsx(CommandGroup, { heading: section.heading, children: section.items.map((item) => (_jsx(CommandItem, { value: getCommandSearchItemValue(item), disabled: item.disabled, onSelect: () => {
86
- var _a;
87
- (_a = item.onSelect) === null || _a === void 0 ? void 0 : _a.call(item);
88
- if (item.closeOnSelect !== false) {
89
- setIsOpen(false);
90
- }
91
- }, children: _jsxs("div", { className: "min-w-0", children: [_jsx(Typography, { as: "p", variant: "label", className: "truncate", children: _jsx(SearchHighlight, { text: item.title, query: value }) }), item.subtitle ? (_jsx(Typography, { as: "p", variant: "caption", className: "truncate", children: _jsx(SearchHighlight, { text: item.subtitle, query: value }) })) : null] }) }, item.id))) })] }, section.key)))) : (_jsx(EmptyState, { icon: CommandSearchEmptyIcon, message: normalizedValue ? "No results found" : "Start typing to search", description: normalizedValue
92
- ? `No results found for "${value}".`
93
- : "Results will appear here as you type." }))) : (_jsx(EmptyState, { icon: CommandSearchEmptyIcon, message: normalizedValue ? "No results found" : "Start typing to search", description: normalizedValue
94
- ? `No results found for "${value}".`
95
- : "Results will appear here as you type." })) }), shouldShowAllResults ? (_jsx(PopoverMenuFooter, { inset: "flush", children: _jsx(PopoverMenuFooterAction, { onClick: () => {
213
+ ? "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
+ minHeight: resultsMinHeight,
216
+ maxHeight: resultsMaxHeight,
217
+ }, children: loading ? (_jsx(CommandSearchSkeleton, { rows: loadingRows })) : children !== undefined ? children : sections ? (visibleSections.length ? (visibleSections.map((section, index) => {
218
+ var _a, _b;
219
+ return (_jsxs(React.Fragment, { children: [index > 0 ? (_jsx(CommandSeparator, { alwaysRender: true, className: "-mx-[var(--space-1)]" })) : null, _jsxs(CommandGroup, { heading: section.heading, children: [section.items.map((item) => (_jsx(CommandItem, { value: getCommandSearchItemValue(item), disabled: item.disabled, tabIndex: item.disabled ? -1 : 0, className: "group min-h-[var(--space-8)] w-full items-start gap-[var(--space-2)] px-[var(--space-2)] py-[var(--space-3)] text-left font-normal whitespace-normal focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring", onSelect: () => {
220
+ var _a;
221
+ (_a = item.onSelect) === null || _a === void 0 ? void 0 : _a.call(item);
222
+ if (item.closeOnSelect !== false) {
223
+ setIsOpen(false);
224
+ }
225
+ }, onKeyDown: (event) => handleCommandSearchItemKeyDown(event, () => {
226
+ var _a;
227
+ (_a = item.onSelect) === null || _a === void 0 ? void 0 : _a.call(item);
228
+ if (item.closeOnSelect !== false) {
229
+ setIsOpen(false);
230
+ }
231
+ }), children: _jsx(CommandSearchResultItem, { item: item, query: value }) }, item.id))), section.action ? (_jsx(CommandItem, { value: getCommandSearchSectionActionValue(section), tabIndex: 0, className: "group min-h-[var(--space-8)] w-full items-center px-[var(--space-0)] py-[var(--space-0)] text-left font-normal whitespace-normal text-muted-foreground focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring data-[selected=true]:bg-transparent data-[selected=true]:text-muted-foreground", onSelect: () => {
232
+ var _a, _b, _c;
233
+ (_b = (_a = section.action) === null || _a === void 0 ? void 0 : _a.onSelect) === null || _b === void 0 ? void 0 : _b.call(_a);
234
+ if (((_c = section.action) === null || _c === void 0 ? void 0 : _c.closeOnSelect) !== false) {
235
+ setIsOpen(false);
236
+ }
237
+ }, onKeyDown: (event) => handleCommandSearchItemKeyDown(event, () => {
238
+ var _a, _b, _c;
239
+ (_b = (_a = section.action) === null || _a === void 0 ? void 0 : _a.onSelect) === null || _b === void 0 ? void 0 : _b.call(_a);
240
+ if (((_c = section.action) === null || _c === void 0 ? void 0 : _c.closeOnSelect) !== false) {
241
+ setIsOpen(false);
242
+ }
243
+ }), children: _jsx(Button, { asChild: true, variant: "link", size: "compact", className: "w-full justify-start text-left font-medium group-data-[selected=true]:underline", children: _jsxs("span", { children: [_jsx("span", { className: "min-w-0 truncate", children: section.action.label }), _jsxs("span", { className: "inline-flex shrink-0 items-center justify-center text-primary", children: [(_a = section.action.affordance) !== null && _a !== void 0 ? _a : (_jsx(ArrowRight, { className: "size-[var(--space-4)]", "aria-hidden": "true" })), _jsx("span", { className: "sr-only", children: (_b = section.action.affordanceLabel) !== null && _b !== void 0 ? _b : "Show all results in group" })] })] }) }) }, `${section.key}-action`)) : null] })] }, section.key));
244
+ })) : (_jsx(CommandSearchEmptyState, { query: value, resultsMinHeight: resultsMinHeight }))) : (_jsx(CommandSearchEmptyState, { query: value, resultsMinHeight: resultsMinHeight })) }), shouldShowAllResults ? (_jsx(PopoverMenuFooter, { inset: "flush", children: _jsxs(PopoverMenuFooterAction, { className: "group", onClick: () => {
96
245
  onShowAllResults === null || onShowAllResults === void 0 ? void 0 : onShowAllResults(normalizedValue);
97
246
  setIsOpen(false);
98
- }, children: _jsx("span", { className: "truncate", children: resolvedShowAllResultsLabel }) }) })) : null] })] }) })] }));
247
+ }, children: [_jsx("span", { className: "min-w-0 truncate", children: resolvedShowAllResultsLabel }), _jsx(ArrowRight, { className: "size-[var(--space-4)] shrink-0 transition-transform duration-150 group-hover:translate-x-[var(--space-1)] group-focus-visible:translate-x-[var(--space-1)]", "aria-hidden": "true" })] }) })) : null] })] }) })] }));
99
248
  }
100
249
  export { CommandSearch };
package/dist/command.d.ts CHANGED
@@ -21,6 +21,8 @@ interface CommandInputProps extends React.ComponentPropsWithoutRef<typeof Comman
21
21
  wrapperClassName?: string;
22
22
  iconClassName?: string;
23
23
  iconSize?: number;
24
+ endAdornment?: React.ReactNode;
25
+ endAdornmentClassName?: string;
24
26
  }
25
27
  declare const CommandInput: React.ForwardRefExoticComponent<CommandInputProps & React.RefAttributes<HTMLInputElement>>;
26
28
  declare const CommandList: React.ForwardRefExoticComponent<Omit<{
package/dist/command.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { Command as CommandPrimitive } from "cmdk";
5
5
  import { inputControlClassName } from "./input-control";
@@ -7,7 +7,7 @@ import { cn } from "./lib/utils";
7
7
  import { SearchInputShell } from "./search-input-shell";
8
8
  const Command = React.forwardRef(({ className, ...props }, ref) => (_jsx(CommandPrimitive, { ref: ref, className: cn("flex h-full w-full flex-col overflow-hidden rounded-overlay bg-popover text-popover-foreground", className), ...props })));
9
9
  Command.displayName = CommandPrimitive.displayName;
10
- const CommandInput = React.forwardRef(({ className, wrapperClassName, iconClassName, iconSize = 16, ...props }, ref) => (_jsx("div", { "cmdk-input-wrapper": "", className: cn("border-b p-4", wrapperClassName), children: _jsx(SearchInputShell, { iconClassName: iconClassName, iconSize: iconSize, children: _jsx(CommandPrimitive.Input, { ref: ref, className: cn(inputControlClassName, "bg-background pl-9 pr-3", className), ...props }) }) })));
10
+ const CommandInput = React.forwardRef(({ className, wrapperClassName, iconClassName, iconSize = 16, endAdornment, endAdornmentClassName, ...props }, ref) => (_jsx("div", { "cmdk-input-wrapper": "", className: cn("border-b p-4", wrapperClassName), children: _jsxs(SearchInputShell, { iconClassName: iconClassName, iconSize: iconSize, children: [_jsx(CommandPrimitive.Input, { ref: ref, className: cn(inputControlClassName, "bg-background pl-9 pr-3", className), ...props }), endAdornment ? (_jsx("div", { "data-slot": "command-input-end-adornment", className: cn("absolute right-[var(--space-3)] top-1/2 flex -translate-y-1/2 items-center gap-[var(--space-1)]", endAdornmentClassName), children: endAdornment })) : null] }) })));
11
11
  CommandInput.displayName = CommandPrimitive.Input.displayName;
12
12
  const CommandList = React.forwardRef(({ className, ...props }, ref) => (_jsx(CommandPrimitive.List, { ref: ref, className: cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className), ...props })));
13
13
  CommandList.displayName = CommandPrimitive.List.displayName;