feat: migrate InputText to shadcn-vue Input

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Matthew McPeak 2026-07-11 14:22:14 -04:00
parent 3a8dd109bb
commit f3002c7eb1
11 changed files with 113 additions and 105 deletions

View file

@ -39,6 +39,7 @@
"dependencies": {
"@lucide/vue": "^1.24.0",
"@tailwindcss/vite": "^4.3.2",
"@vueuse/core": "^14.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"reka-ui": "^2.10.1",

View file

@ -42,6 +42,7 @@
"dependencies": {
"@lucide/vue": "^1.24.0",
"@tailwindcss/vite": "^4.3.2",
"@vueuse/core": "^14.3.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"reka-ui": "^2.10.1",

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ export {
createNychthemeron,
Button,
LoadingIcon,
InputText,
Input,
Textarea,
Checkbox,
CheckboxGroup,

View file

@ -1,6 +1,5 @@
import type { App } from 'vue'
import {
InputText as PrimeInputText,
Textarea as PrimeTextarea,
Checkbox as PrimeCheckbox,
CheckboxGroup as PrimeCheckboxGroup,
@ -14,9 +13,9 @@ import {
Tag as PrimeTag,
} from 'primevue'
import { ThemeLibrary, type Themeable, ThemeEngine, type LibComponent, type Key } from '../engine'
import { NychLoadingIcon, Button, buttonVariants } from '../components'
import { NychLoadingIcon, Button, buttonVariants, Input } from '../components'
export { Button, buttonVariants }
export { Button, buttonVariants, Input }
/**
* Build a Themeable that themes a PrimeVue component through its passthrough (`pt`) API.
@ -60,7 +59,6 @@ const loadingIconThemeable: Themeable<typeof NychLoadingIcon> = {
const nychthemeron: ThemeLibrary = {
components: {
LoadingIcon: loadingIconThemeable,
InputText: ptThemeable(PrimeInputText, 'input'),
Textarea: ptThemeable(PrimeTextarea, 'textarea'),
Checkbox: ptThemeable(PrimeCheckbox, 'checkbox', ['box', 'input', 'icon']),
CheckboxGroup: ptThemeable(PrimeCheckboxGroup, 'checkbox-group'),
@ -111,8 +109,6 @@ const nychthemeron: ThemeLibrary = {
const _engine = new ThemeEngine(nychthemeron)
export const LoadingIcon = _engine.getComponent<typeof NychLoadingIcon>('LoadingIcon')
export const InputText: typeof PrimeInputText =
_engine.getComponent<typeof PrimeInputText>('InputText')
export const Textarea: typeof PrimeTextarea = _engine.getComponent<typeof PrimeTextarea>('Textarea')
export const Checkbox: typeof PrimeCheckbox = _engine.getComponent<typeof PrimeCheckbox>('Checkbox')
export const CheckboxGroup: typeof PrimeCheckboxGroup =
@ -132,7 +128,7 @@ export const Tag: typeof PrimeTag = _engine.getComponent<typeof PrimeTag>('Tag')
export const createNychthemeron = (): {
Button: typeof Button
LoadingIcon: typeof NychLoadingIcon
InputText: typeof PrimeInputText
Input: typeof Input
Textarea: typeof PrimeTextarea
Checkbox: typeof PrimeCheckbox
CheckboxGroup: typeof PrimeCheckboxGroup
@ -148,7 +144,7 @@ export const createNychthemeron = (): {
} => ({
Button,
LoadingIcon,
InputText,
Input,
Textarea,
Checkbox,
CheckboxGroup,
@ -163,7 +159,7 @@ export const createNychthemeron = (): {
install(app: App) {
app.component('NychButton', Button)
app.component('NychLoadingIcon', LoadingIcon)
app.component('NychInputText', InputText)
app.component('NychInput', Input)
app.component('NychTextarea', Textarea)
app.component('NychCheckbox', Checkbox)
app.component('NychCheckboxGroup', CheckboxGroup)

View file

@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { Input } from '../src/lib'
describe('Input', () => {
it('renders a native input element', () => {
const wrapper = mount(Input)
expect(wrapper.element.tagName).toBe('INPUT')
})
it('supports v-model', async () => {
const wrapper = mount(Input, { props: { modelValue: 'Styx' } })
expect((wrapper.element as HTMLInputElement).value).toBe('Styx')
await wrapper.setValue('Lethe')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['Lethe'])
})
it('applies the disabled attribute', () => {
const wrapper = mount(Input, { attrs: { disabled: true } })
expect(wrapper.attributes('disabled')).toBeDefined()
})
})

View file

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

View file

@ -0,0 +1,49 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import { Input as NychInput } from '@nychthemeron/library'
const meta = {
title: 'Components/Input',
component: NychInput,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof NychInput>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: () => ({
components: { NychInput },
setup() {
const value = ref('')
return { value }
},
template: `<NychInput v-model="value" placeholder="Speak, traveller…" style="width: 18rem;" />`,
}),
}
export const Filled: Story = {
render: () => ({
components: { NychInput },
setup() {
const value = ref('Charon')
return { value }
},
template: `<NychInput v-model="value" style="width: 18rem;" />`,
}),
}
export const Disabled: Story = {
render: () => ({
components: { NychInput },
setup() {
const value = ref('Sealed')
return { value }
},
template: `<NychInput v-model="value" disabled style="width: 18rem;" />`,
}),
}

View file

@ -1,93 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import { InputText as NychInputText } from '@nychthemeron/library'
const meta = {
title: 'Components/InputText',
component: NychInputText,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
size: {
control: { type: 'select' },
options: ['small', undefined, 'large'],
description: 'Input size',
},
placeholder: {
control: { type: 'text' },
description: 'Placeholder text',
},
disabled: {
control: { type: 'boolean' },
description: 'Disable the input',
},
invalid: {
control: { type: 'boolean' },
description: 'Mark the input as invalid',
},
},
} satisfies Meta<typeof NychInputText>
export default meta
type Story = StoryObj<typeof meta>
const modelTemplate = (attrs: string) => ({
components: { NychInputText },
setup() {
const value = ref('')
return { value }
},
template: `<NychInputText v-model="value" ${attrs} />`,
})
export const Default: Story = {
render: () => modelTemplate('placeholder="Speak, traveller…"'),
}
export const Filled: Story = {
render: () => ({
components: { NychInputText },
setup() {
const value = ref('Charon')
return { value }
},
template: `<NychInputText v-model="value" />`,
}),
}
export const Invalid: Story = {
render: () => modelTemplate('placeholder="Required" invalid'),
}
export const Disabled: Story = {
render: () => ({
components: { NychInputText },
setup() {
const value = ref('Sealed')
return { value }
},
template: `<NychInputText v-model="value" disabled />`,
}),
}
export const Sizes: Story = {
render: () => ({
components: { NychInputText },
setup() {
const a = ref('')
const b = ref('')
const c = ref('')
return { a, b, c }
},
template: `
<div style="display: flex; flex-direction: column; gap: 1rem; width: 16rem;">
<NychInputText v-model="a" size="small" placeholder="Small" />
<NychInputText v-model="b" placeholder="Normal" />
<NychInputText v-model="c" size="large" placeholder="Large" />
</div>
`,
}),
}