mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
change requests
This commit is contained in:
parent
acbecc0d32
commit
a085646333
@ -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<FileManagerProps> = ({ selectedTool }) => {
|
||||
radius="md"
|
||||
className="overflow-hidden p-0"
|
||||
withCloseButton={false}
|
||||
zIndex={1300}
|
||||
zIndex={Z_INDEX_FILE_MANAGER_MODAL}
|
||||
styles={{
|
||||
content: {
|
||||
position: 'relative',
|
||||
|
||||
@ -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<DrawingCanvasProps> = ({
|
||||
}, [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');
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<FileDetailsProps> = ({
|
||||
};
|
||||
|
||||
// Reset index when selection changes
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (currentFileIndex >= selectedFiles.length) {
|
||||
setCurrentFileIndex(0);
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ const FileHistoryGroup: React.FC<FileHistoryGroupProps> = ({
|
||||
</Group>
|
||||
|
||||
<Box ml="md">
|
||||
{sortedHistory.map((historyFile, _index) => (
|
||||
{sortedHistory.map((historyFile) => (
|
||||
<FileListItem
|
||||
key={`history-${historyFile.id}-${historyFile.versionNumber || 1}`}
|
||||
file={historyFile}
|
||||
@ -56,7 +56,6 @@ const FileHistoryGroup: React.FC<FileHistoryGroupProps> = ({
|
||||
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
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
@ -40,7 +40,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
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<FileListItemProps> = ({
|
||||
leftSection={<RestoreIcon style={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddToRecents(file);
|
||||
}}
|
||||
>
|
||||
{t('fileManager.restore', 'Restore')}
|
||||
|
||||
@ -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<AppConfigModalProps> = ({ 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}
|
||||
|
||||
@ -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<LanguageSelectorProps> = ({ 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<LanguageSelectorProps> = ({ 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,
|
||||
}}
|
||||
>
|
||||
<ScrollArea h={190} type="scroll">
|
||||
|
||||
@ -102,18 +102,18 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
handleReaderToggle();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'sign',
|
||||
name: t("quickAccess.sign", "Sign"),
|
||||
icon: <LocalIcon icon="signature-rounded" width="1.25rem" height="1.25rem" />,
|
||||
size: 'lg',
|
||||
isRound: false,
|
||||
type: 'navigation',
|
||||
onClick: () => {
|
||||
setActiveButton('sign');
|
||||
handleToolSelect('sign');
|
||||
}
|
||||
},
|
||||
// {
|
||||
// id: 'sign',
|
||||
// name: t("quickAccess.sign", "Sign"),
|
||||
// icon: <LocalIcon icon="signature-rounded" width="1.25rem" height="1.25rem" />,
|
||||
// size: 'lg',
|
||||
// isRound: false,
|
||||
// type: 'navigation',
|
||||
// onClick: () => {
|
||||
// setActiveButton('sign');
|
||||
// handleToolSelect('sign');
|
||||
// }
|
||||
// },
|
||||
{
|
||||
id: 'automate',
|
||||
name: t("quickAccess.automate", "Automate"),
|
||||
|
||||
@ -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 (
|
||||
<div className={`right-rail`} data-sidebar="right-rail">
|
||||
<div ref={sidebarRefs.rightRailRef} className={`right-rail`} data-sidebar="right-rail">
|
||||
<div className="right-rail-inner">
|
||||
{topButtons.length > 0 && (
|
||||
<>
|
||||
@ -298,7 +301,10 @@ export default function RightRail() {
|
||||
</Tooltip>
|
||||
|
||||
{/* Annotation Controls */}
|
||||
<ViewerAnnotationControls currentView={currentView} />
|
||||
<ViewerAnnotationControls
|
||||
currentView={currentView}
|
||||
disabled={currentView !== 'viewer' || allButtonsDisabled || disableForFullscreen}
|
||||
/>
|
||||
</div>
|
||||
<Divider className="right-rail-divider" />
|
||||
</div>
|
||||
|
||||
@ -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<TooltipProps> = ({
|
||||
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)',
|
||||
|
||||
@ -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 = () => {
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={preferences.defaultToolPanelMode}
|
||||
onChange={(val) => 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' },
|
||||
|
||||
@ -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 = () => {
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
placeholder="Search tools..."
|
||||
placeholder={t('settings.hotkeys.searchPlaceholder', 'Search tools...')}
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.currentTarget.value)}
|
||||
size="md"
|
||||
@ -125,7 +129,7 @@ const HotkeysSection: React.FC = () => {
|
||||
<Stack gap="md">
|
||||
{filteredTools.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
{searchQuery.trim() ? 'No tools found matching your search.' : 'No tools available.'}
|
||||
{t('toolPicker.noToolsFound', 'No tools found')}
|
||||
</Text>
|
||||
) : (
|
||||
filteredTools.map(([toolId, tool], index) => {
|
||||
|
||||
@ -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}
|
||||
>
|
||||
<LocalIcon
|
||||
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
|
||||
@ -97,7 +95,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={currentView !== 'viewer' || disableForFullscreen}
|
||||
disabled={disabled}
|
||||
aria-label="Drawing mode active"
|
||||
>
|
||||
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
||||
@ -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'}
|
||||
>
|
||||
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
||||
@ -196,7 +194,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={currentView !== 'viewer' || disableForFullscreen}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LocalIcon icon="save" width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>
|
||||
|
||||
@ -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 (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={`tool-panel__fullscreen-item ${getItemClasses(true)} ${isSelected ? 'tool-panel__fullscreen-item--selected' : ''} tool-panel__fullscreen-item--with-star`}
|
||||
onClick={handleClick}
|
||||
aria-disabled={isDisabled}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{tool.icon ? (
|
||||
<span
|
||||
className={iconClasses}
|
||||
aria-hidden
|
||||
style={{
|
||||
background: iconBg,
|
||||
...getIconStyle(),
|
||||
}}
|
||||
>
|
||||
{iconNode}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="tool-panel__fullscreen-body">
|
||||
<Text fw={600} size="sm" className="tool-panel__fullscreen-name">
|
||||
{tool.name}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" className="tool-panel__fullscreen-description">
|
||||
{tool.description}
|
||||
</Text>
|
||||
{binding && (
|
||||
<div className="tool-panel__fullscreen-shortcut">
|
||||
<span style={{ color: 'var(--mantine-color-dimmed)', fontSize: '0.75rem' }}>
|
||||
{t('settings.hotkeys.shortcut', 'Shortcut')}
|
||||
</span>
|
||||
<HotkeyDisplay binding={binding} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
{matchedText && (
|
||||
<Text size="xs" c="dimmed" className="tool-panel__fullscreen-match">
|
||||
{t('toolPanel.fullscreen.matchedSynonym', 'Matches "{{text}}"', { text: matchedText })}
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
{!isDisabled && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
size="sm"
|
||||
onClick={handleStarClick}
|
||||
className="tool-panel__fullscreen-star"
|
||||
aria-label={isFav ? t('toolPanel.fullscreen.unfavorite', 'Remove from favourites') : t('toolPanel.fullscreen.favorite', 'Add to favourites')}
|
||||
>
|
||||
{isFav ? (
|
||||
<StarRoundedIcon fontSize="small" style={{ color: '#FFC107' }} />
|
||||
) : (
|
||||
<StarBorderRoundedIcon fontSize="small" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
</button>
|
||||
return (
|
||||
<DetailedToolItem key={id} id={id} tool={tool} isSelected={isSelected} onClick={handleClick} />
|
||||
);
|
||||
}
|
||||
|
||||
// Compact view
|
||||
const iconBg = getIconBackground(categoryColor, false);
|
||||
const iconClasses = 'tool-panel__fullscreen-list-icon';
|
||||
|
||||
const compactButton = (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
className={`tool-panel__fullscreen-list-item ${getItemClasses(false)} ${isSelected ? 'tool-panel__fullscreen-list-item--selected' : ''} ${!isDisabled ? 'tool-panel__fullscreen-list-item--with-star' : ''}`}
|
||||
onClick={handleClick}
|
||||
aria-disabled={isDisabled}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{tool.icon ? (
|
||||
<span
|
||||
className={iconClasses}
|
||||
aria-hidden
|
||||
style={{
|
||||
background: iconBg,
|
||||
...getIconStyle(),
|
||||
}}
|
||||
>
|
||||
{iconNode}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="tool-panel__fullscreen-list-body">
|
||||
<Text fw={600} size="sm" className="tool-panel__fullscreen-name">
|
||||
{tool.name}
|
||||
</Text>
|
||||
{matchedText && (
|
||||
<Text size="xs" c="dimmed" className="tool-panel__fullscreen-match">
|
||||
{t('toolPanel.fullscreen.matchedSynonym', 'Matches "{{text}}"', { text: matchedText})}
|
||||
</Text>
|
||||
)}
|
||||
</span>
|
||||
{!isDisabled && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
size="xs"
|
||||
onClick={handleStarClick}
|
||||
className="tool-panel__fullscreen-star-compact"
|
||||
aria-label={isFav ? t('toolPanel.fullscreen.unfavorite', 'Remove from favourites') : t('toolPanel.fullscreen.favorite', 'Add to favourites')}
|
||||
>
|
||||
{isFav ? (
|
||||
<StarRoundedIcon fontSize="inherit" style={{ color: '#FFC107', fontSize: '1rem' }} />
|
||||
) : (
|
||||
<StarBorderRoundedIcon fontSize="inherit" style={{ fontSize: '1rem' }} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
const tooltipContent = isDisabled
|
||||
? (
|
||||
<span><strong>{t('toolPanel.fullscreen.comingSoon', 'Coming soon:')}</strong> {tool.description}</span>
|
||||
)
|
||||
: (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
<span>{tool.description}</span>
|
||||
{binding && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem' }}>
|
||||
<span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500 }}>
|
||||
{t('settings.hotkeys.shortcut', 'Shortcut')}
|
||||
</span>
|
||||
<HotkeyDisplay binding={binding} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={id}
|
||||
content={tooltipContent}
|
||||
position="top"
|
||||
portalTarget={tooltipPortalTarget}
|
||||
arrow
|
||||
delay={80}
|
||||
>
|
||||
{compactButton}
|
||||
</Tooltip>
|
||||
<CompactToolItem key={id} id={id} tool={tool} isSelected={isSelected} onClick={handleClick} tooltipPortalTarget={tooltipPortalTarget} />
|
||||
);
|
||||
};
|
||||
|
||||
@ -329,11 +135,11 @@ const FullscreenToolList = ({
|
||||
</header>
|
||||
{showDescriptions ? (
|
||||
<div className="tool-panel__fullscreen-grid tool-panel__fullscreen-grid--detailed">
|
||||
{favoriteToolItems.map((item: any) => renderToolItem(item.id, item.tool))}
|
||||
{favoriteToolItems.map((item) => item && renderToolItem(item.id, item.tool))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="tool-panel__fullscreen-list">
|
||||
{favoriteToolItems.map((item: any) => renderToolItem(item.id, item.tool))}
|
||||
{favoriteToolItems.map((item) => item && renderToolItem(item.id, item.tool))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@ -428,11 +234,11 @@ const FullscreenToolList = ({
|
||||
|
||||
{showDescriptions ? (
|
||||
<div className="tool-panel__fullscreen-grid tool-panel__fullscreen-grid--detailed">
|
||||
{tools.map(({ id, tool }) => renderToolItem(id, tool))}
|
||||
{tools.map(({ id, tool }) => renderToolItem(id as ToolId, tool))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="tool-panel__fullscreen-list">
|
||||
{tools.map(({ id, tool }) => renderToolItem(id, tool))}
|
||||
{tools.map(({ id, tool }) => renderToolItem(id as ToolId, tool))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@ -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<string, ToolRegistryEntry>;
|
||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
||||
toolRegistry: Record<ToolId, ToolRegistryEntry>;
|
||||
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
|
||||
selectedToolKey: string | null;
|
||||
showDescriptions: boolean;
|
||||
matchedTextMap: Map<string, string>;
|
||||
@ -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
|
||||
|
||||
@ -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<SearchResultsProps> = ({ filteredTools, onSelect,
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
key={id}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={false}
|
||||
onSelect={onSelect}
|
||||
matchedSynonym={matchedSynonym}
|
||||
/>
|
||||
<ToolButton
|
||||
key={id}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={false}
|
||||
onSelect={onSelect}
|
||||
matchedSynonym={matchedSynonym}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -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 (
|
||||
<Box
|
||||
@ -243,7 +238,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
||||
|
||||
<Box ref={allToolsRef} w="100%">
|
||||
<Stack p="sm" gap="xs">
|
||||
{allSection?.subcategories.map((sc: any) =>
|
||||
{allSection?.subcategories.map((sc: SubcategoryGroup) =>
|
||||
renderToolButtons(t, sc, selectedToolKey, onSelect, true, false, undefined, true)
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<string, ToolRegistryEntry>;
|
||||
toolRegistry?: Record<ToolId, ToolRegistryEntry>;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@ -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([]);
|
||||
|
||||
@ -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<string, ToolRegistryEntry>;
|
||||
toolRegistry: Record<ToolId, ToolRegistryEntry>;
|
||||
}
|
||||
|
||||
export default function AutomationSelection({
|
||||
|
||||
@ -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<string, ToolRegistryEntry>;
|
||||
toolRegistry: Record<ToolId, ToolRegistryEntry>;
|
||||
onToolUpdate: (index: number, updates: Partial<AutomationTool>) => 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;
|
||||
|
||||
|
||||
@ -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<string, ToolRegistryEntry>; // Pass registry as prop to break circular dependency
|
||||
toolRegistry: Record<ToolId, ToolRegistryEntry>; // 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<string, ToolRegistryEntry> = {};
|
||||
const registry: Record<ToolId, ToolRegistryEntry> = {} as Record<ToolId, ToolRegistryEntry>;
|
||||
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({
|
||||
<div ref={containerRef} className='rounded-xl'>
|
||||
{/* 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
|
||||
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
||||
borderRadius: "var(--mantine-radius-lg)" }}>
|
||||
<ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false}
|
||||
<ToolButton id='tool' tool={toolRegistry[selectedValue as ToolId]} isSelected={false}
|
||||
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
115
frontend/src/components/tools/fullscreen/CompactToolItem.tsx
Normal file
115
frontend/src/components/tools/fullscreen/CompactToolItem.tsx
Normal file
@ -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<CompactToolItemProps> = ({ 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 = (
|
||||
<button
|
||||
type="button"
|
||||
className={`tool-panel__fullscreen-list-item ${getItemClasses(false)} ${isSelected ? 'tool-panel__fullscreen-list-item--selected' : ''} ${!disabled ? 'tool-panel__fullscreen-list-item--with-star' : ''}`}
|
||||
onClick={onClick}
|
||||
aria-disabled={disabled}
|
||||
disabled={disabled}
|
||||
>
|
||||
{tool.icon ? (
|
||||
<span
|
||||
className={iconClasses}
|
||||
aria-hidden
|
||||
style={{
|
||||
background: iconBg,
|
||||
...getIconStyle(),
|
||||
}}
|
||||
>
|
||||
{iconNode}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="tool-panel__fullscreen-list-body">
|
||||
<Text fw={600} size="sm" className="tool-panel__fullscreen-name">
|
||||
{tool.name}
|
||||
</Text>
|
||||
</span>
|
||||
{!disabled && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
size="xs"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); toggleFavorite(); }}
|
||||
className="tool-panel__fullscreen-star-compact"
|
||||
aria-label={isFav ? t('toolPanel.fullscreen.unfavorite', 'Remove from favourites') : t('toolPanel.fullscreen.favorite', 'Add to favourites')}
|
||||
>
|
||||
{/* Star icons kept inline to avoid new dependency here */}
|
||||
{isFav ? (
|
||||
<span className="material-icons" style={{ color: 'var(--special-color-favorites)', fontSize: '16px' }}>star</span>
|
||||
) : (
|
||||
<span className="material-icons" style={{ fontSize: '16px' }}>star_border</span>
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
const tooltipContent = disabled
|
||||
? (
|
||||
<span><strong>{t('toolPanel.fullscreen.comingSoon', 'Coming soon:')}</strong> {tool.description}</span>
|
||||
)
|
||||
: (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
<span>{tool.description}</span>
|
||||
{binding && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem' }}>
|
||||
<span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500 }}>
|
||||
{t('settings.hotkeys.shortcut', 'Shortcut')}
|
||||
</span>
|
||||
<HotkeyDisplay binding={binding} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={tooltipContent}
|
||||
position="top"
|
||||
portalTarget={tooltipPortalTarget}
|
||||
arrow
|
||||
delay={80}
|
||||
>
|
||||
{compactButton}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompactToolItem;
|
||||
|
||||
|
||||
@ -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<DetailedToolItemProps> = ({ 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 (
|
||||
<button
|
||||
type="button"
|
||||
className={`tool-panel__fullscreen-item ${getItemClasses(true)} ${isSelected ? 'tool-panel__fullscreen-item--selected' : ''} tool-panel__fullscreen-item--with-star`}
|
||||
onClick={onClick}
|
||||
aria-disabled={disabled}
|
||||
disabled={disabled}
|
||||
>
|
||||
{tool.icon ? (
|
||||
<span
|
||||
className={iconClasses}
|
||||
aria-hidden
|
||||
style={{
|
||||
background: iconBg,
|
||||
...getIconStyle(),
|
||||
}}
|
||||
>
|
||||
{iconNode}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="tool-panel__fullscreen-body">
|
||||
<Text fw={600} size="sm" className="tool-panel__fullscreen-name">
|
||||
{tool.name}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" className="tool-panel__fullscreen-description">
|
||||
{tool.description}
|
||||
</Text>
|
||||
{binding && (
|
||||
<div className="tool-panel__fullscreen-shortcut">
|
||||
<span style={{ color: 'var(--mantine-color-dimmed)', fontSize: '0.75rem' }}>
|
||||
{t('settings.hotkeys.shortcut', 'Shortcut')}
|
||||
</span>
|
||||
<HotkeyDisplay binding={binding} size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
{!disabled && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
size="sm"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); toggleFavorite(); }}
|
||||
className="tool-panel__fullscreen-star"
|
||||
aria-label={isFav ? t('toolPanel.fullscreen.unfavorite', 'Remove from favourites') : t('toolPanel.fullscreen.favorite', 'Add to favourites')}
|
||||
>
|
||||
{/* Star icons kept inline to avoid new dependency here */}
|
||||
{isFav ? (
|
||||
<span className="material-icons" style={{ color: 'var(--special-color-favorites)', fontSize: '20px' }}>star</span>
|
||||
) : (
|
||||
<span className="material-icons" style={{ fontSize: '20px' }}>star_border</span>
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailedToolItem;
|
||||
|
||||
|
||||
45
frontend/src/components/tools/fullscreen/shared.ts
Normal file
45
frontend/src/components/tools/fullscreen/shared.ts
Normal file
@ -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<string, string> => {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -31,21 +31,21 @@ export const renderToolButtons = (
|
||||
<SubcategoryHeader label={getSubcategoryLabel(t, subcategory.subcategoryId)} />
|
||||
)}
|
||||
<div>
|
||||
{subcategory.tools.map(({ id, tool }, _index) => {
|
||||
const matchedSynonym = matchedTextMap.get(id);
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
key={id}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
disableNavigation={disableNavigation}
|
||||
matchedSynonym={matchedSynonym}
|
||||
hasStars={hasStars}
|
||||
/>
|
||||
);
|
||||
{subcategory.tools.map(({ id, tool }) => {
|
||||
const matchedSynonym = matchedTextMap.get(id);
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
key={id}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
disableNavigation={disableNavigation}
|
||||
matchedSynonym={matchedSynonym}
|
||||
hasStars={hasStars}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
40
frontend/src/components/tools/toolPicker/FavoriteStar.tsx
Normal file
40
frontend/src/components/tools/toolPicker/FavoriteStar.tsx
Normal file
@ -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<FavoriteStarProps> = ({ isFavorite, onToggle, className, size = "xs" }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
size={size}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onToggle();
|
||||
}}
|
||||
className={className}
|
||||
aria-label={isFavorite ? t('toolPanel.fullscreen.unfavorite', 'Remove from favourites') : t('toolPanel.fullscreen.favorite', 'Add to favourites')}
|
||||
>
|
||||
{isFavorite ? (
|
||||
<StarRoundedIcon fontSize="inherit" style={{ color: 'var(--special-color-favorites)', fontSize: '1rem' }} />
|
||||
) : (
|
||||
<StarBorderRoundedIcon fontSize="inherit" style={{ fontSize: '1rem' }} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
);
|
||||
};
|
||||
|
||||
export default FavoriteStar;
|
||||
|
||||
|
||||
@ -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<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
);
|
||||
|
||||
const star = hasStars && !isUnavailable ? (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="xl"
|
||||
size="xs"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(id as ToolId);
|
||||
}}
|
||||
<FavoriteStar
|
||||
isFavorite={fav}
|
||||
onToggle={() => 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 ? (
|
||||
<StarRoundedIcon fontSize="inherit" style={{ color: '#FFC107', fontSize: '1rem' }} />
|
||||
) : (
|
||||
<StarBorderRoundedIcon fontSize="inherit" style={{ fontSize: '1rem' }} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
size="xs"
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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<Record<string, ToolRegistryEntry>>;
|
||||
onToolSelect?: (toolId: string) => void;
|
||||
toolRegistry: Readonly<Record<ToolId, ToolRegistryEntry>>;
|
||||
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<ToolSearchProps> = ({
|
||||
const ToolSearch = ({
|
||||
value,
|
||||
onChange,
|
||||
toolRegistry,
|
||||
@ -31,7 +32,7 @@ const ToolSearch: React.FC<ToolSearchProps> = ({
|
||||
hideIcon = false,
|
||||
onFocus,
|
||||
autoFocus = false,
|
||||
}) => {
|
||||
}: ToolSearchProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
@ -126,7 +127,7 @@ const ToolSearch: React.FC<ToolSearchProps> = ({
|
||||
key={id}
|
||||
variant="subtle"
|
||||
onClick={() => {
|
||||
onToolSelect?.(id);
|
||||
onToolSelect?.(id as ToolId);
|
||||
setDropdownOpen(false);
|
||||
}}
|
||||
leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
const next: Record<string, HotkeyBinding> = {};
|
||||
let changed = false;
|
||||
Object.entries(prev).forEach(([toolId, binding]) => {
|
||||
if (toolRegistry[toolId]) {
|
||||
if (toolRegistry[toolId as ToolId]) {
|
||||
next[toolId] = binding;
|
||||
} else {
|
||||
changed = true;
|
||||
|
||||
@ -7,6 +7,7 @@ export function SidebarProvider({ children }: SidebarProviderProps) {
|
||||
// All sidebar state management
|
||||
const quickAccessRef = useRef<HTMLDivElement>(null);
|
||||
const toolPanelRef = useRef<HTMLDivElement>(null);
|
||||
const rightRailRef = useRef<HTMLDivElement>(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,
|
||||
|
||||
@ -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<string, ToolRegistryEntry>;
|
||||
toolRegistry: Record<ToolId, ToolRegistryEntry>;
|
||||
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,
|
||||
]);
|
||||
|
||||
20
frontend/src/hooks/tools/useFavoriteToolItems.ts
Normal file
20
frontend/src/hooks/tools/useFavoriteToolItems.ts
Normal file
@ -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<Record<ToolId, ToolRegistryEntry>>
|
||||
): 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]);
|
||||
}
|
||||
|
||||
|
||||
@ -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<HTMLDivElement | null>;
|
||||
quickAccessRef: RefObject<HTMLDivElement | null>;
|
||||
rightRailRef?: RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
export function useToolPanelGeometry({
|
||||
enabled,
|
||||
toolPanelRef,
|
||||
quickAccessRef,
|
||||
rightRailRef,
|
||||
}: UseToolPanelGeometryOptions) {
|
||||
const [geometry, setGeometry] = useState<ToolPanelGeometry | null>(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;
|
||||
}
|
||||
|
||||
@ -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<ToolId[]>([]);
|
||||
@ -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,
|
||||
};
|
||||
@ -72,3 +72,5 @@ export function useFocusTrap(containerRef: RefObject<HTMLElement | null>, enable
|
||||
};
|
||||
}, [containerRef, enabled]);
|
||||
}
|
||||
|
||||
|
||||
@ -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<string, ToolRegistryEntry>;
|
||||
toolRegistry: Record<ToolId, ToolRegistryEntry>;
|
||||
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<string, ToolRegistryEntry> = useMemo(() => {
|
||||
const availableToolRegistry: Record<string, ToolRegistryEntry> = {};
|
||||
const toolRegistry: Record<ToolId, ToolRegistryEntry> = useMemo(() => {
|
||||
const availableToolRegistry: Record<ToolId, ToolRegistryEntry> = {} as Record<ToolId, ToolRegistryEntry>;
|
||||
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 {
|
||||
|
||||
50
frontend/src/hooks/useToolPanelModePreference.ts
Normal file
50
frontend/src/hooks/useToolPanelModePreference.ts
Normal file
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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;
|
||||
|
||||
8
frontend/src/styles/zIndex.ts
Normal file
8
frontend/src/styles/zIndex.ts
Normal file
@ -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;
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -7,6 +7,7 @@ export interface SidebarState {
|
||||
export interface SidebarRefs {
|
||||
quickAccessRef: React.RefObject<HTMLDivElement | null>;
|
||||
toolPanelRef: React.RefObject<HTMLDivElement | null>;
|
||||
rightRailRef: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
export interface SidebarInfo {
|
||||
|
||||
@ -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<string, ToolRegistryEntry>,
|
||||
toolRegistry: Record<ToolId, ToolRegistryEntry>,
|
||||
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<string>();
|
||||
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] }));
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user