2025-10-20 18:30:52 +08:00
import type { Meta , StoryObj } from '@storybook/nextjs'
import { useState } from 'react'
import Select , { PortalSelect , SimpleSelect } from '.'
import type { Item } from '.'
const meta = {
2025-10-29 14:33:43 +08:00
title : 'Base/Data Entry/Select' ,
2025-10-20 18:30:52 +08:00
component : SimpleSelect ,
parameters : {
layout : 'centered' ,
docs : {
description : {
component : 'Select component with three variants: Select (with search), SimpleSelect (basic dropdown), and PortalSelect (portal-based positioning). Built on Headless UI.' ,
} ,
} ,
} ,
tags : [ 'autodocs' ] ,
argTypes : {
placeholder : {
control : 'text' ,
description : 'Placeholder text' ,
} ,
disabled : {
control : 'boolean' ,
description : 'Disabled state' ,
} ,
notClearable : {
control : 'boolean' ,
description : 'Hide clear button' ,
} ,
hideChecked : {
control : 'boolean' ,
description : 'Hide check icon on selected item' ,
} ,
} ,
2025-10-21 17:44:26 +08:00
args : {
onSelect : ( item ) = > {
console . log ( 'Selected:' , item )
} ,
} ,
2025-10-20 18:30:52 +08:00
} satisfies Meta < typeof SimpleSelect >
export default meta
type Story = StoryObj < typeof meta >
const fruits : Item [ ] = [
{ value : 'apple' , name : 'Apple' } ,
{ value : 'banana' , name : 'Banana' } ,
{ value : 'cherry' , name : 'Cherry' } ,
{ value : 'date' , name : 'Date' } ,
{ value : 'elderberry' , name : 'Elderberry' } ,
]
const countries : Item [ ] = [
{ value : 'us' , name : 'United States' } ,
{ value : 'uk' , name : 'United Kingdom' } ,
{ value : 'ca' , name : 'Canada' } ,
{ value : 'au' , name : 'Australia' } ,
{ value : 'de' , name : 'Germany' } ,
{ value : 'fr' , name : 'France' } ,
{ value : 'jp' , name : 'Japan' } ,
{ value : 'cn' , name : 'China' } ,
]
// SimpleSelect Demo
const SimpleSelectDemo = ( args : any ) = > {
const [ selected , setSelected ] = useState ( args . defaultValue || '' )
return (
< div style = { { width : '300px' } } >
< SimpleSelect
{ . . . args }
items = { fruits }
defaultValue = { selected }
onSelect = { ( item ) = > {
setSelected ( item . value )
console . log ( 'Selected:' , item )
} }
/ >
{ selected && (
< div className = "mt-3 text-sm text-gray-600" >
Selected : < span className = "font-semibold" > { selected } < / span >
< / div >
) }
< / div >
)
}
// Default SimpleSelect
export const Default : Story = {
render : args = > < SimpleSelectDemo { ...args } / > ,
args : {
placeholder : 'Select a fruit...' ,
defaultValue : 'apple' ,
2025-10-21 17:44:26 +08:00
items : [ ] ,
2025-10-20 18:30:52 +08:00
} ,
}
// With placeholder (no selection)
export const WithPlaceholder : Story = {
render : args = > < SimpleSelectDemo { ...args } / > ,
args : {
placeholder : 'Choose an option...' ,
defaultValue : '' ,
2025-10-21 17:44:26 +08:00
items : [ ] ,
2025-10-20 18:30:52 +08:00
} ,
}
// Disabled state
export const Disabled : Story = {
render : args = > < SimpleSelectDemo { ...args } / > ,
args : {
placeholder : 'Select a fruit...' ,
defaultValue : 'banana' ,
disabled : true ,
2025-10-21 17:44:26 +08:00
items : [ ] ,
2025-10-20 18:30:52 +08:00
} ,
}
// Not clearable
export const NotClearable : Story = {
render : args = > < SimpleSelectDemo { ...args } / > ,
args : {
placeholder : 'Select a fruit...' ,
defaultValue : 'cherry' ,
notClearable : true ,
2025-10-21 17:44:26 +08:00
items : [ ] ,
2025-10-20 18:30:52 +08:00
} ,
}
// Hide checked icon
export const HideChecked : Story = {
render : args = > < SimpleSelectDemo { ...args } / > ,
args : {
placeholder : 'Select a fruit...' ,
defaultValue : 'apple' ,
hideChecked : true ,
2025-10-21 17:44:26 +08:00
items : [ ] ,
2025-10-20 18:30:52 +08:00
} ,
}
// Select with search
const WithSearchDemo = ( ) = > {
const [ selected , setSelected ] = useState ( 'us' )
return (
< div style = { { width : '300px' } } >
< Select
items = { countries }
defaultValue = { selected }
onSelect = { ( item ) = > {
setSelected ( item . value as string )
console . log ( 'Selected:' , item )
} }
allowSearch = { true }
/ >
< div className = "mt-3 text-sm text-gray-600" >
Selected : < span className = "font-semibold" > { selected } < / span >
< / div >
< / div >
)
}
export const WithSearch : Story = {
render : ( ) = > < WithSearchDemo / > ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story
2025-10-20 18:30:52 +08:00
// PortalSelect
const PortalSelectVariantDemo = ( ) = > {
const [ selected , setSelected ] = useState ( 'apple' )
return (
< div style = { { width : '300px' } } >
< PortalSelect
value = { selected }
items = { fruits }
onSelect = { ( item ) = > {
setSelected ( item . value as string )
console . log ( 'Selected:' , item )
} }
placeholder = "Select a fruit..."
/ >
< div className = "mt-3 text-sm text-gray-600" >
Selected : < span className = "font-semibold" > { selected } < / span >
< / div >
< / div >
)
}
export const PortalSelectVariant : Story = {
render : ( ) = > < PortalSelectVariantDemo / > ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story
2025-10-20 18:30:52 +08:00
// Custom render option
const CustomRenderOptionDemo = ( ) = > {
const [ selected , setSelected ] = useState ( 'us' )
const countriesWithFlags = [
{ value : 'us' , name : 'United States' , flag : '🇺🇸' } ,
{ value : 'uk' , name : 'United Kingdom' , flag : '🇬🇧' } ,
{ value : 'ca' , name : 'Canada' , flag : '🇨🇦' } ,
{ value : 'au' , name : 'Australia' , flag : '🇦🇺' } ,
{ value : 'de' , name : 'Germany' , flag : '🇩🇪' } ,
]
return (
< div style = { { width : '300px' } } >
< SimpleSelect
items = { countriesWithFlags }
defaultValue = { selected }
onSelect = { item = > setSelected ( item . value as string ) }
renderOption = { ( { item , selected } ) = > (
< div className = "flex w-full items-center justify-between" >
< div className = "flex items-center gap-2" >
< span className = "text-xl" > { item . flag } < / span >
< span > { item . name } < / span >
< / div >
{ selected && < span className = "text-blue-600" > ✓ < / span > }
< / div >
) }
/ >
< / div >
)
}
export const CustomRenderOption : Story = {
render : ( ) = > < CustomRenderOptionDemo / > ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story
2025-10-20 18:30:52 +08:00
// Loading state
export const LoadingState : Story = {
render : ( ) = > {
return (
< div style = { { width : '300px' } } >
< SimpleSelect
items = { [ ] }
defaultValue = ""
onSelect = { ( ) = > undefined }
placeholder = "Loading options..."
isLoading = { true }
/ >
< / div >
)
} ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story
2025-10-20 18:30:52 +08:00
// Real-world example - Form field
const FormFieldDemo = ( ) = > {
const [ formData , setFormData ] = useState ( {
country : 'us' ,
language : 'en' ,
timezone : 'pst' ,
} )
const languages = [
{ value : 'en' , name : 'English' } ,
{ value : 'es' , name : 'Spanish' } ,
{ value : 'fr' , name : 'French' } ,
{ value : 'de' , name : 'German' } ,
{ value : 'zh' , name : 'Chinese' } ,
]
const timezones = [
{ value : 'pst' , name : 'Pacific Time (PST)' } ,
{ value : 'mst' , name : 'Mountain Time (MST)' } ,
{ value : 'cst' , name : 'Central Time (CST)' } ,
{ value : 'est' , name : 'Eastern Time (EST)' } ,
]
return (
< div style = { { width : '400px' } } className = "rounded-lg border border-gray-200 bg-white p-6" >
< h3 className = "mb-4 text-lg font-semibold" > User Preferences < / h3 >
< div className = "space-y-4" >
< div >
< label className = "mb-2 block text-sm font-medium text-gray-700" > Country < / label >
< SimpleSelect
items = { countries }
defaultValue = { formData . country }
onSelect = { item = > setFormData ( { . . . formData , country : item.value as string } ) }
/ >
< / div >
< div >
< label className = "mb-2 block text-sm font-medium text-gray-700" > Language < / label >
< SimpleSelect
items = { languages }
defaultValue = { formData . language }
onSelect = { item = > setFormData ( { . . . formData , language : item.value as string } ) }
/ >
< / div >
< div >
< label className = "mb-2 block text-sm font-medium text-gray-700" > Timezone < / label >
< SimpleSelect
items = { timezones }
defaultValue = { formData . timezone }
onSelect = { item = > setFormData ( { . . . formData , timezone : item.value as string } ) }
/ >
< / div >
< / div >
< div className = "mt-6 rounded-lg bg-gray-50 p-3 text-xs text-gray-700" >
< div > < strong > Country : < / strong > { formData . country } < / div >
< div > < strong > Language : < / strong > { formData . language } < / div >
< div > < strong > Timezone : < / strong > { formData . timezone } < / div >
< / div >
< / div >
)
}
export const FormField : Story = {
render : ( ) = > < FormFieldDemo / > ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story
2025-10-20 18:30:52 +08:00
// Real-world example - Filter selector
const FilterSelectorDemo = ( ) = > {
const [ status , setStatus ] = useState ( 'all' )
const [ priority , setPriority ] = useState ( 'all' )
const statusOptions = [
{ value : 'all' , name : 'All Status' } ,
{ value : 'active' , name : 'Active' } ,
{ value : 'pending' , name : 'Pending' } ,
{ value : 'completed' , name : 'Completed' } ,
{ value : 'cancelled' , name : 'Cancelled' } ,
]
const priorityOptions = [
{ value : 'all' , name : 'All Priorities' } ,
{ value : 'high' , name : 'High Priority' } ,
{ value : 'medium' , name : 'Medium Priority' } ,
{ value : 'low' , name : 'Low Priority' } ,
]
return (
< div style = { { width : '600px' } } className = "rounded-lg border border-gray-200 bg-white p-6" >
< h3 className = "mb-4 text-lg font-semibold" > Task Filters < / h3 >
< div className = "mb-6 flex gap-4" >
< div className = "flex-1" >
< label className = "mb-2 block text-xs font-medium text-gray-600" > Status < / label >
< SimpleSelect
items = { statusOptions }
defaultValue = { status }
onSelect = { item = > setStatus ( item . value as string ) }
notClearable
/ >
< / div >
< div className = "flex-1" >
< label className = "mb-2 block text-xs font-medium text-gray-600" > Priority < / label >
< SimpleSelect
items = { priorityOptions }
defaultValue = { priority }
onSelect = { item = > setPriority ( item . value as string ) }
notClearable
/ >
< / div >
< / div >
< div className = "rounded-lg bg-blue-50 p-4 text-sm" >
< div className = "mb-2 font-medium text-gray-700" > Active Filters : < / div >
< div className = "flex gap-2" >
< span className = "rounded bg-blue-200 px-2 py-1 text-xs text-blue-800" >
Status : { status }
< / span >
< span className = "rounded bg-blue-200 px-2 py-1 text-xs text-blue-800" >
Priority : { priority }
< / span >
< / div >
< / div >
< / div >
)
}
export const FilterSelector : Story = {
render : ( ) = > < FilterSelectorDemo / > ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story
2025-10-20 18:30:52 +08:00
// Real-world example - Version selector with badge
const VersionSelectorDemo = ( ) = > {
const [ selectedVersion , setSelectedVersion ] = useState ( '2.1.0' )
const versions = [
{ value : '3.0.0' , name : 'v3.0.0 (Beta)' } ,
{ value : '2.1.0' , name : 'v2.1.0 (Latest)' } ,
{ value : '2.0.5' , name : 'v2.0.5' } ,
{ value : '2.0.4' , name : 'v2.0.4' } ,
{ value : '1.9.8' , name : 'v1.9.8' } ,
]
return (
< div style = { { width : '400px' } } className = "rounded-lg border border-gray-200 bg-white p-6" >
< h3 className = "mb-4 text-lg font-semibold" > Select Version < / h3 >
< PortalSelect
value = { selectedVersion }
items = { versions }
onSelect = { item = > setSelectedVersion ( item . value as string ) }
installedValue = "2.0.5"
placeholder = "Choose version..."
/ >
< div className = "mt-4 rounded-lg bg-gray-50 p-3 text-sm text-gray-700" >
{ selectedVersion !== '2.0.5' && (
< div className = "mb-2 text-yellow-600" >
⚠ ️ Version change detected
< / div >
) }
< div > Current : < strong > { selectedVersion } < / strong > < / div >
< div className = "mt-1 text-xs text-gray-500" > Installed : 2.0.5 < / div >
< / div >
< / div >
)
}
export const VersionSelector : Story = {
render : ( ) = > < VersionSelectorDemo / > ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story
2025-10-20 18:30:52 +08:00
// Real-world example - Settings dropdown
const SettingsDropdownDemo = ( ) = > {
const [ theme , setTheme ] = useState ( 'light' )
const [ fontSize , setFontSize ] = useState ( 'medium' )
const themeOptions = [
{ value : 'light' , name : '☀️ Light Mode' } ,
{ value : 'dark' , name : '🌙 Dark Mode' } ,
{ value : 'auto' , name : '🔄 Auto (System)' } ,
]
const fontSizeOptions = [
{ value : 'small' , name : 'Small (12px)' } ,
{ value : 'medium' , name : 'Medium (14px)' } ,
{ value : 'large' , name : 'Large (16px)' } ,
{ value : 'xlarge' , name : 'Extra Large (18px)' } ,
]
return (
< div style = { { width : '400px' } } className = "rounded-lg border border-gray-200 bg-white p-6" >
< h3 className = "mb-4 text-lg font-semibold" > Display Settings < / h3 >
< div className = "space-y-4" >
< div >
< label className = "mb-2 block text-sm font-medium text-gray-700" > Theme < / label >
< SimpleSelect
items = { themeOptions }
defaultValue = { theme }
onSelect = { item = > setTheme ( item . value as string ) }
notClearable
/ >
< / div >
< div >
< label className = "mb-2 block text-sm font-medium text-gray-700" > Font Size < / label >
< SimpleSelect
items = { fontSizeOptions }
defaultValue = { fontSize }
onSelect = { item = > setFontSize ( item . value as string ) }
notClearable
/ >
< / div >
< / div >
< / div >
)
}
export const SettingsDropdown : Story = {
render : ( ) = > < SettingsDropdownDemo / > ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story
2025-10-20 18:30:52 +08:00
// Comparison of variants
const VariantComparisonDemo = ( ) = > {
const [ simple , setSimple ] = useState ( 'apple' )
const [ withSearch , setWithSearch ] = useState ( 'us' )
const [ portal , setPortal ] = useState ( 'banana' )
return (
< div style = { { width : '700px' } } className = "rounded-lg border border-gray-200 bg-white p-6" >
< h3 className = "mb-6 text-lg font-semibold" > Select Variants Comparison < / h3 >
< div className = "space-y-6" >
< div >
< h4 className = "mb-2 text-sm font-medium text-gray-700" > SimpleSelect ( Basic ) < / h4 >
< div style = { { width : '300px' } } >
< SimpleSelect
items = { fruits }
defaultValue = { simple }
onSelect = { item = > setSimple ( item . value as string ) }
placeholder = "Choose a fruit..."
/ >
< / div >
< p className = "mt-2 text-xs text-gray-500" > Standard dropdown without search < / p >
< / div >
< div >
< h4 className = "mb-2 text-sm font-medium text-gray-700" > Select ( With Search ) < / h4 >
< div style = { { width : '300px' } } >
< Select
items = { countries }
defaultValue = { withSearch }
onSelect = { item = > setWithSearch ( item . value as string ) }
allowSearch = { true }
/ >
< / div >
< p className = "mt-2 text-xs text-gray-500" > Dropdown with search / filter capability < / p >
< / div >
< div >
< h4 className = "mb-2 text-sm font-medium text-gray-700" > PortalSelect ( Portal - based ) < / h4 >
< div style = { { width : '300px' } } >
< PortalSelect
value = { portal }
items = { fruits }
onSelect = { item = > setPortal ( item . value as string ) }
placeholder = "Choose a fruit..."
/ >
< / div >
< p className = "mt-2 text-xs text-gray-500" > Portal - based positioning for better overflow handling < / p >
< / div >
< / div >
< / div >
)
}
export const VariantComparison : Story = {
render : ( ) = > < VariantComparisonDemo / > ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story
2025-10-20 18:30:52 +08:00
// Interactive playground
const PlaygroundDemo = ( ) = > {
const [ selected , setSelected ] = useState ( 'apple' )
return (
< div style = { { width : '350px' } } >
< SimpleSelect
items = { fruits }
defaultValue = { selected }
onSelect = { item = > setSelected ( item . value as string ) }
placeholder = "Select an option..."
/ >
< / div >
)
}
export const Playground : Story = {
render : ( ) = > < PlaygroundDemo / > ,
2025-10-21 17:44:26 +08:00
parameters : { controls : { disable : true } } ,
} as unknown as Story