feat: migrate Textarea to shadcn-vue

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Matthew McPeak 2026-07-11 14:24:29 -04:00
parent f3002c7eb1
commit 382d76c2d7
7 changed files with 51 additions and 37 deletions

View file

@ -3,3 +3,4 @@ export { default as NychLoadingIcon } from './NychLoadingIcon.vue'
export { Button, buttonVariants } from './ui/button' export { Button, buttonVariants } from './ui/button'
export type { ButtonVariants } from './ui/button' export type { ButtonVariants } from './ui/button'
export { Input } from './ui/input' export { Input } from './ui/input'
export { Textarea } from './ui/textarea'

View file

@ -0,0 +1,28 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
defaultValue?: string | number
modelValue?: string | number
}>()
const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<textarea
v-model="modelValue"
data-slot="textarea"
:class="cn('border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 rounded-lg border bg-transparent px-2.5 py-2 text-base transition-colors focus-visible:ring-3 aria-invalid:ring-3 md:text-sm flex field-sizing-content min-h-16 w-full outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)"
/>
</template>

View file

@ -0,0 +1 @@
export { default as Textarea } from "./Textarea.vue"

View file

@ -1,6 +1,5 @@
import type { App } from 'vue' import type { App } from 'vue'
import { import {
Textarea as PrimeTextarea,
Checkbox as PrimeCheckbox, Checkbox as PrimeCheckbox,
CheckboxGroup as PrimeCheckboxGroup, CheckboxGroup as PrimeCheckboxGroup,
RadioButton as PrimeRadioButton, RadioButton as PrimeRadioButton,
@ -13,9 +12,9 @@ import {
Tag as PrimeTag, Tag as PrimeTag,
} from 'primevue' } from 'primevue'
import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine' import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine'
import { NychLoadingIcon, Button, buttonVariants, Input } from '../components' import { NychLoadingIcon, Button, buttonVariants, Input, Textarea } from '../components'
export { Button, buttonVariants, Input } export { Button, buttonVariants, Input, Textarea }
/** /**
* Build a Themeable that themes a PrimeVue component through its passthrough (`pt`) API. * Build a Themeable that themes a PrimeVue component through its passthrough (`pt`) API.
@ -59,7 +58,6 @@ const loadingIconThemeable: Themeable<typeof NychLoadingIcon> = {
const nychthemeron: ThemeLibrary = { const nychthemeron: ThemeLibrary = {
components: { components: {
LoadingIcon: loadingIconThemeable, LoadingIcon: loadingIconThemeable,
Textarea: ptThemeable(PrimeTextarea, 'textarea'),
Checkbox: ptThemeable(PrimeCheckbox, 'checkbox', ['box', 'input', 'icon']), Checkbox: ptThemeable(PrimeCheckbox, 'checkbox', ['box', 'input', 'icon']),
CheckboxGroup: ptThemeable(PrimeCheckboxGroup, 'checkbox-group'), CheckboxGroup: ptThemeable(PrimeCheckboxGroup, 'checkbox-group'),
RadioButton: ptThemeable(PrimeRadioButton, 'radio', ['box', 'input', 'icon']), RadioButton: ptThemeable(PrimeRadioButton, 'radio', ['box', 'input', 'icon']),
@ -109,7 +107,6 @@ const nychthemeron: ThemeLibrary = {
const _engine = new ThemeEngine(nychthemeron) const _engine = new ThemeEngine(nychthemeron)
export const LoadingIcon = _engine.getComponent<typeof NychLoadingIcon>('LoadingIcon') export const LoadingIcon = _engine.getComponent<typeof NychLoadingIcon>('LoadingIcon')
export const Textarea: typeof PrimeTextarea = _engine.getComponent<typeof PrimeTextarea>('Textarea')
export const Checkbox: typeof PrimeCheckbox = _engine.getComponent<typeof PrimeCheckbox>('Checkbox') export const Checkbox: typeof PrimeCheckbox = _engine.getComponent<typeof PrimeCheckbox>('Checkbox')
export const CheckboxGroup: typeof PrimeCheckboxGroup = export const CheckboxGroup: typeof PrimeCheckboxGroup =
_engine.getComponent<typeof PrimeCheckboxGroup>('CheckboxGroup') _engine.getComponent<typeof PrimeCheckboxGroup>('CheckboxGroup')
@ -129,7 +126,7 @@ export const createNychthemeron = (): {
Button: typeof Button Button: typeof Button
LoadingIcon: typeof NychLoadingIcon LoadingIcon: typeof NychLoadingIcon
Input: typeof Input Input: typeof Input
Textarea: typeof PrimeTextarea Textarea: typeof Textarea
Checkbox: typeof PrimeCheckbox Checkbox: typeof PrimeCheckbox
CheckboxGroup: typeof PrimeCheckboxGroup CheckboxGroup: typeof PrimeCheckboxGroup
RadioButton: typeof PrimeRadioButton RadioButton: typeof PrimeRadioButton

View file

@ -0,0 +1,18 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { Textarea } from '../src/lib'
describe('Textarea', () => {
it('renders a native textarea element', () => {
const wrapper = mount(Textarea)
expect(wrapper.element.tagName).toBe('TEXTAREA')
})
it('supports v-model', async () => {
const wrapper = mount(Textarea, { props: { modelValue: 'An omen' } })
expect((wrapper.element as HTMLTextAreaElement).value).toBe('An omen')
await wrapper.setValue('A crow')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['A crow'])
})
})

View file

@ -3,7 +3,6 @@ import type { App } from 'vue'
import { import {
createNychthemeron, createNychthemeron,
LoadingIcon, LoadingIcon,
Textarea,
Checkbox, Checkbox,
RadioButton, RadioButton,
CheckboxGroup, CheckboxGroup,
@ -70,7 +69,6 @@ describe('LoadingIcon', () => {
describe('themed pt components', () => { describe('themed pt components', () => {
const cases = [ const cases = [
{ name: 'Textarea', component: Textarea, base: 'nych-textarea' },
{ name: 'Checkbox', component: Checkbox, base: 'nych-checkbox' }, { name: 'Checkbox', component: Checkbox, base: 'nych-checkbox' },
{ name: 'CheckboxGroup', component: CheckboxGroup, base: 'nych-checkbox-group' }, { name: 'CheckboxGroup', component: CheckboxGroup, base: 'nych-checkbox-group' },
{ name: 'RadioButton', component: RadioButton, base: 'nych-radio' }, { name: 'RadioButton', component: RadioButton, base: 'nych-radio' },

View file

@ -10,24 +10,6 @@ const meta = {
layout: 'centered', layout: 'centered',
}, },
tags: ['autodocs'], tags: ['autodocs'],
argTypes: {
placeholder: {
control: { type: 'text' },
description: 'Placeholder text',
},
rows: {
control: { type: 'number' },
description: 'Visible rows',
},
disabled: {
control: { type: 'boolean' },
description: 'Disable the textarea',
},
invalid: {
control: { type: 'boolean' },
description: 'Mark the textarea as invalid',
},
},
} satisfies Meta<typeof NychTextarea> } satisfies Meta<typeof NychTextarea>
export default meta export default meta
@ -57,17 +39,6 @@ export const Filled: Story = {
}), }),
} }
export const Invalid: Story = {
render: () => ({
components: { NychTextarea },
setup() {
const value = ref('')
return { value }
},
template: `<NychTextarea v-model="value" rows="4" invalid placeholder="Required" style="width: 22rem;" />`,
}),
}
export const Disabled: Story = { export const Disabled: Story = {
render: () => ({ render: () => ({
components: { NychTextarea }, components: { NychTextarea },