diff --git a/frontend/src/components/shared/TextInput.tsx b/frontend/src/components/shared/TextInput.tsx new file mode 100644 index 000000000..ec880e30a --- /dev/null +++ b/frontend/src/components/shared/TextInput.tsx @@ -0,0 +1,91 @@ +import React, { forwardRef } from 'react'; +import { useMantineColorScheme } from '@mantine/core'; +import styles from './textInput/TextInput.module.css'; + +export interface TextInputProps { + value: string; + onChange: (value: string) => void; + placeholder?: string; + icon?: React.ReactNode; + showClearButton?: boolean; + onClear?: () => void; + className?: string; + style?: React.CSSProperties; + autoComplete?: string; + disabled?: boolean; + readOnly?: boolean; + 'aria-label'?: string; +} + +export const TextInput = forwardRef(({ + value, + onChange, + placeholder, + icon, + showClearButton = true, + onClear, + className = '', + style, + autoComplete = 'off', + disabled = false, + readOnly = false, + 'aria-label': ariaLabel, + ...props +}, ref) => { + const { colorScheme } = useMantineColorScheme(); + + const handleClear = () => { + if (onClear) { + onClear(); + } else { + onChange(''); + } + }; + + const shouldShowClearButton = showClearButton && value.trim().length > 0 && !disabled && !readOnly; + + return ( +
+ {icon && ( + + {icon} + + )} + onChange(e.currentTarget.value)} + autoComplete={autoComplete} + className={styles.input} + disabled={disabled} + readOnly={readOnly} + aria-label={ariaLabel} + style={{ + backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF', + color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382', + paddingRight: shouldShowClearButton ? '40px' : '12px', + paddingLeft: icon ? '40px' : '12px', + }} + {...props} + /> + {shouldShowClearButton && ( + + )} +
+ ); +}); + +TextInput.displayName = 'TextInput'; diff --git a/frontend/src/components/shared/textInput/TextInput.README.md b/frontend/src/components/shared/textInput/TextInput.README.md new file mode 100644 index 000000000..d7311df3b --- /dev/null +++ b/frontend/src/components/shared/textInput/TextInput.README.md @@ -0,0 +1,82 @@ +# TextInput Component + +A reusable text input component with optional icon, clear button, and theme-aware styling. This was created because Mantine's TextInput has limited styling + +## Features + +- **Theme-aware**: Automatically adapts to light/dark color schemes +- **Icon support**: Optional left icon with proper positioning +- **Clear button**: Optional clear button that appears when input has content +- **Accessible**: Proper ARIA labels and keyboard navigation +- **Customizable**: Flexible props for styling and behavior + +## Usage + +```tsx +import { TextInput } from '../shared/textInput'; + +// Basic usage + + +// With icon +search} +/> + +// With custom clear handler + { + setSearchValue(''); + // Additional cleanup logic + }} +/> + +// Disabled state + +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `value` | `string` | - | The input value (required) | +| `onChange` | `(value: string) => void` | - | Callback when input value changes (required) | +| `placeholder` | `string` | - | Placeholder text | +| `icon` | `React.ReactNode` | - | Optional left icon | +| `showClearButton` | `boolean` | `true` | Whether to show the clear button | +| `onClear` | `() => void` | - | Custom clear handler (defaults to setting value to empty string) | +| `className` | `string` | `''` | Additional CSS classes | +| `style` | `React.CSSProperties` | - | Additional inline styles | +| `autoComplete` | `string` | `'off'` | HTML autocomplete attribute | +| `disabled` | `boolean` | `false` | Whether the input is disabled | +| `readOnly` | `boolean` | `false` | Whether the input is read-only | +| `aria-label` | `string` | - | Accessibility label | + +## Styling + +The component uses CSS modules and automatically adapts to the current color scheme. The styling includes: + +- Proper focus states with blue outline +- Hover effects on the clear button +- Theme-aware colors for text, background, and icons +- Responsive padding based on icon and clear button presence + +## Accessibility + +- Proper ARIA labels +- Keyboard navigation support +- Screen reader friendly clear button +- Focus management diff --git a/frontend/src/components/shared/textInput/TextInput.module.css b/frontend/src/components/shared/textInput/TextInput.module.css new file mode 100644 index 000000000..7ccf30063 --- /dev/null +++ b/frontend/src/components/shared/textInput/TextInput.module.css @@ -0,0 +1,73 @@ +.container { + position: relative; + display: flex; + align-items: center; +} + +.icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + z-index: 1; + font-size: 16px; + pointer-events: none; + display: flex; + align-items: center; + justify-content: center; +} + +.input { + width: 100%; + padding: 8px 12px; + border: none; + border-radius: 6px; + font-size: 14px; + outline: none; + box-shadow: none; + transition: box-shadow 0.2s ease; +} + +.input::placeholder { + color: var(--search-text-and-icon-color); + opacity: 1; +} + +.input:focus { + outline: none; + box-shadow: none; +} + +.input:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.input:read-only { + cursor: default; +} + +.clearButton { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + transition: background-color 0.2s ease; +} + +.clearButton:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +[data-mantine-color-scheme="dark"] .clearButton:hover { + background-color: rgba(255, 255, 255, 0.1); +} diff --git a/frontend/src/components/tools/toolPicker/ToolPicker.css b/frontend/src/components/tools/toolPicker/ToolPicker.css index f72fe3a58..b61f16a9a 100644 --- a/frontend/src/components/tools/toolPicker/ToolPicker.css +++ b/frontend/src/components/tools/toolPicker/ToolPicker.css @@ -73,37 +73,6 @@ } .search-input-container { - position: relative; margin-top: 0.5rem; margin-bottom: 0.5rem; - display: flex; - align-items: center; -} - -.search-icon { - position: absolute; - left: 12px; - z-index: 1; - font-size: 16px; - pointer-events: none; -} - -.search-input-field { - width: 100%; - padding: 8px 12px 8px 40px; - border: none; - border-radius: 6px; - font-size: 14px; - outline: none; - box-shadow: none; -} - -.search-input-field::placeholder { - color: var(--search-text-and-icon-color); - opacity: 1; -} - -.search-input-field:focus { - outline: none; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); } \ No newline at end of file diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx index baa53026f..4497223e3 100644 --- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -1,7 +1,8 @@ import React, { useState, useRef, useEffect, useMemo } from "react"; -import { TextInput, Stack, Button, Text, useMantineColorScheme } from "@mantine/core"; +import { Stack, Button, Text } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { type ToolRegistryEntry } from "../../../data/toolRegistry"; +import { TextInput } from "../../shared/TextInput"; import './ToolPicker.css'; interface ToolSearchProps { @@ -22,7 +23,6 @@ const ToolSearch = ({ selectedToolKey }: ToolSearchProps) => { const { t } = useTranslation(); - const { colorScheme } = useMantineColorScheme(); const [dropdownOpen, setDropdownOpen] = useState(false); const searchRef = useRef(null); @@ -57,21 +57,13 @@ const ToolSearch = ({ const searchInput = (
- - search - - handleSearchChange(e.currentTarget.value)} + onChange={handleSearchChange} + placeholder={t("toolPicker.searchPlaceholder", "Search tools...")} + icon={search} autoComplete="off" - className="search-input-field" - style={{ - backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF', - color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382', - }} />
);