mirror of
https://github.com/hanzoai/o11y-components.git
synced 2026-10-30 14:54:15 +00:00
734 lines
18 KiB
TypeScript
734 lines
18 KiB
TypeScript
import React, { useState } from 'react';
|
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
import {
|
|
Combobox,
|
|
ComboboxTrigger,
|
|
ComboboxContent,
|
|
ComboboxCommand,
|
|
ComboboxInput,
|
|
ComboboxList,
|
|
ComboboxEmpty,
|
|
ComboboxLoading,
|
|
ComboboxItem,
|
|
ComboboxGroup,
|
|
ComboboxLabel,
|
|
ComboboxSeparator,
|
|
} from '@signozhq/combobox';
|
|
import { generateDocs } from '../utils/generateDocs';
|
|
import { Code2, Database, Terminal, GitBranch } from 'lucide-react';
|
|
|
|
const ComboboxExamples = [
|
|
`import {
|
|
Combobox,
|
|
ComboboxTrigger,
|
|
ComboboxContent,
|
|
ComboboxCommand,
|
|
ComboboxInput,
|
|
ComboboxList,
|
|
ComboboxItem,
|
|
} from '@signozhq/combobox';
|
|
|
|
export default function MyComponent() {
|
|
const [value, setValue] = useState('');
|
|
|
|
return (
|
|
<Combobox>
|
|
<ComboboxTrigger placeholder="Select a framework..." value={value} />
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput placeholder="Search frameworks..." />
|
|
<ComboboxList>
|
|
<ComboboxItem value="react">React</ComboboxItem>
|
|
<ComboboxItem value="vue">Vue</ComboboxItem>
|
|
<ComboboxItem value="angular">Angular</ComboboxItem>
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
</Combobox>
|
|
);
|
|
}`,
|
|
];
|
|
|
|
const ComboboxDocs = generateDocs({
|
|
packageName: '@signozhq/combobox',
|
|
description:
|
|
'Autocomplete input and command palette with a list of suggestions',
|
|
examples: ComboboxExamples,
|
|
});
|
|
|
|
const meta: Meta<typeof Combobox> = {
|
|
title: 'Components/Combobox',
|
|
component: Combobox,
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
component: ComboboxDocs,
|
|
},
|
|
},
|
|
layout: 'fullscreen',
|
|
design: [
|
|
{
|
|
name: 'Figma',
|
|
type: 'figma',
|
|
url: 'https://www.figma.com/design/egMidgk6VJDXTumxcCYUl1/Periscope---Primitives?node-id=12-742&p=f',
|
|
},
|
|
],
|
|
},
|
|
tags: ['autodocs'],
|
|
};
|
|
|
|
export default meta;
|
|
type Story = StoryObj<typeof Combobox>;
|
|
|
|
const frameworks = [
|
|
{ value: 'react', label: 'React' },
|
|
{ value: 'vue', label: 'Vue' },
|
|
{ value: 'angular', label: 'Angular' },
|
|
{ value: 'svelte', label: 'Svelte' },
|
|
];
|
|
|
|
const languages = [
|
|
{ value: 'javascript', label: 'JavaScript' },
|
|
{ value: 'typescript', label: 'TypeScript' },
|
|
{ value: 'python', label: 'Python' },
|
|
{ value: 'go', label: 'Go' },
|
|
{ value: 'rust', label: 'Rust' },
|
|
];
|
|
|
|
const databases = [
|
|
{ value: 'postgres', label: 'PostgreSQL' },
|
|
{ value: 'mongodb', label: 'MongoDB' },
|
|
{ value: 'redis', label: 'Redis' },
|
|
{ value: 'mysql', label: 'MySQL' },
|
|
];
|
|
|
|
export const Default: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Select a framework..."
|
|
value={frameworks.find((f) => f.value === value)?.label || ''}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput placeholder="Search frameworks..." />
|
|
<ComboboxList>
|
|
{frameworks.length === 0 ? (
|
|
<ComboboxEmpty>No framework found.</ComboboxEmpty>
|
|
) : (
|
|
frameworks.map((framework) => (
|
|
<ComboboxItem
|
|
key={framework.value}
|
|
value={framework.value}
|
|
onSelect={() => {
|
|
setValue(framework.value);
|
|
setOpen(false);
|
|
}}
|
|
isSelected={value === framework.value}
|
|
>
|
|
{framework.label}
|
|
</ComboboxItem>
|
|
))
|
|
)}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const WithDefaultValue: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('react');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Select a framework..."
|
|
value={frameworks.find((f) => f.value === value)?.label || ''}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput placeholder="Search frameworks..." />
|
|
<ComboboxList>
|
|
{frameworks.map((framework) => (
|
|
<ComboboxItem
|
|
key={framework.value}
|
|
value={framework.value}
|
|
onSelect={() => {
|
|
setValue(framework.value);
|
|
setOpen(false);
|
|
}}
|
|
isSelected={value === framework.value}
|
|
>
|
|
{framework.label}
|
|
</ComboboxItem>
|
|
))}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const WithGroups: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const allItems = [
|
|
{
|
|
group: 'Frameworks',
|
|
items: frameworks,
|
|
},
|
|
{
|
|
group: 'Languages',
|
|
items: languages,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Select an option..."
|
|
value={
|
|
[...frameworks, ...languages].find((f) => f.value === value)?.label || ''
|
|
}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput placeholder="Search..." />
|
|
<ComboboxList>
|
|
{allItems.map((group) => (
|
|
<ComboboxGroup key={group.group}>
|
|
<ComboboxLabel>{group.group}</ComboboxLabel>
|
|
{group.items.map((item) => (
|
|
<ComboboxItem
|
|
key={item.value}
|
|
value={item.value}
|
|
onSelect={() => {
|
|
setValue(item.value);
|
|
setOpen(false);
|
|
}}
|
|
isSelected={value === item.value}
|
|
>
|
|
{item.label}
|
|
</ComboboxItem>
|
|
))}
|
|
</ComboboxGroup>
|
|
))}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const WithMultipleGroups: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const allItems = [
|
|
{
|
|
group: 'Frameworks',
|
|
items: frameworks,
|
|
},
|
|
{
|
|
group: 'Languages',
|
|
items: languages,
|
|
},
|
|
{
|
|
group: 'Databases',
|
|
items: databases,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Select a technology..."
|
|
value={
|
|
[...frameworks, ...languages, ...databases].find(
|
|
(f) => f.value === value,
|
|
)?.label || ''
|
|
}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput placeholder="Search technologies..." />
|
|
<ComboboxList>
|
|
{allItems.map((group, idx) => (
|
|
<div key={group.group}>
|
|
{idx > 0 && <ComboboxSeparator />}
|
|
<ComboboxGroup>
|
|
<ComboboxLabel>{group.group}</ComboboxLabel>
|
|
{group.items.map((item) => (
|
|
<ComboboxItem
|
|
key={item.value}
|
|
value={item.value}
|
|
onSelect={() => {
|
|
setValue(item.value);
|
|
setOpen(false);
|
|
}}
|
|
isSelected={value === item.value}
|
|
>
|
|
{item.label}
|
|
</ComboboxItem>
|
|
))}
|
|
</ComboboxGroup>
|
|
</div>
|
|
))}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const Searchable: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('');
|
|
const [open, setOpen] = useState(false);
|
|
const [search, setSearch] = useState('');
|
|
|
|
const filtered = frameworks.filter((f) =>
|
|
f.label.toLowerCase().includes(search.toLowerCase()),
|
|
);
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Search frameworks..."
|
|
value={frameworks.find((f) => f.value === value)?.label || ''}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand shouldFilter={false}>
|
|
<ComboboxInput
|
|
placeholder="Search frameworks..."
|
|
value={search}
|
|
onValueChange={setSearch}
|
|
/>
|
|
<ComboboxList>
|
|
{filtered.length === 0 ? (
|
|
<ComboboxEmpty>No framework found.</ComboboxEmpty>
|
|
) : (
|
|
filtered.map((framework) => (
|
|
<ComboboxItem
|
|
key={framework.value}
|
|
value={framework.value}
|
|
onSelect={() => {
|
|
setValue(framework.value);
|
|
setSearch('');
|
|
setOpen(false);
|
|
}}
|
|
isSelected={value === framework.value}
|
|
>
|
|
{framework.label}
|
|
</ComboboxItem>
|
|
))
|
|
)}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const Disabled: Story = {
|
|
render: () => (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox>
|
|
<ComboboxTrigger placeholder="Select a framework..." disabled value="" />
|
|
</Combobox>
|
|
</div>
|
|
),
|
|
};
|
|
|
|
export const WithDescription: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const itemsWithDesc = [
|
|
{
|
|
value: 'react',
|
|
label: 'React',
|
|
description: 'A JavaScript library for building user interfaces',
|
|
},
|
|
{
|
|
value: 'vue',
|
|
label: 'Vue',
|
|
description: 'The Progressive JavaScript Framework',
|
|
},
|
|
{
|
|
value: 'angular',
|
|
label: 'Angular',
|
|
description: 'Platform for building mobile and desktop web applications',
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Select a framework..."
|
|
value={itemsWithDesc.find((f) => f.value === value)?.label || ''}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput placeholder="Search frameworks..." />
|
|
<ComboboxList>
|
|
{itemsWithDesc.map((item) => (
|
|
<ComboboxItem
|
|
key={item.value}
|
|
value={item.value}
|
|
onSelect={() => {
|
|
setValue(item.value);
|
|
setOpen(false);
|
|
}}
|
|
isSelected={value === item.value}
|
|
>
|
|
<div className="flex flex-col">
|
|
<span>{item.label}</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{item.description}
|
|
</span>
|
|
</div>
|
|
</ComboboxItem>
|
|
))}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const CustomWidth: Story = {
|
|
render: () => {
|
|
const [value1, setValue1] = useState('');
|
|
const [open1, setOpen1] = useState(false);
|
|
const [value2, setValue2] = useState('');
|
|
const [open2, setOpen2] = useState(false);
|
|
|
|
return (
|
|
<div className="p-8 space-y-6">
|
|
<div>
|
|
<p className="text-sm text-gray-600 mb-2">Small (w-[200px])</p>
|
|
<Combobox open={open1} onOpenChange={setOpen1}>
|
|
<ComboboxTrigger
|
|
placeholder="Select..."
|
|
value={frameworks.find((f) => f.value === value1)?.label || ''}
|
|
className="w-[200px]"
|
|
/>
|
|
{open1 && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput placeholder="Search..." />
|
|
<ComboboxList>
|
|
{frameworks.map((framework) => (
|
|
<ComboboxItem
|
|
key={framework.value}
|
|
value={framework.value}
|
|
onSelect={() => {
|
|
setValue1(framework.value);
|
|
setOpen1(false);
|
|
}}
|
|
isSelected={value1 === framework.value}
|
|
>
|
|
{framework.label}
|
|
</ComboboxItem>
|
|
))}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="text-sm text-gray-600 mb-2">Large (w-[400px])</p>
|
|
<Combobox open={open2} onOpenChange={setOpen2}>
|
|
<ComboboxTrigger
|
|
placeholder="Select a framework..."
|
|
value={frameworks.find((f) => f.value === value2)?.label || ''}
|
|
className="w-[400px]"
|
|
/>
|
|
{open2 && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput placeholder="Search frameworks..." />
|
|
<ComboboxList>
|
|
{frameworks.map((framework) => (
|
|
<ComboboxItem
|
|
key={framework.value}
|
|
value={framework.value}
|
|
onSelect={() => {
|
|
setValue2(framework.value);
|
|
setOpen2(false);
|
|
}}
|
|
isSelected={value2 === framework.value}
|
|
>
|
|
{framework.label}
|
|
</ComboboxItem>
|
|
))}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const Controlled: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('react');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const handleChange = (newValue: string) => {
|
|
console.log('Value changed to:', newValue);
|
|
setValue(newValue);
|
|
setOpen(false);
|
|
// You can add any custom logic here
|
|
// e.g., API calls, validation, etc.
|
|
};
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm space-y-4">
|
|
<div>
|
|
<p className="text-sm font-medium mb-2">
|
|
Selected: <span className="text-primary">{value}</span>
|
|
</p>
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Select a framework..."
|
|
value={frameworks.find((f) => f.value === value)?.label || ''}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput placeholder="Search frameworks..." />
|
|
<ComboboxList>
|
|
{frameworks.map((framework) => (
|
|
<ComboboxItem
|
|
key={framework.value}
|
|
value={framework.value}
|
|
onSelect={() => handleChange(framework.value)}
|
|
isSelected={value === framework.value}
|
|
>
|
|
{framework.label}
|
|
</ComboboxItem>
|
|
))}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const WithoutSearch: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Select a framework..."
|
|
value={frameworks.find((f) => f.value === value)?.label || ''}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
{/* No ComboboxInput - search bar is hidden */}
|
|
<ComboboxList>
|
|
{frameworks.map((framework) => (
|
|
<ComboboxItem
|
|
key={framework.value}
|
|
value={framework.value}
|
|
onSelect={() => {
|
|
setValue(framework.value);
|
|
setOpen(false);
|
|
}}
|
|
isSelected={value === framework.value}
|
|
>
|
|
{framework.label}
|
|
</ComboboxItem>
|
|
))}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const WithoutSearchNoCheck: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('');
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Select a framework..."
|
|
value={frameworks.find((f) => f.value === value)?.label || ''}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
{/* No ComboboxInput - search bar is hidden */}
|
|
<ComboboxList>
|
|
{frameworks.map((framework) => (
|
|
<ComboboxItem
|
|
key={framework.value}
|
|
value={framework.value}
|
|
onSelect={() => {
|
|
setValue(framework.value);
|
|
setOpen(false);
|
|
}}
|
|
isSelected={value === framework.value}
|
|
showCheck={false}
|
|
>
|
|
{framework.label}
|
|
</ComboboxItem>
|
|
))}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
},
|
|
};
|
|
|
|
export const WithIcons: Story = {
|
|
render: () => {
|
|
const [value, setValue] = useState('');
|
|
const [open, setOpen] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const itemsWithIcons = [
|
|
{
|
|
value: 'react',
|
|
label: 'React',
|
|
icon: Code2,
|
|
},
|
|
{
|
|
value: 'nodejs',
|
|
label: 'Node.js',
|
|
icon: Terminal,
|
|
},
|
|
{
|
|
value: 'postgres',
|
|
label: 'PostgreSQL',
|
|
icon: Database,
|
|
},
|
|
{
|
|
value: 'git',
|
|
label: 'Git',
|
|
icon: GitBranch,
|
|
},
|
|
];
|
|
|
|
const handleInputChange = (searchValue: string) => {
|
|
if (searchValue.length > 0) {
|
|
setIsLoading(true);
|
|
// Simulate API call
|
|
setTimeout(() => setIsLoading(false), 1000);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="p-8 w-full max-w-sm">
|
|
<Combobox open={open} onOpenChange={setOpen}>
|
|
<ComboboxTrigger
|
|
placeholder="Select a tool..."
|
|
value={itemsWithIcons.find((f) => f.value === value)?.label || ''}
|
|
/>
|
|
{open && (
|
|
<ComboboxContent>
|
|
<ComboboxCommand>
|
|
<ComboboxInput
|
|
placeholder="Search tools..."
|
|
onValueChange={handleInputChange}
|
|
/>
|
|
<ComboboxList>
|
|
{isLoading ? (
|
|
<ComboboxLoading>Loading tools...</ComboboxLoading>
|
|
) : (
|
|
itemsWithIcons.map((item) => {
|
|
const Icon = item.icon;
|
|
return (
|
|
<ComboboxItem
|
|
key={item.value}
|
|
value={item.value}
|
|
onSelect={() => {
|
|
setValue(item.value);
|
|
setOpen(false);
|
|
setIsLoading(false);
|
|
}}
|
|
isSelected={value === item.value}
|
|
>
|
|
<Icon className="mr-2 h-4 w-4" />
|
|
{item.label}
|
|
</ComboboxItem>
|
|
);
|
|
})
|
|
)}
|
|
</ComboboxList>
|
|
</ComboboxCommand>
|
|
</ComboboxContent>
|
|
)}
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
},
|
|
};
|