From 7a1a2c1abb0b3b0f45ce668d5503640f800eaf2f Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Tue, 20 Jan 2026 23:17:53 -0800 Subject: [PATCH] feat(ui): add shadcn-vue Select components (#8205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds shadcn-vue Select components built on Reka UI primitives with design system styling. ## Changes **New Components** (`src/components/ui/select/`): - `Select.vue` - Root wrapper using `SelectRoot` from Reka UI - `SelectTrigger.vue` - Styled trigger with chevron icon - `SelectContent.vue` - Dropdown content with scroll buttons, z-index 3000 for PrimeVue dialog compatibility - `SelectItem.vue` - Individual option with check icon - `SelectGroup.vue`, `SelectLabel.vue`, `SelectSeparator.vue` - Grouping support - `SelectScrollUpButton.vue`, `SelectScrollDownButton.vue` - Overflow navigation - `SelectValue.vue` - Placeholder/value display **Styling**: - Uses design tokens (`bg-secondary-background`, `text-muted-foreground`, `border-border-default`) - Iconify icons via `icon-[lucide--*]` classes - Smooth transitions and focus states **Documentation**: - Comprehensive Storybook stories covering all variants - `AGENTS.md` with component creation guidelines ## Testing - [x] Storybook stories work correctly - [x] Components build without errors ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8205-feat-ui-add-shadcn-vue-Select-components-2ef6d73d365081fd994ddb1123c063e7) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/components/ui/AGENTS.md | 19 ++ src/components/ui/select/Select.stories.ts | 261 ++++++++++++++++++ src/components/ui/select/Select.vue | 16 ++ src/components/ui/select/SelectContent.vue | 73 +++++ src/components/ui/select/SelectGroup.vue | 17 ++ src/components/ui/select/SelectItem.vue | 37 +++ src/components/ui/select/SelectLabel.vue | 25 ++ .../ui/select/SelectScrollDownButton.vue | 27 ++ .../ui/select/SelectScrollUpButton.vue | 27 ++ src/components/ui/select/SelectSeparator.vue | 18 ++ src/components/ui/select/SelectTrigger.vue | 36 +++ src/components/ui/select/SelectValue.vue | 12 + 12 files changed, 568 insertions(+) create mode 100644 src/components/ui/AGENTS.md create mode 100644 src/components/ui/select/Select.stories.ts create mode 100644 src/components/ui/select/Select.vue create mode 100644 src/components/ui/select/SelectContent.vue create mode 100644 src/components/ui/select/SelectGroup.vue create mode 100644 src/components/ui/select/SelectItem.vue create mode 100644 src/components/ui/select/SelectLabel.vue create mode 100644 src/components/ui/select/SelectScrollDownButton.vue create mode 100644 src/components/ui/select/SelectScrollUpButton.vue create mode 100644 src/components/ui/select/SelectSeparator.vue create mode 100644 src/components/ui/select/SelectTrigger.vue create mode 100644 src/components/ui/select/SelectValue.vue diff --git a/src/components/ui/AGENTS.md b/src/components/ui/AGENTS.md new file mode 100644 index 000000000..53b9979b7 --- /dev/null +++ b/src/components/ui/AGENTS.md @@ -0,0 +1,19 @@ +# UI Component Guidelines + +## Adding New Components + +```bash +pnpm dlx shadcn-vue@latest add --yes +``` + +After adding, create `ComponentName.stories.ts` with Default, Disabled, and variant stories. + +## Reka UI Wrapper Components + +- Use reactive props destructuring with rest: `const { class: className, ...restProps } = defineProps()` +- Use `useForwardProps(restProps)` for prop forwarding, or `computed()` if adding defaults +- Import siblings directly (`./Component.vue`), not from barrel (`'.'`) +- Use `cn()` for class merging with `className` +- Use Iconify icons: `` +- Use design tokens: `bg-secondary-background`, `text-muted-foreground`, `border-border-default` +- Tailwind 4 CSS variables use parentheses: `h-(--my-var)` not `h-[--my-var]` diff --git a/src/components/ui/select/Select.stories.ts b/src/components/ui/select/Select.stories.ts new file mode 100644 index 000000000..ba2a37e48 --- /dev/null +++ b/src/components/ui/select/Select.stories.ts @@ -0,0 +1,261 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import Select from './Select.vue' +import SelectContent from './SelectContent.vue' +import SelectGroup from './SelectGroup.vue' +import SelectItem from './SelectItem.vue' +import SelectLabel from './SelectLabel.vue' +import SelectSeparator from './SelectSeparator.vue' +import SelectTrigger from './SelectTrigger.vue' +import SelectValue from './SelectValue.vue' + +const meta = { + title: 'Components/Select', + component: Select, + tags: ['autodocs'], + argTypes: { + modelValue: { + control: 'text', + description: 'Selected value' + }, + disabled: { + control: 'boolean', + description: 'When true, disables the select' + }, + 'onUpdate:modelValue': { action: 'update:modelValue' } + } +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: (args) => ({ + components: { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue + }, + setup() { + const value = ref(args.modelValue || '') + return { value, args } + }, + template: ` + +
+ Selected: {{ value || 'None' }} +
+ ` + }), + args: { + disabled: false + } +} + +export const WithPlaceholder: Story = { + render: (args) => ({ + components: { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue + }, + setup() { + const value = ref('') + return { value, args } + }, + template: ` + + ` + }), + args: { + disabled: false + } +} + +export const Disabled: Story = { + render: (args) => ({ + components: { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue + }, + setup() { + const value = ref('apple') + return { value, args } + }, + template: ` + + ` + }) +} + +export const WithGroups: Story = { + render: (args) => ({ + components: { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectSeparator, + SelectTrigger, + SelectValue + }, + setup() { + const value = ref('') + return { value, args } + }, + template: ` + +
+ Selected: {{ value || 'None' }} +
+ ` + }), + args: { + disabled: false + } +} + +export const Scrollable: Story = { + render: (args) => ({ + components: { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue + }, + setup() { + const value = ref('') + const items = Array.from({ length: 20 }, (_, i) => ({ + value: `item-${i + 1}`, + label: `Option ${i + 1}` + })) + return { value, items, args } + }, + template: ` + + ` + }), + args: { + disabled: false + } +} + +export const CustomWidth: Story = { + render: (args) => ({ + components: { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue + }, + setup() { + const value = ref('') + return { value, args } + }, + template: ` +
+ + + +
+ ` + }), + args: { + disabled: false + } +} diff --git a/src/components/ui/select/Select.vue b/src/components/ui/select/Select.vue new file mode 100644 index 000000000..f685b3189 --- /dev/null +++ b/src/components/ui/select/Select.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/components/ui/select/SelectContent.vue b/src/components/ui/select/SelectContent.vue new file mode 100644 index 000000000..a88e26b9f --- /dev/null +++ b/src/components/ui/select/SelectContent.vue @@ -0,0 +1,73 @@ + + + diff --git a/src/components/ui/select/SelectGroup.vue b/src/components/ui/select/SelectGroup.vue new file mode 100644 index 000000000..11f3da9f6 --- /dev/null +++ b/src/components/ui/select/SelectGroup.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ui/select/SelectItem.vue b/src/components/ui/select/SelectItem.vue new file mode 100644 index 000000000..4edeeb3ca --- /dev/null +++ b/src/components/ui/select/SelectItem.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/components/ui/select/SelectLabel.vue b/src/components/ui/select/SelectLabel.vue new file mode 100644 index 000000000..bafe45da9 --- /dev/null +++ b/src/components/ui/select/SelectLabel.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/components/ui/select/SelectScrollDownButton.vue b/src/components/ui/select/SelectScrollDownButton.vue new file mode 100644 index 000000000..1b1dc1a27 --- /dev/null +++ b/src/components/ui/select/SelectScrollDownButton.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/ui/select/SelectScrollUpButton.vue b/src/components/ui/select/SelectScrollUpButton.vue new file mode 100644 index 000000000..ee1ef9263 --- /dev/null +++ b/src/components/ui/select/SelectScrollUpButton.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/ui/select/SelectSeparator.vue b/src/components/ui/select/SelectSeparator.vue new file mode 100644 index 000000000..37947fd0d --- /dev/null +++ b/src/components/ui/select/SelectSeparator.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/components/ui/select/SelectTrigger.vue b/src/components/ui/select/SelectTrigger.vue new file mode 100644 index 000000000..768048ab1 --- /dev/null +++ b/src/components/ui/select/SelectTrigger.vue @@ -0,0 +1,36 @@ + + + diff --git a/src/components/ui/select/SelectValue.vue b/src/components/ui/select/SelectValue.vue new file mode 100644 index 000000000..4ffa580ca --- /dev/null +++ b/src/components/ui/select/SelectValue.vue @@ -0,0 +1,12 @@ + + +