diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json
index dd6a0ee42..8257ef8c3 100644
--- a/frontend/public/locales/en-GB/translation.json
+++ b/frontend/public/locales/en-GB/translation.json
@@ -66,6 +66,8 @@
"save": "Save",
"saveToBrowser": "Save to Browser",
"download": "Download",
+ "pin": "Pin",
+ "unpin": "Unpin",
"undoOperationTooltip": "Click to undo the last operation and restore the original files",
"undo": "Undo",
"moreOptions": "More Options",
@@ -281,7 +283,8 @@
"capturing": "Press keys… (Esc to cancel)",
"change": "Change shortcut",
"reset": "Reset",
- "shortcut": "Shortcut"
+ "shortcut": "Shortcut",
+ "noShortcut": "No shortcut set"
}
},
"changeCreds": {
diff --git a/frontend/src/components/fileEditor/FileEditor.module.css b/frontend/src/components/fileEditor/FileEditor.module.css
index 173738c29..17184bbf4 100644
--- a/frontend/src/components/fileEditor/FileEditor.module.css
+++ b/frontend/src/components/fileEditor/FileEditor.module.css
@@ -34,9 +34,9 @@
.header {
height: 2.25rem;
border-radius: 0.0625rem 0.0625rem 0 0;
- display: grid;
- grid-template-columns: 44px 1fr 44px;
+ display: flex;
align-items: center;
+ justify-content: space-between;
padding: 0 6px;
user-select: none;
background: var(--bg-toolbar);
@@ -86,14 +86,23 @@
}
.headerIndex {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
text-align: center;
font-weight: 500;
font-size: 18px;
letter-spacing: 0.04em;
}
-.kebab {
- justify-self: end;
+.headerActions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-left: auto;
+}
+
+.headerIconButton {
color: #FFFFFF !important;
}
@@ -216,6 +225,11 @@
color: rgba(0, 0, 0, 0.35);
}
+.pinned {
+ color: #FFC107 !important;
+}
+
+
/* Unsupported file indicator */
.unsupportedPill {
margin-left: 1.75rem;
@@ -384,4 +398,4 @@
.addFileSubtext {
font-size: 0.875rem;
opacity: 0.8;
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx
index 626eaab4f..90ee6eebe 100644
--- a/frontend/src/components/fileEditor/FileEditor.tsx
+++ b/frontend/src/components/fileEditor/FileEditor.tsx
@@ -288,7 +288,7 @@ const FileEditor = ({
// File operations using context
- const handleDeleteFile = useCallback((fileId: FileId) => {
+ const handleCloseFile = useCallback((fileId: FileId) => {
const record = activeStirlingFileStubs.find(r => r.id === fileId);
const file = record ? selectors.getFile(record.id) : null;
if (record && file) {
@@ -467,7 +467,7 @@ const FileEditor = ({
selectedFiles={localSelectedIds}
selectionMode={selectionMode}
onToggleFile={toggleFile}
- onDeleteFile={handleDeleteFile}
+ onCloseFile={handleCloseFile}
onViewFile={handleViewFile}
_onSetStatus={showStatus}
onReorderFiles={handleReorderFiles}
diff --git a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx
index f09bfeeb1..8159e30a9 100644
--- a/frontend/src/components/fileEditor/FileEditorThumbnail.tsx
+++ b/frontend/src/components/fileEditor/FileEditorThumbnail.tsx
@@ -1,10 +1,10 @@
import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
-import { Text, ActionIcon, CheckboxIndicator } from '@mantine/core';
+import { Text, ActionIcon, CheckboxIndicator, Tooltip } from '@mantine/core';
import { alert } from '../toast';
import { useTranslation } from 'react-i18next';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
-import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
+import CloseIcon from '@mui/icons-material/Close';
import UnarchiveIcon from '@mui/icons-material/Unarchive';
import PushPinIcon from '@mui/icons-material/PushPin';
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
@@ -29,7 +29,7 @@ interface FileEditorThumbnailProps {
selectedFiles: FileId[];
selectionMode: boolean;
onToggleFile: (fileId: FileId) => void;
- onDeleteFile: (fileId: FileId) => void;
+ onCloseFile: (fileId: FileId) => void;
onViewFile: (fileId: FileId) => void;
_onSetStatus: (status: string) => void;
onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void;
@@ -44,7 +44,7 @@ const FileEditorThumbnail = ({
index,
selectedFiles,
onToggleFile,
- onDeleteFile,
+ onCloseFile,
_onSetStatus,
onReorderFiles,
onDownloadFile,
@@ -258,18 +258,60 @@ const FileEditorThumbnail = ({
{index + 1}
- {/* Kebab menu */}
- {
- e.stopPropagation();
- setShowActions((v) => !v);
- }}
- >
-
-
+ {/* Action buttons group */}
+
+ {/* Pin/Unpin icon */}
+
+ {
+ e.stopPropagation();
+ if (actualFile) {
+ if (isPinned) {
+ unpinFile(actualFile);
+ alert({ alertType: 'neutral', title: `Unpinned ${file.name}`, expandable: false, durationMs: 3000 });
+ } else {
+ pinFile(actualFile);
+ alert({ alertType: 'success', title: `Pinned ${file.name}`, expandable: false, durationMs: 3000 });
+ }
+ }
+ }}
+ >
+ {isPinned ? : }
+
+
+
+ {/* Download icon */}
+
+ {
+ e.stopPropagation();
+ onDownloadFile(file.id);
+ alert({ alertType: 'success', title: `Downloading ${file.name}`, expandable: false, durationMs: 2500 });
+ }}
+ >
+
+
+
+
+ {/* Kebab menu */}
+
{
+ e.stopPropagation();
+ setShowActions((v) => !v);
+ }}
+ >
+
+
+
{/* Actions overlay */}
@@ -294,7 +336,7 @@ const FileEditorThumbnail = ({
setShowActions(false);
}}
>
- {isPinned ? : }
+ {isPinned ? : }
{isPinned ? t('unpin', 'Unpin') : t('pin', 'Pin')}
@@ -321,13 +363,13 @@ const FileEditorThumbnail = ({
)}
@@ -394,13 +436,6 @@ const FileEditorThumbnail = ({
)}
- {/* Pin indicator (bottom-left) */}
- {isPinned && (
-
-
-
- )}
-
{/* Drag handle (span wrapper so we can attach a ref reliably) */}
diff --git a/frontend/src/components/hotkeys/HotkeyDisplay.tsx b/frontend/src/components/hotkeys/HotkeyDisplay.tsx
index e240f987e..1b5953a63 100644
--- a/frontend/src/components/hotkeys/HotkeyDisplay.tsx
+++ b/frontend/src/components/hotkeys/HotkeyDisplay.tsx
@@ -55,4 +55,4 @@ export const HotkeyDisplay: React.FC = ({ binding, size = 's
);
};
-export default HotkeyDisplay;
\ No newline at end of file
+export default HotkeyDisplay;
diff --git a/frontend/src/components/shared/config/configSections/HotkeysSection.tsx b/frontend/src/components/shared/config/configSections/HotkeysSection.tsx
index e0240f55a..9630d6f05 100644
--- a/frontend/src/components/shared/config/configSections/HotkeysSection.tsx
+++ b/frontend/src/components/shared/config/configSections/HotkeysSection.tsx
@@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next';
import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext';
import { useHotkeys } from '../../../../contexts/HotkeyContext';
import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay';
-import { bindingEquals, eventToBinding } from '../../../../utils/hotkeys';
+import { bindingEquals, eventToBinding, HotkeyBinding } from '../../../../utils/hotkeys';
+import { ToolId } from 'src/types/toolId';
+import { ToolRegistryEntry } from 'src/data/toolsTaxonomy';
const rowStyle: React.CSSProperties = {
display: 'flex',
@@ -24,10 +26,10 @@ const HotkeysSection: React.FC = () => {
const { t } = useTranslation();
const { toolRegistry } = useToolWorkflow();
const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys();
- const [editingTool, setEditingTool] = useState(null);
+ const [editingTool, setEditingTool] = useState(null);
const [error, setError] = useState(null);
- const tools = useMemo(() => Object.entries(toolRegistry), [toolRegistry]);
+ const tools = useMemo(() => Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][], [toolRegistry]);
useEffect(() => {
if (!editingTool) {
@@ -64,7 +66,7 @@ const HotkeysSection: React.FC = () => {
return;
}
- const conflictEntry = Object.entries(hotkeys).find(([toolId, existing]) => (
+ const conflictEntry = (Object.entries(hotkeys) as [ToolId, HotkeyBinding][]).find(([toolId, existing]) => (
toolId !== editingTool && bindingEquals(existing, binding)
));
@@ -85,7 +87,7 @@ const HotkeysSection: React.FC = () => {
};
}, [editingTool, hotkeys, toolRegistry, updateHotkey, t]);
- const handleStartCapture = (toolId: string) => {
+ const handleStartCapture = (toolId: ToolId) => {
setEditingTool(toolId);
setError(null);
};
@@ -168,4 +170,4 @@ const HotkeysSection: React.FC = () => {
);
};
-export default HotkeysSection;
\ No newline at end of file
+export default HotkeysSection;
diff --git a/frontend/src/components/tools/SearchResults.tsx b/frontend/src/components/tools/SearchResults.tsx
index 5bd6036cc..aca9f88bc 100644
--- a/frontend/src/components/tools/SearchResults.tsx
+++ b/frontend/src/components/tools/SearchResults.tsx
@@ -7,9 +7,10 @@ import { useToolSections } from '../../hooks/useToolSections';
import SubcategoryHeader from './shared/SubcategoryHeader';
import NoToolsFound from './shared/NoToolsFound';
import "./toolPicker/ToolPicker.css";
+import { ToolId } from 'src/types/toolId';
interface SearchResultsProps {
- filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
+ filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
onSelect: (id: string) => void;
searchQuery?: string;
}
@@ -40,13 +41,13 @@ const SearchResults: React.FC = ({ filteredTools, onSelect,
{group.tools.map(({ id, tool }) => {
const matchedText = matchedTextMap.get(id);
// Check if the match was from synonyms and show the actual synonym that matched
- const isSynonymMatch = matchedText && tool.synonyms?.some(synonym =>
+ const isSynonymMatch = matchedText && tool.synonyms?.some(synonym =>
matchedText.toLowerCase().includes(synonym.toLowerCase())
);
- const matchedSynonym = isSynonymMatch ? tool.synonyms?.find(synonym =>
+ const matchedSynonym = isSynonymMatch ? tool.synonyms?.find(synonym =>
matchedText.toLowerCase().includes(synonym.toLowerCase())
) : undefined;
-
+
return (
void;
- filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
+ filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
isSearching?: boolean;
}
diff --git a/frontend/src/components/tools/ToolRenderer.tsx b/frontend/src/components/tools/ToolRenderer.tsx
index fbd8100d1..97353ba09 100644
--- a/frontend/src/components/tools/ToolRenderer.tsx
+++ b/frontend/src/components/tools/ToolRenderer.tsx
@@ -2,9 +2,10 @@ import { Suspense } from "react";
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
import { BaseToolProps } from "../../types/tool";
import ToolLoadingFallback from "./ToolLoadingFallback";
+import { ToolId } from "src/types/toolId";
interface ToolRendererProps extends BaseToolProps {
- selectedToolKey: string;
+ selectedToolKey: ToolId;
}
diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx
index 5cbc4b3ab..02fea2c97 100644
--- a/frontend/src/components/tools/automate/ToolSelector.tsx
+++ b/frontend/src/components/tools/automate/ToolSelector.tsx
@@ -6,6 +6,7 @@ import { useToolSections } from '../../../hooks/useToolSections';
import { renderToolButtons } from '../shared/renderToolButtons';
import ToolSearch from '../toolPicker/ToolSearch';
import ToolButton from '../toolPicker/ToolButton';
+import { ToolId } from 'src/types/toolId';
interface ToolSelectorProps {
onSelect: (toolKey: string) => void;
@@ -30,7 +31,7 @@ export default function ToolSelector({
// Filter out excluded tools (like 'automate' itself) and tools that don't support automation
const baseFilteredTools = useMemo(() => {
- return Object.entries(toolRegistry).filter(([key, tool]) =>
+ return (Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][]).filter(([key, tool]) =>
!excludeTools.includes(key) && getToolSupportsAutomate(tool)
);
}, [toolRegistry, excludeTools]);
@@ -66,7 +67,7 @@ export default function ToolSelector({
}, [filteredTools]);
// Use the same tool sections logic as the main ToolPicker
- const { sections, searchGroups } = useToolSections(transformedFilteredTools);
+ const { sections, searchGroups } = useToolSections(transformedFilteredTools as any /* FIX ME */);
// Determine what to display: search results or organized sections
const isSearching = searchTerm.trim().length > 0;
@@ -156,7 +157,7 @@ export default function ToolSelector({
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
- {}} rounded={true} disableNavigation={true}>
) : (
diff --git a/frontend/src/components/tools/shared/renderToolButtons.tsx b/frontend/src/components/tools/shared/renderToolButtons.tsx
index a4aadf20b..659ffefca 100644
--- a/frontend/src/components/tools/shared/renderToolButtons.tsx
+++ b/frontend/src/components/tools/shared/renderToolButtons.tsx
@@ -5,13 +5,14 @@ import SubcategoryHeader from './SubcategoryHeader';
import { getSubcategoryLabel } from "../../../data/toolsTaxonomy";
import { TFunction } from 'i18next';
import { SubcategoryGroup } from '../../../hooks/useToolSections';
+import { ToolId } from 'src/types/toolId';
// Helper function to render tool buttons for a subcategory
export const renderToolButtons = (
t: TFunction,
subcategory: SubcategoryGroup,
selectedToolKey: string | null,
- onSelect: (id: string) => void,
+ onSelect: (id: ToolId) => void,
showSubcategoryHeader: boolean = true,
disableNavigation: boolean = false,
searchResults?: Array<{ item: [string, any]; matchedText?: string }>
@@ -32,7 +33,7 @@ export const renderToolButtons = (
{subcategory.tools.map(({ id, tool }) => {
const matchedSynonym = matchedTextMap.get(id);
-
+
return (
void;
+ onSelect: (id: ToolId) => void;
rounded?: boolean;
disableNavigation?: boolean;
matchedSynonym?: string;
@@ -28,7 +29,7 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect,
const binding = hotkeys[id];
const { getToolNavigation } = useToolNavigation();
- const handleClick = (id: string) => {
+ const handleClick = (id: ToolId) => {
if (isUnavailable) return;
if (tool.link) {
// Open external link in new tab
@@ -47,12 +48,16 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect,
: (
{tool.description}
- {binding && (
-
+
+ {binding ? (
+ <>
{t('settings.hotkeys.shortcut', 'Shortcut')}
-
+ >
+ ) : (
+
{t('settings.hotkeys.noShortcut', 'No shortcut set')}
)}
+
);
@@ -102,7 +107,7 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect,
fullWidth
justify="flex-start"
className="tool-button"
- styles={{
+ styles={{
root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", overflow: 'visible' },
label: { overflow: 'visible' }
}}
@@ -123,7 +128,7 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect,
fullWidth
justify="flex-start"
className="tool-button"
- styles={{
+ styles={{
root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", overflow: 'visible' },
label: { overflow: 'visible' }
}}
diff --git a/frontend/src/contexts/HotkeyContext.tsx b/frontend/src/contexts/HotkeyContext.tsx
index fe9cbb600..c5a0f2649 100644
--- a/frontend/src/contexts/HotkeyContext.tsx
+++ b/frontend/src/contexts/HotkeyContext.tsx
@@ -2,14 +2,17 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS
import { HotkeyBinding, bindingEquals, bindingMatchesEvent, deserializeBindings, getDisplayParts, isMacLike, normalizeBinding, serializeBindings } from '../utils/hotkeys';
import { useToolWorkflow } from './ToolWorkflowContext';
import { ToolId } from '../types/toolId';
+import { ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy';
+
+type Bindings = Partial>;
interface HotkeyContextValue {
- hotkeys: Record;
- defaults: Record;
+ hotkeys: Bindings;
+ defaults: Bindings;
isMac: boolean;
- updateHotkey: (toolId: string, binding: HotkeyBinding) => void;
- resetHotkey: (toolId: string) => void;
- isBindingAvailable: (binding: HotkeyBinding, excludeToolId?: string) => boolean;
+ updateHotkey: (toolId: ToolId, binding: HotkeyBinding) => void;
+ resetHotkey: (toolId: ToolId) => void;
+ isBindingAvailable: (binding: HotkeyBinding, excludeToolId?: ToolId) => boolean;
pauseHotkeys: () => void;
resumeHotkeys: () => void;
areHotkeysPaused: boolean;
@@ -20,46 +23,29 @@ const HotkeyContext = createContext(undefined);
const STORAGE_KEY = 'stirlingpdf.hotkeys';
-const KEY_ORDER: string[] = [
- 'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9', 'Digit0',
- 'KeyQ', 'KeyW', 'KeyE', 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP',
- 'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', 'KeyH', 'KeyJ', 'KeyK', 'KeyL',
- 'KeyZ', 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', 'KeyM',
- 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
-];
+const generateDefaultHotkeys = (toolEntries: [ToolId, ToolRegistryEntry][], macLike: boolean): Bindings => {
+ const defaults: Bindings = {};
-const generateDefaultHotkeys = (toolIds: string[], macLike: boolean): Record => {
- const defaults: Record = {};
- let index = 0;
- let useShift = false;
+ // Get Quick Access tools (RECOMMENDED_TOOLS category) from registry
+ const quickAccessTools = toolEntries
+ .filter(([_, tool]) => tool.categoryId === ToolCategoryId.RECOMMENDED_TOOLS)
+ .map(([toolId, _]) => toolId);
- const nextBinding = (): HotkeyBinding => {
- if (index >= KEY_ORDER.length) {
- index = 0;
- if (!useShift) {
- useShift = true;
- } else {
- // If we somehow run out of combinations, wrap back around (unlikely given tool count)
- useShift = false;
- }
+ // Assign Cmd+Option+Number (Mac) or Ctrl+Alt+Number (Windows) to Quick Access tools
+ quickAccessTools.forEach((toolId, index) => {
+ if (index < 9) { // Limit to Digit1-9
+ const digitNumber = index + 1;
+ defaults[toolId] = {
+ code: `Digit${digitNumber}`,
+ alt: true,
+ shift: false,
+ meta: macLike,
+ ctrl: !macLike,
+ };
}
-
- const code = KEY_ORDER[index];
- index += 1;
-
- return {
- code,
- alt: true,
- shift: useShift,
- meta: macLike,
- ctrl: !macLike,
- };
- };
-
- toolIds.forEach(toolId => {
- defaults[toolId] = nextBinding();
});
+ // All other tools have no default (will be undefined in the record)
return defaults;
};
@@ -74,7 +60,7 @@ const shouldIgnoreTarget = (target: EventTarget | null): boolean => {
export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { toolRegistry, handleToolSelect } = useToolWorkflow();
const isMac = useMemo(() => isMacLike(), []);
- const [customBindings, setCustomBindings] = useState>(() => {
+ const [customBindings, setCustomBindings] = useState(() => {
if (typeof window === 'undefined') {
return {};
}
@@ -82,16 +68,16 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
});
const [areHotkeysPaused, setHotkeysPaused] = useState(false);
- const toolIds = useMemo(() => Object.keys(toolRegistry), [toolRegistry]);
+ const toolEntries = useMemo(() => Object.entries(toolRegistry), [toolRegistry]) as [ToolId, ToolRegistryEntry][];
- const defaults = useMemo(() => generateDefaultHotkeys(toolIds, isMac), [toolIds, isMac]);
+ const defaults = useMemo(() => generateDefaultHotkeys(toolEntries, isMac), [toolRegistry, isMac]);
// Remove bindings for tools that are no longer present
useEffect(() => {
setCustomBindings(prev => {
- const next: Record = {};
+ const next: Bindings = {};
let changed = false;
- Object.entries(prev).forEach(([toolId, binding]) => {
+ (Object.entries(prev) as [ToolId, HotkeyBinding][]).forEach(([toolId, binding]) => {
if (toolRegistry[toolId]) {
next[toolId] = binding;
} else {
@@ -103,13 +89,21 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
}, [toolRegistry]);
const resolved = useMemo(() => {
- const merged: Record = {};
- toolIds.forEach(toolId => {
+ const merged: Bindings = {};
+ toolEntries.forEach(([toolId, _]) => {
const custom = customBindings[toolId];
- merged[toolId] = custom ? normalizeBinding(custom) : defaults[toolId];
+ const defaultBinding = defaults[toolId];
+
+ // Only add to resolved if there's a custom binding or a default binding
+ if (custom) {
+ merged[toolId] = normalizeBinding(custom);
+ } else if (defaultBinding) {
+ merged[toolId] = defaultBinding;
+ }
+ // If neither exists, don't add to merged (tool has no hotkey)
});
return merged;
- }, [customBindings, defaults, toolIds]);
+ }, [customBindings, defaults, toolEntries]);
useEffect(() => {
if (typeof window === 'undefined') {
@@ -118,7 +112,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
window.localStorage.setItem(STORAGE_KEY, serializeBindings(customBindings));
}, [customBindings]);
- const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: string) => {
+ const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: ToolId) => {
const normalized = normalizeBinding(binding);
return Object.entries(resolved).every(([toolId, existing]) => {
if (toolId === excludeToolId) {
@@ -128,7 +122,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
});
}, [resolved]);
- const updateHotkey = useCallback((toolId: string, binding: HotkeyBinding) => {
+ const updateHotkey = useCallback((toolId: ToolId, binding: HotkeyBinding) => {
setCustomBindings(prev => {
const normalized = normalizeBinding(binding);
const defaultsForTool = defaults[toolId];
@@ -142,7 +136,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
});
}, [defaults]);
- const resetHotkey = useCallback((toolId: string) => {
+ const resetHotkey = useCallback((toolId: ToolId) => {
setCustomBindings(prev => {
if (!(toolId in prev)) {
return prev;
@@ -165,12 +159,12 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
if (event.repeat) return;
if (shouldIgnoreTarget(event.target)) return;
- const entries = Object.entries(resolved) as Array<[string, HotkeyBinding]>;
+ const entries = Object.entries(resolved) as [ToolId, HotkeyBinding][];
for (const [toolId, binding] of entries) {
if (bindingMatchesEvent(binding, event)) {
event.preventDefault();
event.stopPropagation();
- handleToolSelect(toolId as ToolId);
+ handleToolSelect(toolId);
break;
}
}
@@ -208,4 +202,4 @@ export const useHotkeys = (): HotkeyContextValue => {
throw new Error('useHotkeys must be used within a HotkeyProvider');
}
return context;
-};
\ No newline at end of file
+};
diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx
index 98a0fd37d..73ef058bf 100644
--- a/frontend/src/contexts/ToolWorkflowContext.tsx
+++ b/frontend/src/contexts/ToolWorkflowContext.tsx
@@ -73,10 +73,10 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio
// Context value interface
interface ToolWorkflowContextValue extends ToolWorkflowState {
// Tool management (from hook)
- selectedToolKey: string | null;
+ selectedToolKey: ToolId | null;
selectedTool: ToolRegistryEntry | null;
- toolRegistry: Record;
- getSelectedTool: (toolId: string | null) => ToolRegistryEntry | null;
+ toolRegistry: Partial;
+ getSelectedTool: (toolId: ToolId | null) => ToolRegistryEntry | null;
// UI Actions
setSidebarsVisible: (visible: boolean) => void;
@@ -101,7 +101,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
handleReaderToggle: () => void;
// Computed values
- filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
+ filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
isPanelVisible: boolean;
}
diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx
index 2c9571e7f..04ac93fa9 100644
--- a/frontend/src/data/useTranslatedToolRegistry.tsx
+++ b/frontend/src/data/useTranslatedToolRegistry.tsx
@@ -828,7 +828,7 @@ export function useFlatToolRegistry(): ToolRegistry {
name: t("home.compare.title", "Compare"),
component: null,
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
- categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
+ categoryId: ToolCategoryId.STANDARD_TOOLS /* TODO: Change to RECOMMENDED_TOOLS when component is implemented */,
subcategoryId: SubcategoryId.GENERAL,
synonyms: getSynonyms(t, "compare"),
supportsAutomate: false,
diff --git a/frontend/src/hooks/useSidebarNavigation.ts b/frontend/src/hooks/useSidebarNavigation.ts
index a65ea96de..42f92e0f1 100644
--- a/frontend/src/hooks/useSidebarNavigation.ts
+++ b/frontend/src/hooks/useSidebarNavigation.ts
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { useToolNavigation } from './useToolNavigation';
import { useToolWorkflow } from '../contexts/ToolWorkflowContext';
import { handleUnlessSpecialClick } from '../utils/clickHandlers';
+import { ToolId } from 'src/types/toolId';
export interface SidebarNavigationProps {
/** Full URL for the navigation (for href attribute) */
@@ -16,7 +17,7 @@ export interface SidebarNavigationProps {
*/
export function useSidebarNavigation(): {
getHomeNavigation: () => SidebarNavigationProps;
- getToolNavigation: (toolId: string) => SidebarNavigationProps | null;
+ getToolNavigation: (toolId: ToolId) => SidebarNavigationProps | null;
} {
const { getToolNavigation: getToolNavProps } = useToolNavigation();
const { getSelectedTool } = useToolWorkflow();
@@ -32,14 +33,14 @@ export function useSidebarNavigation(): {
return { href, onClick: defaultNavClick };
}, [defaultNavClick]);
- const getToolNavigation = useCallback((toolId: string): SidebarNavigationProps | null => {
+ const getToolNavigation = useCallback((toolId: ToolId): SidebarNavigationProps | null => {
// Handle special nav sections that aren't tools
if (toolId === 'read') return { href: '/read', onClick: defaultNavClick };
if (toolId === 'automate') return { href: '/automate', onClick: defaultNavClick };
const tool = getSelectedTool(toolId);
if (!tool) return null;
-
+
// Delegate to useToolNavigation for true tools
return getToolNavProps(toolId, tool);
}, [getToolNavProps, getSelectedTool, defaultNavClick]);
@@ -48,4 +49,4 @@ export function useSidebarNavigation(): {
getHomeNavigation,
getToolNavigation
};
-}
\ No newline at end of file
+}
diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx
index 3239cbaaa..054e99304 100644
--- a/frontend/src/hooks/useToolManagement.tsx
+++ b/frontend/src/hooks/useToolManagement.tsx
@@ -1,16 +1,17 @@
import { useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
-import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy";
+import { getAllEndpoints, type ToolRegistryEntry, type ToolRegistry } from "../data/toolsTaxonomy";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
import { FileId } from '../types/file';
+import { ToolId } from 'src/types/toolId';
interface ToolManagementResult {
selectedTool: ToolRegistryEntry | null;
toolSelectedFileIds: FileId[];
- toolRegistry: Record;
+ toolRegistry: Partial;
setToolSelectedFileIds: (fileIds: FileId[]) => void;
- getSelectedTool: (toolKey: string | null) => ToolRegistryEntry | null;
+ getSelectedTool: (toolKey: ToolId | null) => ToolRegistryEntry | null;
}
export const useToolManagement = (): ToolManagementResult => {
@@ -30,9 +31,9 @@ export const useToolManagement = (): ToolManagementResult => {
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
}, [endpointsLoading, endpointStatus, baseRegistry]);
- const toolRegistry: Record = useMemo(() => {
- const availableToolRegistry: Record = {};
- Object.keys(baseRegistry).forEach(toolKey => {
+ const toolRegistry: Partial = useMemo(() => {
+ const availableToolRegistry: Partial = {};
+ (Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
if (isToolAvailable(toolKey)) {
const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
availableToolRegistry[toolKey] = {
@@ -45,7 +46,7 @@ export const useToolManagement = (): ToolManagementResult => {
return availableToolRegistry;
}, [isToolAvailable, t, baseRegistry]);
- const getSelectedTool = useCallback((toolKey: string | null): ToolRegistryEntry | null => {
+ const getSelectedTool = useCallback((toolKey: ToolId | null): ToolRegistryEntry | null => {
return toolKey ? toolRegistry[toolKey] || null : null;
}, [toolRegistry]);
diff --git a/frontend/src/hooks/useToolSections.ts b/frontend/src/hooks/useToolSections.ts
index 5c5be8773..40eac31e6 100644
--- a/frontend/src/hooks/useToolSections.ts
+++ b/frontend/src/hooks/useToolSections.ts
@@ -2,9 +2,10 @@ import { useMemo } from 'react';
import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy';
import { useTranslation } from 'react-i18next';
+import { ToolId } from 'src/types/toolId';
type SubcategoryIdMap = {
- [subcategoryId in SubcategoryId]: Array<{ id: string /* FIX ME: Should be ToolId */; tool: ToolRegistryEntry }>;
+ [subcategoryId in SubcategoryId]: Array<{ id: ToolId; tool: ToolRegistryEntry }>;
}
type GroupedTools = {
@@ -14,7 +15,7 @@ type GroupedTools = {
export interface SubcategoryGroup {
subcategoryId: SubcategoryId;
tools: {
- id: string /* FIX ME: Should be ToolId */;
+ id: ToolId;
tool: ToolRegistryEntry;
}[];
};
@@ -28,7 +29,7 @@ export interface ToolSection {
};
export function useToolSections(
- filteredTools: Array<{ item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }>,
+ filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>,
searchQuery?: string
) {
const { t } = useTranslation();
@@ -37,7 +38,7 @@ export function useToolSections(
if (!filteredTools || !Array.isArray(filteredTools)) {
return {} as GroupedTools;
}
-
+
const grouped = {} as GroupedTools;
filteredTools.forEach(({ item: [id, tool] }) => {
const categoryId = tool.categoryId;
@@ -105,11 +106,11 @@ export function useToolSections(
if (!filteredTools || !Array.isArray(filteredTools)) {
return [];
}
-
+
const subMap = {} as SubcategoryIdMap;
- const seen = new Set();
+ const seen = new Set();
filteredTools.forEach(({ item: [id, tool] }) => {
- const toolId = id as string /* FIX ME: Should be ToolId */;
+ const toolId = id as ToolId;
if (seen.has(toolId)) return;
seen.add(toolId);
const sub = tool.subcategoryId;
diff --git a/frontend/src/utils/toolSearch.ts b/frontend/src/utils/toolSearch.ts
index dda5749a8..8bd963564 100644
--- a/frontend/src/utils/toolSearch.ts
+++ b/frontend/src/utils/toolSearch.ts
@@ -1,8 +1,9 @@
+import { ToolId } from "src/types/toolId";
import { ToolRegistryEntry } from "../data/toolsTaxonomy";
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
export interface RankedToolItem {
- item: [string, ToolRegistryEntry];
+ item: [ToolId, ToolRegistryEntry];
matchedText?: string;
}
@@ -10,18 +11,18 @@ export function filterToolRegistryByQuery(
toolRegistry: Record,
query: string
): RankedToolItem[] {
- const entries = Object.entries(toolRegistry);
+ const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][];
if (!query.trim()) {
- return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
+ return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
}
const nq = normalizeForSearch(query);
const threshold = minScoreForQuery(query);
- const exactName: Array<{ id: string; tool: ToolRegistryEntry; pos: number }> = [];
- const exactSyn: Array<{ id: string; tool: ToolRegistryEntry; text: string; pos: number }> = [];
- const fuzzyName: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
- const fuzzySyn: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
+ const exactName: Array<{ id: ToolId; tool: ToolRegistryEntry; pos: number }> = [];
+ const exactSyn: Array<{ id: ToolId; tool: ToolRegistryEntry; text: string; pos: number }> = [];
+ const fuzzyName: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = [];
+ const fuzzySyn: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = [];
for (const [id, tool] of entries) {
const nameNorm = normalizeForSearch(tool.name || '');
@@ -78,7 +79,7 @@ export function filterToolRegistryByQuery(
const seen = new Set();
const ordered: RankedToolItem[] = [];
- const push = (id: string, tool: ToolRegistryEntry, matchedText?: string) => {
+ const push = (id: ToolId, tool: ToolRegistryEntry, matchedText?: string) => {
if (seen.has(id)) return;
seen.add(id);
ordered.push({ item: [id, tool], matchedText });
@@ -92,7 +93,7 @@ export function filterToolRegistryByQuery(
if (ordered.length > 0) return ordered;
// Fallback: return everything unchanged
- return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
+ return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
}