feat: migrate InputText to shadcn-vue Input
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
3a8dd109bb
commit
f3002c7eb1
11 changed files with 113 additions and 105 deletions
1
bun.lock
1
bun.lock
|
|
@ -39,6 +39,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/vue": "^1.24.0",
|
"@lucide/vue": "^1.24.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
|
"@vueuse/core": "^14.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"reka-ui": "^2.10.1",
|
"reka-ui": "^2.10.1",
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/vue": "^1.24.0",
|
"@lucide/vue": "^1.24.0",
|
||||||
"@tailwindcss/vite": "^4.3.2",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
|
"@vueuse/core": "^14.3.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"reka-ui": "^2.10.1",
|
"reka-ui": "^2.10.1",
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@
|
||||||
export { default as NychLoadingIcon } from './NychLoadingIcon.vue'
|
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'
|
||||||
|
|
|
||||||
31
packages/library/src/components/ui/input/Input.vue
Normal file
31
packages/library/src/components/ui/input/Input.vue
Normal 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>
|
||||||
1
packages/library/src/components/ui/input/index.ts
Normal file
1
packages/library/src/components/ui/input/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export { default as Input } from "./Input.vue"
|
||||||
|
|
@ -10,7 +10,7 @@ export {
|
||||||
createNychthemeron,
|
createNychthemeron,
|
||||||
Button,
|
Button,
|
||||||
LoadingIcon,
|
LoadingIcon,
|
||||||
InputText,
|
Input,
|
||||||
Textarea,
|
Textarea,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
import {
|
import {
|
||||||
InputText as PrimeInputText,
|
|
||||||
Textarea as PrimeTextarea,
|
Textarea as PrimeTextarea,
|
||||||
Checkbox as PrimeCheckbox,
|
Checkbox as PrimeCheckbox,
|
||||||
CheckboxGroup as PrimeCheckboxGroup,
|
CheckboxGroup as PrimeCheckboxGroup,
|
||||||
|
|
@ -14,9 +13,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 } 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.
|
* 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 = {
|
const nychthemeron: ThemeLibrary = {
|
||||||
components: {
|
components: {
|
||||||
LoadingIcon: loadingIconThemeable,
|
LoadingIcon: loadingIconThemeable,
|
||||||
InputText: ptThemeable(PrimeInputText, 'input'),
|
|
||||||
Textarea: ptThemeable(PrimeTextarea, 'textarea'),
|
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'),
|
||||||
|
|
@ -111,8 +109,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 InputText: typeof PrimeInputText =
|
|
||||||
_engine.getComponent<typeof PrimeInputText>('InputText')
|
|
||||||
export const Textarea: typeof PrimeTextarea = _engine.getComponent<typeof PrimeTextarea>('Textarea')
|
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 =
|
||||||
|
|
@ -132,7 +128,7 @@ export const Tag: typeof PrimeTag = _engine.getComponent<typeof PrimeTag>('Tag')
|
||||||
export const createNychthemeron = (): {
|
export const createNychthemeron = (): {
|
||||||
Button: typeof Button
|
Button: typeof Button
|
||||||
LoadingIcon: typeof NychLoadingIcon
|
LoadingIcon: typeof NychLoadingIcon
|
||||||
InputText: typeof PrimeInputText
|
Input: typeof Input
|
||||||
Textarea: typeof PrimeTextarea
|
Textarea: typeof PrimeTextarea
|
||||||
Checkbox: typeof PrimeCheckbox
|
Checkbox: typeof PrimeCheckbox
|
||||||
CheckboxGroup: typeof PrimeCheckboxGroup
|
CheckboxGroup: typeof PrimeCheckboxGroup
|
||||||
|
|
@ -148,7 +144,7 @@ export const createNychthemeron = (): {
|
||||||
} => ({
|
} => ({
|
||||||
Button,
|
Button,
|
||||||
LoadingIcon,
|
LoadingIcon,
|
||||||
InputText,
|
Input,
|
||||||
Textarea,
|
Textarea,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
CheckboxGroup,
|
CheckboxGroup,
|
||||||
|
|
@ -163,7 +159,7 @@ export const createNychthemeron = (): {
|
||||||
install(app: App) {
|
install(app: App) {
|
||||||
app.component('NychButton', Button)
|
app.component('NychButton', Button)
|
||||||
app.component('NychLoadingIcon', LoadingIcon)
|
app.component('NychLoadingIcon', LoadingIcon)
|
||||||
app.component('NychInputText', InputText)
|
app.component('NychInput', Input)
|
||||||
app.component('NychTextarea', Textarea)
|
app.component('NychTextarea', Textarea)
|
||||||
app.component('NychCheckbox', Checkbox)
|
app.component('NychCheckbox', Checkbox)
|
||||||
app.component('NychCheckboxGroup', CheckboxGroup)
|
app.component('NychCheckboxGroup', CheckboxGroup)
|
||||||
|
|
|
||||||
23
packages/library/tests/Input.spec.ts
Normal file
23
packages/library/tests/Input.spec.ts
Normal 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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -3,7 +3,6 @@ import type { App } from 'vue'
|
||||||
import {
|
import {
|
||||||
createNychthemeron,
|
createNychthemeron,
|
||||||
LoadingIcon,
|
LoadingIcon,
|
||||||
InputText,
|
|
||||||
Textarea,
|
Textarea,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
RadioButton,
|
RadioButton,
|
||||||
|
|
@ -71,7 +70,6 @@ describe('LoadingIcon', () => {
|
||||||
|
|
||||||
describe('themed pt components', () => {
|
describe('themed pt components', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{ name: 'InputText', component: InputText, base: 'nych-input' },
|
|
||||||
{ name: 'Textarea', component: Textarea, base: 'nych-textarea' },
|
{ 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' },
|
||||||
|
|
|
||||||
49
packages/playground/stories/Input.stories.ts
Normal file
49
packages/playground/stories/Input.stories.ts
Normal 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;" />`,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
@ -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>
|
|
||||||
`,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue