feat(input): refactor input component to support compound pattern

Add new Input component with support for Password and Button variants through compound pattern. Move input implementation to new directory structure and enhance with label and caption support. Remove old input implementation.
This commit is contained in:
icarus 2025-11-02 22:46:49 +08:00
parent 13cacaba7f
commit cad71c3af7
6 changed files with 67 additions and 21 deletions

View File

@ -84,7 +84,7 @@ export { Sortable } from './composites/Sortable'
export * from './primitives/button'
export * from './primitives/command'
export * from './primitives/dialog'
export * from './primitives/input'
export * from './primitives/Input'
export * from './primitives/popover'
export * from './primitives/radioGroup'
export * from './primitives/shadcn-io/dropzone'

View File

@ -0,0 +1 @@
export function WithButton() {}

View File

@ -0,0 +1,14 @@
import { WithButton } from './button'
import { Input as InternalInput } from './input'
import { Password } from './password'
type CompoundedComponent = typeof InternalInput & {
Password: typeof Password
Button: typeof WithButton
}
const Input: CompoundedComponent = InternalInput as CompoundedComponent
Input.Password = Password
Input.Button = WithButton
export { Input }

View File

@ -0,0 +1,50 @@
import { cn } from '@cherrystudio/ui/utils'
import React from 'react'
interface BaseInputProps extends React.ComponentPropsWithRef<'input'> {
startContent?: React.ReactNode
endContent?: React.ReactNode
}
interface WithLabel extends BaseInputProps {
label: string
caption?: string
}
interface WithoutLabel extends BaseInputProps {
label?: never
caption?: never
}
type InputProps = WithLabel | WithoutLabel
export function Input({ className, type, required, label, caption, ...props }: InputProps) {
const id = React.useId()
const input = (
<input
type={type}
data-slot="input"
id={id}
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-2xs border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-primary focus-visible:ring-primary/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
required={required}
{...props}
/>
)
if (label !== undefined) {
return (
<div className="flex flex-col w-full">
<label htmlFor={id}>{label}</label>
{input}
{caption && <div className="text-muted-foreground">{caption}</div>}
</div>
)
}
return input
}

View File

@ -0,0 +1 @@
export function Password() {}

View File

@ -1,20 +0,0 @@
import { cn } from '@cherrystudio/ui/utils'
import * as React from 'react'
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className
)}
{...props}
/>
)
}
export { Input }