diff --git a/packages/react/src/components/combo-box/ComboBox.stories.tsx b/packages/react/src/components/combo-box/ComboBox.stories.tsx new file mode 100644 index 0000000..2e87007 --- /dev/null +++ b/packages/react/src/components/combo-box/ComboBox.stories.tsx @@ -0,0 +1,82 @@ +import type { ComboBoxProps } from '@/components/combo-box' +import type { Meta, StoryFn } from '@storybook/react' + +import { combobox } from '@giantnodes/theme' + +import { Typography } from '../typography' + +import { Avatar } from '@/components/avatar' +import { ComboBox } from '@/components/combo-box' +import { Input } from '@/components/input' + +const Component: Meta = { + title: 'Components/ComboBox', + component: ComboBox, + argTypes: { + size: { + control: { type: 'select' }, + }, + status: { + control: { type: 'select' }, + }, + variant: { + control: { type: 'select' }, + }, + }, +} + +const defaultProps = { + ...combobox.defaultVariants, +} + +const people = [ + { id: 1, name: 'Raymond Chappell' }, + { id: 2, name: 'David Bolden' }, + { id: 3, name: 'Charles Marcano' }, + { id: 4, name: 'Jeanne Livesay' }, + { id: 5, name: 'David Bolden' }, +] + +export const Default: StoryFn> = (args) => ( + + + + + + + {(item) => {item.name}} + + +) + +Default.args = { + ...defaultProps, +} + +export const Custom: StoryFn> = (args) => ( + + + + + + + + {(item) => ( + + + + + + {item.name} + + )} + + + +) + +Custom.args = { + ...defaultProps, +} + +export default Component diff --git a/packages/react/src/components/combo-box/ComboBox.tsx b/packages/react/src/components/combo-box/ComboBox.tsx new file mode 100644 index 0000000..91ee6db --- /dev/null +++ b/packages/react/src/components/combo-box/ComboBox.tsx @@ -0,0 +1,60 @@ +import type * as Polymophic from '@/utilities/polymorphic' +import type { ComboBoxVariantProps } from '@giantnodes/theme' +import type { ComboBoxProps } from 'react-aria-components' + +import React from 'react' +import { ComboBox } from 'react-aria-components' + +import ComboBoxItem from '@/components/combo-box/ComboBoxItem' +import ComboBoxList from '@/components/combo-box/ComboBoxList' +import ComboBoxPopover from '@/components/combo-box/ComboBoxPopover' +import { ComboBoxContext, useComboBox } from '@/components/combo-box/use-combo-box.hook' + +const __ELEMENT_TYPE__ = 'div' + +type ComponentOwnProps = ComboBoxProps & ComboBoxVariantProps + +type ComponentProps< + TData extends object, + TElement extends React.ElementType = typeof __ELEMENT_TYPE__, +> = Polymophic.ComponentPropsWithRef> + +type ComponentType = ( + props: ComponentProps +) => React.ReactNode + +const Component: ComponentType = React.forwardRef( + ( + props: ComponentProps, + ref: Polymophic.Ref + ) => { + const { as, children, className, size, status, ...rest } = props + + const Element = as || ComboBox + + const context = useComboBox({ size, status }) + + const component = React.useMemo>( + () => ({ + className: context.slots.combobox({ className: className?.toString() }), + ...rest, + }), + [className, context.slots, rest] + ) + + return ( + + + {children} + + + ) + } +) + +export type { ComponentOwnProps as ComboBoxOwnProps, ComponentProps as ComboBoxProps } +export default Object.assign(Component, { + Popover: ComboBoxPopover, + List: ComboBoxList, + Item: ComboBoxItem, +}) diff --git a/packages/react/src/components/combo-box/ComboBoxItem.tsx b/packages/react/src/components/combo-box/ComboBoxItem.tsx new file mode 100644 index 0000000..12710c9 --- /dev/null +++ b/packages/react/src/components/combo-box/ComboBoxItem.tsx @@ -0,0 +1,50 @@ +import type * as Polymophic from '@/utilities/polymorphic' +import type { ListBoxItemProps } from 'react-aria-components' + +import React from 'react' +import { ListBoxItem } from 'react-aria-components' + +import { useComboBoxContext } from '@/components/combo-box/use-combo-box.hook' + +const __ELEMENT_TYPE__ = 'div' + +type ComponentOwnProps = ListBoxItemProps + +type ComponentProps = Polymophic.ComponentPropsWithRef< + TElement, + ComponentOwnProps +> + +type ComponentType = ( + props: ComponentProps +) => React.ReactNode + +const Component: ComponentType = React.forwardRef( + ( + props: ComponentProps, + ref: Polymophic.Ref + ) => { + const { as, children, className, ...rest } = props + + const Element = as || ListBoxItem + + const { slots } = useComboBoxContext() + + const component = React.useMemo( + () => ({ + className: slots.item({ className: className?.toString() }), + ...rest, + }), + [className, rest, slots] + ) + + return ( + + {children} + + ) + } +) + +export type { ComponentOwnProps as ComboBoxItemOwnProps, ComponentProps as ComboBoxItemProps } +export default Component diff --git a/packages/react/src/components/combo-box/ComboBoxList.tsx b/packages/react/src/components/combo-box/ComboBoxList.tsx new file mode 100644 index 0000000..008c2ea --- /dev/null +++ b/packages/react/src/components/combo-box/ComboBoxList.tsx @@ -0,0 +1,50 @@ +import type * as Polymophic from '@/utilities/polymorphic' +import type { ListBoxProps } from 'react-aria-components' + +import React from 'react' +import { ListBox } from 'react-aria-components' + +import { useComboBoxContext } from '@/components/combo-box/use-combo-box.hook' + +const __ELEMENT_TYPE__ = 'div' + +type ComponentOwnProps = ListBoxProps + +type ComponentProps< + TData extends object, + TElement extends React.ElementType = typeof __ELEMENT_TYPE__, +> = Polymophic.ComponentPropsWithRef> + +type ComponentType = ( + props: ComponentProps +) => React.ReactNode + +const Component: ComponentType = React.forwardRef( + ( + props: ComponentProps, + ref: Polymophic.Ref + ) => { + const { as, children, className, ...rest } = props + + const Element = as || ListBox + + const { slots } = useComboBoxContext() + + const component = React.useMemo>( + () => ({ + className: slots.list({ className: className?.toString() }), + ...rest, + }), + [className, rest, slots] + ) + + return ( + + {children} + + ) + } +) + +export type { ComponentOwnProps as ComboBoxListOwnProps, ComponentProps as ComboBoxListProps } +export default Component diff --git a/packages/react/src/components/combo-box/ComboBoxPopover.tsx b/packages/react/src/components/combo-box/ComboBoxPopover.tsx new file mode 100644 index 0000000..dbdafe8 --- /dev/null +++ b/packages/react/src/components/combo-box/ComboBoxPopover.tsx @@ -0,0 +1,50 @@ +import type * as Polymophic from '@/utilities/polymorphic' +import type { PopoverProps } from 'react-aria-components' + +import React from 'react' +import { Popover } from 'react-aria-components' + +import { useComboBoxContext } from '@/components/combo-box/use-combo-box.hook' + +const __ELEMENT_TYPE__ = 'div' + +type ComponentOwnProps = PopoverProps + +type ComponentProps = Polymophic.ComponentPropsWithRef< + TElement, + ComponentOwnProps +> + +type ComponentType = ( + props: ComponentProps +) => React.ReactNode + +const Component: ComponentType = React.forwardRef( + ( + props: ComponentProps, + ref: Polymophic.Ref + ) => { + const { as, children, className, ...rest } = props + + const Element = as || Popover + + const { slots } = useComboBoxContext() + + const component = React.useMemo( + () => ({ + className: slots.popover({ className: className?.toString() }), + ...rest, + }), + [className, rest, slots] + ) + + return ( + + {children} + + ) + } +) + +export type { ComponentOwnProps as ComboBoxPopoverOwnProps, ComponentProps as ComboBoxPopoverProps } +export default Component diff --git a/packages/react/src/components/combo-box/index.ts b/packages/react/src/components/combo-box/index.ts new file mode 100644 index 0000000..45d09a9 --- /dev/null +++ b/packages/react/src/components/combo-box/index.ts @@ -0,0 +1,6 @@ +export type * from '@/components/combo-box/ComboBox' +export type * from '@/components/combo-box/ComboBoxItem' +export type * from '@/components/combo-box/ComboBoxList' +export type * from '@/components/combo-box/ComboBoxPopover' + +export { default as ComboBox } from '@/components/combo-box/ComboBox' diff --git a/packages/react/src/components/combo-box/use-combo-box.hook.ts b/packages/react/src/components/combo-box/use-combo-box.hook.ts new file mode 100644 index 0000000..56aa909 --- /dev/null +++ b/packages/react/src/components/combo-box/use-combo-box.hook.ts @@ -0,0 +1,26 @@ +import type { ComboBoxVariantProps } from '@giantnodes/theme' + +import { combobox } from '@giantnodes/theme' +import React from 'react' + +import { createContext } from '@/utilities/context' + +type UseComboBoxProps = ComboBoxVariantProps + +type UseComboBoxReturn = ReturnType + +export const useComboBox = (props: UseComboBoxProps) => { + const { size, status } = props + + const slots = React.useMemo(() => combobox({ size, status }), [size, status]) + + return { + slots, + } +} + +export const [ComboBoxContext, useComboBoxContext] = createContext({ + name: 'ComboBoxContext', + strict: true, + errorMessage: 'useComboBox: `context` is undefined. Seems you forgot to wrap component within ', +}) diff --git a/packages/theme/src/components/combo-box.ts b/packages/theme/src/components/combo-box.ts new file mode 100644 index 0000000..7f894e4 --- /dev/null +++ b/packages/theme/src/components/combo-box.ts @@ -0,0 +1,63 @@ +import type { VariantProps } from 'tailwind-variants' + +import { tv } from 'tailwind-variants' + +export const combobox = tv({ + slots: { + combobox: ['group flex flex-col gap-1'], + popover: ['bg-foreground', 'border border-solid border-partition', 'rounded-md', 'w-[--trigger-width]'], + list: ['flex flex-col gap-1', 'p-1', 'outline-none'], + item: [ + 'flex tems-center', + 'px-2 py-1', + 'rounded-md', + 'cursor-pointer', + 'outline-none', + 'text-content', + 'overflow-hidden', + 'disabled:opacity-50 disabled:cursor-default', + ], + }, + variants: { + size: { + xs: { + item: ['text-xs'], + }, + sm: { + item: ['text-sm'], + }, + md: { + item: ['text-base'], + }, + lg: { + item: ['text-lg'], + }, + }, + status: { + neutral: { + item: ['hover:bg-middleground'], + }, + brand: { + item: ['hover:bg-brand/20'], + }, + success: { + item: ['hover:bg-success/20'], + }, + info: { + item: ['hover:bg-info/20'], + }, + warning: { + item: ['hover:bg-warning/20'], + }, + danger: { + item: ['hover:bg-danger/20'], + }, + }, + }, + defaultVariants: { + size: 'md', + status: 'neutral', + }, +}) + +export type ComboBoxVariantProps = VariantProps diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts index 8c7e384..886cd8e 100644 --- a/packages/theme/src/index.ts +++ b/packages/theme/src/index.ts @@ -7,6 +7,7 @@ export * from '@/components/button' export * from '@/components/card' export * from '@/components/checkbox' export * from '@/components/chip' +export * from '@/components/combo-box' export * from '@/components/dialog' export * from '@/components/divider' export * from '@/components/form'