diff --git a/libs/ui/src/atoms/index.ts b/libs/ui/src/atoms/index.ts index eaf5eea..c107e65 100644 --- a/libs/ui/src/atoms/index.ts +++ b/libs/ui/src/atoms/index.ts @@ -1 +1,4 @@ export * from './button'; +export * from './input'; +export * from './password-input'; +export * from './textarea'; diff --git a/libs/ui/src/atoms/input/index.ts b/libs/ui/src/atoms/input/index.ts new file mode 100644 index 0000000..4ce4a88 --- /dev/null +++ b/libs/ui/src/atoms/input/index.ts @@ -0,0 +1 @@ +export * from "./input"; diff --git a/libs/ui/src/atoms/input/input.spec.tsx b/libs/ui/src/atoms/input/input.spec.tsx new file mode 100644 index 0000000..4fdf16b --- /dev/null +++ b/libs/ui/src/atoms/input/input.spec.tsx @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { Input } from "./input"; + +describe("Test Input Component", () => { + it("renders the input with placeholder text", () => { + render(); + expect(screen.getByPlaceholderText("Placeholder")).toBeInTheDocument(); + }); + + // it("renders the input with different sizes", () => { + // const sizeClasses = { + // sm: "text-[10px] max-h-[28px]", + // md: "text-[12px] max-h-[30px]", + // lg: "text-[15px] max-h-[34px]", + // }; + // Object.entries(sizeClasses).forEach(([size, className]) => { + // render(); + // expect(screen.getByRole("textbox")).toHaveClass(className); + // }); + // }); + + // it("renders the input with different types", () => { + // const types = ["text", "password", "email", "number"]; + // types.forEach((type) => { + // render(); + // const input = screen.getByRole("textbox"); + // expect(input).toHaveAttribute("type", type); + // }); + // }); + + it("disables the input field when 'disabled' prop is set", () => { + render(); + expect(screen.getByRole("textbox")).toBeDisabled(); + }); +}); diff --git a/libs/ui/src/atoms/input/input.stories.tsx b/libs/ui/src/atoms/input/input.stories.tsx new file mode 100644 index 0000000..7bea8f5 --- /dev/null +++ b/libs/ui/src/atoms/input/input.stories.tsx @@ -0,0 +1,71 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { Input } from './input'; + +const meta: Meta = { + title: 'Components/Input', + component: Input, + argTypes: { + type: { + control: 'select', + options: ['text', 'email'], + }, + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Large: Story = { + args: { + size: 'lg', + placeholder: 'Placeholder', + }, +}; + +export const Medium: Story = { + args: { + size: 'md', + placeholder: 'Placeholder', + }, +}; + +export const Small: Story = { + args: { + size: 'sm', + placeholder: 'Placeholder', + }, +}; + +export const TextInput: Story = { + args: { + size: 'md', + type: 'text', + }, +}; + +export const EmailInput: Story = { + args: { + size: 'md', + type: 'email', + }, +}; + +export const Disabled: Story = { + args: { + size: 'md', + type: 'text', + disabled: true, + }, +}; + +export const WithError: Story = { + args: { + size: 'md', + type: 'text', + error: 'This field is required', + }, +}; diff --git a/libs/ui/src/atoms/input/input.tsx b/libs/ui/src/atoms/input/input.tsx new file mode 100644 index 0000000..3a80d49 --- /dev/null +++ b/libs/ui/src/atoms/input/input.tsx @@ -0,0 +1,60 @@ +import { + DetailedHTMLProps, + FC, + InputHTMLAttributes, + ReactElement, +} from 'react'; +import { cn } from '@imphnen-frontend-service/utils'; + +type TInputType = 'text' | 'email'; +type TInputSize = 'sm' | 'md' | 'lg'; + +type TInputProps = Omit< + DetailedHTMLProps, HTMLInputElement>, + 'size' | 'type' +> & { + type?: TInputType; + size?: TInputSize; + error?: string; +}; + +const sizeClasses: Record = { + sm: 'text-[10px] max-h-[28px]', + md: 'text-[12px] max-h-[30px]', + lg: 'text-[15px] max-h-[34px]', +}; + +const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed'; +const errorClass = + 'border-danger-500 hover:border-danger-500 focus:outline-danger-500'; + +export const Input: FC = ({ + size = 'md', + type, + placeholder = 'Placeholder', + disabled, + error, + className, + ...rest +}): ReactElement => { + const mergedClassName = cn( + 'px-[12px] py-[8px] text-neutral-800 placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree', + sizeClasses[size], + disabled && disabledClass, + error && errorClass, + className + ); + + return ( + <> + + {error &&

{error}

} + + ); +}; diff --git a/libs/ui/src/atoms/password-input/index.ts b/libs/ui/src/atoms/password-input/index.ts new file mode 100644 index 0000000..aa25c3b --- /dev/null +++ b/libs/ui/src/atoms/password-input/index.ts @@ -0,0 +1 @@ +export * from "./password-input"; \ No newline at end of file diff --git a/libs/ui/src/atoms/password-input/password-input.stories.tsx b/libs/ui/src/atoms/password-input/password-input.stories.tsx new file mode 100644 index 0000000..d2db711 --- /dev/null +++ b/libs/ui/src/atoms/password-input/password-input.stories.tsx @@ -0,0 +1,58 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { PasswordInput } from './password-input'; + +const meta: Meta = { + title: 'Components/Password Input', + component: PasswordInput, + argTypes: { + size: { + control: 'select', + options: ['sm', 'md', 'lg'], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Large: Story = { + args: { + size: 'lg', + placeholder: 'Placeholder', + }, +}; + +export const Medium: Story = { + args: { + size: 'md', + placeholder: 'Placeholder', + }, +}; + +export const Small: Story = { + args: { + size: 'sm', + placeholder: 'Placeholder', + }, +}; + +export const Input: Story = { + args: { + size: 'md', + placeholder: 'Placeholder', + }, +}; + +export const Disabled: Story = { + args: { + size: 'md', + disabled: true, + }, +}; + +export const WithError: Story = { + args: { + size: 'md', + error: 'This field is required', + }, +}; diff --git a/libs/ui/src/atoms/password-input/password-input.tsx b/libs/ui/src/atoms/password-input/password-input.tsx new file mode 100644 index 0000000..85372fa --- /dev/null +++ b/libs/ui/src/atoms/password-input/password-input.tsx @@ -0,0 +1,111 @@ +import { + DetailedHTMLProps, + FC, + InputHTMLAttributes, + ReactElement, + useState, +} from 'react'; +import { cn } from '@imphnen-frontend-service/utils'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; + +type TPasswordInputType = 'password'; +type TPasswordInputSize = 'sm' | 'md' | 'lg'; + +type TPasswordInputProps = Omit< + DetailedHTMLProps, HTMLInputElement>, + 'size' | 'type' +> & { + type?: TPasswordInputType; + size?: TPasswordInputSize; + error?: string; +}; + +const sizeClasses: Record = { + sm: 'text-[10px] max-h-[28px]', + md: 'text-[12px] max-h-[30px]', + lg: 'text-[15px] max-h-[34px]', +}; + +const iconSizeClasses: Record = { + sm: 'text-[10px]', + md: 'text-[12px]', + lg: 'text-[16px]', +}; + +const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed'; +const errorClass = + 'border-danger-500 hover:border-danger-500 focus:outline-danger-500'; + +export const PasswordInput: FC = ({ + size = 'md', + placeholder = 'Placeholder', + disabled, + error, + className, + ...rest +}): ReactElement => { + const [showPassword, setShowPassword] = useState(false); + + const togglePasswordVisibility = () => { + if (!disabled) setShowPassword((prev) => !prev); + }; + + const mergedClassName = cn( + 'px-[12px] py-[8px] text-neutral-800 placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md font-bai-jamjuree', + sizeClasses[size], + disabled && disabledClass, + error && errorClass, + className + ); + + return ( + <> +
+ +
+ +
+
+ {error &&

{error}

} + + ); +}; diff --git a/libs/ui/src/atoms/textarea/index.ts b/libs/ui/src/atoms/textarea/index.ts new file mode 100644 index 0000000..23f3d74 --- /dev/null +++ b/libs/ui/src/atoms/textarea/index.ts @@ -0,0 +1 @@ +export * from "./textarea"; diff --git a/libs/ui/src/atoms/textarea/textarea.stories.tsx b/libs/ui/src/atoms/textarea/textarea.stories.tsx new file mode 100644 index 0000000..ffe3c44 --- /dev/null +++ b/libs/ui/src/atoms/textarea/textarea.stories.tsx @@ -0,0 +1,49 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { Textarea } from "./textarea"; + +const meta: Meta = { + title: "Components/Textarea", + component: Textarea, + argTypes: { + size: { + control: "select", + options: ["sm", "md", "lg"], + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Large: Story = { + args: { + size: "lg", + placeholder: "Placeholder", + }, +}; + +export const Medium: Story = { + args: { + size: "md", + }, +}; + +export const Small: Story = { + args: { + size: "sm", + }, +}; + +export const Disabled: Story = { + args: { + size: "md", + disabled: true, + }, +}; + +export const WithError: Story = { + args: { + size: "md", + error: "Error message", + }, +}; diff --git a/libs/ui/src/atoms/textarea/textarea.tsx b/libs/ui/src/atoms/textarea/textarea.tsx new file mode 100644 index 0000000..1c31100 --- /dev/null +++ b/libs/ui/src/atoms/textarea/textarea.tsx @@ -0,0 +1,59 @@ +import { cn } from '@imphnen-frontend-service/utils'; +import { + DetailedHTMLProps, + FC, + ReactElement, + TextareaHTMLAttributes, +} from 'react'; + +type TTextareaSize = 'sm' | 'md' | 'lg'; + +type TTextareaProps = Omit< + DetailedHTMLProps< + TextareaHTMLAttributes, + HTMLTextAreaElement + >, + 'size' +> & { + size?: TTextareaSize; + error?: string; +}; + +const sizeClasses: Record = { + sm: 'text-[10px]', + md: 'text-[12px]', + lg: 'text-[15px]', +}; + +const disabledClass = 'opacity-50 hover:border-neutral-200 cursor-not-allowed'; +const errorClass = + 'border-danger-500 hover:border-danger-500 focus:outline-danger-500'; + +export const Textarea: FC = ({ + size = 'md', + placeholder = 'Placeholder', + disabled, + error, + className, + ...rest +}): ReactElement => { + const mergedClassName = cn( + 'rounded-md border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 px-[12px] py-[8px] invalid:border-danger-500 invalid:text-danger-500', + sizeClasses[size], + disabled && disabledClass, + error && errorClass, + className + ); + return ( + <> + + {error &&

{error}

} + + ); +};