Files
zeekay 464e1eece0 State a version once
A version written into a manifest is written into every manifest that wants it,
which is how eslint came to exist in a hundred and four forms across this estate
and how a package came to resolve twice at run time. The catalog moves the number
out of the manifest: a manifest names the package, the workspace names the
version.

Four are held below their latest because the newer release cannot be used, and
the reason is recorded rather than the version alone: @noble/hashes 2 renamed
every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package
that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels
3 points its types condition at the bundle.
2026-09-15 11:45:32 -07:00

473 lines
11 KiB
Plaintext

import { Meta, Controls, Primary } from '@storybook/addon-docs/blocks';
import * as ComboboxStories from './combobox.stories';
import * as ComboboxSimpleStories from './combobox-simple.stories';
import * as ComboboxPrimitiveStories from './combobox-primitive.stories';
import * as ComboboxTriggerStories from './combobox-trigger.stories';
import * as ComboboxContentStories from './combobox-content.stories';
import * as ComboboxCommandStories from './combobox-command.stories';
import * as ComboboxInputStories from './combobox-input.stories';
import * as ComboboxListStories from './combobox-list.stories';
import * as ComboboxEmptyStories from './combobox-empty.stories';
import * as ComboboxGroupStories from './combobox-group.stories';
import * as ComboboxItemStories from './combobox-item.stories';
import * as ComboboxLoadingStories from './combobox-loading.stories';
import * as ComboboxSeparatorStories from './combobox-separator.stories';
<Meta of={ComboboxStories} />
# Combobox
Searchable select built on `Command` and `Popover`. Use for single-value selection with optional filtering and grouped options.
## How to use
### ComboboxSimple (preset)
Minimal preset that accepts a list of items and handles selection, filtering, and value display. Supports flat items, grouped items, and custom label content (ReactNode).
**Flat items (value + label):**
```tsx
import { ComboboxSimple } from '@signozhq/ui';
import { useState } from 'react';
const items = [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'angular', label: 'Angular' },
];
export default function MyComponent() {
const [value, setValue] = useState('');
return (
<ComboboxSimple
items={items}
placeholder="Select a framework..."
value={value}
onChange={setValue}
/>
);
}
```
**Items with icons (ReactNode labels):**
```tsx
import { ComboboxSimple } from '@signozhq/ui';
import { useState } from 'react';
import { Code, Database } from '@signozhq/icons';
const items = [
{ value: 'react', label: <><Code style={{ marginRight: 8, height: 16, width: 16 }} />React</> },
{ value: 'vue', label: <><Code style={{ marginRight: 8, height: 16, width: 16 }} />Vue</> },
{ value: 'postgres', label: <><Database style={{ marginRight: 8, height: 16, width: 16 }} />PostgreSQL</> },
];
export default function MyComponent() {
const [value, setValue] = useState('');
return (
<ComboboxSimple
items={items}
placeholder="Select a tool..."
value={value}
onChange={setValue}
/>
);
}
```
**Items with displayValue (ReactNode in list, plain text in trigger):**
```tsx
import { ComboboxSimple } from '@signozhq/ui';
import { useState } from 'react';
import { Code } from '@signozhq/icons';
const items = [
{ value: 'react', label: <><Code style={{ marginRight: 8, height: 16, width: 16 }} />React</>, displayValue: 'React' },
{ value: 'vue', label: <><Code style={{ marginRight: 8, height: 16, width: 16 }} />Vue</>, displayValue: 'Vue' },
];
export default function MyComponent() {
const [value, setValue] = useState('');
return <ComboboxSimple items={items} value={value} onChange={setValue} />;
}
```
**Grouped items with headings:**
```tsx
<ComboboxSimple
groups={[
{ heading: 'Frameworks', items: [{ value: 'react', label: 'React' }, { value: 'vue', label: 'Vue' }] },
{ heading: 'Languages', items: [{ value: 'ts', label: 'TypeScript' }, { value: 'js', label: 'JavaScript' }] },
]}
placeholder="Select..."
value={value}
onChange={setValue}
/>
```
**Uncontrolled with defaultValue:**
```tsx
<ComboboxSimple
items={items}
defaultValue="react"
placeholder="Select a framework..."
onChange={(v) => console.log('Selected:', v)}
/>
```
**Custom displayValue callback:**
```tsx
<ComboboxSimple
items={items}
value={value}
onChange={setValue}
displayValue={(item) => item ? `${item.displayValue ?? item.value} ✓` : 'Choose...'}
/>
```
**Custom empty placeholder:**
```tsx
<ComboboxSimple
items={items}
value={value}
onChange={setValue}
emptyPlaceholder="No frameworks match your search."
/>
```
**Multi-select mode:**
```tsx
const [values, setValues] = useState<string[]>([]);
<ComboboxSimple
items={items}
multiple
value={values}
onChange={(v) => setValues(v as string[])}
placeholder="Select frameworks..."
/>
```
**Allow creating new items (tags mode):**
```tsx
const [tags, setTags] = useState<string[]>([]);
<ComboboxSimple
items={[]}
multiple
allowCreate
value={tags}
onChange={(v) => setTags(v as string[])}
placeholder="Type to add tags..."
/>
```
**Custom create option text:**
```tsx
<ComboboxSimple
items={items}
multiple
allowCreate={(inputValue) => (
<span>Add <strong>"{inputValue}"</strong> as new tag</span>
)}
value={values}
onChange={(v) => setValues(v as string[])}
/>
```
**Keywords for enhanced filtering:**
Use `keywords` when the `value` differs from what users might search for. The filter searches across `value`, `label` (if string), `displayValue`, `insertValue`, and `keywords`.
```tsx
const items = [
{ value: '15', label: '15 minutes', keywords: ['quarter hour', '15m', '900 seconds'] },
{ value: '30', label: '30 minutes', keywords: ['half hour', '30m'] },
{ value: '60', label: '1 hour', keywords: ['60 minutes', '1h'] },
];
<ComboboxSimple
items={items}
placeholder="Select duration..."
value={value}
onChange={setValue}
/>
```
Now users can find "15 minutes" by typing "15", "minute", "quarter", or "900".
**String labels are automatically searchable:**
When `label` is a string, it's included in the filter. No extra config needed.
```tsx
const items = [
{ value: 'us-east-1', label: 'US East (N. Virginia)' },
{ value: 'eu-west-1', label: 'EU (Ireland)' },
];
// Users can search "us-east" (value) or "Virginia" (label)
<ComboboxSimple items={items} value={value} onChange={setValue} />
```
**Hint items with insertValue (filter suggestions):**
Items with `insertValue` behave as hints - clicking them inserts the value into the input instead of selecting. Useful for filter prefixes like `status:` or `tag:`.
```tsx
<ComboboxSimple
groups={[
{
heading: 'Suggestions',
items: [
{ value: 'hint:status', label: 'status:', insertValue: 'status:' },
{ value: 'hint:priority', label: 'priority:', insertValue: 'priority:' },
],
},
{
heading: 'Status',
items: [
{ value: 'status:active', label: 'Status: Active' },
{ value: 'status:pending', label: 'Status: Pending' },
],
},
{
heading: 'Priority',
items: [
{ value: 'priority:high', label: 'Priority: High' },
{ value: 'priority:low', label: 'Priority: Low' },
],
},
]}
placeholder="Filter by..."
value={value}
onChange={setValue}
/>
```
Hints show when input is empty or doesn't start with any hint's `insertValue`. Once user types a prefix (e.g. `status:`), hints hide and matching items filter normally.
**Loading state (async data):**
Use `loading` and `loadingPlaceholder` props to show a loading spinner in the dropdown while fetching data:
```tsx
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchItems().then((data) => {
setItems(data);
setIsLoading(false);
});
}, []);
<ComboboxSimple
items={items}
loading={isLoading}
loadingPlaceholder="Fetching options..."
placeholder="Select a framework..."
value={value}
onChange={setValue}
/>
```
For primitive composition, use `ComboboxLoading` inside `ComboboxList`:
```tsx
<ComboboxList>
{isLoading ? (
<ComboboxLoading>Loading options...</ComboboxLoading>
) : (
items.map((item) => (
<ComboboxItem key={item.value} value={item.value}>
{item.label}
</ComboboxItem>
))
)}
</ComboboxList>
```
### Combobox inside a popover
When placing a combobox inside a popover (e.g. a filter panel or form), set `withPortal={false}` so the dropdown content stays within the popover DOM instead of portaling to `document.body`. This avoids z-index and stacking issues.
**ComboboxSimple preset:**
```tsx
import { Popover, PopoverTrigger, PopoverContent, ComboboxSimple, Button } from '@signozhq/ui';
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open filters</Button>
</PopoverTrigger>
<PopoverContent style={{ width: 320 }}>
<ComboboxSimple
items={items}
value={value}
onChange={setValue}
withPortal={false}
/>
</PopoverContent>
</Popover>
```
**Primitive composition:**
```tsx
import {
Popover,
PopoverTrigger,
PopoverContent,
Combobox,
ComboboxTrigger,
ComboboxContent,
ComboboxCommand,
ComboboxInput,
ComboboxList,
} from '@signozhq/ui';
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open</Button>
</PopoverTrigger>
<PopoverContent>
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger placeholder="Select..." value={selectedLabel} />
<ComboboxContent withPortal={false}>
<ComboboxCommand>
<ComboboxInput placeholder="Search..." />
<ComboboxList>{/* items */}</ComboboxList>
</ComboboxCommand>
</ComboboxContent>
</Combobox>
</PopoverContent>
</Popover>
```
<Primary />
## ComboboxSimple Props
<Controls of={ComboboxSimpleStories.Default} />
## Basic combobox (composition)
For full control, compose the subcomponents manually:
```tsx
import {
Combobox,
ComboboxTrigger,
ComboboxContent,
ComboboxCommand,
ComboboxInput,
ComboboxList,
ComboboxItem,
ComboboxEmpty,
} from '@signozhq/ui';
import { useState } from 'react';
const frameworks = [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
];
export default function MyComponent() {
const [value, setValue] = useState('');
const [open, setOpen] = useState(false);
const selectedLabel = frameworks.find((f) => f.value === value)?.label ?? '';
return (
<Combobox open={open} onOpenChange={setOpen}>
<ComboboxTrigger placeholder="Select a framework..." value={selectedLabel} />
<ComboboxContent>
<ComboboxCommand>
<ComboboxInput placeholder="Search frameworks..." />
<ComboboxList>
<ComboboxItem
value="react"
onSelect={() => {
setValue('react');
setOpen(false);
}}
isSelected={value === 'react'}
>
React
</ComboboxItem>
<ComboboxItem
value="vue"
onSelect={() => {
setValue('vue');
setOpen(false);
}}
isSelected={value === 'vue'}
>
Vue
</ComboboxItem>
<ComboboxEmpty>No results.</ComboboxEmpty>
</ComboboxList>
</ComboboxCommand>
</ComboboxContent>
</Combobox>
);
}
```
## Combobox Props
<Controls of={ComboboxPrimitiveStories.Default} />
## Subcomponents
### ComboboxTrigger
<Controls of={ComboboxTriggerStories.Default} />
### ComboboxContent
<Controls of={ComboboxContentStories.Default} />
### ComboboxCommand
<Controls of={ComboboxCommandStories.Default} />
### ComboboxInput
<Controls of={ComboboxInputStories.Default} />
### ComboboxList
<Controls of={ComboboxListStories.Default} />
### ComboboxEmpty
<Controls of={ComboboxEmptyStories.Default} />
### ComboboxGroup
<Controls of={ComboboxGroupStories.Default} />
### ComboboxItem
<Controls of={ComboboxItemStories.Default} />
### ComboboxLoading
<Controls of={ComboboxLoadingStories.Default} />
### ComboboxSeparator
<Controls of={ComboboxSeparatorStories.Default} />