@harismawan/stamp-ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,8 +55,9 @@ Light/dark mode is managed by the built-in `useThemeStore` (persisted). Omit
55
55
 
56
56
  ## Components
57
57
 
58
- - **Form:** `Button`, `Input`/`Select`/`Textarea`/`FieldWrap`/`FieldLabel`/`FieldError`, `NumberInput`, `Checkbox`, `Radio`/`RadioGroup`, `Switch`, `Slider`, `ColorPicker`, `IconPicker`
59
- - **Display:** `Card`, `Badge`, `Tag`, `Avatar`/`AvatarGroup`, `Stat`, `EmptyState`, `Divider`, `Progress`, `Spinner`, `Skeleton`/`SkeletonText`/`SkeletonCircle`/`SkeletonGroup`, `Table` primitives
58
+ - **Form:** `Button`, `Input`/`Select`/`Textarea`/`FieldWrap`/`FieldLabel`/`FieldError`, `NumberInput`, `Checkbox`, `Radio`/`RadioGroup`, `Switch`, `Slider`, `ColorPicker`, `IconPicker`, `Combobox`, `TagInput`, `FileUpload`
59
+ - **Display:** `Card`, `Badge`, `Tag`, `Avatar`/`AvatarGroup`, `Stat`, `EmptyState`, `Divider`, `Progress`, `Spinner`, `Skeleton`/`SkeletonText`/`SkeletonCircle`/`SkeletonGroup`, `Table` primitives, `DataTable`, `TreeView`
60
+ - **Pickers & palette:** `DatePicker`, `DateRangePicker`, `Command`
60
61
  - **Overlays:** `Modal`, `Drawer`, `ConfirmDialog` (`confirmDialog`/`ConfirmViewport`), `Toast` (`toast`/`ToastViewport`), `Tooltip`, `Popover`, `Menu`/`MenuButton`/`MenuList`/`MenuItem`, `Alert`
61
62
  - **Disclosure & nav:** `Tabs`, `Accordion`, `Breadcrumb`, `Pagination`, `Stepper`
62
63
  - **Layout:** `Box`, `Stack`/`HStack`/`VStack`, `Grid`, `Container`
@@ -26,14 +26,11 @@ export const GlobalStyles = createGlobalStyle `
26
26
  text-decoration: none;
27
27
  transition: color 120ms ${(p) => p.theme.easing.out};
28
28
  /*
29
- * Hover uses the body text color (theme-aware: ink on cream ~18:1,
30
- * cream-white on dark ~17:1), so links stay highly readable on interaction.
31
- * The previous primaryHover (bright yellow #FFCB05) dropped to ~1.5:1 on the
32
- * cream bg — illegible. An underline provides a non-color affordance too.
29
+ * Hover darkens to the body text color (theme-aware: ink on cream ~18:1,
30
+ * cream-white on dark ~17:1) so links stay highly readable. No underline.
33
31
  */
34
32
  &:hover {
35
33
  color: ${(p) => p.theme.colors.text};
36
- text-decoration: underline;
37
34
  }
38
35
  }
39
36
 
@@ -38,10 +38,8 @@ const variantMap = {
38
38
  ${stamp};
39
39
  background: ${(p) => p.theme.colors.primary};
40
40
  color: ${(p) => p.theme.colors.primaryInk};
41
- &:hover:not(:disabled):not([aria-disabled='true']) {
42
- background: ${(p) => p.theme.colors.primaryHover};
43
- color: ${(p) => p.theme.colors.primaryInk};
44
- }
41
+ /* Color/background stay constant on hover; the stamp press (translate +
42
+ * shadow) is the only hover affordance. */
45
43
  `,
46
44
  ghost: css `
47
45
  background: transparent;
@@ -4,5 +4,11 @@ export interface CheckboxProps extends Omit<React.ComponentPropsWithoutRef<'inpu
4
4
  onChange: (checked: boolean) => void;
5
5
  label?: string;
6
6
  disabled?: boolean;
7
+ /**
8
+ * Mixed state (e.g. a "select all" header where only some rows are selected).
9
+ * Sets the native `input.indeterminate` flag for assistive tech and renders a
10
+ * visible dash glyph. Only applies while `checked` is false.
11
+ */
12
+ indeterminate?: boolean;
7
13
  }
8
14
  export declare const Checkbox: React.ForwardRefExoticComponent<CheckboxProps & React.RefAttributes<HTMLInputElement>>;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import styled from 'styled-components';
4
- import { Check } from 'lucide-react';
4
+ import { Check, Minus } from 'lucide-react';
5
5
  const Root = styled.label `
6
6
  display: inline-flex;
7
7
  align-items: center;
@@ -33,7 +33,7 @@ const Box = styled.span `
33
33
  flex-shrink: 0;
34
34
  border: 2px solid ${(p) => p.theme.colors.border};
35
35
  border-radius: ${(p) => p.theme.radii.sm};
36
- background: ${(p) => (p.$checked ? p.theme.colors.primary : p.theme.colors.surface)};
36
+ background: ${(p) => p.$checked || p.$indeterminate ? p.theme.colors.primary : p.theme.colors.surface};
37
37
  box-shadow: ${(p) => p.theme.shadow.stampSm};
38
38
  color: ${(p) => p.theme.colors.primaryInk};
39
39
  transition: background 80ms ${(p) => p.theme.easing.out};
@@ -43,7 +43,15 @@ const Box = styled.span `
43
43
  outline-offset: 2px;
44
44
  }
45
45
  `;
46
- export const Checkbox = React.forwardRef(({ checked, onChange, label, disabled, ...rest }, ref) => {
47
- return (_jsxs(Root, { "$disabled": disabled, children: [_jsx(HiddenInput, { ref: ref, type: "checkbox", checked: checked, disabled: disabled, onChange: (e) => onChange(e.target.checked), ...rest }), _jsx(Box, { "$checked": checked, "$disabled": disabled, "aria-hidden": "true", children: checked ? _jsx(Check, { size: 16, strokeWidth: 3 }) : null }), label != null ? _jsx("span", { children: label }) : null] }));
46
+ export const Checkbox = React.forwardRef(({ checked, onChange, label, disabled, indeterminate = false, ...rest }, ref) => {
47
+ const innerRef = React.useRef(null);
48
+ React.useImperativeHandle(ref, () => innerRef.current, []);
49
+ // Mixed state only applies while unchecked; a checked box is never "mixed".
50
+ const showIndeterminate = indeterminate && !checked;
51
+ React.useEffect(() => {
52
+ if (innerRef.current)
53
+ innerRef.current.indeterminate = showIndeterminate;
54
+ }, [showIndeterminate]);
55
+ return (_jsxs(Root, { "$disabled": disabled, children: [_jsx(HiddenInput, { ref: innerRef, type: "checkbox", checked: checked, disabled: disabled, onChange: (e) => onChange(e.target.checked), ...rest }), _jsx(Box, { "$checked": checked, "$indeterminate": showIndeterminate, "$disabled": disabled, "aria-hidden": "true", children: checked ? (_jsx(Check, { size: 16, strokeWidth: 3 })) : showIndeterminate ? (_jsx(Minus, { size: 16, strokeWidth: 3 })) : null }), label != null ? _jsx("span", { children: label }) : null] }));
48
56
  });
49
57
  Checkbox.displayName = 'Checkbox';
@@ -0,0 +1,30 @@
1
+ export interface ComboboxOption {
2
+ value: string;
3
+ label: string;
4
+ disabled?: boolean;
5
+ }
6
+ interface ComboboxBaseProps {
7
+ options: ComboboxOption[];
8
+ placeholder?: string;
9
+ disabled?: boolean;
10
+ clearable?: boolean;
11
+ /** Default: case-insensitive substring match on `label`. */
12
+ filter?: (opt: ComboboxOption, query: string) => boolean;
13
+ emptyText?: string;
14
+ id?: string;
15
+ }
16
+ interface SingleComboboxProps extends ComboboxBaseProps {
17
+ multiple?: false;
18
+ value?: string | null;
19
+ defaultValue?: string | null;
20
+ onChange?: (value: string | null) => void;
21
+ }
22
+ interface MultiComboboxProps extends ComboboxBaseProps {
23
+ multiple: true;
24
+ value?: string[];
25
+ defaultValue?: string[];
26
+ onChange?: (value: string[]) => void;
27
+ }
28
+ export type ComboboxProps = SingleComboboxProps | MultiComboboxProps;
29
+ export declare function Combobox(props: ComboboxProps): import("react/jsx-runtime").JSX.Element;
30
+ export {};
@@ -0,0 +1,352 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import styled from 'styled-components';
4
+ import { ChevronDown, Check, X } from 'lucide-react';
5
+ import { useFloating, autoUpdate, offset, flip, shift, size, useDismiss, useRole, useListNavigation, useInteractions, FloatingPortal, } from '@floating-ui/react';
6
+ import { Tag } from './Tag';
7
+ const defaultFilter = (opt, query) => opt.label.toLowerCase().includes(query.toLowerCase());
8
+ const Control = styled.div `
9
+ display: flex;
10
+ align-items: center;
11
+ flex-wrap: wrap;
12
+ gap: ${(p) => p.theme.space[1]};
13
+ width: 100%;
14
+ min-width: 0;
15
+ background: ${(p) => p.theme.colors.surface};
16
+ color: ${(p) => p.theme.colors.text};
17
+ border: 2px solid ${(p) => p.theme.colors.border};
18
+ border-radius: ${(p) => p.theme.radii.md};
19
+ padding: 7px 10px;
20
+ font-family: ${(p) => p.theme.font.body};
21
+ font-size: 1rem;
22
+ cursor: text;
23
+ transition: box-shadow 80ms ${(p) => p.theme.easing.out};
24
+
25
+ ${(p) => p.$disabled
26
+ ? `opacity: 0.6; cursor: not-allowed;`
27
+ : `&:focus-within { box-shadow: ${p.theme.shadow.stamp}; }`}
28
+ `;
29
+ const ControlInput = styled.input `
30
+ flex: 1 1 60px;
31
+ min-width: 60px;
32
+ font-family: inherit;
33
+ font-size: 1rem;
34
+ color: ${(p) => p.theme.colors.text};
35
+ background: transparent;
36
+ border: none;
37
+ padding: 4px 0;
38
+ outline: none;
39
+
40
+ &::placeholder {
41
+ color: ${(p) => p.theme.colors.textSubtle};
42
+ }
43
+
44
+ &:disabled {
45
+ cursor: not-allowed;
46
+ }
47
+ `;
48
+ const IconButton = styled.button `
49
+ display: inline-flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ width: 22px;
53
+ height: 22px;
54
+ padding: 0;
55
+ margin: 0;
56
+ flex: none;
57
+ color: ${(p) => p.theme.colors.textMuted};
58
+ background: transparent;
59
+ border: none;
60
+ border-radius: ${(p) => p.theme.radii.xs};
61
+ cursor: pointer;
62
+ transition: color 80ms ${(p) => p.theme.easing.out};
63
+
64
+ &:hover:not(:disabled) {
65
+ color: ${(p) => p.theme.colors.text};
66
+ }
67
+ &:focus-visible {
68
+ outline: 2px solid ${(p) => p.theme.colors.accent};
69
+ outline-offset: 1px;
70
+ }
71
+ &:disabled {
72
+ cursor: not-allowed;
73
+ }
74
+ `;
75
+ const Chevron = styled.span `
76
+ display: inline-flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ flex: none;
80
+ color: ${(p) => p.theme.colors.textMuted};
81
+ transition: transform 80ms ${(p) => p.theme.easing.out};
82
+ transform: rotate(${(p) => (p.$open ? '180deg' : '0deg')});
83
+ `;
84
+ const Listbox = styled.ul `
85
+ list-style: none;
86
+ margin: 0;
87
+ padding: ${(p) => p.theme.space[1]};
88
+ background: ${(p) => p.theme.colors.surface};
89
+ color: ${(p) => p.theme.colors.text};
90
+ border: 2px solid ${(p) => p.theme.colors.border};
91
+ border-radius: ${(p) => p.theme.radii.md};
92
+ box-shadow: ${(p) => p.theme.shadow.stamp};
93
+ font-family: ${(p) => p.theme.font.body};
94
+ overflow-y: auto;
95
+ z-index: 1000;
96
+ outline: none;
97
+ `;
98
+ const OptionRow = styled.li `
99
+ display: flex;
100
+ align-items: center;
101
+ gap: ${(p) => p.theme.space[2]};
102
+ font-size: 14px;
103
+ font-weight: ${(p) => (p.$selected ? 800 : 600)};
104
+ color: ${(p) => p.theme.colors.text};
105
+ background: ${(p) => (p.$active ? p.theme.colors.primarySoft : 'transparent')};
106
+ border-radius: ${(p) => p.theme.radii.sm};
107
+ padding: ${(p) => p.theme.space[2]} ${(p) => p.theme.space[3]};
108
+ cursor: ${(p) => (p.$disabled ? 'not-allowed' : 'pointer')};
109
+ opacity: ${(p) => (p.$disabled ? 0.5 : 1)};
110
+ user-select: none;
111
+ `;
112
+ const OptionLabel = styled.span `
113
+ flex: 1 1 auto;
114
+ min-width: 0;
115
+ `;
116
+ const CheckIcon = styled.span `
117
+ display: inline-flex;
118
+ flex: none;
119
+ color: ${(p) => p.theme.colors.text};
120
+ `;
121
+ const EmptyRow = styled.li `
122
+ font-size: 14px;
123
+ font-weight: 600;
124
+ color: ${(p) => p.theme.colors.textSubtle};
125
+ padding: ${(p) => p.theme.space[2]} ${(p) => p.theme.space[3]};
126
+ user-select: none;
127
+ `;
128
+ export function Combobox(props) {
129
+ const { options, placeholder, disabled = false, clearable = false, filter = defaultFilter, emptyText = 'No results', id: idProp, } = props;
130
+ const multiple = props.multiple === true;
131
+ // ── Normalize selection to string[] internally ───────────────────────────
132
+ const isControlled = props.value !== undefined;
133
+ const toArray = (v) => {
134
+ if (v == null)
135
+ return [];
136
+ return Array.isArray(v) ? v : [v];
137
+ };
138
+ const [uncontrolled, setUncontrolled] = React.useState(() => toArray(props.value !== undefined ? props.value : props.defaultValue));
139
+ const selected = isControlled ? toArray(props.value) : uncontrolled;
140
+ const labelFor = React.useCallback((value) => options.find((o) => o.value === value)?.label ?? '', [options]);
141
+ // ── Local UI state ────────────────────────────────────────────────────────
142
+ const reactId = React.useId();
143
+ const baseId = idProp ?? reactId;
144
+ const listboxId = `${baseId}-listbox`;
145
+ const optionId = (index) => `${baseId}-option-${index}`;
146
+ const [open, setOpen] = React.useState(false);
147
+ const [query, setQuery] = React.useState('');
148
+ const [activeIndex, setActiveIndex] = React.useState(null);
149
+ const inputRef = React.useRef(null);
150
+ const listRef = React.useRef([]);
151
+ // Single-select: input mirrors the selected option's label when closed.
152
+ const singleSelectedLabel = !multiple && selected.length > 0 ? labelFor(selected[0]) : '';
153
+ const filtered = React.useMemo(() => options.filter((opt) => filter(opt, query)), [options, query, filter]);
154
+ const commitSelection = (next) => {
155
+ if (!isControlled)
156
+ setUncontrolled(next);
157
+ if (multiple) {
158
+ props.onChange?.(next);
159
+ }
160
+ else {
161
+ props.onChange?.(next.length > 0 ? next[0] : null);
162
+ }
163
+ };
164
+ const openList = () => {
165
+ if (disabled)
166
+ return;
167
+ setOpen(true);
168
+ };
169
+ const closeList = () => {
170
+ setOpen(false);
171
+ setActiveIndex(null);
172
+ setQuery('');
173
+ };
174
+ // ── Floating setup (mirror Popover / DropdownMenu) ────────────────────────
175
+ const { refs, floatingStyles, context } = useFloating({
176
+ open,
177
+ onOpenChange: (next) => {
178
+ if (next)
179
+ openList();
180
+ else
181
+ closeList();
182
+ },
183
+ placement: 'bottom-start',
184
+ whileElementsMounted: autoUpdate,
185
+ middleware: [
186
+ offset(8),
187
+ flip({ padding: 8 }),
188
+ shift({ padding: 8 }),
189
+ size({
190
+ apply({ rects, elements, availableHeight }) {
191
+ Object.assign(elements.floating.style, {
192
+ width: `${rects.reference.width}px`,
193
+ maxHeight: `${Math.min(260, availableHeight)}px`,
194
+ });
195
+ },
196
+ padding: 8,
197
+ }),
198
+ ],
199
+ });
200
+ const disabledIndices = React.useMemo(() => {
201
+ const indices = [];
202
+ filtered.forEach((opt, i) => {
203
+ if (opt.disabled)
204
+ indices.push(i);
205
+ });
206
+ return indices;
207
+ }, [filtered]);
208
+ // Escape is handled in `handleKeyDown` to support the two-stage behavior
209
+ // (first clears a non-empty query, second closes), so disable floating-ui's
210
+ // own escape-key dismissal here.
211
+ const dismiss = useDismiss(context, { escapeKey: false });
212
+ const role = useRole(context, { role: 'listbox' });
213
+ const listNavigation = useListNavigation(context, {
214
+ listRef,
215
+ activeIndex,
216
+ onNavigate: setActiveIndex,
217
+ virtual: true,
218
+ loop: true,
219
+ disabledIndices,
220
+ });
221
+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions([
222
+ dismiss,
223
+ role,
224
+ listNavigation,
225
+ ]);
226
+ const selectOption = (opt) => {
227
+ if (opt.disabled)
228
+ return;
229
+ if (multiple) {
230
+ const exists = selected.includes(opt.value);
231
+ const next = exists
232
+ ? selected.filter((v) => v !== opt.value)
233
+ : [...selected, opt.value];
234
+ commitSelection(next);
235
+ // Stay open, clear query, keep focus in input.
236
+ setQuery('');
237
+ inputRef.current?.focus();
238
+ }
239
+ else {
240
+ commitSelection([opt.value]);
241
+ setOpen(false);
242
+ setActiveIndex(null);
243
+ setQuery('');
244
+ }
245
+ };
246
+ const handleClear = (e) => {
247
+ e.preventDefault();
248
+ e.stopPropagation();
249
+ commitSelection([]);
250
+ setQuery('');
251
+ inputRef.current?.focus();
252
+ };
253
+ const handleInputChange = (e) => {
254
+ const value = e.target.value;
255
+ setQuery(value);
256
+ if (!open)
257
+ openList();
258
+ // Derive the next list directly from the new value to avoid the stale
259
+ // `filtered` memo (computed from the previous query). Highlight the first
260
+ // ENABLED option of the next list, or null if the query is empty / no match.
261
+ if (value.length === 0) {
262
+ setActiveIndex(null);
263
+ return;
264
+ }
265
+ const nextFiltered = options.filter((o) => filter(o, value));
266
+ const firstEnabled = nextFiltered.findIndex((o) => !o.disabled);
267
+ setActiveIndex(firstEnabled === -1 ? null : firstEnabled);
268
+ };
269
+ const handleKeyDown = (e) => {
270
+ if (disabled)
271
+ return;
272
+ // Two-stage Escape: the first Escape clears a non-empty query but keeps the
273
+ // list open; a second Escape (empty query) closes the list.
274
+ if (e.key === 'Escape' && open) {
275
+ e.preventDefault();
276
+ e.stopPropagation();
277
+ if (query !== '') {
278
+ setQuery('');
279
+ setActiveIndex(null);
280
+ }
281
+ else {
282
+ closeList();
283
+ }
284
+ return;
285
+ }
286
+ if (e.key === 'Backspace' && query === '' && multiple && selected.length > 0) {
287
+ e.preventDefault();
288
+ commitSelection(selected.slice(0, -1));
289
+ return;
290
+ }
291
+ if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
292
+ openList();
293
+ return;
294
+ }
295
+ if (e.key === 'Enter') {
296
+ if (open && activeIndex != null && filtered[activeIndex]) {
297
+ e.preventDefault();
298
+ selectOption(filtered[activeIndex]);
299
+ }
300
+ return;
301
+ }
302
+ };
303
+ // The text shown in the input. While open we show the live query; when closed
304
+ // (single) we strictly revert to the selected option's label.
305
+ const inputValue = multiple ? query : open ? query : singleSelectedLabel;
306
+ const hasValue = selected.length > 0;
307
+ const showClear = clearable && hasValue && !disabled;
308
+ const activeId = activeIndex != null && filtered[activeIndex] ? optionId(activeIndex) : undefined;
309
+ const referenceProps = getReferenceProps({
310
+ role: 'combobox',
311
+ 'aria-expanded': open,
312
+ 'aria-controls': listboxId,
313
+ 'aria-autocomplete': 'list',
314
+ 'aria-activedescendant': open ? activeId : undefined,
315
+ onChange: handleInputChange,
316
+ onKeyDown: handleKeyDown,
317
+ onFocus: openList,
318
+ });
319
+ return (_jsxs(_Fragment, { children: [_jsxs(Control, { ref: refs.setReference, "$disabled": disabled, onMouseDown: (e) => {
320
+ // Clicking the wrapper focuses the input and opens (ignore the inner
321
+ // buttons / the input itself, which manage their own behavior).
322
+ if (e.target === e.currentTarget && !disabled) {
323
+ e.preventDefault();
324
+ inputRef.current?.focus();
325
+ openList();
326
+ }
327
+ }, children: [multiple &&
328
+ selected.map((value) => (_jsx(Tag, { onRemove: disabled
329
+ ? undefined
330
+ : () => commitSelection(selected.filter((v) => v !== value)), children: labelFor(value) || value }, value))), _jsx(ControlInput, { ref: inputRef, id: baseId, value: inputValue, placeholder: !multiple && hasValue ? undefined : placeholder, disabled: disabled, ...referenceProps }), showClear && (_jsx(IconButton, { type: "button", "aria-label": "Clear selection", onClick: handleClear, children: _jsx(X, { size: 16, strokeWidth: 3, "aria-hidden": "true" }) })), _jsx(Chevron, { "$open": open, "aria-hidden": "true", onMouseDown: (e) => {
331
+ e.preventDefault();
332
+ if (disabled)
333
+ return;
334
+ if (open)
335
+ closeList();
336
+ else {
337
+ openList();
338
+ inputRef.current?.focus();
339
+ }
340
+ }, children: _jsx(ChevronDown, { size: 18, strokeWidth: 3 }) })] }), open && (_jsx(FloatingPortal, { children: _jsx(Listbox, { ref: refs.setFloating, id: listboxId, style: floatingStyles, "aria-multiselectable": multiple ? true : undefined, ...getFloatingProps(), children: filtered.length === 0 ? (_jsx(EmptyRow, { children: emptyText })) : (filtered.map((opt, index) => {
341
+ const isSelected = selected.includes(opt.value);
342
+ const isActive = activeIndex === index;
343
+ return (_jsxs(OptionRow, { id: optionId(index), role: "option", "aria-selected": isSelected, "aria-disabled": opt.disabled || undefined, "$active": isActive, "$selected": isSelected, "$disabled": opt.disabled === true, ref: (node) => {
344
+ listRef.current[index] = node;
345
+ }, ...getItemProps({
346
+ onClick(e) {
347
+ e.preventDefault();
348
+ selectOption(opt);
349
+ },
350
+ }), children: [_jsx(OptionLabel, { children: opt.label }), isSelected && (_jsx(CheckIcon, { children: _jsx(Check, { size: 16, strokeWidth: 3, "aria-hidden": "true" }) }))] }, opt.value));
351
+ })) }) }))] }));
352
+ }
@@ -0,0 +1,20 @@
1
+ import * as React from 'react';
2
+ export interface CommandItem {
3
+ id: string;
4
+ label: string;
5
+ keywords?: string[];
6
+ icon?: React.ReactNode;
7
+ group?: string;
8
+ shortcut?: string;
9
+ onSelect: () => void;
10
+ disabled?: boolean;
11
+ }
12
+ export interface CommandProps {
13
+ open: boolean;
14
+ onClose: () => void;
15
+ items: CommandItem[];
16
+ placeholder?: string;
17
+ emptyText?: string;
18
+ filter?: (item: CommandItem, query: string) => boolean;
19
+ }
20
+ export declare function Command({ open, onClose, items, placeholder, emptyText, filter, }: CommandProps): import("react/jsx-runtime").JSX.Element | null;