diff --git a/ui/content/docs/components/input.mdx b/ui/content/docs/components/input.mdx new file mode 100644 index 0000000..a6a99e8 --- /dev/null +++ b/ui/content/docs/components/input.mdx @@ -0,0 +1,188 @@ +--- +title: Input +description: A text input component +--- + +import { Input } from '@trakteer/input'; +import { TypeTable } from 'fumadocs-ui/components/type-table'; + +
+ +
+ +### Usage + +```bash tab="npm" +npx @fydemy/ui@latest add input +``` + +```bash tab="pnpm" +pnpm dlx @fydemy/ui@latest add input +``` + +```bash tab="yarn" +yarn @fydemy/ui@latest add input +``` + +```bash tab="bun" +bunx --bun @fydemy/ui@latest add input +``` + +```jsx + +``` + +) => void; + onFocus?: (e: React.FocusEvent) => void; + onBlur?: (e: React.FocusEvent) => void; +}; +`} +/> + +### Examples + +import { Mail, Search, Lock, User } from 'lucide-react'; + +#### With Label + +An input with a label for better accessibility and user experience. + +
+ + +
+ +```jsx + + +``` + +#### With Icon + +An input with icon on the left or right side. Use `lucide-react` icon is recommended. + +
+ } iconPosition="left" /> + } iconPosition="right" /> +
+ +```jsx +} + iconPosition="left" +/> +} + iconPosition="right" +/> +``` + +#### Variants + +Control the variant either `default`, `error`, or `success`. `default` is the default value. + +
+ + + +
+ +```jsx + + + +``` + +#### With Helper Text + +Add helper text or error message to provide additional context or validation feedback. + +
+ + +
+ +```jsx + + +``` + +#### States + +Input supports `disabled` and `readonly` states. + +
+ + +
+ +```jsx + + +``` + +#### Input Types + +Support for various HTML input types. + +
+ + } iconPosition="left" /> + } iconPosition="left" /> + + } iconPosition="left" /> +
+ +```jsx + +} iconPosition="left" /> +} iconPosition="left" /> + +} iconPosition="left" /> +``` + +#### Controlled Input + +Use `value` and `onChange` to control the input state. + +```jsx +const [value, setValue] = useState(''); + + setValue(e.target.value)} placeholder="Type something..." />; +``` diff --git a/ui/public/trakteer/input/index.css b/ui/public/trakteer/input/index.css new file mode 100644 index 0000000..7793daf --- /dev/null +++ b/ui/public/trakteer/input/index.css @@ -0,0 +1,157 @@ +/* Input Wrapper */ +.input-wrapper[data-theme='trakteer'] { + font-family: 'Pontano Sans', sans-serif; + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 100%; +} + +/* Input Label */ +.input-wrapper[data-theme='trakteer'] .input-label { + font-size: 14px; + font-weight: 600; + color: var(--trakteer-text-primary); + margin-bottom: 0.25rem; +} + +/* Input Container */ +.input-wrapper[data-theme='trakteer'] .input-container { + position: relative; + display: flex; + align-items: center; + width: 100%; +} + +/* Input Base Styles */ +.input-wrapper[data-theme='trakteer'] .input { + font-family: 'Pontano Sans', sans-serif; + width: 100%; + padding: 0.75rem 1rem; + font-size: 16px; + color: var(--trakteer-text-primary); + background-color: var(--trakteer-bg-primary); + border: 1px solid var(--trakteer-border-medium); + border-bottom: 4px solid var(--trakteer-border-medium); + border-radius: 12px; + outline: none; + transition: all 0.2s ease; +} + +.input-wrapper[data-theme='trakteer'] .input::placeholder { + color: var(--trakteer-text-muted); +} + +/* Input with Icon */ +.input-wrapper[data-theme='trakteer'] .input-container:has(.input-icon-left) .input { + padding-left: 2.75rem; +} + +.input-wrapper[data-theme='trakteer'] .input-container:has(.input-icon-right) .input { + padding-right: 2.75rem; +} + +/* Input Icon */ +.input-wrapper[data-theme='trakteer'] .input-icon { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + color: var(--trakteer-text-secondary); + z-index: 1; +} + +.input-wrapper[data-theme='trakteer'] .input-icon-left { + left: 1rem; +} + +.input-wrapper[data-theme='trakteer'] .input-icon-right { + right: 1rem; +} + +.input-wrapper[data-theme='trakteer'] .input-icon svg { + width: 18px; + height: 18px; +} + +/* Input Default Variant */ +.input-wrapper[data-theme='trakteer'] .input[data-variant='default'] { + border-color: var(--trakteer-border-medium); + border-bottom-color: var(--trakteer-border-medium); +} + +.input-wrapper[data-theme='trakteer'] .input[data-variant='default']:focus { + border-color: var(--trakteer-default-border); + border-bottom-color: var(--trakteer-default-border); + background-color: var(--trakteer-bg-primary); +} + +.input-wrapper[data-theme='trakteer'] .input[data-variant='default']:focus + .input-icon-right, +.input-wrapper[data-theme='trakteer'] .input-container:has(.input[data-variant='default']:focus) .input-icon { + color: var(--trakteer-default); +} + +/* Input Error Variant */ +.input-wrapper[data-theme='trakteer'] .input[data-variant='error'] { + border-color: var(--trakteer-destructive-border); + border-bottom-color: var(--trakteer-destructive-border); + background-color: var(--trakteer-bg-primary); +} + +.input-wrapper[data-theme='trakteer'] .input[data-variant='error']:focus { + border-color: var(--trakteer-destructive-border); + border-bottom-color: var(--trakteer-destructive-border); +} + +.input-wrapper[data-theme='trakteer'] .input[data-variant='error']:focus + .input-icon-right, +.input-wrapper[data-theme='trakteer'] .input-container:has(.input[data-variant='error']:focus) .input-icon { + color: var(--trakteer-destructive); +} + +/* Input Success Variant */ +.input-wrapper[data-theme='trakteer'] .input[data-variant='success'] { + border-color: var(--trakteer-default-border); + border-bottom-color: var(--trakteer-default-border); + background-color: var(--trakteer-bg-primary); +} + +.input-wrapper[data-theme='trakteer'] .input[data-variant='success']:focus { + border-color: var(--trakteer-default-border); + border-bottom-color: var(--trakteer-default-border); +} + +.input-wrapper[data-theme='trakteer'] .input[data-variant='success']:focus + .input-icon-right, +.input-wrapper[data-theme='trakteer'] .input-container:has(.input[data-variant='success']:focus) .input-icon { + color: var(--trakteer-default); +} + +/* Input Disabled State */ +.input-wrapper[data-theme='trakteer'] .input:disabled { + background-color: var(--trakteer-bg-secondary); + border-color: var(--trakteer-border-light); + border-bottom-color: var(--trakteer-border-light); + color: var(--trakteer-text-muted); + cursor: not-allowed; + opacity: 0.6; +} + +.input-wrapper[data-theme='trakteer'] .input:disabled::placeholder { + color: var(--trakteer-text-muted); +} + +/* Input Readonly State */ +.input-wrapper[data-theme='trakteer'] .input[readonly] { + background-color: var(--trakteer-bg-secondary); + cursor: default; +} + +/* Input Helper Text */ +.input-wrapper[data-theme='trakteer'] .input-helper { + font-size: 12px; + color: var(--trakteer-text-secondary); + margin-top: 0.25rem; +} + +.input-wrapper[data-theme='trakteer'] .input-helper-error { + color: var(--trakteer-destructive); +} diff --git a/ui/public/trakteer/input/index.tsx b/ui/public/trakteer/input/index.tsx new file mode 100644 index 0000000..ce1c3dd --- /dev/null +++ b/ui/public/trakteer/input/index.tsx @@ -0,0 +1,71 @@ +import '../index.css'; +import './index.css'; + +import { forwardRef } from 'react'; + +type InputProps = { + type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search'; + placeholder?: string; + value?: string; + defaultValue?: string; + variant?: 'default' | 'error' | 'success'; + disabled?: boolean; + readonly?: boolean; + label?: string; + helperText?: string; + errorMessage?: string; + icon?: React.ReactNode; + iconPosition?: 'left' | 'right'; + className?: string; + onChange?: (e: React.ChangeEvent) => void; + onFocus?: (e: React.FocusEvent) => void; + onBlur?: (e: React.FocusEvent) => void; +} & React.InputHTMLAttributes; + +const Input = forwardRef( + ({ type = 'text', placeholder, value, defaultValue, variant = 'default', disabled = false, readonly = false, label, helperText, errorMessage, icon, iconPosition = 'left', className = '', onChange, onFocus, onBlur, ...props }, ref) => { + const hasError = variant === 'error' || errorMessage; + const hasSuccess = variant === 'success'; + const displayVariant = hasError ? 'error' : hasSuccess ? 'success' : 'default'; + + return ( +
+ {label && ( + + )} +
+ {icon && iconPosition === 'left' && {icon}} + + {icon && iconPosition === 'right' && {icon}} +
+ {(errorMessage || helperText) && ( + + {errorMessage || helperText} + + )} +
+ ); + } +); + +Input.displayName = 'Input'; + +export { Input };