Download src/components/shared/SharedInput.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 6.47 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/shared/SharedInput.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/shared/SharedInput.tsx
-
curl -L -o SharedInput.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/shared/SharedInput.tsx
6.47 kB
| "use client"; | |
| import { forwardRef, InputHTMLAttributes, useId } from "react"; | |
| import { clsx } from "clsx"; | |
| import { designSystem } from "@/lib/design-system"; | |
| const { mobile } = designSystem; // colors/radius/spacing/typography reserved | |
| // ============================================================================= | |
| // Input Types | |
| // ============================================================================= | |
| export interface InputProps extends InputHTMLAttributes<HTMLInputElement> { | |
| label?: string; | |
| error?: string; | |
| hint?: string; | |
| leftIcon?: React.ReactNode; | |
| rightIcon?: React.ReactNode; | |
| fullWidth?: boolean; | |
| } | |
| // ============================================================================= | |
| // SharedInput Component | |
| // ============================================================================= | |
| export const SharedInput = forwardRef<HTMLInputElement, InputProps>( | |
| ( | |
| { | |
| label, | |
| error, | |
| hint, | |
| leftIcon, | |
| rightIcon, | |
| fullWidth: _fullWidth = true, | |
| id: providedId, | |
| className, | |
| disabled, | |
| type = "text", | |
| ...props | |
| }, | |
| ref, | |
| ) => { | |
| const generatedId = useId(); | |
| const inputId = providedId ?? generatedId; | |
| const messageId = `${inputId}-message`; | |
| const hasIcon = leftIcon || rightIcon; | |
| const inputPadding = hasIcon ? "pl-10 pr-10" : "px-4"; | |
| return ( | |
| <div className="w-full" role="presentation"> | |
| {label && ( | |
| <label htmlFor={inputId} className="block mb-2 text-[12px] font-semibold text-[var(--text-primary)]"> | |
| {label} | |
| {props.required && ( | |
| <span className="text-[var(--accent-red)]" aria-hidden="true"> | |
| * | |
| </span> | |
| )} | |
| </label> | |
| )} | |
| <div className="relative"> | |
| {leftIcon && ( | |
| <div className="absolute left-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)] pointer-events-none"> | |
| {leftIcon} | |
| </div> | |
| )} | |
| <input | |
| ref={ref} | |
| id={inputId} | |
| type={type} | |
| disabled={disabled} | |
| aria-invalid={error ? true : undefined} | |
| aria-describedby={error || hint ? messageId : undefined} | |
| // Critical: 16px font-size on mobile to prevent iOS zoom | |
| style={{ | |
| fontSize: mobile.minInputFontSize, | |
| }} | |
| className={clsx( | |
| "w-full", | |
| "h-[44px]", // Fixed height for consistency | |
| "rounded-lg", // Using lg (16px) for inputs - can be adjusted | |
| "border", | |
| "bg-[var(--bg-surface)]", | |
| "text-[var(--text-primary)]", | |
| inputPadding, | |
| "placeholder:text-[var(--text-ghost)]", | |
| "border-[var(--border-subtle)]", | |
| "transition-colors", | |
| "duration-150", | |
| "focus:outline-none", | |
| "focus:border-[var(--accent-primary)]", | |
| "focus:ring-2", | |
| "focus:ring-[var(--accent-primary)]", | |
| "focus:ring-opacity-20", | |
| disabled && "cursor-not-allowed", | |
| disabled && "opacity-40", | |
| disabled && "bg-[var(--bg-elevated)]", | |
| error && "border-[var(--accent-red)]", | |
| error && "focus:border-[var(--accent-red)]", | |
| error && "focus:ring-[var(--accent-red)]", | |
| className, | |
| )} | |
| {...props} | |
| /> | |
| {rightIcon && ( | |
| <div className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)] pointer-events-none"> | |
| {rightIcon} | |
| </div> | |
| )} | |
| </div> | |
| {error && ( | |
| <p id={messageId} className="mt-1.5 text-[12px] text-[var(--accent-red)]" role="alert"> | |
| {error} | |
| </p> | |
| )} | |
| {hint && !error && ( | |
| <p id={messageId} className="mt-1.5 text-[12px] text-[var(--text-muted)]">{hint}</p> | |
| )} | |
| </div> | |
| ); | |
| }, | |
| ); | |
| SharedInput.displayName = "SharedInput"; | |
| // ============================================================================= | |
| // Convenience Input Components | |
| // ============================================================================= | |
| // Search input with icon | |
| export function SearchInput({ | |
| placeholder = "Search...", | |
| className, | |
| ...props | |
| }: Omit<InputProps, "leftIcon">) { | |
| return ( | |
| <SharedInput | |
| type="search" | |
| placeholder={placeholder} | |
| className={clsx("pl-9", className)} | |
| {...props} | |
| /> | |
| ); | |
| } | |
| // Textarea variant | |
| export function SharedTextarea({ | |
| label, | |
| error, | |
| hint, | |
| className, | |
| rows = 4, | |
| ...props | |
| }: { | |
| label?: string; | |
| error?: string; | |
| hint?: string; | |
| className?: string; | |
| rows?: number; | |
| } & Omit<InputHTMLAttributes<HTMLTextAreaElement>, "type" | "leftIcon" | "rightIcon">) { | |
| const generatedId = useId(); | |
| const textareaId = props.id ?? generatedId; | |
| const messageId = `${textareaId}-message`; | |
| return ( | |
| <div className="w-full" role="presentation"> | |
| {label && ( | |
| <label htmlFor={textareaId} className="block mb-2 text-[12px] font-semibold text-[var(--text-primary)]"> | |
| {label} | |
| </label> | |
| )} | |
| <textarea | |
| id={textareaId} | |
| rows={rows} | |
| aria-invalid={error ? true : undefined} | |
| aria-describedby={error || hint ? messageId : undefined} | |
| // Critical: 16px font-size on mobile | |
| style={{ | |
| fontSize: mobile.minInputFontSize, | |
| }} | |
| className={clsx( | |
| "w-full", | |
| "p-4", | |
| "rounded-lg", | |
| "border", | |
| "bg-[var(--bg-surface)]", | |
| "text-[var(--text-primary)]", | |
| "placeholder:text-[var(--text-ghost)]", | |
| "border-[var(--border-subtle)]", | |
| "transition-colors", | |
| "duration-150", | |
| "focus:outline-none", | |
| "focus:border-[var(--accent-primary)]", | |
| "focus:ring-2", | |
| "focus:ring-[var(--accent-primary)]", | |
| "focus:ring-opacity-20", | |
| "resize-none", | |
| error && "border-[var(--accent-red)]", | |
| className, | |
| )} | |
| {...props} | |
| /> | |
| {error && ( | |
| <p id={messageId} className="mt-1.5 text-[12px] text-[var(--accent-red)]" role="alert"> | |
| {error} | |
| </p> | |
| )} | |
| {hint && !error && ( | |
| <p id={messageId} className="mt-1.5 text-[12px] text-[var(--text-muted)]">{hint}</p> | |
| )} | |
| </div> | |
| ); | |
| } | |