@hilum/ui 0.1.1

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/icons.d.ts +1 -0
  4. package/dist/icons.js +3 -0
  5. package/dist/icons.js.map +1 -0
  6. package/dist/index.d.ts +992 -0
  7. package/dist/index.js +4433 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/tokens.css +265 -0
  10. package/dist/tokens.d.ts +189 -0
  11. package/dist/tokens.js +193 -0
  12. package/dist/tokens.js.map +1 -0
  13. package/package.json +80 -0
  14. package/src/components/__tests__/data.test.tsx +180 -0
  15. package/src/components/__tests__/disclosure.test.tsx +198 -0
  16. package/src/components/__tests__/display.test.tsx +262 -0
  17. package/src/components/__tests__/feedback.test.tsx +138 -0
  18. package/src/components/__tests__/form.test.tsx +297 -0
  19. package/src/components/__tests__/layout.test.tsx +388 -0
  20. package/src/components/__tests__/overlay.test.tsx +90 -0
  21. package/src/components/__tests__/primitives.test.tsx +248 -0
  22. package/src/components/accordion.tsx +68 -0
  23. package/src/components/action-panel.tsx +112 -0
  24. package/src/components/activity-feed.tsx +69 -0
  25. package/src/components/alert-dialog.tsx +132 -0
  26. package/src/components/alert.tsx +44 -0
  27. package/src/components/aspect-ratio.tsx +19 -0
  28. package/src/components/avatar-stack.tsx +70 -0
  29. package/src/components/avatar.tsx +103 -0
  30. package/src/components/badge.tsx +33 -0
  31. package/src/components/breadcrumb.tsx +76 -0
  32. package/src/components/button-group.tsx +45 -0
  33. package/src/components/button.tsx +60 -0
  34. package/src/components/calendar.tsx +61 -0
  35. package/src/components/card-heading.tsx +60 -0
  36. package/src/components/card.tsx +120 -0
  37. package/src/components/carousel.tsx +222 -0
  38. package/src/components/chart.tsx +77 -0
  39. package/src/components/checkbox.tsx +35 -0
  40. package/src/components/collapsible.tsx +9 -0
  41. package/src/components/color-input.tsx +90 -0
  42. package/src/components/color-picker.tsx +111 -0
  43. package/src/components/combobox.tsx +207 -0
  44. package/src/components/command-palette.tsx +137 -0
  45. package/src/components/command.tsx +248 -0
  46. package/src/components/context-menu.tsx +201 -0
  47. package/src/components/data-table.tsx +151 -0
  48. package/src/components/date-picker.tsx +56 -0
  49. package/src/components/description-list.tsx +70 -0
  50. package/src/components/dialog.tsx +97 -0
  51. package/src/components/drawer.tsx +90 -0
  52. package/src/components/dropdown-menu.tsx +202 -0
  53. package/src/components/empty-state.tsx +43 -0
  54. package/src/components/field.tsx +45 -0
  55. package/src/components/grid-list.tsx +159 -0
  56. package/src/components/hover-card.tsx +34 -0
  57. package/src/components/input-group.tsx +134 -0
  58. package/src/components/input-number.tsx +142 -0
  59. package/src/components/input-otp.tsx +68 -0
  60. package/src/components/input.tsx +23 -0
  61. package/src/components/kbd.tsx +18 -0
  62. package/src/components/label.tsx +23 -0
  63. package/src/components/media-object.tsx +49 -0
  64. package/src/components/menubar.tsx +235 -0
  65. package/src/components/native-select.tsx +44 -0
  66. package/src/components/navigation-menu.tsx +158 -0
  67. package/src/components/notification.tsx +109 -0
  68. package/src/components/page-heading.tsx +125 -0
  69. package/src/components/pagination.tsx +89 -0
  70. package/src/components/popover.tsx +35 -0
  71. package/src/components/progress.tsx +23 -0
  72. package/src/components/property-row.tsx +56 -0
  73. package/src/components/radio-card.tsx +105 -0
  74. package/src/components/radio-group.tsx +38 -0
  75. package/src/components/resizable.tsx +74 -0
  76. package/src/components/scroll-area.tsx +42 -0
  77. package/src/components/section-heading.tsx +64 -0
  78. package/src/components/select.tsx +130 -0
  79. package/src/components/separator.tsx +25 -0
  80. package/src/components/sheet.tsx +117 -0
  81. package/src/components/sidebar.tsx +636 -0
  82. package/src/components/skeleton.tsx +14 -0
  83. package/src/components/slider.tsx +24 -0
  84. package/src/components/sonner.tsx +34 -0
  85. package/src/components/spinner.tsx +37 -0
  86. package/src/components/stacked-list.tsx +64 -0
  87. package/src/components/stat-card.tsx +54 -0
  88. package/src/components/steps.tsx +179 -0
  89. package/src/components/switch.tsx +33 -0
  90. package/src/components/table.tsx +101 -0
  91. package/src/components/tabs.tsx +55 -0
  92. package/src/components/textarea.tsx +21 -0
  93. package/src/components/toggle-group.tsx +49 -0
  94. package/src/components/toggle.tsx +66 -0
  95. package/src/components/tooltip.tsx +43 -0
  96. package/src/icons.ts +158 -0
  97. package/src/index.ts +84 -0
  98. package/src/lib/utils.ts +6 -0
  99. package/src/tokens/tokens.ts +194 -0
@@ -0,0 +1,207 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Check, ChevronsUpDown } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ export interface ComboboxOption {
8
+ value: string;
9
+ label: string;
10
+ description?: string;
11
+ statusColor?: string;
12
+ avatar?: string;
13
+ }
14
+
15
+ interface ComboboxProps {
16
+ options: ComboboxOption[];
17
+ value?: string;
18
+ onValueChange?: (value: string) => void;
19
+ placeholder?: string;
20
+ searchPlaceholder?: string;
21
+ emptyText?: string;
22
+ className?: string;
23
+ }
24
+
25
+ function Combobox({
26
+ options,
27
+ value,
28
+ onValueChange,
29
+ placeholder = "Select...",
30
+ searchPlaceholder = "Search...",
31
+ emptyText = "No results found.",
32
+ className,
33
+ }: ComboboxProps) {
34
+ const [open, setOpen] = React.useState(false);
35
+ const [query, setQuery] = React.useState("");
36
+ const containerRef = React.useRef<HTMLDivElement>(null);
37
+ const inputRef = React.useRef<HTMLInputElement>(null);
38
+
39
+ const selectedOption = options.find((o) => o.value === value);
40
+
41
+ const filtered =
42
+ query === ""
43
+ ? options
44
+ : options.filter(
45
+ (o) =>
46
+ o.label.toLowerCase().includes(query.toLowerCase()) ||
47
+ o.description?.toLowerCase().includes(query.toLowerCase()),
48
+ );
49
+
50
+ // Close on outside click
51
+ React.useEffect(() => {
52
+ function onMouseDown(e: MouseEvent) {
53
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
54
+ setOpen(false);
55
+ setQuery("");
56
+ }
57
+ }
58
+ document.addEventListener("mousedown", onMouseDown);
59
+ return () => document.removeEventListener("mousedown", onMouseDown);
60
+ }, []);
61
+
62
+ // Close on Escape
63
+ React.useEffect(() => {
64
+ if (!open) return;
65
+ function onKey(e: KeyboardEvent) {
66
+ if (e.key === "Escape") {
67
+ setOpen(false);
68
+ setQuery("");
69
+ }
70
+ }
71
+ document.addEventListener("keydown", onKey);
72
+ return () => document.removeEventListener("keydown", onKey);
73
+ }, [open]);
74
+
75
+ return (
76
+ <div ref={containerRef} className={cn("relative", className)}>
77
+ {/* Trigger */}
78
+ <div className="relative flex items-center">
79
+ {selectedOption?.avatar && !open && (
80
+ <div className="pointer-events-none absolute left-2.5 flex size-5 items-center justify-center rounded-full bg-taupe-200 caption-xs font-semibold text-taupe-700 shrink-0">
81
+ {selectedOption.avatar}
82
+ </div>
83
+ )}
84
+ {selectedOption?.statusColor && !open && (
85
+ <div
86
+ className="pointer-events-none absolute left-3 size-2 rounded-full shrink-0"
87
+ style={{ backgroundColor: selectedOption.statusColor }}
88
+ />
89
+ )}
90
+ <input
91
+ ref={inputRef}
92
+ type="text"
93
+ role="combobox"
94
+ aria-expanded={open}
95
+ className={cn(
96
+ "flex h-9 w-full rounded-lg border border-taupe-200 bg-white pr-8 body text-taupe-900",
97
+ "placeholder:text-taupe-400",
98
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-taupe-400/30 focus-visible:border-taupe-400",
99
+ "disabled:cursor-not-allowed disabled:opacity-50",
100
+ selectedOption?.avatar && !open
101
+ ? "pl-8"
102
+ : selectedOption?.statusColor && !open
103
+ ? "pl-7"
104
+ : "pl-3",
105
+ )}
106
+ placeholder={open ? searchPlaceholder : (selectedOption?.label ?? placeholder)}
107
+ value={open ? query : (selectedOption?.label ?? "")}
108
+ onChange={(e) => {
109
+ setQuery(e.target.value);
110
+ if (!open) setOpen(true);
111
+ }}
112
+ onFocus={() => {
113
+ setOpen(true);
114
+ setQuery("");
115
+ }}
116
+ />
117
+ <button
118
+ type="button"
119
+ tabIndex={-1}
120
+ className="absolute inset-y-0 right-0 flex items-center px-2 text-taupe-400 hover:text-taupe-700 transition-colors"
121
+ onClick={() => {
122
+ if (open) {
123
+ setOpen(false);
124
+ setQuery("");
125
+ } else {
126
+ setOpen(true);
127
+ inputRef.current?.focus();
128
+ }
129
+ }}
130
+ >
131
+ <ChevronsUpDown size={14} />
132
+ </button>
133
+ </div>
134
+
135
+ {/* Dropdown */}
136
+ {open && (
137
+ <div className="absolute z-50 mt-1 w-full overflow-hidden rounded-lg border border-taupe-200 bg-white shadow-elevated">
138
+ <ul role="listbox" className="max-h-60 overflow-auto py-1">
139
+ {filtered.length === 0 ? (
140
+ <li className="px-3 py-2 body text-taupe-400">{emptyText}</li>
141
+ ) : (
142
+ filtered.map((option) => {
143
+ const isSelected = option.value === value;
144
+ return (
145
+ <li
146
+ key={option.value}
147
+ role="option"
148
+ aria-selected={isSelected}
149
+ className={cn(
150
+ "flex cursor-pointer select-none items-center gap-2.5 px-3 py-2 body transition-colors",
151
+ isSelected
152
+ ? "bg-brand-primary text-white"
153
+ : "text-taupe-900 hover:bg-taupe-50",
154
+ )}
155
+ onMouseDown={(e) => e.preventDefault()}
156
+ onClick={() => {
157
+ onValueChange?.(option.value);
158
+ setQuery("");
159
+ setOpen(false);
160
+ }}
161
+ >
162
+ {/* Avatar */}
163
+ {option.avatar && (
164
+ <div
165
+ className={cn(
166
+ "flex size-6 shrink-0 items-center justify-center rounded-full caption-xs font-semibold",
167
+ isSelected ? "bg-white/20 text-white" : "bg-taupe-200 text-taupe-700",
168
+ )}
169
+ >
170
+ {option.avatar}
171
+ </div>
172
+ )}
173
+ {/* Status dot */}
174
+ {option.statusColor && !option.avatar && (
175
+ <div
176
+ className="size-2 shrink-0 rounded-full"
177
+ style={{ backgroundColor: option.statusColor }}
178
+ />
179
+ )}
180
+ <div className="min-w-0 flex-1">
181
+ <p className={cn("truncate", isSelected && "font-semibold")}>
182
+ {option.label}
183
+ </p>
184
+ {option.description && (
185
+ <p
186
+ className={cn(
187
+ "caption truncate",
188
+ isSelected ? "text-white/70" : "text-taupe-400",
189
+ )}
190
+ >
191
+ {option.description}
192
+ </p>
193
+ )}
194
+ </div>
195
+ {isSelected && <Check size={14} className="shrink-0" />}
196
+ </li>
197
+ );
198
+ })
199
+ )}
200
+ </ul>
201
+ </div>
202
+ )}
203
+ </div>
204
+ );
205
+ }
206
+
207
+ export { Combobox };
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Search, ArrowRight } from "lucide-react";
5
+ import { Dialog, DialogContent } from "./dialog";
6
+
7
+ export interface CommandPaletteItem {
8
+ id?: string | number;
9
+ label: string;
10
+ description?: string;
11
+ icon?: React.ReactNode;
12
+ category?: string;
13
+ href?: string;
14
+ onSelect?: () => void;
15
+ }
16
+
17
+ interface CommandPaletteProps {
18
+ open: boolean;
19
+ onClose: () => void;
20
+ items: CommandPaletteItem[];
21
+ placeholder?: string;
22
+ emptyText?: string;
23
+ }
24
+
25
+ function CommandPalette({
26
+ open,
27
+ onClose,
28
+ items,
29
+ placeholder = "Search...",
30
+ emptyText = "No results found.",
31
+ }: CommandPaletteProps) {
32
+ const [query, setQuery] = React.useState("");
33
+ const inputRef = React.useRef<HTMLInputElement>(null);
34
+
35
+ // Focus input when opened
36
+ React.useEffect(() => {
37
+ if (open) {
38
+ setTimeout(() => inputRef.current?.focus(), 50);
39
+ setQuery("");
40
+ }
41
+ }, [open]);
42
+
43
+ const filtered =
44
+ query === ""
45
+ ? items
46
+ : items.filter(
47
+ (item) =>
48
+ item.label.toLowerCase().includes(query.toLowerCase()) ||
49
+ item.description?.toLowerCase().includes(query.toLowerCase()) ||
50
+ item.category?.toLowerCase().includes(query.toLowerCase()),
51
+ );
52
+
53
+ // Group by category
54
+ const grouped = filtered.reduce<Record<string, CommandPaletteItem[]>>((acc, item) => {
55
+ const cat = item.category ?? "";
56
+ if (!acc[cat]) acc[cat] = [];
57
+ acc[cat].push(item);
58
+ return acc;
59
+ }, {});
60
+
61
+ const categories = Object.keys(grouped);
62
+
63
+ function handleSelect(item: CommandPaletteItem) {
64
+ if (item.href) {
65
+ window.location.href = item.href;
66
+ } else {
67
+ item.onSelect?.();
68
+ }
69
+ onClose();
70
+ }
71
+
72
+ return (
73
+ <Dialog open={open} onOpenChange={(v) => !v && onClose()}>
74
+ <DialogContent className="gap-0 overflow-hidden p-0 sm:max-w-xl">
75
+ {/* Search input */}
76
+ <div className="flex items-center gap-3 border-b border-taupe-100 px-4">
77
+ <Search size={16} className="shrink-0 text-taupe-400" />
78
+ <input
79
+ ref={inputRef}
80
+ type="text"
81
+ className="h-12 flex-1 bg-transparent body text-taupe-900 placeholder:text-taupe-400 focus:outline-none"
82
+ placeholder={placeholder}
83
+ value={query}
84
+ onChange={(e) => setQuery(e.target.value)}
85
+ onKeyDown={(e) => {
86
+ if (e.key === "Escape") onClose();
87
+ if (e.key === "Enter" && filtered.length > 0) handleSelect(filtered[0]);
88
+ }}
89
+ />
90
+ <kbd className="hidden items-center gap-0.5 sm:flex">
91
+ <span className="caption-xs font-medium text-taupe-400 rounded border border-taupe-200 px-1.5 py-0.5">
92
+ esc
93
+ </span>
94
+ </kbd>
95
+ </div>
96
+
97
+ {/* Results */}
98
+ <div className="max-h-80 overflow-y-auto">
99
+ {filtered.length === 0 ? (
100
+ <p className="px-4 py-8 text-center body text-taupe-400">{emptyText}</p>
101
+ ) : (
102
+ <ul role="list" className="py-2">
103
+ {categories.map((cat) => (
104
+ <li key={cat}>
105
+ {cat && <p className="label px-4 py-2 text-taupe-400">{cat}</p>}
106
+ {grouped[cat].map((item, i) => (
107
+ <button
108
+ key={item.id ?? i}
109
+ type="button"
110
+ className="flex w-full items-center gap-3 px-4 py-2.5 body text-left transition-colors hover:bg-taupe-50 focus-visible:bg-taupe-50 focus-visible:outline-none"
111
+ onClick={() => handleSelect(item)}
112
+ >
113
+ {item.icon && (
114
+ <span className="flex size-8 shrink-0 items-center justify-center rounded-lg border border-taupe-200 bg-white text-taupe-500">
115
+ {item.icon}
116
+ </span>
117
+ )}
118
+ <div className="min-w-0 flex-1">
119
+ <p className="font-medium text-taupe-900 truncate">{item.label}</p>
120
+ {item.description && (
121
+ <p className="caption text-taupe-400 truncate">{item.description}</p>
122
+ )}
123
+ </div>
124
+ <ArrowRight size={13} className="shrink-0 text-taupe-300" />
125
+ </button>
126
+ ))}
127
+ </li>
128
+ ))}
129
+ </ul>
130
+ )}
131
+ </div>
132
+ </DialogContent>
133
+ </Dialog>
134
+ );
135
+ }
136
+
137
+ export { CommandPalette };
@@ -0,0 +1,248 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Search } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+
7
+ interface CommandContextValue {
8
+ query: string;
9
+ setQuery: React.Dispatch<React.SetStateAction<string>>;
10
+ onSelect?: (value: string) => void;
11
+ reportVisible: (id: string, visible: boolean) => void;
12
+ }
13
+
14
+ const CommandCtx = React.createContext<CommandContextValue>({
15
+ query: "",
16
+ setQuery: () => {},
17
+ reportVisible: () => {},
18
+ });
19
+
20
+ const CommandVisibleCtx = React.createContext(0);
21
+
22
+ interface CommandProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
23
+ onSelect?: (value: string) => void;
24
+ }
25
+
26
+ function Command({ className, children, onSelect, ...props }: CommandProps) {
27
+ const [query, setQuery] = React.useState("");
28
+ const visibleIds = React.useRef(new Set<string>());
29
+ const [visibleCount, setVisibleCount] = React.useState(0);
30
+
31
+ const reportVisible = React.useCallback((id: string, visible: boolean) => {
32
+ const had = visibleIds.current.has(id);
33
+ if (visible && !had) {
34
+ visibleIds.current.add(id);
35
+ setVisibleCount(visibleIds.current.size);
36
+ } else if (!visible && had) {
37
+ visibleIds.current.delete(id);
38
+ setVisibleCount(visibleIds.current.size);
39
+ }
40
+ }, []);
41
+
42
+ return (
43
+ <CommandCtx.Provider value={{ query, setQuery, onSelect, reportVisible }}>
44
+ <CommandVisibleCtx.Provider value={visibleCount}>
45
+ <div
46
+ className={cn(
47
+ "flex flex-col overflow-hidden rounded-xl border border-taupe-200 bg-white",
48
+ className,
49
+ )}
50
+ {...props}
51
+ >
52
+ {children}
53
+ </div>
54
+ </CommandVisibleCtx.Provider>
55
+ </CommandCtx.Provider>
56
+ );
57
+ }
58
+
59
+ const CommandInput = React.forwardRef<
60
+ HTMLInputElement,
61
+ Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "onChange">
62
+ >(({ className, ...props }, ref) => {
63
+ const { query, setQuery } = React.useContext(CommandCtx);
64
+ return (
65
+ <div className="flex items-center gap-2 border-b border-taupe-100 px-3">
66
+ <Search size={14} className="shrink-0 text-taupe-400" />
67
+ <input
68
+ ref={ref}
69
+ type="text"
70
+ autoComplete="off"
71
+ autoCorrect="off"
72
+ spellCheck={false}
73
+ className={cn(
74
+ "flex h-10 w-full bg-transparent body text-taupe-900",
75
+ "placeholder:text-taupe-400 focus:outline-none",
76
+ className,
77
+ )}
78
+ value={query}
79
+ onChange={(e) => setQuery(e.target.value)}
80
+ {...props}
81
+ />
82
+ </div>
83
+ );
84
+ });
85
+ CommandInput.displayName = "CommandInput";
86
+
87
+ const CommandList = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
88
+ ({ className, ...props }, ref) => {
89
+ const listRef = React.useRef<HTMLDivElement | null>(null);
90
+
91
+ const combinedRef = React.useCallback(
92
+ (el: HTMLDivElement | null) => {
93
+ listRef.current = el;
94
+ if (typeof ref === "function") ref(el);
95
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = el;
96
+ },
97
+ [ref],
98
+ );
99
+
100
+ const handleKeyDown = (e: React.KeyboardEvent) => {
101
+ if (!["ArrowDown", "ArrowUp"].includes(e.key)) return;
102
+ e.preventDefault();
103
+ const el = listRef.current;
104
+ if (!el) return;
105
+ const items = Array.from(
106
+ el.querySelectorAll<HTMLElement>("[data-cmd-item]:not([aria-disabled='true'])"),
107
+ );
108
+ if (!items.length) return;
109
+ const focused = el.querySelector<HTMLElement>("[data-cmd-item]:focus");
110
+ const idx = focused ? items.indexOf(focused) : -1;
111
+ const next =
112
+ e.key === "ArrowDown"
113
+ ? items[Math.min(idx + 1, items.length - 1)]
114
+ : items[Math.max(idx - 1, 0)];
115
+ next?.focus();
116
+ };
117
+
118
+ return (
119
+ <div
120
+ ref={combinedRef}
121
+ role="listbox"
122
+ onKeyDown={handleKeyDown}
123
+ className={cn("max-h-[300px] overflow-y-auto py-1", className)}
124
+ {...props}
125
+ />
126
+ );
127
+ },
128
+ );
129
+ CommandList.displayName = "CommandList";
130
+
131
+ const CommandEmpty = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
132
+ ({ className, children = "No results found.", ...props }, ref) => {
133
+ const visibleCount = React.useContext(CommandVisibleCtx);
134
+ if (visibleCount > 0) return null;
135
+ return (
136
+ <div
137
+ ref={ref}
138
+ className={cn("py-6 text-center caption text-taupe-400", className)}
139
+ {...props}
140
+ >
141
+ {children}
142
+ </div>
143
+ );
144
+ },
145
+ );
146
+ CommandEmpty.displayName = "CommandEmpty";
147
+
148
+ interface CommandGroupProps extends React.HTMLAttributes<HTMLDivElement> {
149
+ heading?: string;
150
+ }
151
+
152
+ const CommandGroup = React.forwardRef<HTMLDivElement, CommandGroupProps>(
153
+ ({ className, heading, children, ...props }, ref) => (
154
+ <div ref={ref} role="group" className={cn("py-1", className)} {...props}>
155
+ {heading && <p className="px-3 pb-1 pt-0.5 label text-taupe-400">{heading}</p>}
156
+ {children}
157
+ </div>
158
+ ),
159
+ );
160
+ CommandGroup.displayName = "CommandGroup";
161
+
162
+ interface CommandItemProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
163
+ value?: string;
164
+ keywords?: string[];
165
+ disabled?: boolean;
166
+ onSelect?: (value: string) => void;
167
+ }
168
+
169
+ const CommandItem = React.forwardRef<HTMLDivElement, CommandItemProps>(
170
+ ({ className, value = "", keywords, disabled, onSelect, children, ...props }, ref) => {
171
+ const { query, onSelect: ctxOnSelect, reportVisible } = React.useContext(CommandCtx);
172
+ const id = React.useId();
173
+
174
+ const searchText = (keywords ?? [value]).join(" ").toLowerCase();
175
+ const isVisible = !query || searchText.includes(query.toLowerCase());
176
+
177
+ React.useLayoutEffect(() => {
178
+ reportVisible(id, isVisible && !disabled);
179
+ return () => reportVisible(id, false);
180
+ }, [id, isVisible, disabled, reportVisible]);
181
+
182
+ if (!isVisible) return null;
183
+
184
+ const handleSelect = () => {
185
+ if (disabled) return;
186
+ onSelect?.(value);
187
+ ctxOnSelect?.(value);
188
+ };
189
+
190
+ return (
191
+ <div
192
+ ref={ref}
193
+ role="option"
194
+ aria-selected={false}
195
+ aria-disabled={disabled || undefined}
196
+ data-cmd-item
197
+ tabIndex={disabled ? undefined : 0}
198
+ className={cn(
199
+ "relative flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-1.5 mx-1",
200
+ "body text-taupe-700 outline-none transition-colors",
201
+ "hover:bg-taupe-50 hover:text-taupe-900",
202
+ "focus:bg-taupe-50 focus:text-taupe-900",
203
+ disabled && "pointer-events-none opacity-40",
204
+ className,
205
+ )}
206
+ onClick={handleSelect}
207
+ onKeyDown={(e) => {
208
+ if (e.key === "Enter") {
209
+ e.preventDefault();
210
+ handleSelect();
211
+ }
212
+ }}
213
+ {...props}
214
+ >
215
+ {children}
216
+ </div>
217
+ );
218
+ },
219
+ );
220
+ CommandItem.displayName = "CommandItem";
221
+
222
+ const CommandSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
223
+ ({ className, ...props }, ref) => (
224
+ <div ref={ref} className={cn("my-1 h-px bg-taupe-100", className)} {...props} />
225
+ ),
226
+ );
227
+ CommandSeparator.displayName = "CommandSeparator";
228
+
229
+ function CommandShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
230
+ return (
231
+ <span
232
+ className={cn("ml-auto caption text-taupe-300 tracking-widest font-medium", className)}
233
+ {...props}
234
+ />
235
+ );
236
+ }
237
+ CommandShortcut.displayName = "CommandShortcut";
238
+
239
+ export {
240
+ Command,
241
+ CommandInput,
242
+ CommandList,
243
+ CommandEmpty,
244
+ CommandGroup,
245
+ CommandItem,
246
+ CommandSeparator,
247
+ CommandShortcut,
248
+ };