mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-22 04:09:22 +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 {
|
.search-input-container {
|
||||||
position: relative;
|
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
margin-bottom: 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 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 { useTranslation } from "react-i18next";
|
||||||
import { type ToolRegistryEntry } from "../../../data/toolRegistry";
|
import { type ToolRegistryEntry } from "../../../data/toolRegistry";
|
||||||
|
import { TextInput } from "../../shared/TextInput";
|
||||||
import './ToolPicker.css';
|
import './ToolPicker.css';
|
||||||
|
|
||||||
interface ToolSearchProps {
|
interface ToolSearchProps {
|
||||||
@ -22,7 +23,6 @@ const ToolSearch = ({
|
|||||||
selectedToolKey
|
selectedToolKey
|
||||||
}: ToolSearchProps) => {
|
}: ToolSearchProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@ -57,21 +57,13 @@ const ToolSearch = ({
|
|||||||
|
|
||||||
const searchInput = (
|
const searchInput = (
|
||||||
<div className="search-input-container">
|
<div className="search-input-container">
|
||||||
<span className="material-symbols-rounded search-icon" style={{ color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}>
|
<TextInput
|
||||||
search
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
ref={searchRef}
|
ref={searchRef}
|
||||||
type="text"
|
|
||||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
|
||||||
value={value}
|
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"
|
autoComplete="off"
|
||||||
className="search-input-field"
|
|
||||||
style={{
|
|
||||||
backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF',
|
|
||||||
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user