From a0856463337557ff1553df6d1ef2776074f020d8 Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 9 Oct 2025 02:51:47 +0100 Subject: [PATCH] change requests --- frontend/src/components/FileManager.tsx | 3 +- .../annotation/shared/DrawingCanvas.tsx | 4 +- .../src/components/fileEditor/FileEditor.tsx | 4 +- .../components/fileManager/FileDetails.tsx | 4 +- .../fileManager/FileHistoryGroup.tsx | 3 +- .../components/fileManager/FileListItem.tsx | 3 +- .../src/components/shared/AppConfigModal.tsx | 3 +- .../components/shared/LanguageSelector.tsx | 5 +- .../src/components/shared/QuickAccessBar.tsx | 24 +- frontend/src/components/shared/RightRail.tsx | 10 +- frontend/src/components/shared/Tooltip.tsx | 3 +- .../config/configSections/GeneralSection.tsx | 3 +- .../config/configSections/HotkeysSection.tsx | 10 +- .../rightRail/ViewerAnnotationControls.tsx | 14 +- .../components/tools/FullscreenToolList.tsx | 230 ++---------------- .../tools/FullscreenToolSurface.tsx | 26 +- .../src/components/tools/SearchResults.tsx | 19 +- frontend/src/components/tools/ToolPanel.css | 11 +- frontend/src/components/tools/ToolPanel.tsx | 12 +- .../components/tools/ToolPanelModePrompt.tsx | 37 +-- frontend/src/components/tools/ToolPicker.tsx | 25 +- .../src/components/tools/ToolRenderer.tsx | 7 +- .../tools/automate/AutomationEntry.tsx | 7 +- .../tools/automate/AutomationRun.tsx | 6 +- .../tools/automate/AutomationSelection.tsx | 3 +- .../components/tools/automate/ToolList.tsx | 5 +- .../tools/automate/ToolSelector.tsx | 21 +- .../tools/fullscreen/CompactToolItem.tsx | 115 +++++++++ .../tools/fullscreen/DetailedToolItem.tsx | 95 ++++++++ .../src/components/tools/fullscreen/shared.ts | 45 ++++ .../tools/shared/renderToolButtons.tsx | 30 +-- .../tools/toolPicker/FavoriteStar.tsx | 40 +++ .../tools/toolPicker/ToolButton.tsx | 26 +- .../tools/toolPicker/ToolPicker.css | 2 +- .../tools/toolPicker/ToolSearch.tsx | 11 +- .../src/components/viewer/EmbedPdfViewer.tsx | 4 +- frontend/src/contexts/HotkeyContext.tsx | 2 +- frontend/src/contexts/SidebarContext.tsx | 4 +- frontend/src/contexts/ToolWorkflowContext.tsx | 21 +- .../{state.ts => toolWorkflowState.ts} | 0 .../src/hooks/tools/useFavoriteToolItems.ts | 20 ++ ...geState.ts => useJsonLocalStorageState.ts} | 0 .../src/hooks/tools/useToolPanelGeometry.ts | 104 ++++---- ...eToolHistory.ts => useUserToolActivity.ts} | 17 -- .../src/hooks/{tools => }/useFocusTrap.ts | 2 + frontend/src/hooks/useToolManagement.tsx | 11 +- .../src/hooks/useToolPanelModePreference.ts | 50 ++++ frontend/src/hooks/useToolSections.ts | 9 +- frontend/src/services/preferencesService.ts | 3 +- frontend/src/styles/theme.css | 11 + frontend/src/styles/zIndex.ts | 8 + frontend/src/tools/Automate.tsx | 4 +- frontend/src/types/sidebar.ts | 1 + frontend/src/utils/toolSearch.ts | 19 +- 54 files changed, 669 insertions(+), 487 deletions(-) create mode 100644 frontend/src/components/tools/fullscreen/CompactToolItem.tsx create mode 100644 frontend/src/components/tools/fullscreen/DetailedToolItem.tsx create mode 100644 frontend/src/components/tools/fullscreen/shared.ts create mode 100644 frontend/src/components/tools/toolPicker/FavoriteStar.tsx rename frontend/src/contexts/toolWorkflow/{state.ts => toolWorkflowState.ts} (100%) create mode 100644 frontend/src/hooks/tools/useFavoriteToolItems.ts rename frontend/src/hooks/tools/{useLocalStorageState.ts => useJsonLocalStorageState.ts} (100%) rename frontend/src/hooks/tools/{useToolHistory.ts => useUserToolActivity.ts} (76%) rename frontend/src/hooks/{tools => }/useFocusTrap.ts (99%) create mode 100644 frontend/src/hooks/useToolPanelModePreference.ts create mode 100644 frontend/src/styles/zIndex.ts diff --git a/frontend/src/components/FileManager.tsx b/frontend/src/components/FileManager.tsx index 8cc81ca56..fce70a022 100644 --- a/frontend/src/components/FileManager.tsx +++ b/frontend/src/components/FileManager.tsx @@ -9,6 +9,7 @@ import MobileLayout from './fileManager/MobileLayout'; import DesktopLayout from './fileManager/DesktopLayout'; import DragOverlay from './fileManager/DragOverlay'; import { FileManagerProvider } from '../contexts/FileManagerContext'; +import { Z_INDEX_FILE_MANAGER_MODAL } from '../styles/zIndex'; interface FileManagerProps { selectedTool?: Tool | null; @@ -100,7 +101,7 @@ const FileManager: React.FC = ({ selectedTool }) => { radius="md" className="overflow-hidden p-0" withCloseButton={false} - zIndex={1300} + zIndex={Z_INDEX_FILE_MANAGER_MODAL} styles={{ content: { position: 'relative', diff --git a/frontend/src/components/annotation/shared/DrawingCanvas.tsx b/frontend/src/components/annotation/shared/DrawingCanvas.tsx index d4ae74ad0..39d1581e9 100644 --- a/frontend/src/components/annotation/shared/DrawingCanvas.tsx +++ b/frontend/src/components/annotation/shared/DrawingCanvas.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useCallback } from 'react'; +import React, { useRef, useState, useCallback, useEffect } from 'react'; import { Paper, Group, Button, Modal, Stack, Text } from '@mantine/core'; import { ColorSwatchButton } from './ColorPicker'; import PenSizeSelector from '../../tools/sign/PenSizeSelector'; @@ -252,7 +252,7 @@ export const DrawingCanvas: React.FC = ({ }, [selectedColor, penSize]); // Initialize canvas settings whenever color or pen size changes - React.useEffect(() => { + useEffect(() => { const updateCanvas = (canvas: HTMLCanvasElement | null) => { if (!canvas) return; const ctx = canvas.getContext('2d'); diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index 90ee6eebe..5c76d2248 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback, useRef, useMemo } from 'react'; +import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react'; import { Text, Center, Box, LoadingOverlay, Stack, Group } from '@mantine/core'; @@ -62,7 +62,7 @@ const FileEditor = ({ const [selectionMode, setSelectionMode] = useState(toolMode); // Enable selection mode automatically in tool mode - React.useEffect(() => { + useEffect(() => { if (toolMode) { setSelectionMode(true); } diff --git a/frontend/src/components/fileManager/FileDetails.tsx b/frontend/src/components/fileManager/FileDetails.tsx index b1e7e93e3..d4ef7bd07 100644 --- a/frontend/src/components/fileManager/FileDetails.tsx +++ b/frontend/src/components/fileManager/FileDetails.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Stack, Button, Box } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail'; @@ -50,7 +50,7 @@ const FileDetails: React.FC = ({ }; // Reset index when selection changes - React.useEffect(() => { + useEffect(() => { if (currentFileIndex >= selectedFiles.length) { setCurrentFileIndex(0); } diff --git a/frontend/src/components/fileManager/FileHistoryGroup.tsx b/frontend/src/components/fileManager/FileHistoryGroup.tsx index 16bc02aa1..771499584 100644 --- a/frontend/src/components/fileManager/FileHistoryGroup.tsx +++ b/frontend/src/components/fileManager/FileHistoryGroup.tsx @@ -44,7 +44,7 @@ const FileHistoryGroup: React.FC = ({ - {sortedHistory.map((historyFile, _index) => ( + {sortedHistory.map((historyFile) => ( = ({ onDoubleClick={() => onFileDoubleClick(historyFile)} isHistoryFile={true} // This enables "Add to Recents" in menu isLatestVersion={false} // History files are never latest - // onAddToRecents is accessed from context by FileListItem /> ))} diff --git a/frontend/src/components/fileManager/FileListItem.tsx b/frontend/src/components/fileManager/FileListItem.tsx index a3353df19..8436d2e29 100644 --- a/frontend/src/components/fileManager/FileListItem.tsx +++ b/frontend/src/components/fileManager/FileListItem.tsx @@ -40,7 +40,7 @@ const FileListItem: React.FC = ({ const [isHovered, setIsHovered] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); const { t } = useTranslation(); - const {expandedFileIds, onToggleExpansion, onAddToRecents, onUnzipFile } = useFileManagerContext(); + const {expandedFileIds, onToggleExpansion, onUnzipFile } = useFileManagerContext(); // Check if this is a ZIP file const isZipFile = zipFileService.isZipFileStub(file); @@ -188,7 +188,6 @@ const FileListItem: React.FC = ({ leftSection={} onClick={(e) => { e.stopPropagation(); - onAddToRecents(file); }} > {t('fileManager.restore', 'Restore')} diff --git a/frontend/src/components/shared/AppConfigModal.tsx b/frontend/src/components/shared/AppConfigModal.tsx index 4cbe94717..160547180 100644 --- a/frontend/src/components/shared/AppConfigModal.tsx +++ b/frontend/src/components/shared/AppConfigModal.tsx @@ -6,6 +6,7 @@ import Overview from './config/configSections/Overview'; import { createConfigNavSections } from './config/configNavSections'; import { NavKey } from './config/types'; import './AppConfigModal.css'; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex'; interface AppConfigModalProps { opened: boolean; @@ -77,7 +78,7 @@ const AppConfigModal: React.FC = ({ opened, onClose }) => { centered radius="lg" withCloseButton={false} - zIndex={200000} + zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE} overlayProps={{ opacity: 0.35, blur: 2 }} padding={0} fullScreen={isMobile} diff --git a/frontend/src/components/shared/LanguageSelector.tsx b/frontend/src/components/shared/LanguageSelector.tsx index f21cc93bd..507dab16b 100644 --- a/frontend/src/components/shared/LanguageSelector.tsx +++ b/frontend/src/components/shared/LanguageSelector.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { supportedLanguages } from '../../i18n'; import LocalIcon from './LocalIcon'; import styles from './LanguageSelector.module.css'; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex'; // Types interface LanguageSelectorProps { @@ -209,7 +210,7 @@ const LanguageSelector: React.FC = ({ position = 'bottom- width={600} position={position} offset={offset} - zIndex={1400} + zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE} transitionProps={{ transition: 'scale-y', duration: 200, @@ -265,7 +266,7 @@ const LanguageSelector: React.FC = ({ position = 'bottom- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)', backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))', border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))', - zIndex: 1400, + zIndex: Z_INDEX_OVER_FULLSCREEN_SURFACE, }} > diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index d41812b8d..7a8dd7fdd 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -102,18 +102,18 @@ const QuickAccessBar = forwardRef((_, ref) => { handleReaderToggle(); } }, - { - id: 'sign', - name: t("quickAccess.sign", "Sign"), - icon: , - size: 'lg', - isRound: false, - type: 'navigation', - onClick: () => { - setActiveButton('sign'); - handleToolSelect('sign'); - } - }, + // { + // id: 'sign', + // name: t("quickAccess.sign", "Sign"), + // icon: , + // size: 'lg', + // isRound: false, + // type: 'navigation', + // onClick: () => { + // setActiveButton('sign'); + // handleToolSelect('sign'); + // } + // }, { id: 'automate', name: t("quickAccess.automate", "Automate"), diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx index ed492caa5..44af1e215 100644 --- a/frontend/src/components/shared/RightRail.tsx +++ b/frontend/src/components/shared/RightRail.tsx @@ -20,7 +20,10 @@ import ViewerAnnotationControls from './rightRail/ViewerAnnotationControls'; import { parseSelection } from '../../utils/bulkselection/parseSelection'; +import { useSidebarContext } from '../../contexts/SidebarContext'; + export default function RightRail() { + const { sidebarRefs } = useSidebarContext(); const { t } = useTranslation(); const [isPanning, setIsPanning] = useState(false); @@ -178,7 +181,7 @@ export default function RightRail() { }, [currentView]); return ( -
+
{topButtons.length > 0 && ( <> @@ -298,7 +301,10 @@ export default function RightRail() { {/* Annotation Controls */} - +
diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx index 543422609..b5cbab947 100644 --- a/frontend/src/components/shared/Tooltip.tsx +++ b/frontend/src/components/shared/Tooltip.tsx @@ -8,6 +8,7 @@ import { TooltipContent } from './tooltip/TooltipContent'; import { useSidebarContext } from '../../contexts/SidebarContext'; import { BASE_PATH } from '../../constants/app'; import styles from './tooltip/Tooltip.module.css'; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex'; export interface TooltipProps { sidebarTooltip?: boolean; @@ -291,7 +292,7 @@ export const Tooltip: React.FC = ({ left: coords.left, width: maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' as const : undefined), minWidth, - zIndex: 9999, + zIndex: Z_INDEX_OVER_FULLSCREEN_SURFACE, visibility: positionReady ? 'visible' : 'hidden', opacity: positionReady ? 1 : 0, color: 'var(--text-primary)', diff --git a/frontend/src/components/shared/config/configSections/GeneralSection.tsx b/frontend/src/components/shared/config/configSections/GeneralSection.tsx index 48421d872..a79c15d72 100644 --- a/frontend/src/components/shared/config/configSections/GeneralSection.tsx +++ b/frontend/src/components/shared/config/configSections/GeneralSection.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Paper, Stack, Switch, Text, Tooltip, NumberInput, SegmentedControl } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { usePreferences } from '../../../../contexts/PreferencesContext'; +import { ToolPanelMode } from 'src/contexts/toolWorkflow/toolWorkflowState'; const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4; @@ -37,7 +38,7 @@ const GeneralSection: React.FC = () => {
updatePreference('defaultToolPanelMode', val as any)} + onChange={(val: string) => updatePreference('defaultToolPanelMode', val as ToolPanelMode)} data={[ { label: t('settings.general.mode.sidebar', 'Sidebar'), value: 'sidebar' }, { label: t('settings.general.mode.fullscreen', 'Fullscreen'), value: 'fullscreen' }, diff --git a/frontend/src/components/shared/config/configSections/HotkeysSection.tsx b/frontend/src/components/shared/config/configSections/HotkeysSection.tsx index 7c5bb8bce..e6b96ce22 100644 --- a/frontend/src/components/shared/config/configSections/HotkeysSection.tsx +++ b/frontend/src/components/shared/config/configSections/HotkeysSection.tsx @@ -3,6 +3,7 @@ import { Alert, Badge, Box, Button, Divider, Group, Paper, Stack, Text, TextInpu import { useTranslation } from 'react-i18next'; import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext'; import { useHotkeys } from '../../../../contexts/HotkeyContext'; +import { ToolId } from '../../../../types/toolId'; import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay'; import { bindingEquals, eventToBinding } from '../../../../utils/hotkeys'; @@ -83,7 +84,10 @@ const HotkeysSection: React.FC = () => { )); if (conflictEntry) { - const conflictTool = toolRegistry[conflictEntry[0]]?.name ?? conflictEntry[0]; + const conflictKey = conflictEntry[0]; + const conflictTool = (conflictKey in toolRegistry) + ? toolRegistry[conflictKey as ToolId]?.name + : conflictKey; setError(t('settings.hotkeys.errorConflict', 'Shortcut already used by {{tool}}.', { tool: conflictTool })); return; } @@ -114,7 +118,7 @@ const HotkeysSection: React.FC = () => { setSearchQuery(event.currentTarget.value)} size="md" @@ -125,7 +129,7 @@ const HotkeysSection: React.FC = () => { {filteredTools.length === 0 ? ( - {searchQuery.trim() ? 'No tools found matching your search.' : 'No tools available.'} + {t('toolPicker.noToolsFound', 'No tools found')} ) : ( filteredTools.map(([toolId, tool], index) => { diff --git a/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx b/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx index e0844a6d5..d686153c4 100644 --- a/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx +++ b/frontend/src/components/shared/rightRail/ViewerAnnotationControls.tsx @@ -10,16 +10,14 @@ import { useFileState, useFileContext } from '../../../contexts/FileContext'; import { generateThumbnailWithMetadata } from '../../../utils/thumbnailUtils'; import { createProcessedFile } from '../../../contexts/file/fileActions'; import { createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext'; -import { useToolWorkflow } from '../../../contexts/ToolWorkflowContext'; interface ViewerAnnotationControlsProps { currentView: string; + disabled?: boolean; } -export default function ViewerAnnotationControls({ currentView }: ViewerAnnotationControlsProps) { +export default function ViewerAnnotationControls({ currentView, disabled = false }: ViewerAnnotationControlsProps) { const { t } = useTranslation(); - const { toolPanelMode, leftPanelView } = useToolWorkflow(); - const disableForFullscreen = toolPanelMode === 'fullscreen' && leftPanelView === 'toolPicker'; const [selectedColor, setSelectedColor] = useState('#000000'); const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [isHoverColorPickerOpen, setIsHoverColorPickerOpen] = useState(false); @@ -53,7 +51,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati onClick={() => { viewerContext?.toggleAnnotationsVisibility(); }} - disabled={currentView !== 'viewer' || viewerContext?.isAnnotationMode || disableForFullscreen} + disabled={disabled || viewerContext?.isAnnotationMode} > @@ -139,7 +137,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati } } }} - disabled={currentView !== 'viewer' || disableForFullscreen} + disabled={disabled} aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'} > @@ -196,7 +194,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati } } }} - disabled={currentView !== 'viewer' || disableForFullscreen} + disabled={disabled} > diff --git a/frontend/src/components/tools/FullscreenToolList.tsx b/frontend/src/components/tools/FullscreenToolList.tsx index a52ec569c..ce76b7375 100644 --- a/frontend/src/components/tools/FullscreenToolList.tsx +++ b/frontend/src/components/tools/FullscreenToolList.tsx @@ -1,22 +1,21 @@ import React, { useMemo } from 'react'; -import { ActionIcon, Text } from '@mantine/core'; -import { Tooltip } from '../shared/Tooltip'; +import { Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { ToolRegistryEntry, getSubcategoryLabel, getSubcategoryColor, getSubcategoryIcon } from '../../data/toolsTaxonomy'; import { ToolId } from '../../types/toolId'; import { useToolSections } from '../../hooks/useToolSections'; import NoToolsFound from './shared/NoToolsFound'; -import { useHotkeys } from '../../contexts/HotkeyContext'; -import HotkeyDisplay from '../hotkeys/HotkeyDisplay'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import StarRoundedIcon from '@mui/icons-material/StarRounded'; -import StarBorderRoundedIcon from '@mui/icons-material/StarBorderRounded'; import ThumbUpRoundedIcon from '@mui/icons-material/ThumbUpRounded'; import Badge from '../shared/Badge'; import './ToolPanel.css'; +import DetailedToolItem from './fullscreen/DetailedToolItem'; +import CompactToolItem from './fullscreen/CompactToolItem'; +import { useFavoriteToolItems } from '../../hooks/tools/useFavoriteToolItems'; interface FullscreenToolListProps { - filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; + filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; searchQuery: string; showDescriptions: boolean; selectedToolKey: string | null; @@ -29,33 +28,25 @@ const FullscreenToolList = ({ searchQuery, showDescriptions, selectedToolKey, - matchedTextMap, + matchedTextMap: _matchedTextMap, onSelect, }: FullscreenToolListProps) => { const { t } = useTranslation(); - const { hotkeys } = useHotkeys(); - const { toolRegistry, favoriteTools, toggleFavorite, isFavorite } = useToolWorkflow(); + const { toolRegistry, favoriteTools } = useToolWorkflow(); const { sections, searchGroups } = useToolSections(filteredTools, searchQuery); const tooltipPortalTarget = typeof document !== 'undefined' ? document.body : undefined; - const favoriteToolItems = useMemo(() => { - return favoriteTools - .map((toolId) => { - const tool = toolRegistry[toolId]; - return tool ? { id: toolId, tool } : null; - }) - .filter(Boolean); - }, [favoriteTools, toolRegistry]); + const favoriteToolItems = useFavoriteToolItems(favoriteTools, toolRegistry); const quickSection = useMemo(() => sections.find(section => section.key === 'quick'), [sections]); const recommendedItems = useMemo(() => { if (!quickSection) return [] as Array<{ id: string, tool: ToolRegistryEntry }>; const items: Array<{ id: string, tool: ToolRegistryEntry }> = []; quickSection.subcategories.forEach(sc => sc.tools.forEach(t => items.push(t))); - return items.slice(0, 5); + return items; }, [quickSection]); // Show recommended/favorites section only when not searching @@ -84,50 +75,12 @@ const FullscreenToolList = ({ ? 'tool-panel__fullscreen-groups tool-panel__fullscreen-groups--detailed' : 'tool-panel__fullscreen-groups tool-panel__fullscreen-groups--compact'; - const getItemClasses = (isDetailed: boolean) => { - const base = isDetailed ? 'tool-panel__fullscreen-item--detailed' : ''; - return base; - }; - - const getIconBackground = (categoryColor: string, isDetailed: boolean) => { - const baseColor = isDetailed ? 'var(--fullscreen-bg-icon-detailed)' : 'var(--fullscreen-bg-icon-compact)'; - const blend1 = isDetailed ? '18%' : '15%'; - const blend2 = isDetailed ? '8%' : '6%'; - - return `linear-gradient(135deg, - color-mix(in srgb, ${categoryColor} ${blend1}, ${baseColor}), - color-mix(in srgb, ${categoryColor} ${blend2}, ${baseColor}) - )`; - }; - - const getIconStyle = () => { - return {}; - }; - // Helper function to render a tool item - const renderToolItem = (id: string, tool: ToolRegistryEntry) => { - const matchedText = matchedTextMap.get(id); + const renderToolItem = (id: ToolId, tool: ToolRegistryEntry) => { const isSelected = selectedToolKey === id; - const isDisabled = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; - const binding = hotkeys[id]; - const isFav = isFavorite(id as ToolId); - const categoryColor = getSubcategoryColor(tool.subcategoryId); - - let iconNode: React.ReactNode = null; - if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) { - const element = tool.icon as React.ReactElement<{ style?: React.CSSProperties }>; - iconNode = React.cloneElement(element, { - style: { - ...(element.props.style || {}), - fontSize: showDescriptions ? '1.75rem' : '1.5rem', - }, - }); - } else { - iconNode = tool.icon; - } const handleClick = () => { - if (isDisabled) return; + if (!tool.component && !tool.link && id !== 'read' && id !== 'multiTool') return; if (tool.link) { window.open(tool.link, '_blank', 'noopener,noreferrer'); return; @@ -135,161 +88,14 @@ const FullscreenToolList = ({ onSelect(id as ToolId); }; - const handleStarClick = (e: React.MouseEvent) => { - e.stopPropagation(); - toggleFavorite(id as ToolId); - }; - - // Detailed view if (showDescriptions) { - const iconBg = getIconBackground(categoryColor, true); - const iconClasses = 'tool-panel__fullscreen-icon'; - - return ( - + return ( + ); } - // Compact view - const iconBg = getIconBackground(categoryColor, false); - const iconClasses = 'tool-panel__fullscreen-list-icon'; - - const compactButton = ( - - ); - - const tooltipContent = isDisabled - ? ( - {t('toolPanel.fullscreen.comingSoon', 'Coming soon:')} {tool.description} - ) - : ( -
- {tool.description} - {binding && ( -
- - {t('settings.hotkeys.shortcut', 'Shortcut')} - - -
- )} -
- ); - return ( - - {compactButton} - + ); }; @@ -329,11 +135,11 @@ const FullscreenToolList = ({ {showDescriptions ? (
- {favoriteToolItems.map((item: any) => renderToolItem(item.id, item.tool))} + {favoriteToolItems.map((item) => item && renderToolItem(item.id, item.tool))}
) : (
- {favoriteToolItems.map((item: any) => renderToolItem(item.id, item.tool))} + {favoriteToolItems.map((item) => item && renderToolItem(item.id, item.tool))}
)} @@ -428,11 +234,11 @@ const FullscreenToolList = ({ {showDescriptions ? (
- {tools.map(({ id, tool }) => renderToolItem(id, tool))} + {tools.map(({ id, tool }) => renderToolItem(id as ToolId, tool))}
) : (
- {tools.map(({ id, tool }) => renderToolItem(id, tool))} + {tools.map(({ id, tool }) => renderToolItem(id as ToolId, tool))}
)} diff --git a/frontend/src/components/tools/FullscreenToolSurface.tsx b/frontend/src/components/tools/FullscreenToolSurface.tsx index f56ec19a8..1e8a8cd2a 100644 --- a/frontend/src/components/tools/FullscreenToolSurface.tsx +++ b/frontend/src/components/tools/FullscreenToolSurface.tsx @@ -6,14 +6,15 @@ import ToolSearch from './toolPicker/ToolSearch'; import FullscreenToolList from './FullscreenToolList'; import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; import { ToolId } from '../../types/toolId'; -import { useFocusTrap } from '../../hooks/tools/useFocusTrap'; +import { useFocusTrap } from '../../hooks/useFocusTrap'; import { BASE_PATH } from '../../constants/app'; import './ToolPanel.css'; +import { ToolPanelGeometry } from '../../hooks/tools/useToolPanelGeometry'; interface FullscreenToolSurfaceProps { searchQuery: string; - toolRegistry: Record; - filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; + toolRegistry: Record; + filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; selectedToolKey: string | null; showDescriptions: boolean; matchedTextMap: Map; @@ -22,12 +23,7 @@ interface FullscreenToolSurfaceProps { onToggleDescriptions: () => void; onExitFullscreenMode: () => void; toggleLabel: string; - geometry: { - left: number; - top: number; - width: number; - height: number; - } | null; + geometry: ToolPanelGeometry | null; } const FullscreenToolSurface = ({ @@ -69,10 +65,16 @@ const FullscreenToolSurface = ({ } setIsExiting(true); - setTimeout(() => { + const el = surfaceRef.current; + if (!el) { onExitFullscreenMode(); - }, 220); // Match animation duration (0.22s) - }; + return; + } + // Rely on CSS animation end rather than duplicating timing in JS + el.addEventListener('animationend', () => { + onExitFullscreenMode(); + }, { once: true }); +}; const style = geometry diff --git a/frontend/src/components/tools/SearchResults.tsx b/frontend/src/components/tools/SearchResults.tsx index f51c41b78..1195b72aa 100644 --- a/frontend/src/components/tools/SearchResults.tsx +++ b/frontend/src/components/tools/SearchResults.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Box, Stack } from '@mantine/core'; import { getSubcategoryLabel, ToolRegistryEntry } from '../../data/toolsTaxonomy'; +import { ToolId } from '../../types/toolId'; import ToolButton from './toolPicker/ToolButton'; import { useTranslation } from 'react-i18next'; import { useToolSections } from '../../hooks/useToolSections'; @@ -9,7 +10,7 @@ import NoToolsFound from './shared/NoToolsFound'; import "./toolPicker/ToolPicker.css"; interface SearchResultsProps { - filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; + filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; onSelect: (id: string) => void; searchQuery?: string; } @@ -48,14 +49,14 @@ const SearchResults: React.FC = ({ filteredTools, onSelect, ) : undefined; return ( - + ); })}
diff --git a/frontend/src/components/tools/ToolPanel.css b/frontend/src/components/tools/ToolPanel.css index 34eaca105..b8b50ae3d 100644 --- a/frontend/src/components/tools/ToolPanel.css +++ b/frontend/src/components/tools/ToolPanel.css @@ -76,7 +76,7 @@ position: fixed; display: flex; pointer-events: none; - z-index: 1200; + z-index: var(--z-fullscreen-surface); top: 0; left: 0; width: 0; @@ -101,11 +101,14 @@ box-shadow: none; backdrop-filter: blur(18px); overflow: hidden; - animation: tool-panel-fullscreen-slide-in 0.28s ease forwards; + /* Shared animation durations for JS + CSS (sourced from theme.css) */ + --fullscreen-anim-in-duration: var(--fullscreen-anim-duration-in); + --fullscreen-anim-out-duration: var(--fullscreen-anim-duration-out); + animation: tool-panel-fullscreen-slide-in var(--fullscreen-anim-in-duration) ease forwards; } .tool-panel__fullscreen-surface-inner--exiting { - animation: tool-panel-fullscreen-slide-out 0.22s ease forwards; + animation: tool-panel-fullscreen-slide-out var(--fullscreen-anim-out-duration) ease forwards; } .tool-panel__fullscreen-header { @@ -340,7 +343,7 @@ .tool-panel__fullscreen-icon svg { font-size: 1.65rem; position: relative; - z-index: 1; + z-index: var(--z-fullscreen-icon-svg); } .tool-panel__fullscreen-body { diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx index 101af1649..6e54199a5 100644 --- a/frontend/src/components/tools/ToolPanel.tsx +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import ToolPicker from './ToolPicker'; @@ -15,9 +15,10 @@ import DashboardCustomizeRoundedIcon from '@mui/icons-material/DashboardCustomiz import { useTranslation } from 'react-i18next'; import FullscreenToolSurface from './FullscreenToolSurface'; import { useToolPanelGeometry } from '../../hooks/tools/useToolPanelGeometry'; -import { useLocalStorageState } from '../../hooks/tools/useLocalStorageState'; +import { useLocalStorageState } from '../../hooks/tools/useJsonLocalStorageState'; import { useRightRail } from '../../contexts/RightRailContext'; import './ToolPanel.css'; +import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex'; // No props needed - component uses context @@ -25,7 +26,7 @@ export default function ToolPanel() { const { t } = useTranslation(); const { isRainbowMode } = useRainbowThemeContext(); const { sidebarRefs } = useSidebarContext(); - const { toolPanelRef, quickAccessRef } = sidebarRefs; + const { toolPanelRef, quickAccessRef, rightRailRef } = sidebarRefs; const isMobile = useMediaQuery('(max-width: 1024px)'); const { @@ -52,7 +53,7 @@ export default function ToolPanel() { // Disable right rail buttons when fullscreen mode is active - React.useEffect(() => { + useEffect(() => { setAllRightRailButtonsDisabled(fullscreenExpanded); }, [fullscreenExpanded, setAllRightRailButtonsDisabled]); @@ -62,6 +63,7 @@ export default function ToolPanel() { enabled: fullscreenExpanded, toolPanelRef, quickAccessRef, + rightRailRef, }); const toggleLabel = isFullscreenMode @@ -141,7 +143,7 @@ export default function ToolPanel() { withArrow styles={{ tooltip: { - zIndex: 1400, // Higher than fullscreen surface + zIndex: Z_INDEX_OVER_FULLSCREEN_SURFACE, } }} > diff --git a/frontend/src/components/tools/ToolPanelModePrompt.tsx b/frontend/src/components/tools/ToolPanelModePrompt.tsx index 68d7aa7e8..c6e9d3201 100644 --- a/frontend/src/components/tools/ToolPanelModePrompt.tsx +++ b/frontend/src/components/tools/ToolPanelModePrompt.tsx @@ -2,50 +2,33 @@ import { useEffect, useState } from 'react'; import { Badge, Button, Card, Group, Modal, Stack, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; -import { TOOL_PANEL_MODE_STORAGE_KEY } from '../../contexts/toolWorkflow/state'; import './ToolPanelModePrompt.css'; +import { useToolPanelModePreference } from '../../hooks/useToolPanelModePreference'; +import { ToolPanelMode } from 'src/contexts/toolWorkflow/toolWorkflowState'; -type ToolPanelModeOption = 'sidebar' | 'fullscreen'; - -const PROMPT_SEEN_KEY = 'toolPanelModePromptSeen'; +// type moved to hook const ToolPanelModePrompt = () => { const { t } = useTranslation(); const { toolPanelMode, setToolPanelMode } = useToolWorkflow(); const [opened, setOpened] = useState(false); - const [hydrated, setHydrated] = useState(false); + const { hydrated, shouldShowPrompt, markPromptSeen, setPreferredMode } = useToolPanelModePreference(); useEffect(() => { - if (typeof window === 'undefined') { - return; - } - - const hasSeenPrompt = window.localStorage.getItem(PROMPT_SEEN_KEY); - const storedPreference = window.localStorage.getItem(TOOL_PANEL_MODE_STORAGE_KEY); - - if (!hasSeenPrompt && !storedPreference) { + if (shouldShowPrompt) { setOpened(true); } + }, [shouldShowPrompt]); - setHydrated(true); - }, []); - - const persistSeen = () => { - if (typeof window === 'undefined') { - return; - } - - window.localStorage.setItem(PROMPT_SEEN_KEY, 'true'); - }; - - const handleSelect = (mode: ToolPanelModeOption) => { + const handleSelect = (mode: ToolPanelMode) => { setToolPanelMode(mode); - persistSeen(); + setPreferredMode(mode); + markPromptSeen(); setOpened(false); }; const handleDismiss = () => { - persistSeen(); + markPromptSeen(); setOpened(false); }; diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx index fc750f549..ed244acc1 100644 --- a/frontend/src/components/tools/ToolPicker.tsx +++ b/frontend/src/components/tools/ToolPicker.tsx @@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next"; import { ToolRegistryEntry } from "../../data/toolsTaxonomy"; import "./toolPicker/ToolPicker.css"; import { useToolSections } from "../../hooks/useToolSections"; +import type { SubcategoryGroup } from "../../hooks/useToolSections"; +import { useFavoriteToolItems } from "../../hooks/tools/useFavoriteToolItems"; import NoToolsFound from "./shared/NoToolsFound"; import { renderToolButtons } from "./shared/renderToolButtons"; import Badge from "../shared/Badge"; @@ -15,7 +17,7 @@ import { ToolId } from "../../types/toolId"; interface ToolPickerProps { selectedToolKey: string | null; onSelect: (id: string) => void; - filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; + filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; isSearching?: boolean; } @@ -68,16 +70,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa const { sections: visibleSections } = useToolSections(filteredTools); const { favoriteTools, toolRegistry } = useToolWorkflow(); - const favoriteToolItems = useMemo(() => { - return favoriteTools - .map((toolId) => { - const tool = (toolRegistry as any)[toolId as ToolId] as ToolRegistryEntry | undefined; - return tool ? { id: toolId as string, tool } : null; - }) - .filter(Boolean) - // Only include ready tools (component or link) and navigational exceptions - .filter((item: any) => item && (item.tool.component || item.tool.link || item.id === 'read' || item.id === 'multiTool')) as Array<{ id: string; tool: ToolRegistryEntry }>; - }, [favoriteTools, toolRegistry]); + const favoriteToolItems = useFavoriteToolItems(favoriteTools, toolRegistry); const quickSection = useMemo( () => visibleSections.find(s => s.key === 'quick'), @@ -87,8 +80,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa const recommendedItems = useMemo(() => { if (!quickSection) return [] as Array<{ id: string; tool: ToolRegistryEntry }>; const items: Array<{ id: string; tool: ToolRegistryEntry }> = []; - quickSection.subcategories.forEach((sc: any) => sc.tools.forEach((toolEntry: any) => items.push(toolEntry))); - return items.slice(0, 5); + quickSection.subcategories.forEach((sc: SubcategoryGroup) => sc.tools.forEach((toolEntry) => items.push(toolEntry))); + return items; }, [quickSection]); const recommendedCount = useMemo(() => favoriteToolItems.length + recommendedItems.length, [favoriteToolItems.length, recommendedItems.length]); @@ -113,7 +106,9 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa }; // Build flat list by subcategory for search mode - const { searchGroups } = useToolSections(isSearching ? filteredTools : []); + const emptyFilteredTools: ToolPickerProps['filteredTools'] = []; + const effectiveFilteredForSearch: ToolPickerProps['filteredTools'] = isSearching ? filteredTools : emptyFilteredTools; + const { searchGroups } = useToolSections(effectiveFilteredForSearch); return ( - {allSection?.subcategories.map((sc: any) => + {allSection?.subcategories.map((sc: SubcategoryGroup) => renderToolButtons(t, sc, selectedToolKey, onSelect, true, false, undefined, true) )} diff --git a/frontend/src/components/tools/ToolRenderer.tsx b/frontend/src/components/tools/ToolRenderer.tsx index 1503bec20..f72bc59a4 100644 --- a/frontend/src/components/tools/ToolRenderer.tsx +++ b/frontend/src/components/tools/ToolRenderer.tsx @@ -1,10 +1,11 @@ import { Suspense } from "react"; import { useToolWorkflow } from "../../contexts/ToolWorkflowContext"; import { BaseToolProps } from "../../types/tool"; +import { ToolId } from "../../types/toolId"; import ToolLoadingFallback from "./ToolLoadingFallback"; interface ToolRendererProps extends BaseToolProps { - selectedToolKey: string; + selectedToolKey: ToolId | string; } @@ -16,7 +17,9 @@ const ToolRenderer = ({ }: ToolRendererProps) => { // Get the tool from context (instead of direct hook call) const { toolRegistry } = useToolWorkflow(); - const selectedTool = toolRegistry[selectedToolKey]; + const selectedTool = (selectedToolKey in toolRegistry) + ? toolRegistry[selectedToolKey as ToolId] + : undefined; // Handle tools that only work in workbenches (read, multiTool) if (selectedTool && !selectedTool.component && selectedTool.workbench) { diff --git a/frontend/src/components/tools/automate/AutomationEntry.tsx b/frontend/src/components/tools/automate/AutomationEntry.tsx index 8f739bcbf..57f480f1f 100644 --- a/frontend/src/components/tools/automate/AutomationEntry.tsx +++ b/frontend/src/components/tools/automate/AutomationEntry.tsx @@ -8,6 +8,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import { Tooltip } from '../../shared/Tooltip'; import { ToolIcon } from '../../shared/ToolIcon'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import { ToolId } from 'src/types/toolId'; interface AutomationEntryProps { /** Optional title for the automation (usually for custom ones) */ @@ -31,7 +32,7 @@ interface AutomationEntryProps { /** Copy handler (for suggested automations) */ onCopy?: () => void; /** Tool registry to resolve operation names */ - toolRegistry?: Record; + toolRegistry?: Record; } export default function AutomationEntry({ @@ -55,8 +56,8 @@ export default function AutomationEntry({ // Helper function to resolve tool display names const getToolDisplayName = (operation: string): string => { - if (toolRegistry?.[operation]?.name) { - return toolRegistry[operation].name; + if (toolRegistry?.[operation as ToolId]?.name) { + return toolRegistry[operation as ToolId].name; } // Fallback to translation or operation key return t(`${operation}.title`, operation); diff --git a/frontend/src/components/tools/automate/AutomationRun.tsx b/frontend/src/components/tools/automate/AutomationRun.tsx index 2d6bda6fc..d25008a3a 100644 --- a/frontend/src/components/tools/automate/AutomationRun.tsx +++ b/frontend/src/components/tools/automate/AutomationRun.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, Text, Stack, Group, Card, Progress } from "@mantine/core"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; @@ -30,7 +30,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio const hasResults = automateOperation?.files.length > 0 || automateOperation?.downloadUrl !== null; // Initialize execution steps from automation - React.useEffect(() => { + useEffect(() => { if (automation?.operations) { const steps = automation.operations.map((op: any, index: number) => { const tool = toolRegistry[op.operation as keyof typeof toolRegistry]; @@ -47,7 +47,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio }, [automation, toolRegistry]); // Cleanup when component unmounts - React.useEffect(() => { + useEffect(() => { return () => { // Reset progress state when component unmounts setExecutionSteps([]); diff --git a/frontend/src/components/tools/automate/AutomationSelection.tsx b/frontend/src/components/tools/automate/AutomationSelection.tsx index a10ebb139..edbec569a 100644 --- a/frontend/src/components/tools/automate/AutomationSelection.tsx +++ b/frontend/src/components/tools/automate/AutomationSelection.tsx @@ -7,6 +7,7 @@ import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSugges import { AutomationConfig, SuggestedAutomation } from "../../../types/automation"; import { iconMap } from './iconMap'; import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import { ToolId } from '../../../types/toolId'; interface AutomationSelectionProps { savedAutomations: AutomationConfig[]; @@ -15,7 +16,7 @@ interface AutomationSelectionProps { onEdit: (automation: AutomationConfig) => void; onDelete: (automation: AutomationConfig) => void; onCopyFromSuggested: (automation: SuggestedAutomation) => void; - toolRegistry: Record; + toolRegistry: Record; } export default function AutomationSelection({ diff --git a/frontend/src/components/tools/automate/ToolList.tsx b/frontend/src/components/tools/automate/ToolList.tsx index 3cb2b7a7e..bde0625f8 100644 --- a/frontend/src/components/tools/automate/ToolList.tsx +++ b/frontend/src/components/tools/automate/ToolList.tsx @@ -6,12 +6,13 @@ import CloseIcon from "@mui/icons-material/Close"; import AddCircleOutline from "@mui/icons-material/AddCircleOutline"; import { AutomationTool } from "../../../types/automation"; import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; +import { ToolId } from "../../../types/toolId"; import ToolSelector from "./ToolSelector"; import AutomationEntry from "./AutomationEntry"; interface ToolListProps { tools: AutomationTool[]; - toolRegistry: Record; + toolRegistry: Record; onToolUpdate: (index: number, updates: Partial) => void; onToolRemove: (index: number) => void; onToolConfigure: (index: number) => void; @@ -34,7 +35,7 @@ export default function ToolList({ const handleToolSelect = (index: number, newOperation: string) => { const defaultParams = getToolDefaultParameters(newOperation); - const toolEntry = toolRegistry[newOperation]; + const toolEntry = toolRegistry[newOperation as ToolId]; // If tool has no settingsComponent, it's automatically configured const isConfigured = !toolEntry?.automationSettings; diff --git a/frontend/src/components/tools/automate/ToolSelector.tsx b/frontend/src/components/tools/automate/ToolSelector.tsx index 5cbc4b3ab..478e5d080 100644 --- a/frontend/src/components/tools/automate/ToolSelector.tsx +++ b/frontend/src/components/tools/automate/ToolSelector.tsx @@ -6,11 +6,12 @@ import { useToolSections } from '../../../hooks/useToolSections'; import { renderToolButtons } from '../shared/renderToolButtons'; import ToolSearch from '../toolPicker/ToolSearch'; import ToolButton from '../toolPicker/ToolButton'; +import { ToolId } from '../../../types/toolId'; interface ToolSelectorProps { onSelect: (toolKey: string) => void; excludeTools?: string[]; - toolRegistry: Record; // Pass registry as prop to break circular dependency + toolRegistry: Record; // Pass registry as prop to break circular dependency selectedValue?: string; // For showing current selection when editing existing tool placeholder?: string; // Custom placeholder text } @@ -53,16 +54,16 @@ export default function ToolSelector({ // Create filtered tool registry for ToolSearch const filteredToolRegistry = useMemo(() => { - const registry: Record = {}; + const registry: Record = {} as Record; baseFilteredTools.forEach(([key, tool]) => { - registry[key] = tool; + registry[key as ToolId] = tool; }); return registry; }, [baseFilteredTools]); // Transform filteredTools to the expected format for useToolSections const transformedFilteredTools = useMemo(() => { - return filteredTools.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] })); + return filteredTools.map(([id, tool]) => ({ item: [id as ToolId, tool] as [ToolId, ToolRegistryEntry] })); }, [filteredTools]); // Use the same tool sections logic as the main ToolPicker @@ -88,7 +89,7 @@ export default function ToolSelector({ } // Find the "all" section which contains all tools without duplicates - const allSection = sections.find(s => (s as any).key === 'all'); + const allSection = sections.find(s => s.key === 'all'); return allSection?.subcategories || []; }, [isSearching, searchGroups, sections, baseFilteredTools]); @@ -100,7 +101,7 @@ export default function ToolSelector({ const renderedTools = useMemo(() => displayGroups.map((subcategory) => - renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching, true) + renderToolButtons(t, subcategory as any, null, handleToolSelect, !isSearching, true) ), [displayGroups, handleToolSelect, isSearching, t] ); @@ -142,8 +143,8 @@ export default function ToolSelector({ // Get display value for selected tool const getDisplayValue = () => { - if (selectedValue && toolRegistry[selectedValue]) { - return toolRegistry[selectedValue].name; + if (selectedValue && toolRegistry[selectedValue as ToolId]) { + return toolRegistry[selectedValue as ToolId].name; } return placeholder || t('automate.creation.tools.add', 'Add a tool...'); }; @@ -152,11 +153,11 @@ export default function ToolSelector({
{/* Always show the target - either selected tool or search input */} - {selectedValue && toolRegistry[selectedValue] && !opened ? ( + {selectedValue && toolRegistry[selectedValue as ToolId] && !opened ? ( // Show selected tool in AutomationEntry style when tool is selected and dropdown closed
- {}} rounded={true} disableNavigation={true}>
) : ( diff --git a/frontend/src/components/tools/fullscreen/CompactToolItem.tsx b/frontend/src/components/tools/fullscreen/CompactToolItem.tsx new file mode 100644 index 000000000..a49900099 --- /dev/null +++ b/frontend/src/components/tools/fullscreen/CompactToolItem.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import { ActionIcon, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { Tooltip } from '../../shared/Tooltip'; +import HotkeyDisplay from '../../hotkeys/HotkeyDisplay'; +import { ToolRegistryEntry, getSubcategoryColor } from '../../../data/toolsTaxonomy'; +import { getIconBackground, getIconStyle, getItemClasses, useToolMeta } from './shared'; + +interface CompactToolItemProps { + id: string; + tool: ToolRegistryEntry; + isSelected: boolean; + onClick: () => void; + tooltipPortalTarget?: HTMLElement | undefined; +} + +const CompactToolItem: React.FC = ({ id, tool, isSelected, onClick, tooltipPortalTarget }) => { + const { t } = useTranslation(); + const { binding, isFav, toggleFavorite, disabled } = useToolMeta(id, tool); + const categoryColor = getSubcategoryColor(tool.subcategoryId); + const iconBg = getIconBackground(categoryColor, false); + const iconClasses = 'tool-panel__fullscreen-list-icon'; + + let iconNode: React.ReactNode = null; + if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) { + const element = tool.icon as React.ReactElement<{ style?: React.CSSProperties }>; + iconNode = React.cloneElement(element, { + style: { + ...(element.props.style || {}), + fontSize: '1.5rem', + }, + }); + } else { + iconNode = tool.icon; + } + + const compactButton = ( + + ); + + const tooltipContent = disabled + ? ( + {t('toolPanel.fullscreen.comingSoon', 'Coming soon:')} {tool.description} + ) + : ( +
+ {tool.description} + {binding && ( +
+ + {t('settings.hotkeys.shortcut', 'Shortcut')} + + +
+ )} +
+ ); + + return ( + + {compactButton} + + ); +}; + +export default CompactToolItem; + + diff --git a/frontend/src/components/tools/fullscreen/DetailedToolItem.tsx b/frontend/src/components/tools/fullscreen/DetailedToolItem.tsx new file mode 100644 index 000000000..ea1e766b4 --- /dev/null +++ b/frontend/src/components/tools/fullscreen/DetailedToolItem.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { ActionIcon, Text } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import HotkeyDisplay from '../../hotkeys/HotkeyDisplay'; +import { ToolRegistryEntry, getSubcategoryColor } from '../../../data/toolsTaxonomy'; +import { getIconBackground, getIconStyle, getItemClasses, useToolMeta } from './shared'; + +interface DetailedToolItemProps { + id: string; + tool: ToolRegistryEntry; + isSelected: boolean; + onClick: () => void; +} + +const DetailedToolItem: React.FC = ({ id, tool, isSelected, onClick }) => { + const { t } = useTranslation(); + const { binding, isFav, toggleFavorite, disabled } = useToolMeta(id, tool); + + const categoryColor = getSubcategoryColor(tool.subcategoryId); + const iconBg = getIconBackground(categoryColor, true); + const iconClasses = 'tool-panel__fullscreen-icon'; + + let iconNode: React.ReactNode = null; + if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) { + const element = tool.icon as React.ReactElement<{ style?: React.CSSProperties }>; + iconNode = React.cloneElement(element, { + style: { + ...(element.props.style || {}), + fontSize: '1.75rem', + }, + }); + } else { + iconNode = tool.icon; + } + + return ( + + ); +}; + +export default DetailedToolItem; + + diff --git a/frontend/src/components/tools/fullscreen/shared.ts b/frontend/src/components/tools/fullscreen/shared.ts new file mode 100644 index 000000000..393aa4894 --- /dev/null +++ b/frontend/src/components/tools/fullscreen/shared.ts @@ -0,0 +1,45 @@ +import { useHotkeys } from '../../../contexts/HotkeyContext'; +import { useToolWorkflow } from '../../../contexts/ToolWorkflowContext'; +import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import { ToolId } from '../../../types/toolId'; + +export const getItemClasses = (isDetailed: boolean): string => { + return isDetailed ? 'tool-panel__fullscreen-item--detailed' : ''; +}; + +export const getIconBackground = (categoryColor: string, isDetailed: boolean): string => { + const baseColor = isDetailed ? 'var(--fullscreen-bg-icon-detailed)' : 'var(--fullscreen-bg-icon-compact)'; + const blend1 = isDetailed ? '18%' : '15%'; + const blend2 = isDetailed ? '8%' : '6%'; + + return `linear-gradient(135deg, + color-mix(in srgb, ${categoryColor} ${blend1}, ${baseColor}), + color-mix(in srgb, ${categoryColor} ${blend2}, ${baseColor}) + )`; +}; + +export const getIconStyle = (): Record => { + return {}; +}; + +export const isToolDisabled = (id: string, tool: ToolRegistryEntry): boolean => { + return !tool.component && !tool.link && id !== 'read' && id !== 'multiTool'; +}; + +export function useToolMeta(id: string, tool: ToolRegistryEntry) { + const { hotkeys } = useHotkeys(); + const { isFavorite, toggleFavorite } = useToolWorkflow(); + + const isFav = isFavorite(id as ToolId); + const binding = hotkeys[id]; + const disabled = isToolDisabled(id, tool); + + return { + binding, + isFav, + toggleFavorite: () => toggleFavorite(id as ToolId), + disabled, + }; +} + + diff --git a/frontend/src/components/tools/shared/renderToolButtons.tsx b/frontend/src/components/tools/shared/renderToolButtons.tsx index 5a84c16f3..c5e89e76e 100644 --- a/frontend/src/components/tools/shared/renderToolButtons.tsx +++ b/frontend/src/components/tools/shared/renderToolButtons.tsx @@ -31,21 +31,21 @@ export const renderToolButtons = ( )}
- {subcategory.tools.map(({ id, tool }, _index) => { - const matchedSynonym = matchedTextMap.get(id); - - return ( - - ); + {subcategory.tools.map(({ id, tool }) => { + const matchedSynonym = matchedTextMap.get(id); + + return ( + + ); })}
diff --git a/frontend/src/components/tools/toolPicker/FavoriteStar.tsx b/frontend/src/components/tools/toolPicker/FavoriteStar.tsx new file mode 100644 index 000000000..535223452 --- /dev/null +++ b/frontend/src/components/tools/toolPicker/FavoriteStar.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { ActionIcon } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import StarRoundedIcon from '@mui/icons-material/StarRounded'; +import StarBorderRoundedIcon from '@mui/icons-material/StarBorderRounded'; + +interface FavoriteStarProps { + isFavorite: boolean; + onToggle: () => void; + className?: string; + size?: "xs" | "sm" | "md" | "lg" | "xl"; +} + +const FavoriteStar: React.FC = ({ isFavorite, onToggle, className, size = "xs" }) => { + const { t } = useTranslation(); + + return ( + { + e.stopPropagation(); + onToggle(); + }} + className={className} + aria-label={isFavorite ? t('toolPanel.fullscreen.unfavorite', 'Remove from favourites') : t('toolPanel.fullscreen.favorite', 'Add to favourites')} + > + {isFavorite ? ( + + ) : ( + + )} + + ); +}; + +export default FavoriteStar; + + diff --git a/frontend/src/components/tools/toolPicker/ToolButton.tsx b/frontend/src/components/tools/toolPicker/ToolButton.tsx index 4a86a5d85..89c8f301c 100644 --- a/frontend/src/components/tools/toolPicker/ToolButton.tsx +++ b/frontend/src/components/tools/toolPicker/ToolButton.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ActionIcon, Button } from "@mantine/core"; +import { Button } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { Tooltip } from "../../shared/Tooltip"; import { ToolIcon } from "../../shared/ToolIcon"; @@ -9,8 +9,7 @@ import { handleUnlessSpecialClick } from "../../../utils/clickHandlers"; import FitText from "../../shared/FitText"; import { useHotkeys } from "../../../contexts/HotkeyContext"; import HotkeyDisplay from "../../hotkeys/HotkeyDisplay"; -import StarRoundedIcon from '@mui/icons-material/StarRounded'; -import StarBorderRoundedIcon from '@mui/icons-material/StarBorderRounded'; +import FavoriteStar from "./FavoriteStar"; import { useToolWorkflow } from "../../../contexts/ToolWorkflowContext"; import { ToolId } from "../../../types/toolId"; @@ -171,23 +170,12 @@ const ToolButton: React.FC = ({ id, tool, isSelected, onSelect, ); const star = hasStars && !isUnavailable ? ( - { - e.stopPropagation(); - toggleFavorite(id as ToolId); - }} + toggleFavorite(id as ToolId)} className="tool-button-star" - aria-label={fav ? t('toolPanel.fullscreen.unfavorite', 'Remove from favourites') : t('toolPanel.fullscreen.favorite', 'Add to favourites')} - > - {fav ? ( - - ) : ( - - )} - + size="xs" + /> ) : null; return ( diff --git a/frontend/src/components/tools/toolPicker/ToolPicker.css b/frontend/src/components/tools/toolPicker/ToolPicker.css index 9f1068482..b88215471 100644 --- a/frontend/src/components/tools/toolPicker/ToolPicker.css +++ b/frontend/src/components/tools/toolPicker/ToolPicker.css @@ -82,7 +82,7 @@ right: 0.35rem; opacity: 0; transition: opacity 0.2s ease; - z-index: 1; /* lower than sticky section headers */ + z-index: var(--z-toolpicker-star); /* lower than sticky section headers */ } .tool-button-container:hover .tool-button-star { diff --git a/frontend/src/components/tools/toolPicker/ToolSearch.tsx b/frontend/src/components/tools/toolPicker/ToolSearch.tsx index 17e435519..42965c57c 100644 --- a/frontend/src/components/tools/toolPicker/ToolSearch.tsx +++ b/frontend/src/components/tools/toolPicker/ToolSearch.tsx @@ -6,12 +6,13 @@ import { ToolRegistryEntry } from "../../../data/toolsTaxonomy"; import { TextInput } from "../../shared/TextInput"; import "./ToolPicker.css"; import { rankByFuzzy, idToWords } from "../../../utils/fuzzySearch"; +import { ToolId } from "src/types/toolId"; interface ToolSearchProps { value: string; onChange: (value: string) => void; - toolRegistry: Readonly>; - onToolSelect?: (toolId: string) => void; + toolRegistry: Readonly>; + onToolSelect?: (toolId: ToolId) => void; mode: "filter" | "dropdown" | "unstyled"; selectedToolKey?: string | null; placeholder?: string; @@ -20,7 +21,7 @@ interface ToolSearchProps { autoFocus?: boolean; } -const ToolSearch: React.FC = ({ +const ToolSearch = ({ value, onChange, toolRegistry, @@ -31,7 +32,7 @@ const ToolSearch: React.FC = ({ hideIcon = false, onFocus, autoFocus = false, -}) => { +}: ToolSearchProps) => { const { t } = useTranslation(); const [dropdownOpen, setDropdownOpen] = useState(false); const searchRef = useRef(null); @@ -126,7 +127,7 @@ const ToolSearch: React.FC = ({ key={id} variant="subtle" onClick={() => { - onToolSelect?.(id); + onToolSelect?.(id as ToolId); setDropdownOpen(false); }} leftSection={
{tool.icon}
} diff --git a/frontend/src/components/viewer/EmbedPdfViewer.tsx b/frontend/src/components/viewer/EmbedPdfViewer.tsx index f8c6d0e4f..117402cbf 100644 --- a/frontend/src/components/viewer/EmbedPdfViewer.tsx +++ b/frontend/src/components/viewer/EmbedPdfViewer.tsx @@ -93,7 +93,7 @@ const EmbedPdfViewerContent = ({ }, [previewFile, fileWithUrl]); // Handle scroll wheel zoom with accumulator for smooth trackpad pinch - React.useEffect(() => { + useEffect(() => { let accumulator = 0; const handleWheel = (event: WheelEvent) => { @@ -127,7 +127,7 @@ const EmbedPdfViewerContent = ({ }, [zoomActions]); // Handle keyboard zoom shortcuts - React.useEffect(() => { + useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!isViewerHovered) return; diff --git a/frontend/src/contexts/HotkeyContext.tsx b/frontend/src/contexts/HotkeyContext.tsx index fe9cbb600..066318c42 100644 --- a/frontend/src/contexts/HotkeyContext.tsx +++ b/frontend/src/contexts/HotkeyContext.tsx @@ -92,7 +92,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr const next: Record = {}; let changed = false; Object.entries(prev).forEach(([toolId, binding]) => { - if (toolRegistry[toolId]) { + if (toolRegistry[toolId as ToolId]) { next[toolId] = binding; } else { changed = true; diff --git a/frontend/src/contexts/SidebarContext.tsx b/frontend/src/contexts/SidebarContext.tsx index 1035cfb99..edcbc0711 100644 --- a/frontend/src/contexts/SidebarContext.tsx +++ b/frontend/src/contexts/SidebarContext.tsx @@ -7,6 +7,7 @@ export function SidebarProvider({ children }: SidebarProviderProps) { // All sidebar state management const quickAccessRef = useRef(null); const toolPanelRef = useRef(null); + const rightRailRef = useRef(null); const [sidebarsVisible, setSidebarsVisible] = useState(true); const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker'); @@ -21,7 +22,8 @@ export function SidebarProvider({ children }: SidebarProviderProps) { const sidebarRefs: SidebarRefs = useMemo(() => ({ quickAccessRef, toolPanelRef, - }), [quickAccessRef, toolPanelRef]); + rightRailRef, + }), [quickAccessRef, toolPanelRef, rightRailRef]); const contextValue: SidebarContextValue = useMemo(() => ({ sidebarState, diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index f9ffe2cea..e90e83262 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -11,14 +11,14 @@ import { useNavigationActions, useNavigationState } from './NavigationContext'; import { ToolId, isValidToolId } from '../types/toolId'; import { getDefaultWorkbench } from '../types/workbench'; import { filterToolRegistryByQuery } from '../utils/toolSearch'; -import { useToolHistory } from '../hooks/tools/useToolHistory'; +import { useToolHistory } from '../hooks/tools/useUserToolActivity'; import { ToolWorkflowState, TOOL_PANEL_MODE_STORAGE_KEY, createInitialState, toolWorkflowReducer, ToolPanelMode, -} from './toolWorkflow/state'; +} from './toolWorkflow/toolWorkflowState'; import { usePreferences } from './PreferencesContext'; // State interface @@ -29,7 +29,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { // Tool management (from hook) selectedToolKey: string | null; selectedTool: ToolRegistryEntry | null; - toolRegistry: Record; + toolRegistry: Record; getSelectedTool: (toolId: string | null) => ToolRegistryEntry | null; // UI Actions @@ -56,13 +56,11 @@ 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; // Tool History - recentTools: ToolId[]; favoriteTools: ToolId[]; - addToRecent: (toolId: ToolId) => void; toggleFavorite: (toolId: ToolId) => void; isFavorite: (toolId: ToolId) => boolean; } @@ -93,9 +91,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { // Tool history hook const { - recentTools, favoriteTools, - addToRecent, toggleFavorite, isFavorite, } = useToolHistory(); @@ -180,9 +176,6 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { // Workflow actions (compound actions that coordinate multiple state changes) const handleToolSelect = useCallback((toolId: ToolId) => { - // Track tool usage in recent history - addToRecent(toolId); - // Handle read tool selection - should behave exactly like QuickAccessBar read button if (toolId === 'read') { setReaderMode(true); @@ -218,7 +211,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setSearchQuery(''); setLeftPanelView('toolContent'); setReaderMode(false); // Disable read mode when selecting tools - }, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery, addToRecent]); + }, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]); const handleBackToTools = useCallback(() => { setLeftPanelView('toolPicker'); @@ -276,9 +269,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { isPanelVisible, // Tool History - recentTools, favoriteTools, - addToRecent, toggleFavorite, isFavorite, }), [ @@ -302,9 +293,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { handleReaderToggle, filteredTools, isPanelVisible, - recentTools, favoriteTools, - addToRecent, toggleFavorite, isFavorite, ]); diff --git a/frontend/src/contexts/toolWorkflow/state.ts b/frontend/src/contexts/toolWorkflow/toolWorkflowState.ts similarity index 100% rename from frontend/src/contexts/toolWorkflow/state.ts rename to frontend/src/contexts/toolWorkflow/toolWorkflowState.ts diff --git a/frontend/src/hooks/tools/useFavoriteToolItems.ts b/frontend/src/hooks/tools/useFavoriteToolItems.ts new file mode 100644 index 000000000..267909a08 --- /dev/null +++ b/frontend/src/hooks/tools/useFavoriteToolItems.ts @@ -0,0 +1,20 @@ +import { useMemo } from 'react'; +import { ToolId } from '../../types/toolId'; +import { ToolRegistryEntry } from '../../data/toolsTaxonomy'; + +export function useFavoriteToolItems( + favoriteTools: ToolId[], + toolRegistry: Readonly> +): Array<{ id: ToolId; tool: ToolRegistryEntry }> { + return useMemo(() => { + return favoriteTools + .map((toolId) => { + const tool = toolRegistry[toolId as ToolId]; + return tool ? { id: toolId as ToolId, tool } : null; + }) + .filter((x): x is { id: ToolId; tool: ToolRegistryEntry } => x !== null) + .filter(({ id, tool }) => Boolean(tool.component) || Boolean(tool.link) || id === 'read' || id === 'multiTool'); + }, [favoriteTools, toolRegistry]); +} + + diff --git a/frontend/src/hooks/tools/useLocalStorageState.ts b/frontend/src/hooks/tools/useJsonLocalStorageState.ts similarity index 100% rename from frontend/src/hooks/tools/useLocalStorageState.ts rename to frontend/src/hooks/tools/useJsonLocalStorageState.ts diff --git a/frontend/src/hooks/tools/useToolPanelGeometry.ts b/frontend/src/hooks/tools/useToolPanelGeometry.ts index af47408ba..df7b62b42 100644 --- a/frontend/src/hooks/tools/useToolPanelGeometry.ts +++ b/frontend/src/hooks/tools/useToolPanelGeometry.ts @@ -1,4 +1,4 @@ -import { useLayoutEffect, useState, RefObject } from 'react'; +import { useLayoutEffect, useState, RefObject, useRef } from 'react'; export interface ToolPanelGeometry { left: number; @@ -11,14 +11,17 @@ interface UseToolPanelGeometryOptions { enabled: boolean; toolPanelRef: RefObject; quickAccessRef: RefObject; + rightRailRef?: RefObject; } export function useToolPanelGeometry({ enabled, toolPanelRef, quickAccessRef, + rightRailRef, }: UseToolPanelGeometryOptions) { const [geometry, setGeometry] = useState(null); + const scheduleUpdateRef = useRef<() => void>(() => {}); useLayoutEffect(() => { if (!enabled) { @@ -32,52 +35,41 @@ export function useToolPanelGeometry({ return; } - const rightRailEl = () => document.querySelector('[data-sidebar="right-rail"]') as HTMLElement | null; + const rightRailEl = () => (rightRailRef?.current ?? null); - let timeoutId: number | null = null; + let rafId: number | null = null; - const updateGeometry = () => { - // Debounce: clear any pending update - if (timeoutId !== null) { - window.clearTimeout(timeoutId); - } - - // Schedule update after 150ms of inactivity - timeoutId = window.setTimeout(() => { - const rect = panelEl.getBoundingClientRect(); - const rail = rightRailEl(); - const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0; - const width = Math.max(360, window.innerWidth - rect.left - rightOffset); - const height = Math.max(rect.height, window.innerHeight - rect.top); - setGeometry({ - left: rect.left, - top: rect.top, - width, - height, - }); - timeoutId = null; - }, 150); + const computeAndSetGeometry = () => { + const rect = panelEl.getBoundingClientRect(); + const rail = rightRailEl(); + const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0; + const width = Math.max(360, window.innerWidth - rect.left - rightOffset); + const height = Math.max(rect.height, window.innerHeight - rect.top); + setGeometry({ + left: rect.left, + top: rect.top, + width, + height, + }); }; - // Initial geometry calculation (no debounce) - const rect = panelEl.getBoundingClientRect(); - const rail = rightRailEl(); - const rightOffset = rail ? Math.max(0, window.innerWidth - rail.getBoundingClientRect().left) : 0; - const width = Math.max(360, window.innerWidth - rect.left - rightOffset); - const height = Math.max(rect.height, window.innerHeight - rect.top); - setGeometry({ - left: rect.left, - top: rect.top, - width, - height, - }); + const scheduleUpdate = () => { + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + rafId = requestAnimationFrame(() => { + computeAndSetGeometry(); + rafId = null; + }); + }; + scheduleUpdateRef.current = scheduleUpdate; - const handleResize = () => updateGeometry(); - window.addEventListener('resize', handleResize); + // Initial geometry calculation (no debounce) + computeAndSetGeometry(); let resizeObserver: ResizeObserver | null = null; if (typeof ResizeObserver !== 'undefined') { - resizeObserver = new ResizeObserver(() => updateGeometry()); + resizeObserver = new ResizeObserver(() => scheduleUpdate()); resizeObserver.observe(panelEl); if (quickAccessRef.current) { resizeObserver.observe(quickAccessRef.current); @@ -86,16 +78,42 @@ export function useToolPanelGeometry({ if (rail) { resizeObserver.observe(rail); } + // Observe root element to react to viewport-driven layout changes + if (document.documentElement) { + resizeObserver.observe(document.documentElement); + } + } else { + // Fallback for environments without ResizeObserver + const handleResize = () => scheduleUpdate(); + window.addEventListener('resize', handleResize); + // Ensure cleanup of the fallback listener + resizeObserver = { + disconnect: () => window.removeEventListener('resize', handleResize), + } as unknown as ResizeObserver; } return () => { - if (timeoutId !== null) { - window.clearTimeout(timeoutId); + if (rafId !== null) { + cancelAnimationFrame(rafId); } - window.removeEventListener('resize', handleResize); + scheduleUpdateRef.current = () => {}; resizeObserver?.disconnect(); }; - }, [enabled, quickAccessRef, toolPanelRef]); + }, [enabled, quickAccessRef, toolPanelRef, rightRailRef]); + + // Secondary effect: (re)attach observers when refs' .current become available later + useLayoutEffect(() => { + if (!enabled) return; + if (typeof ResizeObserver === 'undefined') return; + const qa = quickAccessRef.current; + const rail = rightRailRef?.current ?? null; + if (!qa && !rail) return; + + const ro = new ResizeObserver(() => scheduleUpdateRef.current()); + if (qa) ro.observe(qa); + if (rail) ro.observe(rail); + return () => ro.disconnect(); + }, [enabled, quickAccessRef.current, rightRailRef?.current]); return geometry; } diff --git a/frontend/src/hooks/tools/useToolHistory.ts b/frontend/src/hooks/tools/useUserToolActivity.ts similarity index 76% rename from frontend/src/hooks/tools/useToolHistory.ts rename to frontend/src/hooks/tools/useUserToolActivity.ts index fc8c9ce4a..a1149c443 100644 --- a/frontend/src/hooks/tools/useToolHistory.ts +++ b/frontend/src/hooks/tools/useUserToolActivity.ts @@ -3,7 +3,6 @@ import { ToolId } from '../../types/toolId'; const RECENT_TOOLS_KEY = 'stirlingpdf.recentTools'; const FAVORITE_TOOLS_KEY = 'stirlingpdf.favoriteTools'; -const MAX_RECENT_TOOLS = 10; export function useToolHistory() { const [recentTools, setRecentTools] = useState([]); @@ -37,21 +36,6 @@ export function useToolHistory() { } }, []); - // Add a tool to recent history - const addToRecent = useCallback((toolId: ToolId) => { - if (typeof window === 'undefined') { - return; - } - - setRecentTools((prev) => { - // Remove if already exists - const filtered = prev.filter((id) => id !== toolId); - // Add to front - const updated = [toolId, ...filtered].slice(0, MAX_RECENT_TOOLS); - window.localStorage.setItem(RECENT_TOOLS_KEY, JSON.stringify(updated)); - return updated; - }); - }, []); // Toggle favorite status const toggleFavorite = useCallback((toolId: ToolId) => { @@ -80,7 +64,6 @@ export function useToolHistory() { return { recentTools, favoriteTools, - addToRecent, toggleFavorite, isFavorite, }; diff --git a/frontend/src/hooks/tools/useFocusTrap.ts b/frontend/src/hooks/useFocusTrap.ts similarity index 99% rename from frontend/src/hooks/tools/useFocusTrap.ts rename to frontend/src/hooks/useFocusTrap.ts index 863d0930a..0fa783bdb 100644 --- a/frontend/src/hooks/tools/useFocusTrap.ts +++ b/frontend/src/hooks/useFocusTrap.ts @@ -72,3 +72,5 @@ export function useFocusTrap(containerRef: RefObject, enable }; }, [containerRef, enabled]); } + + diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index 3239cbaaa..0beaf82a3 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -4,11 +4,12 @@ import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry"; import { getAllEndpoints, type ToolRegistryEntry } 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: Record; setToolSelectedFileIds: (fileIds: FileId[]) => void; getSelectedTool: (toolKey: string | null) => ToolRegistryEntry | null; } @@ -30,12 +31,12 @@ 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 = {}; + const toolRegistry: Record = useMemo(() => { + const availableToolRegistry: Record = {} as Record; Object.keys(baseRegistry).forEach(toolKey => { if (isToolAvailable(toolKey)) { const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry]; - availableToolRegistry[toolKey] = { + availableToolRegistry[toolKey as ToolId] = { ...baseTool, name: baseTool.name, description: baseTool.description, @@ -46,7 +47,7 @@ export const useToolManagement = (): ToolManagementResult => { }, [isToolAvailable, t, baseRegistry]); const getSelectedTool = useCallback((toolKey: string | null): ToolRegistryEntry | null => { - return toolKey ? toolRegistry[toolKey] || null : null; + return toolKey ? toolRegistry[toolKey as ToolId] || null : null; }, [toolRegistry]); return { diff --git a/frontend/src/hooks/useToolPanelModePreference.ts b/frontend/src/hooks/useToolPanelModePreference.ts new file mode 100644 index 000000000..b8656eeae --- /dev/null +++ b/frontend/src/hooks/useToolPanelModePreference.ts @@ -0,0 +1,50 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { TOOL_PANEL_MODE_STORAGE_KEY, ToolPanelMode } from '../contexts/toolWorkflow/toolWorkflowState'; + +const PROMPT_SEEN_KEY = 'toolPanelModePromptSeen'; + +export function useToolPanelModePreference() { + const [hydrated, setHydrated] = useState(false); + + const getPreferredMode = useCallback((): ToolPanelMode | null => { + if (typeof window === 'undefined') return null; + const stored = window.localStorage.getItem(TOOL_PANEL_MODE_STORAGE_KEY); + return stored === 'sidebar' || stored === 'fullscreen' ? stored : null; + }, []); + + const setPreferredMode = useCallback((mode: ToolPanelMode) => { + if (typeof window === 'undefined') return; + window.localStorage.setItem(TOOL_PANEL_MODE_STORAGE_KEY, mode); + }, []); + + const hasSeenPrompt = useCallback((): boolean => { + if (typeof window === 'undefined') return true; + return window.localStorage.getItem(PROMPT_SEEN_KEY) === 'true'; + }, []); + + const markPromptSeen = useCallback(() => { + if (typeof window === 'undefined') return; + window.localStorage.setItem(PROMPT_SEEN_KEY, 'true'); + }, []); + + const shouldShowPrompt = useMemo(() => { + const seen = hasSeenPrompt(); + const pref = getPreferredMode(); + return !seen && !pref; + }, [getPreferredMode, hasSeenPrompt]); + + useEffect(() => { + setHydrated(true); + }, []); + + return { + hydrated, + getPreferredMode, + setPreferredMode, + hasSeenPrompt, + markPromptSeen, + shouldShowPrompt, + } as const; +} + + diff --git a/frontend/src/hooks/useToolSections.ts b/frontend/src/hooks/useToolSections.ts index 5c5be8773..1482c7bef 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(); @@ -114,7 +115,7 @@ export function useToolSections( seen.add(toolId); const sub = tool.subcategoryId; if (!subMap[sub]) subMap[sub] = []; - subMap[sub].push({ id: toolId, tool }); + subMap[sub].push({ id: toolId as ToolId, tool }); }); const entries = Object.entries(subMap); diff --git a/frontend/src/services/preferencesService.ts b/frontend/src/services/preferencesService.ts index 992fe9f2f..2f126b297 100644 --- a/frontend/src/services/preferencesService.ts +++ b/frontend/src/services/preferencesService.ts @@ -1,9 +1,10 @@ +import { ToolPanelMode } from 'src/contexts/toolWorkflow/toolWorkflowState'; import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager'; export interface UserPreferences { autoUnzip: boolean; autoUnzipFileLimit: number; - defaultToolPanelMode: 'sidebar' | 'fullscreen'; + defaultToolPanelMode: ToolPanelMode; } export const DEFAULT_PREFERENCES: UserPreferences = { diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css index cd895bd14..e15e4e26d 100644 --- a/frontend/src/styles/theme.css +++ b/frontend/src/styles/theme.css @@ -1,6 +1,17 @@ /* CSS variables for Tailwind + Mantine integration */ :root { + /* Global animation durations */ + --fullscreen-anim-duration-in: 0.28s; + --fullscreen-anim-duration-out: 0.22s; + + /* Z-index constants (added in this PR) */ + --z-over-fullscreen-surface: 1400; + --z-fullscreen-surface: 1200; + --z-fullscreen-favorite-star: 2; + --z-fullscreen-icon-svg: 1; + --z-toolpicker-star: 1; + /* Standard gray scale */ --gray-50: 249 250 251; --gray-100: 243 244 246; diff --git a/frontend/src/styles/zIndex.ts b/frontend/src/styles/zIndex.ts new file mode 100644 index 000000000..d485ac605 --- /dev/null +++ b/frontend/src/styles/zIndex.ts @@ -0,0 +1,8 @@ +// Centralized z-index constants for new usages added in this branch. +// Keep values identical to their original inline usages. + +export const Z_INDEX_OVER_FULLSCREEN_SURFACE = 1400; +export const Z_INDEX_FULLSCREEN_SURFACE = 1200; +export const Z_INDEX_FILE_MANAGER_MODAL = 1300; + + diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index ba47df4f1..4ec6dc646 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useFileSelection } from "../contexts/FileContext"; import { useNavigationActions } from "../contexts/NavigationContext"; @@ -48,7 +48,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { }; // Register reset function with the tool workflow context - only once on mount - React.useEffect(() => { + useEffect(() => { const stableResetFunction = () => { if (resetFunctionRef.current) { resetFunctionRef.current(); diff --git a/frontend/src/types/sidebar.ts b/frontend/src/types/sidebar.ts index b93db61d6..ba6a8683e 100644 --- a/frontend/src/types/sidebar.ts +++ b/frontend/src/types/sidebar.ts @@ -7,6 +7,7 @@ export interface SidebarState { export interface SidebarRefs { quickAccessRef: React.RefObject; toolPanelRef: React.RefObject; + rightRailRef: React.RefObject; } export interface SidebarInfo { diff --git a/frontend/src/utils/toolSearch.ts b/frontend/src/utils/toolSearch.ts index dda5749a8..cbb2ca7a1 100644 --- a/frontend/src/utils/toolSearch.ts +++ b/frontend/src/utils/toolSearch.ts @@ -1,18 +1,19 @@ +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; } export function filterToolRegistryByQuery( - toolRegistry: Record, + toolRegistry: Record, query: string ): RankedToolItem[] { const entries = Object.entries(toolRegistry); 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); @@ -78,21 +79,21 @@ 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 }); }; - for (const { id, tool } of exactName) push(id, tool, tool.name); - for (const { id, tool, text } of exactSyn) push(id, tool, text); - for (const { id, tool, text } of fuzzyName) push(id, tool, text); - for (const { id, tool, text } of fuzzySyn) push(id, tool, text); + for (const { id, tool } of exactName) push(id as ToolId, tool, tool.name); + for (const { id, tool, text } of exactSyn) push(id as ToolId, tool, text); + for (const { id, tool, text } of fuzzyName) push(id as ToolId, tool, text); + for (const { id, tool, text } of fuzzySyn) push(id as ToolId, tool, text); 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] })); }