From 30f1315908cea8d69f76aff379266ddfa3912567 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 9 Jul 2025 16:25:30 -0600 Subject: [PATCH] Initial select 2 component --- .../web/src/components/ui/command/Command.tsx | 8 +- .../components/ui/select/Select2.stories.tsx | 236 +++++++++++ apps/web/src/components/ui/select/Select2.tsx | 379 ++++++++++++++++++ 3 files changed, 620 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/components/ui/select/Select2.stories.tsx create mode 100644 apps/web/src/components/ui/select/Select2.tsx diff --git a/apps/web/src/components/ui/command/Command.tsx b/apps/web/src/components/ui/command/Command.tsx index 5b06f4f7f..e35d4d0ba 100644 --- a/apps/web/src/components/ui/command/Command.tsx +++ b/apps/web/src/components/ui/command/Command.tsx @@ -23,9 +23,11 @@ Command.displayName = CommandPrimitive.displayName; const CommandInput = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( -
+ React.ComponentPropsWithoutRef & { + parentClassName?: string; + } +>(({ className, parentClassName, ...props }, ref) => ( +
diff --git a/apps/web/src/components/ui/select/Select2.stories.tsx b/apps/web/src/components/ui/select/Select2.stories.tsx new file mode 100644 index 000000000..eb310b1d5 --- /dev/null +++ b/apps/web/src/components/ui/select/Select2.stories.tsx @@ -0,0 +1,236 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; +import { fn } from '@storybook/test'; +import { Select, type SelectItem, type SelectProps } from './Select2'; +import { User, Gear, PowerOff } from '@/components/ui/icons/NucleoIconOutlined'; + +const meta = { + title: 'UI/select/Select2', + component: Select, + parameters: { + layout: 'centered' + }, + argTypes: { + search: { + control: { type: 'boolean' }, + description: 'Enable/disable search functionality' + }, + disabled: { + control: { type: 'boolean' }, + description: 'Disable the select' + }, + loading: { + control: { type: 'boolean' }, + description: 'Show loading state' + }, + showIndex: { + control: { type: 'boolean' }, + description: 'Show index numbers for items' + }, + placeholder: { + control: { type: 'text' }, + description: 'Placeholder text when no item is selected' + }, + onChange: { + action: 'onChange' + } + }, + decorators: [ + (Story) => ( +
+ +
+ ) + ] +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Basic select with simple string options +export const BasicSelect: Story = { + args: { + placeholder: 'Select a fruit', + items: [ + { value: 'apple', label: 'Apple' }, + { value: 'banana', label: 'Banana' }, + { value: 'orange', label: 'Orange' }, + { value: 'grape', label: 'Grape' }, + { value: 'strawberry', label: 'Strawberry' }, + { value: 'watermelon', label: 'Watermelon' }, + { value: 'pineapple', label: 'Pineapple' }, + { value: 'mango', label: 'Mango' } + ] as SelectItem[], + onChange: fn() + }, + render: function RenderBasicSelect(args) { + const [value, setValue] = React.useState(); + + return ( + { + setValue(newValue as string); + args.onChange(newValue); + }} + /> + ); + } +}; + +// Select with search disabled +export const NoSearchSelect: Story = { + args: { + placeholder: 'Select a color', + search: false, + items: [ + { value: 'red', label: 'Red' }, + { value: 'green', label: 'Green' }, + { value: 'blue', label: 'Blue' }, + { value: 'yellow', label: 'Yellow' }, + { value: 'purple', label: 'Purple' }, + { value: 'orange', label: 'Orange' } + ] as SelectItem[], + onChange: fn() + }, + render: function RenderNoSearchSelect(args) { + const [value, setValue] = React.useState(); + + return ( + { + setValue(newValue as string); + args.onChange(newValue); + }} + /> + ); + } +}; + +// Select with clearable option +export const ClearableSelect: Story = { + args: { + placeholder: 'Select an option (clearable)', + clearable: true, + items: [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, + { value: 'option4', label: 'Option 4' }, + { value: 'option5', label: 'Option 5' } + ] as SelectItem[], + onChange: fn() + }, + render: function RenderClearableSelect(args) { + const [value, setValue] = React.useState('option2'); + + return ( + +
+ {clearable && selectedItem && !isFocused && ( + + )} + {!open && ( +
+ +
+ )} +
+
+ + { + e.preventDefault(); + inputRef.current?.focus(); + }}> + + {/* Hidden input that Command uses for keyboard navigation */} + + + + ); +}