mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Feature/v2/all tools sidebar (#4151)
# Description of Changes - Added the all tools sidebar - Added a TextFit component that shrinks text to fit containers - Added a TopToolIcon on the nav, that animates down to give users feedback on what tool is selected - Added the baseToolRegistry, to replace the old pattern of listing tools, allowing us to clean up the ToolRegistry code - Fixed Mantine light/dark theme race condition - General styling tweaks --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { Stack, Button, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
|
||||
import { TextInput } from "../../shared/TextInput";
|
||||
import './ToolPicker.css';
|
||||
|
||||
interface ToolSearchProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
toolRegistry: Readonly<Record<string, ToolRegistryEntry>>;
|
||||
onToolSelect?: (toolId: string) => void;
|
||||
mode: 'filter' | 'dropdown';
|
||||
selectedToolKey?: string | null;
|
||||
}
|
||||
|
||||
const ToolSearch = ({
|
||||
value,
|
||||
onChange,
|
||||
toolRegistry,
|
||||
onToolSelect,
|
||||
mode = 'filter',
|
||||
selectedToolKey
|
||||
}: ToolSearchProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const filteredTools = useMemo(() => {
|
||||
if (!value.trim()) return [];
|
||||
return Object.entries(toolRegistry)
|
||||
.filter(([id, tool]) => {
|
||||
if (mode === 'dropdown' && id === selectedToolKey) return false;
|
||||
return tool.name.toLowerCase().includes(value.toLowerCase()) ||
|
||||
tool.description.toLowerCase().includes(value.toLowerCase());
|
||||
})
|
||||
.slice(0, 6)
|
||||
.map(([id, tool]) => ({ id, tool }));
|
||||
}, [value, toolRegistry, mode, selectedToolKey]);
|
||||
|
||||
const handleSearchChange = (searchValue: string) => {
|
||||
onChange(searchValue);
|
||||
if (mode === 'dropdown') {
|
||||
setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const searchInput = (
|
||||
<div className="search-input-container">
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
value={value}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
icon={<span className="material-symbols-rounded">search</span>}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (mode === 'filter') {
|
||||
return searchInput;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={searchRef} style={{ position: 'relative' }}>
|
||||
{searchInput}
|
||||
{dropdownOpen && filteredTools.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
border: '1px solid var(--border-default)',
|
||||
borderRadius: '8px',
|
||||
marginTop: '4px',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
|
||||
}}
|
||||
>
|
||||
<Stack gap="xs" style={{ padding: '8px' }}>
|
||||
{filteredTools.map(({ id, tool }) => (
|
||||
<Button
|
||||
key={id}
|
||||
variant="subtle"
|
||||
onClick={() => onToolSelect && onToolSelect(id)}
|
||||
leftSection={
|
||||
<div style={{ color: 'var(--tools-text-and-icon-color)' }}>
|
||||
{tool.icon}
|
||||
</div>
|
||||
}
|
||||
fullWidth
|
||||
justify="flex-start"
|
||||
style={{
|
||||
borderRadius: '6px',
|
||||
color: 'var(--tools-text-and-icon-color)',
|
||||
padding: '8px 12px'
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<div style={{ fontWeight: 500 }}>{tool.name}</div>
|
||||
<Text size="xs" c="dimmed" style={{ marginTop: '2px' }}>
|
||||
{tool.description}
|
||||
</Text>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolSearch;
|
||||
Reference in New Issue
Block a user