diff --git a/packages/design-system/lib/components/Select/Select.stories.tsx b/packages/design-system/lib/components/Select/Select.stories.tsx new file mode 100644 index 000000000..a04fb7e75 --- /dev/null +++ b/packages/design-system/lib/components/Select/Select.stories.tsx @@ -0,0 +1,26 @@ +import 'react-material-symbols/rounded' + +import type { Meta, StoryObj } from '@storybook/react' + +import { Select } from './Select' + +const meta: Meta = { + title: 'Components/Select', + component: Select, + argTypes: {}, +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + icon: 'star', + itemIcon: 'check', + items: ['Foo', 'Bar', 'Baz'], + // items: new Array(30).fill(null).map((_, idx) => idx), + label: 'Select an item', + name: 'foo', + }, +} diff --git a/packages/design-system/lib/components/Select/Select.tsx b/packages/design-system/lib/components/Select/Select.tsx new file mode 100644 index 000000000..343c07f54 --- /dev/null +++ b/packages/design-system/lib/components/Select/Select.tsx @@ -0,0 +1,89 @@ +import { + Select as AriaSelect, + SelectValue, + Popover, + ListBox, + Button, +} from 'react-aria-components' +import { Typography } from '../Typography' + +import styles from './select.module.css' +import { MaterialIcon } from '../Icons/MaterialIcon' +import { SelectItem } from './SelectItem' + +import type { SelectProps } from './types' + +export function Select({ + name, + label, + items, + isRequired, + isDisabled, + icon, + itemIcon, +}: SelectProps) { + const iconColor = isDisabled ? 'Icon/Interactive/Disabled' : 'Icon/Default' + + return ( + + + + + {items.map((item, idx) => ( + + {typeof item === 'object' ? ( + + {item.label} + + ) : ( + + {item.toString()} + + )} + + ))} + + + + ) +} diff --git a/packages/design-system/lib/components/Select/SelectItem.tsx b/packages/design-system/lib/components/Select/SelectItem.tsx new file mode 100644 index 000000000..2e8efbc81 --- /dev/null +++ b/packages/design-system/lib/components/Select/SelectItem.tsx @@ -0,0 +1,35 @@ +import { ListBoxItem } from 'react-aria-components' +import { Typography } from '../Typography' +import styles from './select.module.css' +import { MaterialIcon } from '../Icons/MaterialIcon' +import { SelectItemProps } from './types' + +export function SelectItem({ children, icon, isDisabled }: SelectItemProps) { + return ( + + {({ isSelected }) => ( + <> + {icon ? ( + + ) +} diff --git a/packages/design-system/lib/components/Select/index.ts b/packages/design-system/lib/components/Select/index.ts new file mode 100644 index 000000000..65e5c3fbf --- /dev/null +++ b/packages/design-system/lib/components/Select/index.ts @@ -0,0 +1,2 @@ +export { Select } from './Select' +export { SelectItem } from './SelectItem' diff --git a/packages/design-system/lib/components/Select/select.module.css b/packages/design-system/lib/components/Select/select.module.css new file mode 100644 index 000000000..35c839131 --- /dev/null +++ b/packages/design-system/lib/components/Select/select.module.css @@ -0,0 +1,96 @@ +.select { + position: relative; + max-width: 300px; + + &[data-required] .label::after { + content: '*'; + } + &[data-open] .chevron { + rotate: -90deg; + } + &[data-focused] { + .button { + border: 1px solid var(--Border-Interactive-Focus); + outline: none; + } + .selectValue { + color: var(--Text-Interactive-Focus); + } + } +} + +.chevron { + rotate: 90deg; +} + +.button { + background-color: var(--Surface-UI-Fill-Default); + border: 1px solid var(--Border-Default); + border-radius: var(--Corner-radius-md); + padding: var(--Space-x1); + display: flex; + align-items: center; + gap: var(--Space-x1); + width: 100%; + height: 56px; + + &[disabled] { + color: var(--Text-Interactive-Disabled); + background-color: var(--Surface-Primary-Disabled); + + .label, + .selectValue { + color: var(--Text-Interactive-Disabled); + } + } +} + +.selectValue { + display: flex; + flex-direction: column; + gap: calc(var(--Space-x05) / 2); + align-items: flex-start; + flex: 1; + color: var(--Text-Default); +} + +.label { + color: var(--Text-Interactive-Placeholder); +} + +.popover { + background-color: var(--Surface-Primary-Default); + border-radius: var(--Corner-radius-md); + box-shadow: 0 0 14px 6px rgb(0 0 0 / 10%); + display: inline-flex; + flex-direction: column; + gap: var(--Space-x1); + overflow: auto; + padding: var(--Space-x2); + outline: none; + min-width: 280px; +} + +.listBox { + display: flex; + flex-direction: column; + gap: var(--Space-x1); + outline: none; +} + +.listBoxItem { + padding: var(--Space-x1) var(--Space-x1) var(--Space-x1) var(--Space-x15); + color: var(--Text-Default); + border-radius: var(--Corner-radius-md); + display: flex; + align-items: center; + gap: var(--Space-x1); + + &[data-focused] { + outline: none; + } + &[data-focused], + &[data-hovered] { + background-color: var(--Surface-Primary-Hover); + } +} diff --git a/packages/design-system/lib/components/Select/types.ts b/packages/design-system/lib/components/Select/types.ts new file mode 100644 index 000000000..a92502315 --- /dev/null +++ b/packages/design-system/lib/components/Select/types.ts @@ -0,0 +1,25 @@ +import { ComponentProps } from 'react' +import { Key, ListBoxItem, Select } from 'react-aria-components' +import { MaterialIconProps } from '../Icons/MaterialIcon' + +interface Item extends Record { + label: string + value: Key + isDisabled?: boolean + icon?: MaterialIconProps['icon'] +} + +export interface SelectProps extends ComponentProps { + icon?: MaterialIconProps['icon'] + itemIcon?: MaterialIconProps['icon'] + items: (Key | Item)[] + name: string + label: string + isRequired?: boolean + isDisabled?: boolean +} + +export interface SelectItemProps extends ComponentProps { + icon?: MaterialIconProps['icon'] + children: string +}