mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-21 19:59:24 +00:00
had to add a custom text input component because mantine's TextInput wasn't playing ball
This commit is contained in:
parent
b2bea0ede6
commit
ba179cca7b
91
frontend/src/components/shared/TextInput.tsx
Normal file
91
frontend/src/components/shared/TextInput.tsx
Normal file
@ -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<HTMLInputElement, TextInputProps>(({
|
||||
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 (
|
||||
<div className={`${styles.container} ${className}`} style={style}>
|
||||
{icon && (
|
||||
<span
|
||||
className={styles.icon}
|
||||
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => 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 && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.clearButton}
|
||||
onClick={handleClear}
|
||||
style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}
|
||||
aria-label="Clear input"
|
||||
>
|
||||
<span className="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TextInput.displayName = 'TextInput';
|
82
frontend/src/components/shared/textInput/TextInput.README.md
Normal file
82
frontend/src/components/shared/textInput/TextInput.README.md
Normal file
@ -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
|
||||
<TextInput
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
|
||||
// With icon
|
||||
<TextInput
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
placeholder="Search tools..."
|
||||
icon={<span className="material-symbols-rounded">search</span>}
|
||||
/>
|
||||
|
||||
// With custom clear handler
|
||||
<TextInput
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
onClear={() => {
|
||||
setSearchValue('');
|
||||
// Additional cleanup logic
|
||||
}}
|
||||
/>
|
||||
|
||||
// Disabled state
|
||||
<TextInput
|
||||
value={searchValue}
|
||||
onChange={setSearchValue}
|
||||
disabled={true}
|
||||
/>
|
||||
```
|
||||
|
||||
## 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
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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<HTMLInputElement>(null);
|
||||
|
||||
@ -57,21 +57,13 @@ const ToolSearch = ({
|
||||
|
||||
const searchInput = (
|
||||
<div className="search-input-container">
|
||||
<span className="material-symbols-rounded search-icon" style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}>
|
||||
search
|
||||
</span>
|
||||
<input
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
type="text"
|
||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
value={value}
|
||||
onChange={(e) => handleSearchChange(e.currentTarget.value)}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
icon={<span className="material-symbols-rounded">search</span>}
|
||||
autoComplete="off"
|
||||
className="search-input-field"
|
||||
style={{
|
||||
backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF',
|
||||
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user