From 4755f9ab6e5e58d13a627141235783e62eb9a5fc Mon Sep 17 00:00:00 2001 From: Matthew L McPeak Date: Sat, 11 Jul 2026 14:50:36 -0400 Subject: [PATCH] feat: migrate Select to shadcn-vue compound API Co-Authored-By: Claude Sonnet 5 --- packages/library/src/components/index.ts | 10 +++ .../src/components/ui/select/Select.vue | 19 +++++ .../components/ui/select/SelectContent.vue | 58 ++++++++++++++ .../src/components/ui/select/SelectGroup.vue | 21 +++++ .../src/components/ui/select/SelectItem.vue | 45 +++++++++++ .../components/ui/select/SelectItemText.vue | 15 ++++ .../src/components/ui/select/SelectLabel.vue | 17 ++++ .../ui/select/SelectScrollDownButton.vue | 27 +++++++ .../ui/select/SelectScrollUpButton.vue | 27 +++++++ .../components/ui/select/SelectSeparator.vue | 19 +++++ .../components/ui/select/SelectTrigger.vue | 34 ++++++++ .../src/components/ui/select/SelectValue.vue | 15 ++++ .../library/src/components/ui/select/index.ts | 11 +++ packages/library/src/index.ts | 7 ++ packages/library/src/lib/index.ts | 53 +++++++++---- packages/library/tests/Select.spec.ts | 38 +++++++++ packages/library/tests/lib.spec.ts | 8 -- packages/playground/stories/Select.stories.ts | 78 +++++++++++++------ 18 files changed, 454 insertions(+), 48 deletions(-) create mode 100644 packages/library/src/components/ui/select/Select.vue create mode 100644 packages/library/src/components/ui/select/SelectContent.vue create mode 100644 packages/library/src/components/ui/select/SelectGroup.vue create mode 100644 packages/library/src/components/ui/select/SelectItem.vue create mode 100644 packages/library/src/components/ui/select/SelectItemText.vue create mode 100644 packages/library/src/components/ui/select/SelectLabel.vue create mode 100644 packages/library/src/components/ui/select/SelectScrollDownButton.vue create mode 100644 packages/library/src/components/ui/select/SelectScrollUpButton.vue create mode 100644 packages/library/src/components/ui/select/SelectSeparator.vue create mode 100644 packages/library/src/components/ui/select/SelectTrigger.vue create mode 100644 packages/library/src/components/ui/select/SelectValue.vue create mode 100644 packages/library/src/components/ui/select/index.ts create mode 100644 packages/library/tests/Select.spec.ts diff --git a/packages/library/src/components/index.ts b/packages/library/src/components/index.ts index ee48c97..6fac0a2 100644 --- a/packages/library/src/components/index.ts +++ b/packages/library/src/components/index.ts @@ -11,3 +11,13 @@ export { CheckboxGroup } from './ui/checkbox-group' export { RadioGroup, RadioGroupItem } from './ui/radio-group' export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, CardAction } from './ui/card' export { Alert, AlertTitle, AlertDescription, AlertAction, alertVariants } from './ui/alert' +export { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + SelectGroup, + SelectLabel, + SelectSeparator, +} from './ui/select' diff --git a/packages/library/src/components/ui/select/Select.vue b/packages/library/src/components/ui/select/Select.vue new file mode 100644 index 0000000..c94bbe8 --- /dev/null +++ b/packages/library/src/components/ui/select/Select.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectContent.vue b/packages/library/src/components/ui/select/SelectContent.vue new file mode 100644 index 0000000..0b0a09c --- /dev/null +++ b/packages/library/src/components/ui/select/SelectContent.vue @@ -0,0 +1,58 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectGroup.vue b/packages/library/src/components/ui/select/SelectGroup.vue new file mode 100644 index 0000000..1662d6c --- /dev/null +++ b/packages/library/src/components/ui/select/SelectGroup.vue @@ -0,0 +1,21 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectItem.vue b/packages/library/src/components/ui/select/SelectItem.vue new file mode 100644 index 0000000..c1b1107 --- /dev/null +++ b/packages/library/src/components/ui/select/SelectItem.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectItemText.vue b/packages/library/src/components/ui/select/SelectItemText.vue new file mode 100644 index 0000000..b6700b1 --- /dev/null +++ b/packages/library/src/components/ui/select/SelectItemText.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectLabel.vue b/packages/library/src/components/ui/select/SelectLabel.vue new file mode 100644 index 0000000..a3dd751 --- /dev/null +++ b/packages/library/src/components/ui/select/SelectLabel.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectScrollDownButton.vue b/packages/library/src/components/ui/select/SelectScrollDownButton.vue new file mode 100644 index 0000000..fa40f25 --- /dev/null +++ b/packages/library/src/components/ui/select/SelectScrollDownButton.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectScrollUpButton.vue b/packages/library/src/components/ui/select/SelectScrollUpButton.vue new file mode 100644 index 0000000..235b8df --- /dev/null +++ b/packages/library/src/components/ui/select/SelectScrollUpButton.vue @@ -0,0 +1,27 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectSeparator.vue b/packages/library/src/components/ui/select/SelectSeparator.vue new file mode 100644 index 0000000..3279645 --- /dev/null +++ b/packages/library/src/components/ui/select/SelectSeparator.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectTrigger.vue b/packages/library/src/components/ui/select/SelectTrigger.vue new file mode 100644 index 0000000..3a87db5 --- /dev/null +++ b/packages/library/src/components/ui/select/SelectTrigger.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/library/src/components/ui/select/SelectValue.vue b/packages/library/src/components/ui/select/SelectValue.vue new file mode 100644 index 0000000..d5ce58b --- /dev/null +++ b/packages/library/src/components/ui/select/SelectValue.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/library/src/components/ui/select/index.ts b/packages/library/src/components/ui/select/index.ts new file mode 100644 index 0000000..96eae60 --- /dev/null +++ b/packages/library/src/components/ui/select/index.ts @@ -0,0 +1,11 @@ +export { default as Select } from "./Select.vue" +export { default as SelectContent } from "./SelectContent.vue" +export { default as SelectGroup } from "./SelectGroup.vue" +export { default as SelectItem } from "./SelectItem.vue" +export { default as SelectItemText } from "./SelectItemText.vue" +export { default as SelectLabel } from "./SelectLabel.vue" +export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue" +export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue" +export { default as SelectSeparator } from "./SelectSeparator.vue" +export { default as SelectTrigger } from "./SelectTrigger.vue" +export { default as SelectValue } from "./SelectValue.vue" diff --git a/packages/library/src/index.ts b/packages/library/src/index.ts index d0e62cd..5c54557 100644 --- a/packages/library/src/index.ts +++ b/packages/library/src/index.ts @@ -18,6 +18,13 @@ export { RadioGroupItem, Switch, Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + SelectGroup, + SelectLabel, + SelectSeparator, Card, CardHeader, CardTitle, diff --git a/packages/library/src/lib/index.ts b/packages/library/src/lib/index.ts index d44a8d3..df08540 100644 --- a/packages/library/src/lib/index.ts +++ b/packages/library/src/lib/index.ts @@ -1,6 +1,5 @@ import type { App } from 'vue' import { - Select as PrimeSelect, Dialog as PrimeDialog, } from 'primevue' import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine' @@ -29,6 +28,14 @@ import { AlertDescription, AlertAction, alertVariants, + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + SelectGroup, + SelectLabel, + SelectSeparator, } from '../components' export { @@ -55,6 +62,14 @@ export { AlertDescription, AlertAction, alertVariants, + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + SelectGroup, + SelectLabel, + SelectSeparator, } /** @@ -99,18 +114,6 @@ const loadingIconThemeable: Themeable = { const nychthemeron: ThemeLibrary = { components: { LoadingIcon: loadingIconThemeable, - Select: ptThemeable(PrimeSelect, 'select', [ - 'label', - 'dropdown', - 'dropdownIcon', - 'overlay', - 'header', - 'listContainer', - 'list', - 'option', - 'optionLabel', - 'emptyMessage', - ]), Dialog: ptThemeable(PrimeDialog, 'dialog', [ 'mask', 'header', @@ -125,7 +128,6 @@ const nychthemeron: ThemeLibrary = { const _engine = new ThemeEngine(nychthemeron) export const LoadingIcon = _engine.getComponent('LoadingIcon') -export const Select: typeof PrimeSelect = _engine.getComponent('Select') export const Dialog: typeof PrimeDialog = _engine.getComponent('Dialog') export const createNychthemeron = (): { @@ -138,7 +140,14 @@ export const createNychthemeron = (): { RadioGroup: typeof RadioGroup RadioGroupItem: typeof RadioGroupItem Switch: typeof Switch - Select: typeof PrimeSelect + Select: typeof Select + SelectTrigger: typeof SelectTrigger + SelectValue: typeof SelectValue + SelectContent: typeof SelectContent + SelectItem: typeof SelectItem + SelectGroup: typeof SelectGroup + SelectLabel: typeof SelectLabel + SelectSeparator: typeof SelectSeparator Card: typeof Card CardHeader: typeof CardHeader CardTitle: typeof CardTitle @@ -164,6 +173,13 @@ export const createNychthemeron = (): { RadioGroupItem, Switch, Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + SelectGroup, + SelectLabel, + SelectSeparator, Card, CardHeader, CardTitle, @@ -188,6 +204,13 @@ export const createNychthemeron = (): { app.component('NychRadioGroupItem', RadioGroupItem) app.component('NychSwitch', Switch) app.component('NychSelect', Select) + app.component('NychSelectTrigger', SelectTrigger) + app.component('NychSelectValue', SelectValue) + app.component('NychSelectContent', SelectContent) + app.component('NychSelectItem', SelectItem) + app.component('NychSelectGroup', SelectGroup) + app.component('NychSelectLabel', SelectLabel) + app.component('NychSelectSeparator', SelectSeparator) app.component('NychCard', Card) app.component('NychCardHeader', CardHeader) app.component('NychCardTitle', CardTitle) diff --git a/packages/library/tests/Select.spec.ts b/packages/library/tests/Select.spec.ts new file mode 100644 index 0000000..2ba1043 --- /dev/null +++ b/packages/library/tests/Select.spec.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest' +import { h } from 'vue' +import { mount } from '@vue/test-utils' +import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../src/lib' + +const RIVERS = ['Styx', 'Acheron', 'Cocytus'] + +function mountSelect(modelValue: string | undefined) { + return mount(Select, { + props: { modelValue }, + slots: { + default: () => [ + h(SelectTrigger, undefined, { + default: () => h(SelectValue, { placeholder: 'Choose a river' }), + }), + h(SelectContent, undefined, { + default: () => RIVERS.map((r) => h(SelectItem, { value: r }, { default: () => r })), + }), + ], + }, + attachTo: document.body, + }) +} + +describe('Select', () => { + it('renders the trigger with the placeholder when no value is selected', () => { + const wrapper = mountSelect(undefined) + expect(wrapper.text()).toContain('Choose a river') + }) + + it('shows the selected value in the trigger once the item has rendered', async () => { + const wrapper = mountSelect('Styx') + // SelectValue resolves its display label from the matching SelectItem, which only + // mounts once the dropdown has opened at least once. + await wrapper.get('button[role="combobox"]').trigger('click') + expect(wrapper.text()).toContain('Styx') + }) +}) diff --git a/packages/library/tests/lib.spec.ts b/packages/library/tests/lib.spec.ts index c6aa745..dec9161 100644 --- a/packages/library/tests/lib.spec.ts +++ b/packages/library/tests/lib.spec.ts @@ -3,7 +3,6 @@ import type { App } from 'vue' import { createNychthemeron, LoadingIcon, - Select, Dialog, } from '../src/index' @@ -61,7 +60,6 @@ describe('LoadingIcon', () => { describe('themed pt components', () => { const cases = [ - { name: 'Select', component: Select, base: 'nych-select' }, { name: 'Dialog', component: Dialog, base: 'nych-dialog' }, ] @@ -81,10 +79,4 @@ describe('themed pt components', () => { expect(result).not.toHaveProperty('injectedStyles') expect(result).not.toHaveProperty('stylePlug') }) - - it('Select themes its overlay and option sections', () => { - const result = callSetup(Select, { pt: {} }) - expect(result.pt?.overlay?.class).toContain('nych-select-overlay') - expect(result.pt?.option?.class).toContain('nych-select-option') - }) }) diff --git a/packages/playground/stories/Select.stories.ts b/packages/playground/stories/Select.stories.ts index 7ed9872..8c5f21e 100644 --- a/packages/playground/stories/Select.stories.ts +++ b/packages/playground/stories/Select.stories.ts @@ -1,7 +1,13 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite' import { ref } from 'vue' -import { Select as NychSelect } from '@nychthemeron/library' +import { + Select as NychSelect, + SelectTrigger as NychSelectTrigger, + SelectValue as NychSelectValue, + SelectContent as NychSelectContent, + SelectItem as NychSelectItem, +} from '@nychthemeron/library' const meta = { title: 'Components/Select', @@ -10,16 +16,6 @@ const meta = { layout: 'centered', }, tags: ['autodocs'], - argTypes: { - placeholder: { - control: { type: 'text' }, - description: 'Placeholder text', - }, - disabled: { - control: { type: 'boolean' }, - description: 'Disable the select', - }, - }, } satisfies Meta export default meta @@ -29,42 +25,65 @@ const RIVERS = ['Styx', 'Acheron', 'Cocytus', 'Phlegethon', 'Lethe'] export const Default: Story = { render: () => ({ - components: { NychSelect }, + components: { NychSelect, NychSelectTrigger, NychSelectValue, NychSelectContent, NychSelectItem }, setup() { - const river = ref(null) + const river = ref() return { river, RIVERS } }, - template: ``, + template: ` + + + + + + {{ r }} + + + `, }), } export const Selected: Story = { render: () => ({ - components: { NychSelect }, + components: { NychSelect, NychSelectTrigger, NychSelectValue, NychSelectContent, NychSelectItem }, setup() { const river = ref('Lethe') return { river, RIVERS } }, - template: ``, + template: ` + + + + + + {{ r }} + + + `, }), } export const Disabled: Story = { render: () => ({ - components: { NychSelect }, - setup() { - const river = ref('Styx') - return { river, RIVERS } - }, - template: ``, + components: { NychSelect, NychSelectTrigger, NychSelectValue, NychSelectContent, NychSelectItem }, + template: ` + + + + + + {{ r }} + + + `, }), } export const ObjectOptions: Story = { render: () => ({ - components: { NychSelect }, + components: { NychSelect, NychSelectTrigger, NychSelectValue, NychSelectContent, NychSelectItem }, setup() { - const god = ref(null) + const god = ref() const gods = [ { name: 'Apollo', domain: 'apollo' }, { name: 'Hades', domain: 'hades' }, @@ -72,6 +91,15 @@ export const ObjectOptions: Story = { ] return { god, gods } }, - template: ``, + template: ` + + + + + + {{ g.name }} + + + `, }), }