diff --git a/.changeset/breezy-swans-brush.md b/.changeset/breezy-swans-brush.md
new file mode 100644
index 00000000..0bfab4da
--- /dev/null
+++ b/.changeset/breezy-swans-brush.md
@@ -0,0 +1,5 @@
+---
+"@devup-ui/components": patch
+---
+
+Add Input component
diff --git a/.changeset/config.json b/.changeset/config.json
index 14fd38ad..50fc841a 100644
--- a/.changeset/config.json
+++ b/.changeset/config.json
@@ -10,7 +10,6 @@
"ignore": [
"*-example",
"*-benchmark",
- "landing",
- "@devup-ui/components"
+ "landing"
]
}
\ No newline at end of file
diff --git a/.changeset/stupid-brooms-see.md b/.changeset/stupid-brooms-see.md
new file mode 100644
index 00000000..f91296dd
--- /dev/null
+++ b/.changeset/stupid-brooms-see.md
@@ -0,0 +1,5 @@
+---
+"@devup-ui/components": patch
+---
+
+Fix Input comp test
diff --git a/packages/components/src/__tests__/index.browser.test.ts b/packages/components/src/__tests__/index.browser.test.ts
index 7c1a48da..e19fbb2f 100644
--- a/packages/components/src/__tests__/index.browser.test.ts
+++ b/packages/components/src/__tests__/index.browser.test.ts
@@ -3,6 +3,7 @@ describe('export', () => {
const index = await import('../index')
expect({ ...index }).toEqual({
Button: expect.any(Function),
+ Input: expect.any(Function),
})
})
})
diff --git a/packages/components/src/components/Input/Controlled.tsx b/packages/components/src/components/Input/Controlled.tsx
new file mode 100644
index 00000000..bbeae7ad
--- /dev/null
+++ b/packages/components/src/components/Input/Controlled.tsx
@@ -0,0 +1,9 @@
+import { useState } from 'react'
+
+import { Input } from '.'
+
+export function Controlled() {
+ const [value, setValue] = useState('')
+
+ return setValue(e.target.value)} value={value} />
+}
diff --git a/packages/components/src/components/Input/GlassIcon.tsx b/packages/components/src/components/Input/GlassIcon.tsx
new file mode 100644
index 00000000..4f660af5
--- /dev/null
+++ b/packages/components/src/components/Input/GlassIcon.tsx
@@ -0,0 +1,21 @@
+import { ComponentProps } from 'react'
+
+export function GlassIcon(props: ComponentProps<'svg'>) {
+ return (
+
+ )
+}
diff --git a/packages/components/src/components/Input/Input.stories.tsx b/packages/components/src/components/Input/Input.stories.tsx
new file mode 100644
index 00000000..f8135b6b
--- /dev/null
+++ b/packages/components/src/components/Input/Input.stories.tsx
@@ -0,0 +1,58 @@
+import { Meta, StoryObj } from '@storybook/react-vite'
+
+import { Controlled } from './Controlled'
+import { GlassIcon } from './GlassIcon'
+import { Input } from './index'
+
+type Story = StoryObj
+
+// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
+const meta: Meta = {
+ title: 'Devfive/Input',
+ component: Input,
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+}
+
+export const Default: Story = {
+ args: {
+ placeholder: 'Input text',
+ },
+}
+
+export const ControlledInput: Story = {
+ args: {
+ placeholder: 'Input text',
+ },
+ render: () => ,
+}
+
+export const Error: Story = {
+ args: {
+ placeholder: 'Input text',
+ error: true,
+ errorMessage: 'Error message',
+ },
+}
+
+export const Disabled: Story = {
+ args: {
+ placeholder: 'Input text',
+ disabled: true,
+ },
+}
+
+export const WithIcon: Story = {
+ args: {
+ placeholder: 'Input text',
+ allowClear: true,
+ icon: ,
+ },
+}
+
+export default meta
diff --git a/packages/components/src/components/Input/__tests__/__snapshots__/index.browser.test.tsx.snap b/packages/components/src/components/Input/__tests__/__snapshots__/index.browser.test.tsx.snap
new file mode 100644
index 00000000..d81cafbd
--- /dev/null
+++ b/packages/components/src/components/Input/__tests__/__snapshots__/index.browser.test.tsx.snap
@@ -0,0 +1,247 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Controlled Input > should render with value 1`] = `
+
+`;
+
+exports[`Input > should have typography when typography is provided 1`] = `
+
+`;
+
+exports[`Input > should not have padding right when allowClear is false 1`] = `
+
+`;
+
+exports[`Input > should not show clear button when value is empty 1`] = `
+
+`;
+
+exports[`Input > should pass className prop to error message component 1`] = `
+
+
+
+
+ Error message
+
+
+
+`;
+
+exports[`Input > should pass className prop to icon component 1`] = `
+
+`;
+
+exports[`Input > should pass props to ClearButton component 1`] = `
+
+`;
+
+exports[`Input > should render disabled icon style when disabled is true 1`] = `
+
+`;
+
+exports[`Input > should render error style when error is true 1`] = `
+
+`;
+
+exports[`Input > should render with allowClear prop 1`] = `
+
+`;
+
+exports[`Input > should render with default props 1`] = `
+
+`;
+
+exports[`Input > should render with disabled prop 1`] = `
+
+`;
+
+exports[`Input > should show clear button when value is not empty 1`] = `
+
+`;
diff --git a/packages/components/src/components/Input/__tests__/index.browser.test.tsx b/packages/components/src/components/Input/__tests__/index.browser.test.tsx
new file mode 100644
index 00000000..87da4497
--- /dev/null
+++ b/packages/components/src/components/Input/__tests__/index.browser.test.tsx
@@ -0,0 +1,173 @@
+import { fireEvent, render } from '@testing-library/react'
+import { DevupThemeTypography } from 'node_modules/@devup-ui/react/dist/types/typography'
+
+import { ClearButton, Input } from '..'
+import { Controlled } from '../Controlled'
+import { GlassIcon } from '../GlassIcon'
+
+describe('Input', () => {
+ it('should render with default props', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ })
+
+ it('should render with disabled prop', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ })
+
+ it('should render with allowClear prop', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ expect(container.querySelector('[aria-label="input"]')).toHaveClass(
+ 'padding-right-0-36px--1',
+ )
+ })
+
+ it('should not have padding right when allowClear is false', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ expect(container.querySelector('[aria-label="input"]')).not.toHaveClass(
+ 'padding-right-0-36px--1',
+ )
+ })
+
+ it('should show clear button when value is not empty', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ fireEvent.change(container.querySelector('input')!, {
+ target: { value: 'test' },
+ })
+ expect(container.querySelector('button')).toBeInTheDocument()
+ })
+
+ it('should not show clear button when value is empty', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ })
+
+ it('should be able to clear value by clicking clear button', () => {
+ const { container } = render()
+ fireEvent.change(container.querySelector('input')!, {
+ target: { value: 'test' },
+ })
+ expect(container.querySelector('button')).toBeInTheDocument()
+ fireEvent.click(container.querySelector('button')!)
+ expect(container.querySelector('input')!.value).toBe('')
+ })
+
+ it('should be able to render with icon', () => {
+ const { container } = render(
+ } />,
+ )
+ expect(container.querySelector('[data-testid="icon"]')).toBeInTheDocument()
+ })
+
+ it('should render error style when error is true', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ expect(container.querySelector('[aria-label="input"]')).toHaveClass(
+ 'border-color-0-var(--error,light-dark(#D52B2E,#FF5B5E))--1',
+ )
+ })
+
+ it('should be able to render with error message', () => {
+ const { container } = render()
+ expect(
+ container.querySelector('[aria-label="error-message"]'),
+ ).toBeInTheDocument()
+ })
+
+ it('should pass colors prop', () => {
+ const { container } = render(
+ ,
+ )
+ const input = container.querySelector('[aria-label="input"]')
+ expect(input).toHaveStyle({
+ '--primary': 'red',
+ '--error': 'blue',
+ '--text': 'green',
+ })
+ })
+
+ it('should have typography when typography is provided', () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toMatchSnapshot()
+ expect(container.querySelector('input')).toHaveClass('typo-inlineLabelS')
+ })
+
+ it('should pass className prop to error message component', () => {
+ const { container } = render(
+ ,
+ )
+ expect(container).toMatchSnapshot()
+ expect(container.querySelector('[aria-label="error-message"]')).toHaveClass(
+ 'error-message',
+ )
+ })
+
+ it('should pass className prop to icon component', () => {
+ const { container } = render(
+ }
+ />,
+ )
+ expect(container).toMatchSnapshot()
+ expect(container.querySelector('[aria-label="icon"]')).toHaveClass('icon')
+ })
+
+ it('should pass props to ClearButton component', async () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ const clearButton = container.querySelector('[aria-label="clear-button"]')
+ expect(clearButton).toBeInTheDocument()
+ })
+
+ it('should render disabled icon style when disabled is true', () => {
+ const { container } = render(} />)
+ expect(container).toMatchSnapshot()
+ expect(container.querySelector('[aria-label="icon"]')).toHaveClass(
+ 'color-0-var(--inputDisabledText,light-dark(#D6D7DE,#373737))--1',
+ )
+ })
+
+ it('should call onChange prop when it is provided andvalue is changed', () => {
+ const onChange = vi.fn()
+ const { container } = render()
+ fireEvent.change(container.querySelector('input')!, {
+ target: { value: 'test' },
+ })
+ expect(onChange).toHaveBeenCalledWith(expect.any(Object))
+ })
+})
+
+describe('Controlled Input', () => {
+ it('should render with value', () => {
+ const { container } = render()
+ expect(container).toMatchSnapshot()
+ })
+
+ it('should update value when it is changed', () => {
+ const { container } = render()
+ fireEvent.change(container.querySelector('input')!, {
+ target: { value: 'test' },
+ })
+ expect(container.querySelector('input')!.value).toBe('test')
+ })
+})
diff --git a/packages/components/src/components/Input/index.tsx b/packages/components/src/components/Input/index.tsx
new file mode 100644
index 00000000..83a59c53
--- /dev/null
+++ b/packages/components/src/components/Input/index.tsx
@@ -0,0 +1,208 @@
+'use client'
+
+import {
+ Box,
+ Button,
+ Center,
+ DevupThemeTypography,
+ Input as DevupInput,
+ Text,
+} from '@devup-ui/react'
+import { ComponentProps, useState } from 'react'
+
+interface InputProps
+ extends Omit, 'className' | 'type'> {
+ type?: Exclude['type'], 'file'>
+ typography?: keyof DevupThemeTypography
+ error?: boolean
+ errorMessage?: string
+ allowClear?: boolean
+ classNames?: {
+ input?: string
+ icon?: string
+ errorMessage?: string
+ }
+ colors?: {
+ primary?: string
+ error?: string
+ text?: string
+ base?: string
+ iconBold?: string
+ border?: string
+ inputBackground?: string
+ primaryFocus?: string
+ negative20?: string
+ }
+ icon?: React.ReactNode
+}
+
+export function Input({
+ defaultValue,
+ value: valueProp,
+ onChange: onChangeProp,
+ typography,
+ error = false,
+ errorMessage,
+ allowClear = true,
+ icon,
+ colors,
+ disabled,
+ classNames,
+ ref,
+ ...props
+}: InputProps) {
+ const [value, setValue] = useState(defaultValue || '')
+ const handleChange = (e: React.ChangeEvent) => {
+ setValue(e.target.value)
+ onChangeProp?.(e)
+ }
+ const handleClear = () => {
+ setValue('')
+ }
+ const clearButtonVisible = value && !disabled && allowClear
+
+ return (
+ *': { boxSizing: 'border-box' } }}
+ >
+ {icon && (
+
+ {icon}
+
+ )}
+
+ {clearButtonVisible && }
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+
+ )
+}
+
+export function ClearButton(props: ComponentProps<'button'>) {
+ return (
+
+ )
+}
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index e58450b5..819f4919 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -1 +1,2 @@
export { Button } from './components/Button'
+export { Input } from './components/Input'