mirror of
https://github.com/hanzoai/o11y-components.git
synced 2026-10-30 14:54:15 +00:00
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.
473 lines
11 KiB
Plaintext
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} />
|