From 5a3a7324bfd3b377fe0ac67a38cc83cb2a6111d6 Mon Sep 17 00:00:00 2001 From: myusername Date: Tue, 19 May 2026 16:39:48 +0530 Subject: [PATCH] feat: Input primitive created --- .../composites/Topbar/Topbar.stories.tsx | 17 +- src/shared/composites/Topbar/Topbar.tsx | 21 +- src/shared/primitives/Input/Input.stories.tsx | 114 +++++++++++ src/shared/primitives/Input/Input.tsx | 188 ++++++++++++++++++ src/shared/primitives/Input/index.ts | 3 + 5 files changed, 326 insertions(+), 17 deletions(-) create mode 100644 src/shared/primitives/Input/Input.stories.tsx create mode 100644 src/shared/primitives/Input/Input.tsx create mode 100644 src/shared/primitives/Input/index.ts diff --git a/src/shared/composites/Topbar/Topbar.stories.tsx b/src/shared/composites/Topbar/Topbar.stories.tsx index 337a260..4705708 100644 --- a/src/shared/composites/Topbar/Topbar.stories.tsx +++ b/src/shared/composites/Topbar/Topbar.stories.tsx @@ -7,6 +7,7 @@ import { Topbar } from './Topbar' import { AvatarMenu } from '@/shared/composites/AvatarMenu/AvatarMenu' import { Button } from '@/shared/primitives/Button' +import { Input } from '@/shared/primitives/Input' const meta = { title: 'Shared/Composites/Topbar', @@ -41,14 +42,14 @@ export const WithSearch: Story = { title: 'Moderation Queue', searchSlot: ( -
- - - -
+ {}} + placeholder={'Search...'} + prefixIcon={} + className="py-2" + /> ), actionsSlot: , diff --git a/src/shared/composites/Topbar/Topbar.tsx b/src/shared/composites/Topbar/Topbar.tsx index 0e1003b..54bde76 100644 --- a/src/shared/composites/Topbar/Topbar.tsx +++ b/src/shared/composites/Topbar/Topbar.tsx @@ -1,7 +1,8 @@ -import React from 'react' +import React, { useState } from 'react' import { Bars3Icon } from '@heroicons/react/24/outline' import { MagnifyingGlassIcon } from '@heroicons/react/24/solid' +import { Input } from '@/shared/primitives/Input' export interface TopbarProps { title: string @@ -30,6 +31,7 @@ export function Topbar({ searchPlaceholder, className = '', }: TopbarProps) { + const [searchQuery, overwriteSearchQuery] = useState('') return (
{searchSlot || ( -
- - - -
+ { + overwriteSearchQuery(e) + }} + placeholder={searchPlaceholder || 'Search...'} + prefixIcon={} + /> )}
diff --git a/src/shared/primitives/Input/Input.stories.tsx b/src/shared/primitives/Input/Input.stories.tsx new file mode 100644 index 0000000..d1bc2b8 --- /dev/null +++ b/src/shared/primitives/Input/Input.stories.tsx @@ -0,0 +1,114 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { useState } from 'react' + +import { MagnifyingGlassIcon, EyeIcon } from '@heroicons/react/24/outline' + +import { Input, type InputProps } from './Input' + +const meta: Meta = { + title: 'Shared/Primitives/Input', + + component: Input, + + tags: ['autodocs'], + + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export default meta + +type Story = StoryObj + +function StatefulInput(args: InputProps) { + const [value, setValue] = useState('') + + return ( +
+ +
+ ) +} + +export const Default: Story = { + render: (args) => , + + args: { + label: 'Username', + placeholder: 'Enter username', + }, +} + +export const Error: Story = { + render: (args) => , + + args: { + label: 'Email', + + placeholder: 'Enter email', + + error: 'Invalid email address', + }, +} + +export const Disabled: Story = { + render: (args) => , + + args: { + label: 'Disabled', + + value: 'Readonly', + + isDisabled: true, + }, +} + +export const WithIcons: Story = { + render: (args) => , + + args: { + label: 'Password', + + type: 'password', + + placeholder: 'Enter password', + + prefixIcon: , + + suffixIcon: , + }, +} + +export const Search: Story = { + render: (args) => , + + args: { + type: 'search', + + placeholder: 'Search reports...', + + prefixIcon: , + }, +} + +export const Focus: Story = { + render: (args) => , + + args: { + label: 'Focused Input', + + placeholder: 'Click to focus', + }, + + play: async ({ canvasElement }) => { + const input = canvasElement.querySelector('input') + + input?.focus() + }, +} diff --git a/src/shared/primitives/Input/Input.tsx b/src/shared/primitives/Input/Input.tsx new file mode 100644 index 0000000..148426b --- /dev/null +++ b/src/shared/primitives/Input/Input.tsx @@ -0,0 +1,188 @@ +import React, { useId } from 'react' + +import { cva } from 'class-variance-authority' + +export interface InputProps { + label?: string + + placeholder?: string + + value: string + + onChange: (value: string) => void + + type?: 'text' | 'email' | 'password' | 'search' | 'number' + + error?: string | null + + helperText?: string + + prefixIcon?: React.ReactNode + + suffixIcon?: React.ReactNode + + isDisabled?: boolean + + isReadOnly?: boolean + + autoComplete?: string + + testId?: string + + className?: string +} + +const inputStyles = cva( + ` + w-full + rounded-md + border + bg-bg-secondary + py-2 + text-sm + text-text-primary + transition-colors + outline-none + placeholder:text-text-tertiary + `, + { + variants: { + hasError: { + true: ` + border-border-danger + focus:border-border-danger + `, + + false: ` + border-border-secondary + focus:border-border-primary + focus:ring-0 + focus:outline-none + `, + }, + + disabled: { + true: ` + cursor-not-allowed + opacity-50 + disabled:pointer-events-none + `, + + false: '', + }, + + hasPrefix: { + true: 'pl-10', + + false: 'pl-3', + }, + + hasSuffix: { + true: 'pr-10', + + false: 'pr-3', + }, + }, + + defaultVariants: { + hasError: false, + disabled: false, + hasPrefix: false, + hasSuffix: false, + }, + } +) + +export function Input({ + label, + + placeholder, + + value, + + onChange, + + type = 'text', + + error = null, + + helperText, + + prefixIcon, + + suffixIcon, + + isDisabled = false, + + isReadOnly = false, + + autoComplete, + + testId, + + className = '', +}: InputProps) { + const id = useId() + + return ( +
+ {label && ( + + )} + +
+ {prefixIcon && ( +
+ {prefixIcon} +
+ )} + + { + onChange(e.target.value) + }} + className={inputStyles({ + hasError: Boolean(error), + disabled: isDisabled, + hasPrefix: Boolean(prefixIcon), + hasSuffix: Boolean(suffixIcon), + className, + })} + /> + + {suffixIcon && ( +
+ {suffixIcon} +
+ )} +
+ + {error ? ( +

{error}

+ ) : helperText ? ( +

{helperText}

+ ) : null} +
+ ) +} + +export default Input diff --git a/src/shared/primitives/Input/index.ts b/src/shared/primitives/Input/index.ts new file mode 100644 index 0000000..16c6723 --- /dev/null +++ b/src/shared/primitives/Input/index.ts @@ -0,0 +1,3 @@ +export { Input } from './Input' + +export type { InputProps } from './Input'