mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
legacy UI (#4612)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Claude <[email protected]> Co-authored-by: EthanHealy01 <[email protected]> Co-authored-by: EthanHealy01 <[email protected]>
This commit is contained in:
co-authored by
Claude
EthanHealy01
EthanHealy01
parent
a339f71116
commit
e542f4bc61
@@ -1946,6 +1946,25 @@ viewer.zoomIn=Zoom in
|
|||||||
# Tool Picker
|
# Tool Picker
|
||||||
toolPicker.searchPlaceholder=Search tools...
|
toolPicker.searchPlaceholder=Search tools...
|
||||||
toolPicker.noToolsFound=No tools found
|
toolPicker.noToolsFound=No tools found
|
||||||
|
toolPanel.toggle.legacy=Switch to legacy mode
|
||||||
|
toolPanel.toggle.sidebar=Switch to sidebar mode
|
||||||
|
toolPanel.placeholder=Choose a tool to get started
|
||||||
|
toolPanel.legacy.heading=All tools (legacy view)
|
||||||
|
toolPanel.legacy.tagline=Browse and launch tools while keeping the classic full-width gallery.
|
||||||
|
toolPanel.legacy.descriptionsOn=Showing descriptions
|
||||||
|
toolPanel.legacy.descriptionsOff=Descriptions hidden
|
||||||
|
toolPanel.legacy.noResults=Try adjusting your search or toggle descriptions to find what you need.
|
||||||
|
toolPanel.legacy.matchedSynonym=Matches "{{text}}"
|
||||||
|
toolPanel.modePrompt.title=Choose how you browse tools
|
||||||
|
toolPanel.modePrompt.description=Preview both layouts and decide how you want to explore Stirling PDF tools.
|
||||||
|
toolPanel.modePrompt.sidebarTitle=Advanced sidebar
|
||||||
|
toolPanel.modePrompt.sidebarDescription=Keep tools alongside your workspace for quick switching.
|
||||||
|
toolPanel.modePrompt.recommended=Recommended
|
||||||
|
toolPanel.modePrompt.chooseSidebar=Use advanced sidebar
|
||||||
|
toolPanel.modePrompt.legacyTitle=Legacy fullscreen
|
||||||
|
toolPanel.modePrompt.legacyDescription=Browse every tool in a catalogue that covers the workspace until you pick one.
|
||||||
|
toolPanel.modePrompt.chooseLegacy=Use legacy fullscreen
|
||||||
|
toolPanel.modePrompt.dismiss=Maybe later
|
||||||
pageEditor.reset=Reset Changes
|
pageEditor.reset=Reset Changes
|
||||||
pageEditor.zoomIn=Zoom In
|
pageEditor.zoomIn=Zoom In
|
||||||
pageEditor.zoomOut=Zoom Out
|
pageEditor.zoomOut=Zoom Out
|
||||||
@@ -1958,3 +1977,39 @@ viewer.nextPage=Next Page
|
|||||||
viewer.pageNavigation=Page Navigation
|
viewer.pageNavigation=Page Navigation
|
||||||
viewer.currentPage=Current Page
|
viewer.currentPage=Current Page
|
||||||
viewer.totalPages=Total Pages
|
viewer.totalPages=Total Pages
|
||||||
|
toolPanel.legacy.favorites=Favourites
|
||||||
|
toolPanel.legacy.recent=Recently used
|
||||||
|
toolPanel.legacy.favorite=Add to favourites
|
||||||
|
toolPanel.legacy.unfavorite=Remove from favourites
|
||||||
|
toolPanel.legacy.settings.title=Customise appearance
|
||||||
|
toolPanel.legacy.settings.iconBackground.label=Tool icon background
|
||||||
|
toolPanel.legacy.settings.iconBackground.description=When to show coloured backgrounds behind tool icons
|
||||||
|
toolPanel.legacy.settings.iconBackground.none=None
|
||||||
|
toolPanel.legacy.settings.iconBackground.hover=On hover
|
||||||
|
toolPanel.legacy.settings.iconBackground.always=Always
|
||||||
|
toolPanel.legacy.settings.iconColor.label=Tool icon colour
|
||||||
|
toolPanel.legacy.settings.iconColor.description=Colour scheme for tool icons
|
||||||
|
toolPanel.legacy.settings.iconColor.colored=Coloured
|
||||||
|
toolPanel.legacy.settings.iconColor.vibrant=Vibrant
|
||||||
|
toolPanel.legacy.settings.iconColor.monochrome=Monochrome
|
||||||
|
toolPanel.legacy.settings.sectionTitle.label=Section titles
|
||||||
|
toolPanel.legacy.settings.sectionTitle.description=Colour for category section titles
|
||||||
|
toolPanel.legacy.settings.sectionTitle.colored=Coloured
|
||||||
|
toolPanel.legacy.settings.sectionTitle.neutral=Neutral
|
||||||
|
toolPanel.legacy.settings.headerIcon.label=Section header icons
|
||||||
|
toolPanel.legacy.settings.headerIcon.description=Colour for Favourites/Recent icons
|
||||||
|
toolPanel.legacy.settings.headerIcon.colored=Coloured
|
||||||
|
toolPanel.legacy.settings.headerIcon.monochrome=Monochrome
|
||||||
|
toolPanel.legacy.settings.headerBadge.label=Section header badges
|
||||||
|
toolPanel.legacy.settings.headerBadge.description=Colour for count badges in section headers
|
||||||
|
toolPanel.legacy.settings.headerBadge.colored=Coloured
|
||||||
|
toolPanel.legacy.settings.headerBadge.neutral=Neutral
|
||||||
|
toolPanel.legacy.settings.border.label=Tool item borders
|
||||||
|
toolPanel.legacy.settings.border.description=Show borders around tool items
|
||||||
|
toolPanel.legacy.settings.border.visible=Visible
|
||||||
|
toolPanel.legacy.settings.border.hidden=Hidden
|
||||||
|
toolPanel.legacy.settings.hover.label=Hover effect intensity
|
||||||
|
toolPanel.legacy.settings.hover.description=How prominent the hover effect should be
|
||||||
|
toolPanel.legacy.settings.hover.subtle=Subtle
|
||||||
|
toolPanel.legacy.settings.hover.moderate=Moderate
|
||||||
|
toolPanel.legacy.settings.hover.prominent=Prominent
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
{
|
{
|
||||||
|
"toolPanel": {
|
||||||
|
"modePrompt": {
|
||||||
|
"title": "Choose how you browse tools",
|
||||||
|
"description": "Preview both layouts and decide how you want to explore Stirling PDF tools.",
|
||||||
|
"sidebarTitle": "Sidebar mode",
|
||||||
|
"sidebarDescription": "Keep tools alongside your workspace for quick switching.",
|
||||||
|
"recommended": "Recommended",
|
||||||
|
"chooseSidebar": "Use sidebar mode",
|
||||||
|
"fullscreenTitle": "Fullscreen mode - (legacy)",
|
||||||
|
"fullscreenDescription": "Browse every tool in a catalogue that covers the workspace until you pick one.",
|
||||||
|
"chooseFullscreen": "Use fullscreen mode",
|
||||||
|
"dismiss": "Maybe later"
|
||||||
|
},
|
||||||
|
"fullscreen": {
|
||||||
|
"showDetails": "Show Details"
|
||||||
|
}
|
||||||
|
},
|
||||||
"unsavedChanges": "You have unsaved changes to your PDF.",
|
"unsavedChanges": "You have unsaved changes to your PDF.",
|
||||||
"areYouSure": "Are you sure you want to leave?",
|
"areYouSure": "Are you sure you want to leave?",
|
||||||
"unsavedChangesTitle": "Unsaved Changes",
|
"unsavedChangesTitle": "Unsaved Changes",
|
||||||
|
|||||||
@@ -2,6 +2,23 @@
|
|||||||
"language": {
|
"language": {
|
||||||
"direction": "ltr"
|
"direction": "ltr"
|
||||||
},
|
},
|
||||||
|
"toolPanel": {
|
||||||
|
"modePrompt": {
|
||||||
|
"title": "Choose how you browse tools",
|
||||||
|
"description": "Preview both layouts and decide how you want to explore Stirling PDF tools.",
|
||||||
|
"sidebarTitle": "Sidebar mode",
|
||||||
|
"sidebarDescription": "Keep tools alongside your workspace for quick switching.",
|
||||||
|
"recommended": "Recommended",
|
||||||
|
"chooseSidebar": "Use sidebar mode",
|
||||||
|
"fullscreenTitle": "Fullscreen mode - (legacy)",
|
||||||
|
"fullscreenDescription": "Browse every tool in a catalogue that covers the workspace until you pick one.",
|
||||||
|
"chooseFullscreen": "Use fullscreen mode",
|
||||||
|
"dismiss": "Maybe later"
|
||||||
|
},
|
||||||
|
"fullscreen": {
|
||||||
|
"showDetails": "Show Details"
|
||||||
|
}
|
||||||
|
},
|
||||||
"addPageNumbers": {
|
"addPageNumbers": {
|
||||||
"fontSize": "Font Size",
|
"fontSize": "Font Size",
|
||||||
"fontName": "Font Name",
|
"fontName": "Font Name",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import MobileLayout from './fileManager/MobileLayout';
|
|||||||
import DesktopLayout from './fileManager/DesktopLayout';
|
import DesktopLayout from './fileManager/DesktopLayout';
|
||||||
import DragOverlay from './fileManager/DragOverlay';
|
import DragOverlay from './fileManager/DragOverlay';
|
||||||
import { FileManagerProvider } from '../contexts/FileManagerContext';
|
import { FileManagerProvider } from '../contexts/FileManagerContext';
|
||||||
|
import { Z_INDEX_FILE_MANAGER_MODAL } from '../styles/zIndex';
|
||||||
import { isGoogleDriveConfigured } from '../services/googleDrivePickerService';
|
import { isGoogleDriveConfigured } from '../services/googleDrivePickerService';
|
||||||
import { loadScript } from '../utils/scriptLoader';
|
import { loadScript } from '../utils/scriptLoader';
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Preload Google Drive scripts if configured
|
// Preload Google Drive scripts if configured
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isGoogleDriveConfigured()) {
|
if (isGoogleDriveConfigured()) {
|
||||||
// Load scripts in parallel without blocking
|
// Load scripts in parallel without blocking
|
||||||
@@ -125,7 +127,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
radius="md"
|
radius="md"
|
||||||
className="overflow-hidden p-0"
|
className="overflow-hidden p-0"
|
||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
zIndex={1100}
|
zIndex={Z_INDEX_FILE_MANAGER_MODAL}
|
||||||
styles={{
|
styles={{
|
||||||
content: {
|
content: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
import React, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Text, Center, Box, LoadingOverlay, Stack, Group
|
Text, Center, Box, LoadingOverlay, Stack, Group
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
@@ -62,7 +62,7 @@ const FileEditor = ({
|
|||||||
const [selectionMode, setSelectionMode] = useState(toolMode);
|
const [selectionMode, setSelectionMode] = useState(toolMode);
|
||||||
|
|
||||||
// Enable selection mode automatically in tool mode
|
// Enable selection mode automatically in tool mode
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (toolMode) {
|
if (toolMode) {
|
||||||
setSelectionMode(true);
|
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 { Stack, Button, Box } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail';
|
import { useIndexedDBThumbnail } from '../../hooks/useIndexedDBThumbnail';
|
||||||
@@ -50,7 +50,7 @@ const FileDetails: React.FC<FileDetailsProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Reset index when selection changes
|
// Reset index when selection changes
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentFileIndex >= selectedFiles.length) {
|
if (currentFileIndex >= selectedFiles.length) {
|
||||||
setCurrentFileIndex(0);
|
setCurrentFileIndex(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const FileHistoryGroup: React.FC<FileHistoryGroupProps> = ({
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box ml="md">
|
<Box ml="md">
|
||||||
{sortedHistory.map((historyFile, _index) => (
|
{sortedHistory.map((historyFile) => (
|
||||||
<FileListItem
|
<FileListItem
|
||||||
key={`history-${historyFile.id}-${historyFile.versionNumber || 1}`}
|
key={`history-${historyFile.id}-${historyFile.versionNumber || 1}`}
|
||||||
file={historyFile}
|
file={historyFile}
|
||||||
@@ -56,7 +56,6 @@ const FileHistoryGroup: React.FC<FileHistoryGroupProps> = ({
|
|||||||
onDoubleClick={() => onFileDoubleClick(historyFile)}
|
onDoubleClick={() => onFileDoubleClick(historyFile)}
|
||||||
isHistoryFile={true} // This enables "Add to Recents" in menu
|
isHistoryFile={true} // This enables "Add to Recents" in menu
|
||||||
isLatestVersion={false} // History files are never latest
|
isLatestVersion={false} // History files are never latest
|
||||||
// onAddToRecents is accessed from context by FileListItem
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {expandedFileIds, onToggleExpansion, onAddToRecents, onUnzipFile } = useFileManagerContext();
|
const {expandedFileIds, onToggleExpansion, onUnzipFile } = useFileManagerContext();
|
||||||
|
|
||||||
// Check if this is a ZIP file
|
// Check if this is a ZIP file
|
||||||
const isZipFile = zipFileService.isZipFileStub(file);
|
const isZipFile = zipFileService.isZipFileStub(file);
|
||||||
@@ -188,7 +188,6 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
leftSection={<RestoreIcon style={{ fontSize: 16 }} />}
|
leftSection={<RestoreIcon style={{ fontSize: 16 }} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onAddToRecents(file);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('fileManager.restore', 'Restore')}
|
{t('fileManager.restore', 'Restore')}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Overview from './config/configSections/Overview';
|
|||||||
import { createConfigNavSections } from './config/configNavSections';
|
import { createConfigNavSections } from './config/configNavSections';
|
||||||
import { NavKey } from './config/types';
|
import { NavKey } from './config/types';
|
||||||
import './AppConfigModal.css';
|
import './AppConfigModal.css';
|
||||||
|
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex';
|
||||||
|
|
||||||
interface AppConfigModalProps {
|
interface AppConfigModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
@@ -77,7 +78,7 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
|||||||
centered
|
centered
|
||||||
radius="lg"
|
radius="lg"
|
||||||
withCloseButton={false}
|
withCloseButton={false}
|
||||||
style={{ zIndex: 1000 }}
|
zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE}
|
||||||
overlayProps={{ opacity: 0.35, blur: 2 }}
|
overlayProps={{ opacity: 0.35, blur: 2 }}
|
||||||
padding={0}
|
padding={0}
|
||||||
fullScreen={isMobile}
|
fullScreen={isMobile}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box } from '@mantine/core';
|
||||||
|
|
||||||
|
interface BadgeProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
variant?: 'default' | 'colored';
|
||||||
|
color?: string;
|
||||||
|
textColor?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Badge: React.FC<BadgeProps> = ({
|
||||||
|
children,
|
||||||
|
size = 'sm',
|
||||||
|
variant = 'default',
|
||||||
|
color,
|
||||||
|
textColor,
|
||||||
|
backgroundColor,
|
||||||
|
className,
|
||||||
|
style
|
||||||
|
}) => {
|
||||||
|
const getSizeStyles = () => {
|
||||||
|
switch (size) {
|
||||||
|
case 'sm':
|
||||||
|
return {
|
||||||
|
padding: '0.125rem 0.5rem',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
};
|
||||||
|
case 'md':
|
||||||
|
return {
|
||||||
|
padding: '0.25rem 0.75rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: '0.625rem',
|
||||||
|
};
|
||||||
|
case 'lg':
|
||||||
|
return {
|
||||||
|
padding: '0.375rem 1rem',
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVariantStyles = () => {
|
||||||
|
// If explicit colors are provided, use them
|
||||||
|
if (textColor && backgroundColor) {
|
||||||
|
return {
|
||||||
|
backgroundColor,
|
||||||
|
color: textColor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a single color is provided, use it for text and 20% opacity for background
|
||||||
|
if (color) {
|
||||||
|
return {
|
||||||
|
backgroundColor: `color-mix(in srgb, ${color} 20%, transparent)`,
|
||||||
|
color: color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If variant is colored but no color provided, use default colored styling
|
||||||
|
if (variant === 'colored') {
|
||||||
|
return {
|
||||||
|
backgroundColor: `color-mix(in srgb, var(--category-color-default) 15%, transparent)`,
|
||||||
|
color: 'var(--category-color-default)',
|
||||||
|
borderColor: `color-mix(in srgb, var(--category-color-default) 30%, transparent)`,
|
||||||
|
border: '1px solid',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default styling
|
||||||
|
return {
|
||||||
|
background: 'var(--tool-header-badge-bg)',
|
||||||
|
color: 'var(--tool-header-badge-text)',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
...getSizeStyles(),
|
||||||
|
...getVariantStyles(),
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Badge;
|
||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { supportedLanguages } from '../../i18n';
|
import { supportedLanguages } from '../../i18n';
|
||||||
import LocalIcon from './LocalIcon';
|
import LocalIcon from './LocalIcon';
|
||||||
import styles from './LanguageSelector.module.css';
|
import styles from './LanguageSelector.module.css';
|
||||||
|
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface LanguageSelectorProps {
|
interface LanguageSelectorProps {
|
||||||
@@ -209,6 +210,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({ position = 'bottom-
|
|||||||
width={600}
|
width={600}
|
||||||
position={position}
|
position={position}
|
||||||
offset={offset}
|
offset={offset}
|
||||||
|
zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE}
|
||||||
transitionProps={{
|
transitionProps={{
|
||||||
transition: 'scale-y',
|
transition: 'scale-y',
|
||||||
duration: 200,
|
duration: 200,
|
||||||
@@ -264,6 +266,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({ position = 'bottom-
|
|||||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||||
backgroundColor: 'light-dark(var(--mantine-color-white), var(--mantine-color-dark-6))',
|
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))',
|
border: 'light-dark(1px solid var(--mantine-color-gray-3), 1px solid var(--mantine-color-dark-4))',
|
||||||
|
zIndex: Z_INDEX_OVER_FULLSCREEN_SURFACE,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollArea h={190} type="scroll">
|
<ScrollArea h={190} type="scroll">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
||||||
const { handleReaderToggle, handleBackToTools, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow();
|
const { handleReaderToggle, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow();
|
||||||
const { getToolNavigation } = useSidebarNavigation();
|
const { getToolNavigation } = useSidebarNavigation();
|
||||||
const { config } = useAppConfig();
|
const { config } = useAppConfig();
|
||||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
@@ -68,7 +68,8 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
onClick: (e: React.MouseEvent) => handleClick(e),
|
onClick: (e: React.MouseEvent) => handleClick(e),
|
||||||
'aria-label': config.name
|
'aria-label': config.name
|
||||||
} : {
|
} : {
|
||||||
onClick: () => handleClick()
|
onClick: () => handleClick(),
|
||||||
|
'aria-label': config.name
|
||||||
})}
|
})}
|
||||||
size={isActive ? (config.size || 'lg') : 'lg'}
|
size={isActive ? (config.size || 'lg') : 'lg'}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
@@ -98,11 +99,9 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
type: 'navigation',
|
type: 'navigation',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setActiveButton('read');
|
setActiveButton('read');
|
||||||
handleBackToTools();
|
|
||||||
handleReaderToggle();
|
handleReaderToggle();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// TODO: Add sign
|
|
||||||
// {
|
// {
|
||||||
// id: 'sign',
|
// id: 'sign',
|
||||||
// name: t("quickAccess.sign", "Sign"),
|
// name: t("quickAccess.sign", "Sign"),
|
||||||
@@ -230,6 +229,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
|||||||
onClick={config.onClick}
|
onClick={config.onClick}
|
||||||
style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)}
|
style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)}
|
||||||
className={isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView) ? 'activeIconScale' : ''}
|
className={isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView) ? 'activeIconScale' : ''}
|
||||||
|
aria-label={config.name}
|
||||||
data-testid={`${config.id}-button`}
|
data-testid={`${config.id}-button`}
|
||||||
>
|
>
|
||||||
<span className="iconContainer">
|
<span className="iconContainer">
|
||||||
|
|||||||
@@ -20,18 +20,23 @@ import ViewerAnnotationControls from './rightRail/ViewerAnnotationControls';
|
|||||||
import { parseSelection } from '../../utils/bulkselection/parseSelection';
|
import { parseSelection } from '../../utils/bulkselection/parseSelection';
|
||||||
|
|
||||||
|
|
||||||
|
import { useSidebarContext } from '../../contexts/SidebarContext';
|
||||||
|
|
||||||
export default function RightRail() {
|
export default function RightRail() {
|
||||||
|
const { sidebarRefs } = useSidebarContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isPanning, setIsPanning] = useState(false);
|
const [isPanning, setIsPanning] = useState(false);
|
||||||
|
|
||||||
// Viewer context for PDF controls - safely handle when not available
|
// Viewer context for PDF controls - safely handle when not available
|
||||||
const viewerContext = React.useContext(ViewerContext);
|
const viewerContext = React.useContext(ViewerContext);
|
||||||
const { toggleTheme } = useRainbowThemeContext();
|
const { toggleTheme } = useRainbowThemeContext();
|
||||||
const { buttons, actions } = useRightRail();
|
const { buttons, actions, allButtonsDisabled } = useRightRail();
|
||||||
|
|
||||||
const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]);
|
const topButtons = useMemo(() => buttons.filter(b => (b.section || 'top') === 'top' && (b.visible ?? true)), [buttons]);
|
||||||
|
|
||||||
// Access PageEditor functions for page-editor-specific actions
|
// Access PageEditor functions for page-editor-specific actions
|
||||||
const { pageEditorFunctions } = useToolWorkflow();
|
const { pageEditorFunctions, toolPanelMode, leftPanelView } = useToolWorkflow();
|
||||||
|
const disableForFullscreen = toolPanelMode === 'fullscreen' && leftPanelView === 'toolPicker';
|
||||||
|
|
||||||
// CSV input state for page selection
|
// CSV input state for page selection
|
||||||
const [csvInput, setCsvInput] = useState<string>("");
|
const [csvInput, setCsvInput] = useState<string>("");
|
||||||
@@ -176,19 +181,19 @@ export default function RightRail() {
|
|||||||
}, [currentView]);
|
}, [currentView]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="right-rail">
|
<div ref={sidebarRefs.rightRailRef} className={`right-rail`} data-sidebar="right-rail">
|
||||||
<div className="right-rail-inner">
|
<div className="right-rail-inner">
|
||||||
{topButtons.length > 0 && (
|
{topButtons.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="right-rail-section">
|
<div className="right-rail-section">
|
||||||
{topButtons.map(btn => (
|
{topButtons.map(btn => (
|
||||||
<Tooltip key={btn.id} content={btn.tooltip} position="left" offset={12} arrow>
|
<Tooltip key={btn.id} content={btn.tooltip} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => actions[btn.id]?.()}
|
onClick={() => actions[btn.id]?.()}
|
||||||
disabled={btn.disabled}
|
disabled={btn.disabled || allButtonsDisabled || disableForFullscreen}
|
||||||
>
|
>
|
||||||
{btn.icon}
|
{btn.icon}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -206,7 +211,7 @@ export default function RightRail() {
|
|||||||
>
|
>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<Tooltip content={t('rightRail.search', 'Search PDF')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.search', 'Search PDF')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<Popover position="left" withArrow shadow="md" offset={8}>
|
<Popover position="left" withArrow shadow="md" offset={8}>
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<div style={{ display: 'inline-flex' }}>
|
<div style={{ display: 'inline-flex' }}>
|
||||||
@@ -214,7 +219,7 @@ export default function RightRail() {
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer' || allButtonsDisabled || disableForFullscreen}
|
||||||
aria-label={typeof t === 'function' ? t('rightRail.search', 'Search PDF') : 'Search PDF'}
|
aria-label={typeof t === 'function' ? t('rightRail.search', 'Search PDF') : 'Search PDF'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="search" width="1.5rem" height="1.5rem" />
|
||||||
@@ -234,7 +239,7 @@ export default function RightRail() {
|
|||||||
|
|
||||||
|
|
||||||
{/* Pan Mode */}
|
{/* Pan Mode */}
|
||||||
<Tooltip content={t('rightRail.panMode', 'Pan Mode')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.panMode', 'Pan Mode')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant={isPanning ? "filled" : "subtle"}
|
variant={isPanning ? "filled" : "subtle"}
|
||||||
color={isPanning ? "blue" : undefined}
|
color={isPanning ? "blue" : undefined}
|
||||||
@@ -244,14 +249,14 @@ export default function RightRail() {
|
|||||||
viewerContext?.panActions.togglePan();
|
viewerContext?.panActions.togglePan();
|
||||||
setIsPanning(!isPanning);
|
setIsPanning(!isPanning);
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer' || allButtonsDisabled || disableForFullscreen}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="pan-tool-rounded" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="pan-tool-rounded" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Rotate Left */}
|
{/* Rotate Left */}
|
||||||
<Tooltip content={t('rightRail.rotateLeft', 'Rotate Left')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.rotateLeft', 'Rotate Left')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -259,14 +264,14 @@ export default function RightRail() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
viewerContext?.rotationActions.rotateBackward();
|
viewerContext?.rotationActions.rotateBackward();
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer' || allButtonsDisabled || disableForFullscreen}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="rotate-left" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="rotate-left" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Rotate Right */}
|
{/* Rotate Right */}
|
||||||
<Tooltip content={t('rightRail.rotateRight', 'Rotate Right')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.rotateRight', 'Rotate Right')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -274,14 +279,14 @@ export default function RightRail() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
viewerContext?.rotationActions.rotateForward();
|
viewerContext?.rotationActions.rotateForward();
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer' || allButtonsDisabled || disableForFullscreen}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="rotate-right" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="rotate-right" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Sidebar Toggle */}
|
{/* Sidebar Toggle */}
|
||||||
<Tooltip content={t('rightRail.toggleSidebar', 'Toggle Sidebar')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.toggleSidebar', 'Toggle Sidebar')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -289,14 +294,17 @@ export default function RightRail() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
viewerContext?.toggleThumbnailSidebar();
|
viewerContext?.toggleThumbnailSidebar();
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={currentView !== 'viewer' || allButtonsDisabled || disableForFullscreen}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="view-list" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="view-list" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Annotation Controls */}
|
{/* Annotation Controls */}
|
||||||
<ViewerAnnotationControls currentView={currentView} />
|
<ViewerAnnotationControls
|
||||||
|
currentView={currentView}
|
||||||
|
disabled={currentView !== 'viewer' || allButtonsDisabled || disableForFullscreen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Divider className="right-rail-divider" />
|
<Divider className="right-rail-divider" />
|
||||||
</div>
|
</div>
|
||||||
@@ -308,14 +316,14 @@ export default function RightRail() {
|
|||||||
>
|
>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
||||||
{/* Select All Button */}
|
{/* Select All Button */}
|
||||||
<Tooltip content={t('rightRail.selectAll', 'Select All')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.selectAll', 'Select All')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<div>
|
<div>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={handleSelectAll}
|
onClick={handleSelectAll}
|
||||||
disabled={currentView === 'viewer' || totalItems === 0 || selectedCount === totalItems}
|
disabled={currentView === 'viewer' || totalItems === 0 || selectedCount === totalItems || allButtonsDisabled || disableForFullscreen}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="select-all" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="select-all" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -323,14 +331,14 @@ export default function RightRail() {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* Deselect All Button */}
|
{/* Deselect All Button */}
|
||||||
<Tooltip content={t('rightRail.deselectAll', 'Deselect All')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.deselectAll', 'Deselect All')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<div>
|
<div>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={handleDeselectAll}
|
onClick={handleDeselectAll}
|
||||||
disabled={currentView === 'viewer' || selectedCount === 0}
|
disabled={currentView === 'viewer' || selectedCount === 0 || allButtonsDisabled || disableForFullscreen}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="crop-square-outline" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="crop-square-outline" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -339,7 +347,7 @@ export default function RightRail() {
|
|||||||
|
|
||||||
{/* Select by Numbers - page editor only, with animated presence */}
|
{/* Select by Numbers - page editor only, with animated presence */}
|
||||||
{pageControlsMounted && (
|
{pageControlsMounted && (
|
||||||
<Tooltip content={t('rightRail.selectByNumber', 'Select by Page Numbers')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.selectByNumber', 'Select by Page Numbers')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
|
|
||||||
<div className={`right-rail-fade ${pageControlsVisible ? 'enter' : 'exit'}`} aria-hidden={!pageControlsVisible}>
|
<div className={`right-rail-fade ${pageControlsVisible ? 'enter' : 'exit'}`} aria-hidden={!pageControlsVisible}>
|
||||||
<Popover position="left" withArrow shadow="md" offset={8}>
|
<Popover position="left" withArrow shadow="md" offset={8}>
|
||||||
@@ -349,7 +357,7 @@ export default function RightRail() {
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
disabled={!pageControlsVisible || totalItems === 0}
|
disabled={!pageControlsVisible || totalItems === 0 || allButtonsDisabled || disableForFullscreen}
|
||||||
aria-label={typeof t === 'function' ? t('rightRail.selectByNumber', 'Select by Page Numbers') : 'Select by Page Numbers'}
|
aria-label={typeof t === 'function' ? t('rightRail.selectByNumber', 'Select by Page Numbers') : 'Select by Page Numbers'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="pin-end" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="pin-end" width="1.5rem" height="1.5rem" />
|
||||||
@@ -376,7 +384,7 @@ export default function RightRail() {
|
|||||||
|
|
||||||
{/* Delete Selected Pages - page editor only, with animated presence */}
|
{/* Delete Selected Pages - page editor only, with animated presence */}
|
||||||
{pageControlsMounted && (
|
{pageControlsMounted && (
|
||||||
<Tooltip content={t('rightRail.deleteSelected', 'Delete Selected Pages')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.deleteSelected', 'Delete Selected Pages')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
|
|
||||||
<div className={`right-rail-fade ${pageControlsVisible ? 'enter' : 'exit'}`} aria-hidden={!pageControlsVisible}>
|
<div className={`right-rail-fade ${pageControlsVisible ? 'enter' : 'exit'}`} aria-hidden={!pageControlsVisible}>
|
||||||
<div style={{ display: 'inline-flex' }}>
|
<div style={{ display: 'inline-flex' }}>
|
||||||
@@ -385,7 +393,7 @@ export default function RightRail() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => { pageEditorFunctions?.handleDelete?.(); }}
|
onClick={() => { pageEditorFunctions?.handleDelete?.(); }}
|
||||||
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length || 0) === 0}
|
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length || 0) === 0 || allButtonsDisabled || disableForFullscreen}
|
||||||
aria-label={typeof t === 'function' ? t('rightRail.deleteSelected', 'Delete Selected Pages') : 'Delete Selected Pages'}
|
aria-label={typeof t === 'function' ? t('rightRail.deleteSelected', 'Delete Selected Pages') : 'Delete Selected Pages'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />
|
||||||
@@ -398,7 +406,7 @@ export default function RightRail() {
|
|||||||
|
|
||||||
{/* Export Selected Pages - page editor only */}
|
{/* Export Selected Pages - page editor only */}
|
||||||
{pageControlsMounted && (
|
{pageControlsMounted && (
|
||||||
<Tooltip content={t('rightRail.exportSelected', 'Export Selected Pages')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.exportSelected', 'Export Selected Pages')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<div className={`right-rail-fade ${pageControlsVisible ? 'enter' : 'exit'}`} aria-hidden={!pageControlsVisible}>
|
<div className={`right-rail-fade ${pageControlsVisible ? 'enter' : 'exit'}`} aria-hidden={!pageControlsVisible}>
|
||||||
<div style={{ display: 'inline-flex' }}>
|
<div style={{ display: 'inline-flex' }}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
@@ -406,7 +414,7 @@ export default function RightRail() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={() => { pageEditorFunctions?.onExportSelected?.(); }}
|
onClick={() => { pageEditorFunctions?.onExportSelected?.(); }}
|
||||||
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length || 0) === 0 || pageEditorFunctions?.exportLoading}
|
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length || 0) === 0 || pageEditorFunctions?.exportLoading || allButtonsDisabled || disableForFullscreen}
|
||||||
aria-label={typeof t === 'function' ? t('rightRail.exportSelected', 'Export Selected Pages') : 'Export Selected Pages'}
|
aria-label={typeof t === 'function' ? t('rightRail.exportSelected', 'Export Selected Pages') : 'Export Selected Pages'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
||||||
@@ -417,7 +425,7 @@ export default function RightRail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Close (File Editor: Close Selected | Page Editor: Close PDF) */}
|
{/* Close (File Editor: Close Selected | Page Editor: Close PDF) */}
|
||||||
<Tooltip content={currentView === 'pageEditor' ? t('rightRail.closePdf', 'Close PDF') : t('rightRail.closeSelected', 'Close Selected Files')} position="left" offset={12} arrow>
|
<Tooltip content={currentView === 'pageEditor' ? t('rightRail.closePdf', 'Close PDF') : t('rightRail.closeSelected', 'Close Selected Files')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<div>
|
<div>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
@@ -427,7 +435,8 @@ export default function RightRail() {
|
|||||||
disabled={
|
disabled={
|
||||||
currentView === 'viewer' ||
|
currentView === 'viewer' ||
|
||||||
(currentView === 'fileEditor' && selectedCount === 0) ||
|
(currentView === 'fileEditor' && selectedCount === 0) ||
|
||||||
(currentView === 'pageEditor' && (activeFiles.length === 0 || !pageEditorFunctions?.closePdf))
|
(currentView === 'pageEditor' && (activeFiles.length === 0 || !pageEditorFunctions?.closePdf)) ||
|
||||||
|
allButtonsDisabled || disableForFullscreen
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="close-rounded" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="close-rounded" width="1.5rem" height="1.5rem" />
|
||||||
@@ -441,7 +450,8 @@ export default function RightRail() {
|
|||||||
|
|
||||||
{/* Theme toggle and Language dropdown */}
|
{/* Theme toggle and Language dropdown */}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
||||||
<Tooltip content={t('rightRail.toggleTheme', 'Toggle Theme')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.toggleTheme', 'Toggle Theme')} position="left" offset={12} arrow portalTarget={document.body}
|
||||||
|
>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -452,13 +462,17 @@ export default function RightRail() {
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip content={t('rightRail.language', 'Language')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
|
<div style={{ display: 'inline-flex' }}>
|
||||||
<LanguageSelector position="left-start" offset={6} compact />
|
<LanguageSelector position="left-start" offset={6} compact />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content={
|
<Tooltip content={
|
||||||
currentView === 'pageEditor'
|
currentView === 'pageEditor'
|
||||||
? t('rightRail.exportAll', 'Export PDF')
|
? t('rightRail.exportAll', 'Export PDF')
|
||||||
: (selectedCount > 0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All'))
|
: (selectedCount > 0 ? t('rightRail.downloadSelected', 'Download Selected Files') : t('rightRail.downloadAll', 'Download All'))
|
||||||
} position="left" offset={12} arrow>
|
} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<div>
|
<div>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
@@ -466,7 +480,7 @@ export default function RightRail() {
|
|||||||
className="right-rail-icon"
|
className="right-rail-icon"
|
||||||
onClick={handleExportAll}
|
onClick={handleExportAll}
|
||||||
disabled={
|
disabled={
|
||||||
currentView === 'viewer' ? !exportState?.canExport : totalItems === 0
|
disableForFullscreen || (currentView === 'viewer' ? !exportState?.canExport : totalItems === 0 || allButtonsDisabled)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
||||||
@@ -481,4 +495,3 @@ export default function RightRail() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { TooltipContent } from './tooltip/TooltipContent';
|
|||||||
import { useSidebarContext } from '../../contexts/SidebarContext';
|
import { useSidebarContext } from '../../contexts/SidebarContext';
|
||||||
import { BASE_PATH } from '../../constants/app';
|
import { BASE_PATH } from '../../constants/app';
|
||||||
import styles from './tooltip/Tooltip.module.css';
|
import styles from './tooltip/Tooltip.module.css';
|
||||||
|
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex';
|
||||||
|
|
||||||
export interface TooltipProps {
|
export interface TooltipProps {
|
||||||
sidebarTooltip?: boolean;
|
sidebarTooltip?: boolean;
|
||||||
@@ -28,6 +29,10 @@ export interface TooltipProps {
|
|||||||
pinOnClick?: boolean;
|
pinOnClick?: boolean;
|
||||||
/** If true, clicking outside also closes when not pinned (default true) */
|
/** If true, clicking outside also closes when not pinned (default true) */
|
||||||
closeOnOutside?: boolean;
|
closeOnOutside?: boolean;
|
||||||
|
/** If true, tooltip interaction is disabled entirely */
|
||||||
|
disabled?: boolean;
|
||||||
|
/** If false, tooltip will not open on focus (hover only) */
|
||||||
|
openOnFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tooltip: React.FC<TooltipProps> = ({
|
export const Tooltip: React.FC<TooltipProps> = ({
|
||||||
@@ -48,6 +53,8 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
containerStyle = {},
|
containerStyle = {},
|
||||||
pinOnClick = false,
|
pinOnClick = false,
|
||||||
closeOnOutside = true,
|
closeOnOutside = true,
|
||||||
|
disabled = false,
|
||||||
|
openOnFocus = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [internalOpen, setInternalOpen] = useState(false);
|
const [internalOpen, setInternalOpen] = useState(false);
|
||||||
const [isPinned, setIsPinned] = useState(false);
|
const [isPinned, setIsPinned] = useState(false);
|
||||||
@@ -68,7 +75,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
const sidebarContext = sidebarTooltip ? useSidebarContext() : null;
|
const sidebarContext = sidebarTooltip ? useSidebarContext() : null;
|
||||||
|
|
||||||
const isControlled = controlledOpen !== undefined;
|
const isControlled = controlledOpen !== undefined;
|
||||||
const open = isControlled ? !!controlledOpen : internalOpen;
|
const open = (isControlled ? !!controlledOpen : internalOpen) && !disabled;
|
||||||
|
|
||||||
const setOpen = useCallback(
|
const setOpen = useCallback(
|
||||||
(newOpen: boolean) => {
|
(newOpen: boolean) => {
|
||||||
@@ -148,15 +155,16 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
// === Trigger handlers ===
|
// === Trigger handlers ===
|
||||||
const openWithDelay = useCallback(() => {
|
const openWithDelay = useCallback(() => {
|
||||||
clearTimers();
|
clearTimers();
|
||||||
|
if (disabled) return;
|
||||||
openTimeoutRef.current = setTimeout(() => setOpen(true), Math.max(0, delay || 0));
|
openTimeoutRef.current = setTimeout(() => setOpen(true), Math.max(0, delay || 0));
|
||||||
}, [clearTimers, setOpen, delay]);
|
}, [clearTimers, setOpen, delay, disabled]);
|
||||||
|
|
||||||
const handlePointerEnter = useCallback(
|
const handlePointerEnter = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (!isPinned) openWithDelay();
|
if (!isPinned && !disabled) openWithDelay();
|
||||||
(children.props as any)?.onPointerEnter?.(e);
|
(children.props as any)?.onPointerEnter?.(e);
|
||||||
},
|
},
|
||||||
[isPinned, openWithDelay, children.props]
|
[isPinned, openWithDelay, children.props, disabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerLeave = useCallback(
|
const handlePointerLeave = useCallback(
|
||||||
@@ -219,10 +227,10 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
// Keyboard / focus accessibility
|
// Keyboard / focus accessibility
|
||||||
const handleFocus = useCallback(
|
const handleFocus = useCallback(
|
||||||
(e: React.FocusEvent) => {
|
(e: React.FocusEvent) => {
|
||||||
if (!isPinned) openWithDelay();
|
if (!isPinned && !disabled && openOnFocus) openWithDelay();
|
||||||
(children.props as any)?.onFocus?.(e);
|
(children.props as any)?.onFocus?.(e);
|
||||||
},
|
},
|
||||||
[isPinned, openWithDelay, children.props]
|
[isPinned, openWithDelay, children.props, disabled, openOnFocus]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBlur = useCallback(
|
const handleBlur = useCallback(
|
||||||
@@ -291,7 +299,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
left: coords.left,
|
left: coords.left,
|
||||||
width: maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' as const : undefined),
|
width: maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' as const : undefined),
|
||||||
minWidth,
|
minWidth,
|
||||||
zIndex: 9999,
|
zIndex: Z_INDEX_OVER_FULLSCREEN_SURFACE,
|
||||||
visibility: positionReady ? 'visible' : 'hidden',
|
visibility: positionReady ? 'visible' : 'hidden',
|
||||||
opacity: positionReady ? 1 : 0,
|
opacity: positionReady ? 1 : 0,
|
||||||
color: 'var(--text-primary)',
|
color: 'var(--text-primary)',
|
||||||
@@ -345,9 +353,13 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{childWithHandlers}
|
{childWithHandlers}
|
||||||
{portalTarget && document.body.contains(portalTarget)
|
{(() => {
|
||||||
? tooltipElement && createPortal(tooltipElement, portalTarget)
|
const defaultTarget = typeof document !== 'undefined' ? document.body : null;
|
||||||
: tooltipElement}
|
const target = portalTarget ?? defaultTarget;
|
||||||
|
return tooltipElement && target
|
||||||
|
? createPortal(tooltipElement, target)
|
||||||
|
: tooltipElement;
|
||||||
|
})()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Paper, Stack, Switch, Text, Tooltip, NumberInput } from '@mantine/core';
|
import { Paper, Stack, Switch, Text, Tooltip, NumberInput, SegmentedControl } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { usePreferences } from '../../../../contexts/PreferencesContext';
|
import { usePreferences } from '../../../../contexts/PreferencesContext';
|
||||||
|
import { ToolPanelMode } from 'src/contexts/toolWorkflow/toolWorkflowState';
|
||||||
|
|
||||||
const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4;
|
const DEFAULT_AUTO_UNZIP_FILE_LIMIT = 4;
|
||||||
|
|
||||||
@@ -26,6 +27,24 @@ const GeneralSection: React.FC = () => {
|
|||||||
|
|
||||||
<Paper withBorder p="md" radius="md">
|
<Paper withBorder p="md" radius="md">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<div>
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
{t('settings.general.defaultToolPickerMode', 'Default tool picker mode')}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed" mt={4}>
|
||||||
|
{t('settings.general.defaultToolPickerModeDescription', 'Choose whether the tool picker opens in fullscreen or sidebar by default')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<SegmentedControl
|
||||||
|
value={preferences.defaultToolPanelMode}
|
||||||
|
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' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label={t('settings.general.autoUnzipTooltip', 'Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.')}
|
label={t('settings.general.autoUnzipTooltip', 'Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.')}
|
||||||
multiline
|
multiline
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { Alert, Badge, Box, Button, Divider, Group, Paper, Stack, Text } from '@mantine/core';
|
import { Alert, Badge, Box, Button, Divider, Group, Paper, Stack, Text, TextInput } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext';
|
||||||
import { useHotkeys } from '../../../../contexts/HotkeyContext';
|
import { useHotkeys } from '../../../../contexts/HotkeyContext';
|
||||||
|
import { ToolId } from '../../../../types/toolId';
|
||||||
import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay';
|
import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay';
|
||||||
import { bindingEquals, eventToBinding, HotkeyBinding } from '../../../../utils/hotkeys';
|
import { bindingEquals, eventToBinding, HotkeyBinding } from '../../../../utils/hotkeys';
|
||||||
import { ToolId } from 'src/types/toolId';
|
|
||||||
import { ToolRegistryEntry } from 'src/data/toolsTaxonomy';
|
import { ToolRegistryEntry } from 'src/data/toolsTaxonomy';
|
||||||
|
|
||||||
const rowStyle: React.CSSProperties = {
|
const rowStyle: React.CSSProperties = {
|
||||||
@@ -28,9 +28,21 @@ const HotkeysSection: React.FC = () => {
|
|||||||
const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys();
|
const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys();
|
||||||
const [editingTool, setEditingTool] = useState<ToolId | null>(null);
|
const [editingTool, setEditingTool] = useState<ToolId | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
const tools = useMemo(() => Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][], [toolRegistry]);
|
const tools = useMemo(() => Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][], [toolRegistry]);
|
||||||
|
|
||||||
|
const filteredTools = useMemo(() => {
|
||||||
|
if (!searchQuery.trim()) return tools;
|
||||||
|
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
return tools.filter(([toolId, tool]) =>
|
||||||
|
tool.name.toLowerCase().includes(query) ||
|
||||||
|
tool.description.toLowerCase().includes(query) ||
|
||||||
|
toolId.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}, [tools, searchQuery]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editingTool) {
|
if (!editingTool) {
|
||||||
return;
|
return;
|
||||||
@@ -47,15 +59,17 @@ const HotkeysSection: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
setEditingTool(null);
|
setEditingTool(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
const binding = eventToBinding(event as KeyboardEvent);
|
const binding = eventToBinding(event as KeyboardEvent);
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
const osKey = isMac ? 'mac' : 'windows';
|
const osKey = isMac ? 'mac' : 'windows';
|
||||||
@@ -71,7 +85,10 @@ const HotkeysSection: React.FC = () => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
if (conflictEntry) {
|
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 }));
|
setError(t('settings.hotkeys.errorConflict', 'Shortcut already used by {{tool}}.', { tool: conflictTool }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -101,9 +118,22 @@ const HotkeysSection: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
placeholder={t('settings.hotkeys.searchPlaceholder', 'Search tools...')}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(event) => setSearchQuery(event.currentTarget.value)}
|
||||||
|
size="md"
|
||||||
|
radius="md"
|
||||||
|
/>
|
||||||
|
|
||||||
<Paper withBorder p="md" radius="md">
|
<Paper withBorder p="md" radius="md">
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{tools.map(([toolId, tool], index) => {
|
{filteredTools.length === 0 ? (
|
||||||
|
<Text c="dimmed" ta="center" py="xl">
|
||||||
|
{t('toolPicker.noToolsFound', 'No tools found')}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
filteredTools.map(([toolId, tool], index) => {
|
||||||
const currentBinding = hotkeys[toolId];
|
const currentBinding = hotkeys[toolId];
|
||||||
const defaultBinding = defaults[toolId];
|
const defaultBinding = defaults[toolId];
|
||||||
const isEditing = editingTool === toolId;
|
const isEditing = editingTool === toolId;
|
||||||
@@ -160,10 +190,11 @@ const HotkeysSection: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{index < tools.length - 1 && <Divider />}
|
{index < filteredTools.length - 1 && <Divider />}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -46,6 +46,13 @@
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* When all buttons are disabled via context */
|
||||||
|
.right-rail--all-disabled .right-rail-icon {
|
||||||
|
color: var(--right-rail-icon-disabled) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.right-rail-spacer {
|
.right-rail-spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import { useNavigationState } from '../../../contexts/NavigationContext';
|
|||||||
|
|
||||||
interface ViewerAnnotationControlsProps {
|
interface ViewerAnnotationControlsProps {
|
||||||
currentView: string;
|
currentView: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ViewerAnnotationControls({ currentView }: ViewerAnnotationControlsProps) {
|
export default function ViewerAnnotationControls({ currentView, disabled = false }: ViewerAnnotationControlsProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedColor, setSelectedColor] = useState('#000000');
|
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||||
@@ -52,7 +53,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Annotation Visibility Toggle */}
|
{/* Annotation Visibility Toggle */}
|
||||||
<Tooltip content={t('rightRail.toggleAnnotations', 'Toggle Annotations Visibility')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.toggleAnnotations', 'Toggle Annotations Visibility')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -60,7 +61,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
viewerContext?.toggleAnnotationsVisibility();
|
viewerContext?.toggleAnnotationsVisibility();
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer' || viewerContext?.isAnnotationMode || isPlacementMode}
|
disabled={disabled || currentView !== 'viewer' || viewerContext?.isAnnotationMode || isPlacementMode}
|
||||||
>
|
>
|
||||||
<LocalIcon
|
<LocalIcon
|
||||||
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
|
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
|
||||||
@@ -104,7 +105,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={disabled}
|
||||||
aria-label="Drawing mode active"
|
aria-label="Drawing mode active"
|
||||||
>
|
>
|
||||||
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
||||||
@@ -129,7 +130,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// When inactive: Show "Draw" tooltip
|
// When inactive: Show "Draw" tooltip
|
||||||
<Tooltip content={t('rightRail.draw', 'Draw')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.draw', 'Draw')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -146,7 +147,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={disabled}
|
||||||
aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'}
|
aria-label={typeof t === 'function' ? t('rightRail.draw', 'Draw') : 'Draw'}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="edit" width="1.5rem" height="1.5rem" />
|
||||||
@@ -155,7 +156,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Save PDF with Annotations */}
|
{/* Save PDF with Annotations */}
|
||||||
<Tooltip content={t('rightRail.save', 'Save')} position="left" offset={12} arrow>
|
<Tooltip content={t('rightRail.save', 'Save')} position="left" offset={12} arrow portalTarget={document.body}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -203,7 +204,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer'}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<LocalIcon icon="save" width="1.5rem" height="1.5rem" />
|
<LocalIcon icon="save" width="1.5rem" height="1.5rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
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 { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||||
|
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||||
|
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: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
|
||||||
|
searchQuery: string;
|
||||||
|
showDescriptions: boolean;
|
||||||
|
selectedToolKey: string | null;
|
||||||
|
matchedTextMap: Map<string, string>;
|
||||||
|
onSelect: (id: ToolId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FullscreenToolList = ({
|
||||||
|
filteredTools,
|
||||||
|
searchQuery,
|
||||||
|
showDescriptions,
|
||||||
|
selectedToolKey,
|
||||||
|
matchedTextMap: _matchedTextMap,
|
||||||
|
onSelect,
|
||||||
|
}: FullscreenToolListProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { toolRegistry, favoriteTools } = useToolWorkflow();
|
||||||
|
|
||||||
|
const { sections, searchGroups } = useToolSections(filteredTools, searchQuery);
|
||||||
|
|
||||||
|
const tooltipPortalTarget = typeof document !== 'undefined' ? document.body : undefined;
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}, [quickSection]);
|
||||||
|
|
||||||
|
// Show recommended/favorites section only when not searching
|
||||||
|
const showRecentFavorites = searchQuery.trim().length === 0 && ((recommendedItems.length > 0) || favoriteToolItems.length > 0);
|
||||||
|
|
||||||
|
const subcategoryGroups = useMemo(() => {
|
||||||
|
if (searchQuery.trim().length > 0) {
|
||||||
|
return searchGroups;
|
||||||
|
}
|
||||||
|
const allSection = sections.find(section => section.key === 'all');
|
||||||
|
return allSection ? allSection.subcategories : [];
|
||||||
|
}, [searchGroups, sections, searchQuery]);
|
||||||
|
|
||||||
|
if (subcategoryGroups.length === 0 && !showRecentFavorites) {
|
||||||
|
return (
|
||||||
|
<div className="tool-panel__fullscreen-empty">
|
||||||
|
<NoToolsFound />
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('toolPanel.fullscreen.noResults', 'Try adjusting your search or toggle descriptions to find what you need.')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerClass = showDescriptions
|
||||||
|
? 'tool-panel__fullscreen-groups tool-panel__fullscreen-groups--detailed'
|
||||||
|
: 'tool-panel__fullscreen-groups tool-panel__fullscreen-groups--compact';
|
||||||
|
|
||||||
|
// Helper function to render a tool item
|
||||||
|
const renderToolItem = (id: ToolId, tool: ToolRegistryEntry) => {
|
||||||
|
const isSelected = selectedToolKey === id;
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (!tool.component && !tool.link && id !== 'read' && id !== 'multiTool') return;
|
||||||
|
if (tool.link) {
|
||||||
|
window.open(tool.link, '_blank', 'noopener,noreferrer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSelect(id as ToolId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showDescriptions) {
|
||||||
|
return (
|
||||||
|
<DetailedToolItem key={id} id={id} tool={tool} isSelected={isSelected} onClick={handleClick} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CompactToolItem key={id} id={id} tool={tool} isSelected={isSelected} onClick={handleClick} tooltipPortalTarget={tooltipPortalTarget} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClass}>
|
||||||
|
{showRecentFavorites && (
|
||||||
|
<>
|
||||||
|
{favoriteToolItems.length > 0 && (
|
||||||
|
<section
|
||||||
|
className="tool-panel__fullscreen-group tool-panel__fullscreen-group--special"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--fullscreen-border-favorites)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<header className="tool-panel__fullscreen-section-header">
|
||||||
|
<div className="tool-panel__fullscreen-section-title">
|
||||||
|
<span
|
||||||
|
className="tool-panel__fullscreen-section-icon"
|
||||||
|
style={{
|
||||||
|
color: 'var(--special-color-favorites)',
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
<StarRoundedIcon />
|
||||||
|
</span>
|
||||||
|
<Text size="sm" fw={600} tt="uppercase" lts={0.5} c="dimmed">
|
||||||
|
{t('toolPanel.fullscreen.favorites', 'Favourites')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
variant="colored"
|
||||||
|
color="var(--special-color-favorites)"
|
||||||
|
>
|
||||||
|
{favoriteToolItems.length}
|
||||||
|
</Badge>
|
||||||
|
</header>
|
||||||
|
{showDescriptions ? (
|
||||||
|
<div className="tool-panel__fullscreen-grid tool-panel__fullscreen-grid--detailed">
|
||||||
|
{favoriteToolItems.map((item) => item && renderToolItem(item.id, item.tool))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="tool-panel__fullscreen-list">
|
||||||
|
{favoriteToolItems.map((item) => item && renderToolItem(item.id, item.tool))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{recommendedItems.length > 0 && (
|
||||||
|
<section
|
||||||
|
className="tool-panel__fullscreen-group tool-panel__fullscreen-group--special"
|
||||||
|
style={{
|
||||||
|
borderColor: 'var(--fullscreen-border-recommended)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<header className="tool-panel__fullscreen-section-header">
|
||||||
|
<div className="tool-panel__fullscreen-section-title">
|
||||||
|
<span
|
||||||
|
className="tool-panel__fullscreen-section-icon"
|
||||||
|
style={{
|
||||||
|
color: 'var(--special-color-recommended)',
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
<ThumbUpRoundedIcon />
|
||||||
|
</span>
|
||||||
|
<Text size="sm" fw={600} tt="uppercase" lts={0.5} c="dimmed">
|
||||||
|
{t('toolPanel.fullscreen.recommended', 'Recommended')}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
variant="colored"
|
||||||
|
color="var(--special-color-recommended)"
|
||||||
|
>
|
||||||
|
{recommendedItems.length}
|
||||||
|
</Badge>
|
||||||
|
</header>
|
||||||
|
{showDescriptions ? (
|
||||||
|
<div className="tool-panel__fullscreen-grid tool-panel__fullscreen-grid--detailed">
|
||||||
|
{recommendedItems.map((item: any) => renderToolItem(item.id, item.tool))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="tool-panel__fullscreen-list">
|
||||||
|
{recommendedItems.map((item: any) => renderToolItem(item.id, item.tool))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subcategoryGroups.map(({ subcategoryId, tools }) => {
|
||||||
|
const categoryColor = getSubcategoryColor(subcategoryId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
key={subcategoryId}
|
||||||
|
className={`tool-panel__fullscreen-group ${showDescriptions ? 'tool-panel__fullscreen-group--detailed' : 'tool-panel__fullscreen-group--compact'}`}
|
||||||
|
style={{
|
||||||
|
borderColor: `color-mix(in srgb, ${categoryColor} 25%, var(--fullscreen-border-subtle-65))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<header className="tool-panel__fullscreen-section-header">
|
||||||
|
<div className="tool-panel__fullscreen-section-title">
|
||||||
|
<span
|
||||||
|
className="tool-panel__fullscreen-section-icon"
|
||||||
|
style={{
|
||||||
|
color: categoryColor,
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{getSubcategoryIcon(subcategoryId)}
|
||||||
|
</span>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
fw={600}
|
||||||
|
tt="uppercase"
|
||||||
|
lts={0.5}
|
||||||
|
style={{
|
||||||
|
color: categoryColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getSubcategoryLabel(t, subcategoryId)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Badge
|
||||||
|
size="sm"
|
||||||
|
variant="colored"
|
||||||
|
color={categoryColor}
|
||||||
|
>
|
||||||
|
{tools.length}
|
||||||
|
</Badge>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{showDescriptions ? (
|
||||||
|
<div className="tool-panel__fullscreen-grid tool-panel__fullscreen-grid--detailed">
|
||||||
|
{tools.map(({ id, tool }) => renderToolItem(id as ToolId, tool))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="tool-panel__fullscreen-list">
|
||||||
|
{tools.map(({ id, tool }) => renderToolItem(id as ToolId, tool))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FullscreenToolList;
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { useState, useRef } from 'react';
|
||||||
|
import { ActionIcon, ScrollArea, Switch, useMantineColorScheme } from '@mantine/core';
|
||||||
|
import DoubleArrowIcon from '@mui/icons-material/DoubleArrow';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ToolSearch from './toolPicker/ToolSearch';
|
||||||
|
import FullscreenToolList from './FullscreenToolList';
|
||||||
|
import { ToolRegistryEntry } from '../../data/toolsTaxonomy';
|
||||||
|
import { ToolId } from '../../types/toolId';
|
||||||
|
import { useFocusTrap } from '../../hooks/useFocusTrap';
|
||||||
|
import { BASE_PATH } from '../../constants/app';
|
||||||
|
import { Tooltip } from '../shared/Tooltip';
|
||||||
|
import './ToolPanel.css';
|
||||||
|
import { ToolPanelGeometry } from '../../hooks/tools/useToolPanelGeometry';
|
||||||
|
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex';
|
||||||
|
|
||||||
|
interface FullscreenToolSurfaceProps {
|
||||||
|
searchQuery: string;
|
||||||
|
toolRegistry: Partial<Record<ToolId, ToolRegistryEntry>>;
|
||||||
|
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
|
||||||
|
selectedToolKey: string | null;
|
||||||
|
showDescriptions: boolean;
|
||||||
|
matchedTextMap: Map<string, string>;
|
||||||
|
onSearchChange: (value: string) => void;
|
||||||
|
onSelect: (id: ToolId) => void;
|
||||||
|
onToggleDescriptions: () => void;
|
||||||
|
onExitFullscreenMode: () => void;
|
||||||
|
toggleLabel: string;
|
||||||
|
geometry: ToolPanelGeometry | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FullscreenToolSurface = ({
|
||||||
|
searchQuery,
|
||||||
|
toolRegistry,
|
||||||
|
filteredTools,
|
||||||
|
selectedToolKey,
|
||||||
|
showDescriptions,
|
||||||
|
matchedTextMap,
|
||||||
|
onSearchChange,
|
||||||
|
onSelect,
|
||||||
|
onToggleDescriptions,
|
||||||
|
onExitFullscreenMode,
|
||||||
|
toggleLabel,
|
||||||
|
geometry,
|
||||||
|
}: FullscreenToolSurfaceProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { colorScheme } = useMantineColorScheme();
|
||||||
|
const [isExiting, setIsExiting] = useState(false);
|
||||||
|
const surfaceRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Enable focus trap when surface is active
|
||||||
|
useFocusTrap(surfaceRef, !isExiting);
|
||||||
|
|
||||||
|
const brandAltText = t("home.mobile.brandAlt", "Stirling PDF logo");
|
||||||
|
const brandIconSrc = `${BASE_PATH}/branding/StirlingPDFLogoNoText${
|
||||||
|
colorScheme === "dark" ? "Dark" : "Light"
|
||||||
|
}.svg`;
|
||||||
|
const brandTextSrc = `${BASE_PATH}/branding/StirlingPDFLogo${
|
||||||
|
colorScheme === "dark" ? "White" : "Black"
|
||||||
|
}Text.svg`;
|
||||||
|
|
||||||
|
const handleExit = () => {
|
||||||
|
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
if (prefersReducedMotion) {
|
||||||
|
onExitFullscreenMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsExiting(true);
|
||||||
|
const el = surfaceRef.current;
|
||||||
|
if (!el) {
|
||||||
|
onExitFullscreenMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Rely on CSS animation end rather than duplicating timing in JS
|
||||||
|
el.addEventListener('animationend', () => {
|
||||||
|
onExitFullscreenMode();
|
||||||
|
}, { once: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const style = geometry
|
||||||
|
? {
|
||||||
|
left: `${geometry.left}px`,
|
||||||
|
top: `${geometry.top}px`,
|
||||||
|
width: `${geometry.width}px`,
|
||||||
|
height: `${geometry.height}px`,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="tool-panel__fullscreen-surface"
|
||||||
|
style={style}
|
||||||
|
role="region"
|
||||||
|
aria-label={t('toolPanel.fullscreen.heading', 'All tools (fullscreen view)')}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={surfaceRef}
|
||||||
|
className={`tool-panel__fullscreen-surface-inner ${isExiting ? 'tool-panel__fullscreen-surface-inner--exiting' : ''}`}
|
||||||
|
>
|
||||||
|
<header className="tool-panel__fullscreen-header">
|
||||||
|
<div className="tool-panel__fullscreen-brand">
|
||||||
|
<img src={brandIconSrc} alt="" className="tool-panel__fullscreen-brand-icon" />
|
||||||
|
<img src={brandTextSrc} alt={brandAltText} className="tool-panel__fullscreen-brand-text" />
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||||
|
<Tooltip content={toggleLabel} position="bottom" arrow={true} openOnFocus={false} containerStyle={{ zIndex: Z_INDEX_OVER_FULLSCREEN_SURFACE }}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
radius="xl"
|
||||||
|
size="md"
|
||||||
|
onClick={handleExit}
|
||||||
|
aria-label={toggleLabel}
|
||||||
|
style={{ color: 'var(--right-rail-icon)' }}
|
||||||
|
>
|
||||||
|
<DoubleArrowIcon fontSize="small" style={{ transform: 'rotate(180deg)' }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="tool-panel__fullscreen-controls">
|
||||||
|
<ToolSearch
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={onSearchChange}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
|
mode="filter"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
checked={showDescriptions}
|
||||||
|
onChange={() => onToggleDescriptions()}
|
||||||
|
size="md"
|
||||||
|
labelPosition="left"
|
||||||
|
label={t('toolPanel.fullscreen.showDetails', 'Show Details')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tool-panel__fullscreen-body">
|
||||||
|
<ScrollArea className="tool-panel__fullscreen-scroll" offsetScrollbars>
|
||||||
|
<FullscreenToolList
|
||||||
|
filteredTools={filteredTools}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
showDescriptions={showDescriptions}
|
||||||
|
selectedToolKey={selectedToolKey}
|
||||||
|
matchedTextMap={matchedTextMap}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FullscreenToolSurface;
|
||||||
|
|
||||||
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Stack } from '@mantine/core';
|
import { Box, Stack } from '@mantine/core';
|
||||||
import { getSubcategoryLabel, ToolRegistryEntry } from '../../data/toolsTaxonomy';
|
import { getSubcategoryLabel, ToolRegistryEntry } from '../../data/toolsTaxonomy';
|
||||||
|
import { ToolId } from '../../types/toolId';
|
||||||
import ToolButton from './toolPicker/ToolButton';
|
import ToolButton from './toolPicker/ToolButton';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useToolSections } from '../../hooks/useToolSections';
|
import { useToolSections } from '../../hooks/useToolSections';
|
||||||
import SubcategoryHeader from './shared/SubcategoryHeader';
|
import SubcategoryHeader from './shared/SubcategoryHeader';
|
||||||
import NoToolsFound from './shared/NoToolsFound';
|
import NoToolsFound from './shared/NoToolsFound';
|
||||||
import "./toolPicker/ToolPicker.css";
|
import "./toolPicker/ToolPicker.css";
|
||||||
import { ToolId } from 'src/types/toolId';
|
|
||||||
|
|
||||||
interface SearchResultsProps {
|
interface SearchResultsProps {
|
||||||
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
|
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
|
||||||
|
|||||||
@@ -0,0 +1,548 @@
|
|||||||
|
/* CSS Custom Properties for Fullscreen Mode */
|
||||||
|
.tool-panel__fullscreen-surface-inner {
|
||||||
|
--fullscreen-bg-surface-1: color-mix(in srgb, var(--bg-toolbar) 96%, transparent);
|
||||||
|
--fullscreen-bg-surface-2: color-mix(in srgb, var(--bg-background) 90%, transparent);
|
||||||
|
--fullscreen-bg-header: var(--bg-toolbar);
|
||||||
|
--fullscreen-bg-controls-1: var(--bg-toolbar);
|
||||||
|
--fullscreen-bg-controls-2: color-mix(in srgb, var(--bg-toolbar) 95%, var(--bg-background));
|
||||||
|
--fullscreen-bg-body-1: color-mix(in srgb, var(--bg-background) 86%, transparent);
|
||||||
|
--fullscreen-bg-body-2: color-mix(in srgb, var(--bg-toolbar) 78%, transparent);
|
||||||
|
--fullscreen-bg-group: color-mix(in srgb, var(--bg-toolbar) 82%, transparent);
|
||||||
|
--fullscreen-bg-item: color-mix(in srgb, var(--bg-toolbar) 88%, transparent);
|
||||||
|
--fullscreen-bg-list-item: color-mix(in srgb, var(--bg-toolbar) 86%, transparent);
|
||||||
|
--fullscreen-bg-icon-detailed: color-mix(in srgb, var(--bg-muted) 75%, transparent);
|
||||||
|
--fullscreen-bg-icon-compact: color-mix(in srgb, var(--bg-muted) 70%, transparent);
|
||||||
|
--fullscreen-border-subtle-75: color-mix(in srgb, var(--border-subtle) 75%, transparent);
|
||||||
|
--fullscreen-border-subtle-70: color-mix(in srgb, var(--border-subtle) 70%, transparent);
|
||||||
|
--fullscreen-border-subtle-65: color-mix(in srgb, var(--border-subtle) 65%, transparent);
|
||||||
|
--fullscreen-border-favorites: color-mix(in srgb, var(--special-color-favorites) 25%, var(--border-subtle));
|
||||||
|
--fullscreen-border-recommended: color-mix(in srgb, var(--special-color-recommended) 25%, var(--border-subtle));
|
||||||
|
--fullscreen-shadow-primary: color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.55)) 25%, transparent);
|
||||||
|
--fullscreen-shadow-secondary: color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.35)) 30%, transparent);
|
||||||
|
--fullscreen-shadow-group: color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.45)) 18%, transparent);
|
||||||
|
--fullscreen-accent-hover: color-mix(in srgb, var(--text-primary) 20%, var(--border-subtle));
|
||||||
|
--fullscreen-accent-selected: color-mix(in srgb, var(--text-primary) 30%, var(--border-subtle));
|
||||||
|
--fullscreen-accent-ring: color-mix(in srgb, var(--text-primary) 15%, transparent);
|
||||||
|
--fullscreen-accent-list-bg: color-mix(in srgb, var(--text-primary) 8%, var(--bg-toolbar));
|
||||||
|
--fullscreen-accent-list-border: color-mix(in srgb, var(--text-primary) 20%, var(--border-subtle));
|
||||||
|
--fullscreen-text-icon: color-mix(in srgb, var(--text-primary) 90%, var(--text-muted));
|
||||||
|
--fullscreen-text-icon-compact: color-mix(in srgb, var(--text-primary) 88%, var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel {
|
||||||
|
position: relative;
|
||||||
|
transition: width 0.3s ease, max-width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel--fullscreen-active {
|
||||||
|
overflow: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__search-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__search-row .search-input-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__mode-toggle {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__mode-toggle:hover {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel--fullscreen {
|
||||||
|
background: var(--bg-toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__placeholder {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-surface {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: var(--z-fullscreen-surface);
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-surface-inner {
|
||||||
|
pointer-events: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 0;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
140deg,
|
||||||
|
var(--fullscreen-bg-surface-1),
|
||||||
|
var(--fullscreen-bg-surface-2)
|
||||||
|
)
|
||||||
|
padding-box;
|
||||||
|
border: 1px solid var(--fullscreen-border-subtle-75);
|
||||||
|
box-shadow: none;
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
overflow: hidden;
|
||||||
|
/* 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 var(--fullscreen-anim-out-duration) ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem 1.75rem;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
background: var(--bg-toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-brand-icon {
|
||||||
|
height: 1.75rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-brand-text {
|
||||||
|
height: 1.25rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.75rem 1.75rem;
|
||||||
|
border-bottom: 1px solid var(--tool-panel-search-border-bottom);
|
||||||
|
background: var(--tool-panel-search-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-controls .search-input-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-scroll {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fullscreen group layout */
|
||||||
|
.tool-panel__fullscreen-groups {
|
||||||
|
padding: 1.5rem 1.75rem;
|
||||||
|
column-width: 18rem;
|
||||||
|
column-gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-groups--compact {
|
||||||
|
column-width: 17rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-groups--detailed {
|
||||||
|
column-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
padding: 0.65rem 0.75rem 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--fullscreen-bg-group);
|
||||||
|
border: 1px solid var(--fullscreen-border-subtle-65);
|
||||||
|
box-shadow: 0 14px 32px var(--fullscreen-shadow-group);
|
||||||
|
break-inside: avoid;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.1rem 0.15rem 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-section-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-grid--detailed {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item {
|
||||||
|
all: unset;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 0.85rem 0.95rem;
|
||||||
|
border: 1px solid var(--fullscreen-border-subtle-70);
|
||||||
|
border-radius: 0.95rem;
|
||||||
|
background: var(--fullscreen-bg-item);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* Allow flex children to shrink without forcing horizontal overflow */
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item:focus-visible {
|
||||||
|
outline: 2px solid var(--fullscreen-accent-selected);
|
||||||
|
outline-offset: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item[aria-disabled="true"],
|
||||||
|
.tool-panel__fullscreen-item:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item:hover:not([aria-disabled="true"]):not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--fullscreen-accent-hover);
|
||||||
|
box-shadow: var(--shadow-xl, 0 18px 34px rgba(15, 23, 42, 0.14));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item--selected {
|
||||||
|
border-color: var(--fullscreen-accent-selected);
|
||||||
|
box-shadow: 0 0 0 2px var(--fullscreen-accent-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item--detailed {
|
||||||
|
min-height: 7.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item--with-star {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-star {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item:hover .tool-panel__fullscreen-star {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-item--with-star {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-star-compact {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.35rem;
|
||||||
|
right: 0.35rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-item:hover .tool-panel__fullscreen-star-compact {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-group--special {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: var(--fullscreen-bg-icon-detailed);
|
||||||
|
color: var(--fullscreen-text-icon);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-icon::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
color-mix(in srgb, var(--text-primary) 12%, transparent),
|
||||||
|
color-mix(in srgb, var(--text-primary) 4%, transparent)
|
||||||
|
);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item:hover:not([aria-disabled="true"]) .tool-panel__fullscreen-icon {
|
||||||
|
transform: scale(1.08);
|
||||||
|
box-shadow: 0 4px 12px color-mix(in srgb, var(--text-primary) 15%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item:hover:not([aria-disabled="true"]) .tool-panel__fullscreen-icon::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-icon svg {
|
||||||
|
font-size: 1.65rem;
|
||||||
|
position: relative;
|
||||||
|
z-index: var(--z-fullscreen-icon-svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
text-align: left;
|
||||||
|
/* Prevent long content (names, descriptions, shortcuts) from overflowing */
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-description {
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure long words or unbroken strings wrap instead of overflowing */
|
||||||
|
.tool-panel__fullscreen-name,
|
||||||
|
.tool-panel__fullscreen-description,
|
||||||
|
.tool-panel__fullscreen-match {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-match {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-shortcut {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
/* Allow hotkey chips to wrap on small widths to avoid overflow */
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-item {
|
||||||
|
all: unset;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.55rem 0.5rem 0.55rem 0.65rem;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease, transform 0.2s ease;
|
||||||
|
background: var(--fullscreen-bg-list-item);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-item[aria-disabled="true"],
|
||||||
|
.tool-panel__fullscreen-list-item:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-item:hover:not([aria-disabled="true"]):not(:disabled),
|
||||||
|
.tool-panel__fullscreen-list-item--selected {
|
||||||
|
background: var(--fullscreen-accent-list-bg);
|
||||||
|
border-color: var(--fullscreen-accent-list-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-item--selected {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.1rem;
|
||||||
|
height: 2.1rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
background: var(--fullscreen-bg-icon-compact);
|
||||||
|
color: var(--fullscreen-text-icon-compact);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-icon::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
color-mix(in srgb, var(--text-primary) 10%, transparent),
|
||||||
|
color-mix(in srgb, var(--text-primary) 3%, transparent)
|
||||||
|
);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-item:hover:not([aria-disabled="true"]) .tool-panel__fullscreen-list-icon {
|
||||||
|
transform: scale(1.06);
|
||||||
|
box-shadow: 0 2px 8px color-mix(in srgb, var(--text-primary) 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-item:hover:not([aria-disabled="true"]) .tool-panel__fullscreen-list-icon::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-icon svg {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-list-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.2rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-empty {
|
||||||
|
padding: 2rem 1.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tool-panel-fullscreen-slide-in {
|
||||||
|
from {
|
||||||
|
transform: translateX(-6%) scaleX(0.85);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0) scaleX(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tool-panel-fullscreen-slide-out {
|
||||||
|
from {
|
||||||
|
transform: translateX(0) scaleX(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(-6%) scaleX(0.85);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.tool-panel__fullscreen-surface-inner {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__mode-toggle {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-item,
|
||||||
|
.tool-panel__fullscreen-list-item {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1440px) {
|
||||||
|
.tool-panel__fullscreen-content {
|
||||||
|
padding-inline: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-grid--compact {
|
||||||
|
column-width: 15rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.tool-panel__fullscreen-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-controls .mantine-Switch-root {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel__fullscreen-grid--compact {
|
||||||
|
column-width: 14rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||||
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
|
||||||
import ToolPicker from './ToolPicker';
|
import ToolPicker from './ToolPicker';
|
||||||
@@ -6,20 +7,27 @@ import ToolRenderer from './ToolRenderer';
|
|||||||
import ToolSearch from './toolPicker/ToolSearch';
|
import ToolSearch from './toolPicker/ToolSearch';
|
||||||
import { useSidebarContext } from "../../contexts/SidebarContext";
|
import { useSidebarContext } from "../../contexts/SidebarContext";
|
||||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||||
import { ScrollArea } from '@mantine/core';
|
import { ActionIcon, ScrollArea } from '@mantine/core';
|
||||||
import { ToolId } from '../../types/toolId';
|
import { ToolId } from '../../types/toolId';
|
||||||
import { useMediaQuery } from '@mantine/hooks';
|
import { useMediaQuery } from '@mantine/hooks';
|
||||||
|
import DoubleArrowIcon from '@mui/icons-material/DoubleArrow';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import FullscreenToolSurface from './FullscreenToolSurface';
|
||||||
|
import { useToolPanelGeometry } from '../../hooks/tools/useToolPanelGeometry';
|
||||||
|
import { useLocalStorageState } from '../../hooks/tools/useJsonLocalStorageState';
|
||||||
|
import { useRightRail } from '../../contexts/RightRailContext';
|
||||||
|
import { Tooltip } from '../shared/Tooltip';
|
||||||
|
import './ToolPanel.css';
|
||||||
|
|
||||||
// No props needed - component uses context
|
// No props needed - component uses context
|
||||||
|
|
||||||
export default function ToolPanel() {
|
export default function ToolPanel() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const { sidebarRefs } = useSidebarContext();
|
const { sidebarRefs } = useSidebarContext();
|
||||||
const { toolPanelRef } = sidebarRefs;
|
const { toolPanelRef, quickAccessRef, rightRailRef } = sidebarRefs;
|
||||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||||
|
|
||||||
|
|
||||||
// Use context-based hooks to eliminate prop drilling
|
|
||||||
const {
|
const {
|
||||||
leftPanelView,
|
leftPanelView,
|
||||||
isPanelVisible,
|
isPanelVisible,
|
||||||
@@ -27,23 +35,84 @@ export default function ToolPanel() {
|
|||||||
filteredTools,
|
filteredTools,
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
|
selectedToolKey,
|
||||||
|
handleToolSelect,
|
||||||
|
setPreviewFile,
|
||||||
|
toolPanelMode,
|
||||||
|
setToolPanelMode,
|
||||||
|
setLeftPanelView,
|
||||||
|
readerMode,
|
||||||
} = useToolWorkflow();
|
} = useToolWorkflow();
|
||||||
|
|
||||||
const { selectedToolKey, handleToolSelect } = useToolWorkflow();
|
const { setAllRightRailButtonsDisabled } = useRightRail();
|
||||||
const { setPreviewFile } = useToolWorkflow();
|
|
||||||
|
const isFullscreenMode = toolPanelMode === 'fullscreen';
|
||||||
|
const toolPickerVisible = !readerMode;
|
||||||
|
const fullscreenExpanded = isFullscreenMode && leftPanelView === 'toolPicker' && !isMobile && toolPickerVisible;
|
||||||
|
|
||||||
|
|
||||||
|
// Disable right rail buttons when fullscreen mode is active
|
||||||
|
useEffect(() => {
|
||||||
|
setAllRightRailButtonsDisabled(fullscreenExpanded);
|
||||||
|
}, [fullscreenExpanded, setAllRightRailButtonsDisabled]);
|
||||||
|
|
||||||
|
// Use custom hooks for state management
|
||||||
|
const [showLegacyDescriptions, setShowLegacyDescriptions] = useLocalStorageState('legacyToolDescriptions', false);
|
||||||
|
const fullscreenGeometry = useToolPanelGeometry({
|
||||||
|
enabled: fullscreenExpanded,
|
||||||
|
toolPanelRef,
|
||||||
|
quickAccessRef,
|
||||||
|
rightRailRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleLabel = isFullscreenMode
|
||||||
|
? t('toolPanel.toggle.sidebar', 'Switch to sidebar mode')
|
||||||
|
: t('toolPanel.toggle.fullscreen', 'Switch to fullscreen mode');
|
||||||
|
|
||||||
|
const handleModeToggle = () => {
|
||||||
|
const nextMode = isFullscreenMode ? 'sidebar' : 'fullscreen';
|
||||||
|
setToolPanelMode(nextMode);
|
||||||
|
|
||||||
|
if (nextMode === 'fullscreen' && leftPanelView !== 'toolPicker') {
|
||||||
|
setLeftPanelView('toolPicker');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const computedWidth = () => {
|
||||||
|
if (isMobile) {
|
||||||
|
return '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPanelVisible) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '18.5rem';
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchedTextMap = useMemo(() => {
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
filteredTools.forEach(({ item: [id], matchedText }) => {
|
||||||
|
if (matchedText) {
|
||||||
|
map.set(id, matchedText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [filteredTools]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={toolPanelRef}
|
ref={toolPanelRef}
|
||||||
data-sidebar="tool-panel"
|
data-sidebar="tool-panel"
|
||||||
className={`flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
className={`tool-panel flex flex-col ${fullscreenExpanded ? 'tool-panel--fullscreen-active' : 'overflow-hidden'} bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||||
isRainbowMode ? rainbowStyles.rainbowPaper : ''
|
isRainbowMode ? rainbowStyles.rainbowPaper : ''
|
||||||
} ${isMobile ? 'h-full border-r-0' : 'h-screen'}`}
|
} ${isMobile ? 'h-full border-r-0' : 'h-screen'} ${fullscreenExpanded ? 'tool-panel--fullscreen' : ''}`}
|
||||||
style={{
|
style={{
|
||||||
width: isMobile ? '100%' : isPanelVisible ? '18.5rem' : '0',
|
width: computedWidth(),
|
||||||
padding: '0'
|
padding: '0'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!fullscreenExpanded && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
opacity: isMobile || isPanelVisible ? 1 : 0,
|
opacity: isMobile || isPanelVisible ? 1 : 0,
|
||||||
@@ -53,12 +122,11 @@ export default function ToolPanel() {
|
|||||||
flexDirection: 'column'
|
flexDirection: 'column'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Search Bar - Always visible at the top */}
|
|
||||||
<div
|
<div
|
||||||
|
className="tool-panel__search-row"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--tool-panel-search-bg)',
|
backgroundColor: 'var(--tool-panel-search-bg)',
|
||||||
borderBottom: '1px solid var(--tool-panel-search-border-bottom)',
|
borderBottom: '1px solid var(--tool-panel-search-border-bottom)'
|
||||||
padding: '0.75rem 1rem',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToolSearch
|
<ToolSearch
|
||||||
@@ -67,10 +135,28 @@ export default function ToolPanel() {
|
|||||||
toolRegistry={toolRegistry}
|
toolRegistry={toolRegistry}
|
||||||
mode="filter"
|
mode="filter"
|
||||||
/>
|
/>
|
||||||
|
{!isMobile && leftPanelView === 'toolPicker' && (
|
||||||
|
<Tooltip
|
||||||
|
content={toggleLabel}
|
||||||
|
position="bottom"
|
||||||
|
arrow={true}
|
||||||
|
openOnFocus={false}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
radius="xl"
|
||||||
|
style={{ color: 'var(--right-rail-icon)' }}
|
||||||
|
onClick={handleModeToggle}
|
||||||
|
aria-label={toggleLabel}
|
||||||
|
className="tool-panel__mode-toggle"
|
||||||
|
>
|
||||||
|
<DoubleArrowIcon fontSize="small" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{searchQuery.trim().length > 0 ? (
|
{searchQuery.trim().length > 0 ? (
|
||||||
// Searching view (replaces both picker and content)
|
|
||||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||||
<SearchResults
|
<SearchResults
|
||||||
filteredTools={filteredTools}
|
filteredTools={filteredTools}
|
||||||
@@ -79,7 +165,6 @@ export default function ToolPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : leftPanelView === 'toolPicker' ? (
|
) : leftPanelView === 'toolPicker' ? (
|
||||||
// Tool Picker View
|
|
||||||
<div className="flex-1 flex flex-col overflow-auto">
|
<div className="flex-1 flex flex-col overflow-auto">
|
||||||
<ToolPicker
|
<ToolPicker
|
||||||
selectedToolKey={selectedToolKey}
|
selectedToolKey={selectedToolKey}
|
||||||
@@ -89,22 +174,42 @@ export default function ToolPanel() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Selected Tool Content View
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
{/* Tool content */}
|
|
||||||
<div className="flex-1 min-h-0 overflow-hidden">
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
<ScrollArea h="100%">
|
<ScrollArea h="100%">
|
||||||
{selectedToolKey && (
|
{selectedToolKey ? (
|
||||||
<ToolRenderer
|
<ToolRenderer
|
||||||
selectedToolKey={selectedToolKey}
|
selectedToolKey={selectedToolKey}
|
||||||
onPreviewFile={setPreviewFile}
|
onPreviewFile={setPreviewFile}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="tool-panel__placeholder">
|
||||||
|
{t('toolPanel.placeholder', 'Choose a tool to get started')}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fullscreenExpanded && (
|
||||||
|
<FullscreenToolSurface
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
|
filteredTools={filteredTools}
|
||||||
|
selectedToolKey={selectedToolKey}
|
||||||
|
showDescriptions={showLegacyDescriptions}
|
||||||
|
matchedTextMap={matchedTextMap}
|
||||||
|
onSearchChange={setSearchQuery}
|
||||||
|
onSelect={(id: ToolId) => handleToolSelect(id)}
|
||||||
|
onToggleDescriptions={() => setShowLegacyDescriptions((prev) => !prev)}
|
||||||
|
onExitFullscreenMode={() => setToolPanelMode('sidebar')}
|
||||||
|
toggleLabel={toggleLabel}
|
||||||
|
geometry={fullscreenGeometry}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
.tool-panel-mode-prompt__modal {
|
||||||
|
background: color-mix(in srgb, var(--bg-toolbar) 94%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent);
|
||||||
|
box-shadow: 0 32px 64px color-mix(in srgb, var(--shadow-color, rgba(15, 23, 42, 0.55)) 20%, transparent);
|
||||||
|
max-width: min(46rem, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
background: linear-gradient(145deg,
|
||||||
|
color-mix(in srgb, var(--bg-surface) 96%, transparent),
|
||||||
|
color-mix(in srgb, var(--bg-muted) 70%, transparent)
|
||||||
|
);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 19rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__card--sidebar {
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 18%, var(--border-subtle));
|
||||||
|
background: linear-gradient(165deg,
|
||||||
|
color-mix(in srgb, var(--bg-surface) 96%, transparent),
|
||||||
|
color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 8%, transparent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__preview {
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-subtle) 70%, transparent);
|
||||||
|
background: linear-gradient(135deg, color-mix(in srgb, var(--bg-muted) 82%, transparent), transparent 75%);
|
||||||
|
padding: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.65rem;
|
||||||
|
min-height: 6.5rem;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__preview--sidebar {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__sidebar-panel {
|
||||||
|
width: 3rem;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
padding: 0.45rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.45rem;
|
||||||
|
background: linear-gradient(180deg,
|
||||||
|
color-mix(in srgb, var(--bg-muted) 88%, transparent),
|
||||||
|
color-mix(in srgb, var(--bg-muted) 72%, transparent)
|
||||||
|
);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-subtle) 65%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__sidebar-search {
|
||||||
|
height: 0.5rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: color-mix(in srgb, var(--bg-background) 90%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-subtle) 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__sidebar-item {
|
||||||
|
height: 0.55rem;
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
background: color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 32%, var(--bg-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__sidebar-item--muted {
|
||||||
|
background: color-mix(in srgb, var(--bg-background) 88%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__workspace {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-subtle) 65%, transparent);
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
grid-template-rows: 1.4fr 0.6fr;
|
||||||
|
background: linear-gradient(160deg,
|
||||||
|
color-mix(in srgb, var(--bg-background) 94%, transparent),
|
||||||
|
color-mix(in srgb, var(--bg-muted) 68%, transparent)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__workspace-page {
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
background: color-mix(in srgb, var(--bg-surface) 96%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-subtle) 55%, transparent);
|
||||||
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bg-background) 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__workspace-page--secondary {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__preview--legacy {
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.65rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__legacy-columns {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.45rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__legacy-column {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__legacy-card {
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-subtle) 55%, transparent);
|
||||||
|
background: linear-gradient(150deg,
|
||||||
|
color-mix(in srgb, var(--bg-muted) 88%, transparent),
|
||||||
|
color-mix(in srgb, var(--bg-background) 76%, transparent)
|
||||||
|
);
|
||||||
|
height: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__legacy-card--muted {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__preview--fullscreen {
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.65rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__fullscreen-columns {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.45rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__fullscreen-column {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__fullscreen-card {
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--border-subtle) 55%, transparent);
|
||||||
|
background: linear-gradient(150deg,
|
||||||
|
color-mix(in srgb, var(--bg-muted) 88%, transparent),
|
||||||
|
color-mix(in srgb, var(--bg-background) 76%, transparent)
|
||||||
|
);
|
||||||
|
height: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__fullscreen-card--muted {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__action {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__action:hover {
|
||||||
|
box-shadow: 0 10px 18px color-mix(in srgb, var(--accent-primary, var(--mantine-color-blue-6, #228be6)) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__maybe-later {
|
||||||
|
color: color-mix(in srgb, var(--text-secondary) 90%, var(--text-muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-panel-mode-prompt__maybe-later:hover {
|
||||||
|
background: color-mix(in srgb, var(--bg-muted) 78%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.tool-panel-mode-prompt__options {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
justify-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
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 './ToolPanelModePrompt.css';
|
||||||
|
import { useToolPanelModePreference } from '../../hooks/useToolPanelModePreference';
|
||||||
|
import { ToolPanelMode } from 'src/contexts/toolWorkflow/toolWorkflowState';
|
||||||
|
|
||||||
|
// type moved to hook
|
||||||
|
|
||||||
|
const ToolPanelModePrompt = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { toolPanelMode, setToolPanelMode } = useToolWorkflow();
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
const { hydrated, shouldShowPrompt, markPromptSeen, setPreferredMode } = useToolPanelModePreference();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldShowPrompt) {
|
||||||
|
setOpened(true);
|
||||||
|
}
|
||||||
|
}, [shouldShowPrompt]);
|
||||||
|
|
||||||
|
const handleSelect = (mode: ToolPanelMode) => {
|
||||||
|
setToolPanelMode(mode);
|
||||||
|
setPreferredMode(mode);
|
||||||
|
markPromptSeen();
|
||||||
|
setOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDismiss = () => {
|
||||||
|
markPromptSeen();
|
||||||
|
setOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hydrated) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={handleDismiss}
|
||||||
|
centered
|
||||||
|
size="xl"
|
||||||
|
radius="lg"
|
||||||
|
overlayProps={{ blur: 6, opacity: 0.35 }}
|
||||||
|
classNames={{ content: 'tool-panel-mode-prompt__modal' }}
|
||||||
|
title={t('toolPanel.modePrompt.title', 'Choose how you browse tools')}
|
||||||
|
>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('toolPanel.modePrompt.description', 'Preview both layouts and decide how you want to explore Stirling PDF tools.')}
|
||||||
|
</Text>
|
||||||
|
<div className="tool-panel-mode-prompt__options">
|
||||||
|
<Card withBorder radius="lg" shadow="sm" padding="lg" className="tool-panel-mode-prompt__card tool-panel-mode-prompt__card--sidebar">
|
||||||
|
<Stack gap="md" className="tool-panel-mode-prompt__card-content">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text fw={600}>{t('toolPanel.modePrompt.sidebarTitle', 'Sidebar mode')}</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('toolPanel.modePrompt.sidebarDescription', 'Keep tools alongside your workspace for quick switching.')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Badge color="blue" variant="filled">
|
||||||
|
{t('toolPanel.modePrompt.recommended', 'Recommended')}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
<div className="tool-panel-mode-prompt__preview tool-panel-mode-prompt__preview--sidebar" aria-hidden>
|
||||||
|
<div className="tool-panel-mode-prompt__sidebar-panel">
|
||||||
|
<span className="tool-panel-mode-prompt__sidebar-search" />
|
||||||
|
<span className="tool-panel-mode-prompt__sidebar-item" />
|
||||||
|
<span className="tool-panel-mode-prompt__sidebar-item" />
|
||||||
|
<span className="tool-panel-mode-prompt__sidebar-item" />
|
||||||
|
<span className="tool-panel-mode-prompt__sidebar-item tool-panel-mode-prompt__sidebar-item--muted" />
|
||||||
|
</div>
|
||||||
|
<div className="tool-panel-mode-prompt__workspace" aria-hidden>
|
||||||
|
<div className="tool-panel-mode-prompt__workspace-page" />
|
||||||
|
<div className="tool-panel-mode-prompt__workspace-page tool-panel-mode-prompt__workspace-page--secondary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={toolPanelMode === 'sidebar' ? 'filled' : 'light'}
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
className="tool-panel-mode-prompt__action"
|
||||||
|
onClick={() => handleSelect('sidebar')}
|
||||||
|
>
|
||||||
|
{t('toolPanel.modePrompt.chooseSidebar', 'Use sidebar mode')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<Card withBorder radius="lg" shadow="xs" padding="lg" className="tool-panel-mode-prompt__card">
|
||||||
|
<Stack gap="md" className="tool-panel-mode-prompt__card-content">
|
||||||
|
<Stack gap={2}>
|
||||||
|
<Text fw={600}>{t('toolPanel.modePrompt.fullscreenTitle', 'Fullscreen mode')}</Text>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('toolPanel.modePrompt.fullscreenDescription', 'Browse every tool in a catalogue that covers the workspace until you pick one.')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<div className="tool-panel-mode-prompt__preview tool-panel-mode-prompt__preview--fullscreen" aria-hidden>
|
||||||
|
<div className="tool-panel-mode-prompt__fullscreen-columns">
|
||||||
|
<div className="tool-panel-mode-prompt__fullscreen-column">
|
||||||
|
<span className="tool-panel-mode-prompt__fullscreen-card" />
|
||||||
|
<span className="tool-panel-mode-prompt__fullscreen-card" />
|
||||||
|
<span className="tool-panel-mode-prompt__fullscreen-card tool-panel-mode-prompt__fullscreen-card--muted" />
|
||||||
|
</div>
|
||||||
|
<div className="tool-panel-mode-prompt__fullscreen-column">
|
||||||
|
<span className="tool-panel-mode-prompt__fullscreen-card" />
|
||||||
|
<span className="tool-panel-mode-prompt__fullscreen-card" />
|
||||||
|
<span className="tool-panel-mode-prompt__fullscreen-card tool-panel-mode-prompt__fullscreen-card--muted" />
|
||||||
|
</div>
|
||||||
|
<div className="tool-panel-mode-prompt__fullscreen-column">
|
||||||
|
<span className="tool-panel-mode-prompt__fullscreen-card" />
|
||||||
|
<span className="tool-panel-mode-prompt__fullscreen-card" />
|
||||||
|
<span className="tool-panel-mode-prompt__fullscreen-card tool-panel-mode-prompt__fullscreen-card--muted" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={toolPanelMode === 'fullscreen' ? 'filled' : 'outline'}
|
||||||
|
color="blue"
|
||||||
|
radius="md"
|
||||||
|
className="tool-panel-mode-prompt__action"
|
||||||
|
onClick={() => handleSelect('fullscreen')}
|
||||||
|
>
|
||||||
|
{t('toolPanel.modePrompt.chooseFullscreen', 'Use fullscreen mode')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
className="tool-panel-mode-prompt__maybe-later"
|
||||||
|
onClick={handleDismiss}
|
||||||
|
>
|
||||||
|
{t('toolPanel.modePrompt.dismiss', 'Maybe later')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolPanelModePrompt;
|
||||||
@@ -4,9 +4,15 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { ToolRegistryEntry } from "../../data/toolsTaxonomy";
|
import { ToolRegistryEntry } from "../../data/toolsTaxonomy";
|
||||||
import "./toolPicker/ToolPicker.css";
|
import "./toolPicker/ToolPicker.css";
|
||||||
import { useToolSections } from "../../hooks/useToolSections";
|
import { useToolSections } from "../../hooks/useToolSections";
|
||||||
|
import type { SubcategoryGroup } from "../../hooks/useToolSections";
|
||||||
|
import { useFavoriteToolItems } from "../../hooks/tools/useFavoriteToolItems";
|
||||||
import NoToolsFound from "./shared/NoToolsFound";
|
import NoToolsFound from "./shared/NoToolsFound";
|
||||||
import { renderToolButtons } from "./shared/renderToolButtons";
|
import { renderToolButtons } from "./shared/renderToolButtons";
|
||||||
import { ToolId } from "src/types/toolId";
|
import Badge from "../shared/Badge";
|
||||||
|
import SubcategoryHeader from "./shared/SubcategoryHeader";
|
||||||
|
import ToolButton from "./toolPicker/ToolButton";
|
||||||
|
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
|
||||||
|
import { ToolId } from "../../types/toolId";
|
||||||
|
|
||||||
interface ToolPickerProps {
|
interface ToolPickerProps {
|
||||||
selectedToolKey: string | null;
|
selectedToolKey: string | null;
|
||||||
@@ -62,11 +68,23 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { sections: visibleSections } = useToolSections(filteredTools);
|
const { sections: visibleSections } = useToolSections(filteredTools);
|
||||||
|
const { favoriteTools, toolRegistry } = useToolWorkflow();
|
||||||
|
|
||||||
|
const favoriteToolItems = useFavoriteToolItems(favoriteTools, toolRegistry);
|
||||||
|
|
||||||
const quickSection = useMemo(
|
const quickSection = useMemo(
|
||||||
() => visibleSections.find(s => s.key === 'quick'),
|
() => visibleSections.find(s => s.key === 'quick'),
|
||||||
[visibleSections]
|
[visibleSections]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const recommendedItems = useMemo(() => {
|
||||||
|
if (!quickSection) return [] as Array<{ id: string; tool: ToolRegistryEntry }>;
|
||||||
|
const items: Array<{ id: string; tool: ToolRegistryEntry }> = [];
|
||||||
|
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]);
|
||||||
const allSection = useMemo(
|
const allSection = useMemo(
|
||||||
() => visibleSections.find(s => s.key === 'all'),
|
() => visibleSections.find(s => s.key === 'all'),
|
||||||
[visibleSections]
|
[visibleSections]
|
||||||
@@ -88,7 +106,9 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build flat list by subcategory for search mode
|
// 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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -116,7 +136,7 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
|||||||
{searchGroups.length === 0 ? (
|
{searchGroups.length === 0 ? (
|
||||||
<NoToolsFound />
|
<NoToolsFound />
|
||||||
) : (
|
) : (
|
||||||
searchGroups.map(group => renderToolButtons(t, group, selectedToolKey, onSelect))
|
searchGroups.map(group => renderToolButtons(t, group, selectedToolKey, onSelect, true, false, filteredTools, true))
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
@@ -143,24 +163,46 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
|||||||
onClick={() => scrollTo(quickAccessRef)}
|
onClick={() => scrollTo(quickAccessRef)}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: "1rem" }}>{t("toolPicker.quickAccess", "QUICK ACCESS")}</span>
|
<span style={{ fontSize: "1rem" }}>{t("toolPicker.quickAccess", "QUICK ACCESS")}</span>
|
||||||
<span
|
<Badge>
|
||||||
style={{
|
{recommendedCount}
|
||||||
background: "var(--tool-header-badge-bg)",
|
</Badge>
|
||||||
color: "var(--tool-header-badge-text)",
|
|
||||||
borderRadius: ".5rem",
|
|
||||||
padding: "0.125rem 0.5rem",
|
|
||||||
fontSize: ".75rem",
|
|
||||||
fontWeight: 700
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{quickSection?.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Box ref={quickAccessRef} w="100%" my="sm">
|
<Box ref={quickAccessRef} w="100%" my="sm">
|
||||||
<Stack p="sm" gap="xs">
|
<Stack p="sm" gap="xs">
|
||||||
{quickSection?.subcategories.map(sc =>
|
{favoriteToolItems.length > 0 && (
|
||||||
renderToolButtons(t, sc, selectedToolKey, onSelect, false)
|
<Box w="100%">
|
||||||
|
<SubcategoryHeader label={t('toolPanel.fullscreen.favorites', 'Favourites')} mt={0} />
|
||||||
|
<div>
|
||||||
|
{favoriteToolItems.map(({ id, tool }) => (
|
||||||
|
<ToolButton
|
||||||
|
key={`fav-${id}`}
|
||||||
|
id={id}
|
||||||
|
tool={tool}
|
||||||
|
isSelected={selectedToolKey === id}
|
||||||
|
onSelect={onSelect}
|
||||||
|
hasStars
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{recommendedItems.length > 0 && (
|
||||||
|
<Box w="100%">
|
||||||
|
<SubcategoryHeader label={t('toolPanel.fullscreen.recommended', 'Recommended')} />
|
||||||
|
<div>
|
||||||
|
{recommendedItems.map(({ id, tool }) => (
|
||||||
|
<ToolButton
|
||||||
|
key={`rec-${id}`}
|
||||||
|
id={id as ToolId}
|
||||||
|
tool={tool}
|
||||||
|
isSelected={selectedToolKey === id}
|
||||||
|
onSelect={onSelect}
|
||||||
|
hasStars
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -189,24 +231,15 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = fa
|
|||||||
onClick={() => scrollTo(allToolsRef)}
|
onClick={() => scrollTo(allToolsRef)}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: "1rem" }}>{t("toolPicker.allTools", "ALL TOOLS")}</span>
|
<span style={{ fontSize: "1rem" }}>{t("toolPicker.allTools", "ALL TOOLS")}</span>
|
||||||
<span
|
<Badge>
|
||||||
style={{
|
|
||||||
background: "var(--tool-header-badge-bg)",
|
|
||||||
color: "var(--tool-header-badge-text)",
|
|
||||||
borderRadius: ".5rem",
|
|
||||||
padding: "0.125rem 0.5rem",
|
|
||||||
fontSize: ".75rem",
|
|
||||||
fontWeight: 700
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{allSection?.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
|
{allSection?.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Box ref={allToolsRef} w="100%">
|
<Box ref={allToolsRef} w="100%">
|
||||||
<Stack p="sm" gap="xs">
|
<Stack p="sm" gap="xs">
|
||||||
{allSection?.subcategories.map(sc =>
|
{allSection?.subcategories.map((sc: SubcategoryGroup) =>
|
||||||
renderToolButtons(t, sc, selectedToolKey, onSelect, true)
|
renderToolButtons(t, sc, selectedToolKey, onSelect, true, false, undefined, true)
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
|
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
|
||||||
import { BaseToolProps } from "../../types/tool";
|
import { BaseToolProps } from "../../types/tool";
|
||||||
|
import { ToolId } from "../../types/toolId";
|
||||||
import ToolLoadingFallback from "./ToolLoadingFallback";
|
import ToolLoadingFallback from "./ToolLoadingFallback";
|
||||||
import { ToolId } from "src/types/toolId";
|
|
||||||
|
|
||||||
interface ToolRendererProps extends BaseToolProps {
|
interface ToolRendererProps extends BaseToolProps {
|
||||||
selectedToolKey: ToolId;
|
selectedToolKey: ToolId;
|
||||||
@@ -17,7 +17,14 @@ const ToolRenderer = ({
|
|||||||
}: ToolRendererProps) => {
|
}: ToolRendererProps) => {
|
||||||
// Get the tool from context (instead of direct hook call)
|
// Get the tool from context (instead of direct hook call)
|
||||||
const { toolRegistry } = useToolWorkflow();
|
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) {
|
||||||
|
return null; // These tools render in their workbench, not in the sidebar
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedTool || !selectedTool.component) {
|
if (!selectedTool || !selectedTool.component) {
|
||||||
return <div>Tool not found: {selectedToolKey}</div>;
|
return <div>Tool not found: {selectedToolKey}</div>;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Modal
|
Modal
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { Z_INDEX_AUTOMATE_MODAL } from '../../../styles/zIndex';
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistry } from '../../../data/toolsTaxonomy';
|
||||||
import ToolConfigurationModal from './ToolConfigurationModal';
|
import ToolConfigurationModal from './ToolConfigurationModal';
|
||||||
@@ -221,6 +222,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
|
|||||||
onClose={handleCancelBack}
|
onClose={handleCancelBack}
|
||||||
title={t('automate.creation.unsavedChanges.title', 'Unsaved Changes')}
|
title={t('automate.creation.unsavedChanges.title', 'Unsaved Changes')}
|
||||||
centered
|
centered
|
||||||
|
zIndex={Z_INDEX_AUTOMATE_MODAL}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text>
|
<Text>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|||||||
import { Tooltip } from '../../shared/Tooltip';
|
import { Tooltip } from '../../shared/Tooltip';
|
||||||
import { ToolIcon } from '../../shared/ToolIcon';
|
import { ToolIcon } from '../../shared/ToolIcon';
|
||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
|
||||||
interface AutomationEntryProps {
|
interface AutomationEntryProps {
|
||||||
/** Optional title for the automation (usually for custom ones) */
|
/** Optional title for the automation (usually for custom ones) */
|
||||||
@@ -31,7 +32,7 @@ interface AutomationEntryProps {
|
|||||||
/** Copy handler (for suggested automations) */
|
/** Copy handler (for suggested automations) */
|
||||||
onCopy?: () => void;
|
onCopy?: () => void;
|
||||||
/** Tool registry to resolve operation names */
|
/** Tool registry to resolve operation names */
|
||||||
toolRegistry?: Record<string, ToolRegistryEntry>;
|
toolRegistry?: Record<ToolId, ToolRegistryEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationEntry({
|
export default function AutomationEntry({
|
||||||
@@ -55,8 +56,8 @@ export default function AutomationEntry({
|
|||||||
|
|
||||||
// Helper function to resolve tool display names
|
// Helper function to resolve tool display names
|
||||||
const getToolDisplayName = (operation: string): string => {
|
const getToolDisplayName = (operation: string): string => {
|
||||||
if (toolRegistry?.[operation]?.name) {
|
if (toolRegistry?.[operation as ToolId]?.name) {
|
||||||
return toolRegistry[operation].name;
|
return toolRegistry[operation as ToolId].name;
|
||||||
}
|
}
|
||||||
// Fallback to translation or operation key
|
// Fallback to translation or operation key
|
||||||
return t(`${operation}.title`, operation);
|
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 { useTranslation } from "react-i18next";
|
||||||
import { Button, Text, Stack, Group, Card, Progress } from "@mantine/core";
|
import { Button, Text, Stack, Group, Card, Progress } from "@mantine/core";
|
||||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
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;
|
const hasResults = automateOperation?.files.length > 0 || automateOperation?.downloadUrl !== null;
|
||||||
|
|
||||||
// Initialize execution steps from automation
|
// Initialize execution steps from automation
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (automation?.operations) {
|
if (automation?.operations) {
|
||||||
const steps = automation.operations.map((op: any, index: number) => {
|
const steps = automation.operations.map((op: any, index: number) => {
|
||||||
const tool = toolRegistry[op.operation as keyof typeof toolRegistry];
|
const tool = toolRegistry[op.operation as keyof typeof toolRegistry];
|
||||||
@@ -47,7 +47,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio
|
|||||||
}, [automation, toolRegistry]);
|
}, [automation, toolRegistry]);
|
||||||
|
|
||||||
// Cleanup when component unmounts
|
// Cleanup when component unmounts
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
// Reset progress state when component unmounts
|
// Reset progress state when component unmounts
|
||||||
setExecutionSteps([]);
|
setExecutionSteps([]);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useSuggestedAutomations } from "../../../hooks/tools/automate/useSugges
|
|||||||
import { AutomationConfig, SuggestedAutomation } from "../../../types/automation";
|
import { AutomationConfig, SuggestedAutomation } from "../../../types/automation";
|
||||||
import { iconMap } from './iconMap';
|
import { iconMap } from './iconMap';
|
||||||
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
||||||
|
import { ToolId } from '../../../types/toolId';
|
||||||
|
|
||||||
interface AutomationSelectionProps {
|
interface AutomationSelectionProps {
|
||||||
savedAutomations: AutomationConfig[];
|
savedAutomations: AutomationConfig[];
|
||||||
@@ -15,7 +16,7 @@ interface AutomationSelectionProps {
|
|||||||
onEdit: (automation: AutomationConfig) => void;
|
onEdit: (automation: AutomationConfig) => void;
|
||||||
onDelete: (automation: AutomationConfig) => void;
|
onDelete: (automation: AutomationConfig) => void;
|
||||||
onCopyFromSuggested: (automation: SuggestedAutomation) => void;
|
onCopyFromSuggested: (automation: SuggestedAutomation) => void;
|
||||||
toolRegistry: Record<string, ToolRegistryEntry>;
|
toolRegistry: Record<ToolId, ToolRegistryEntry>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationSelection({
|
export default function AutomationSelection({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Alert
|
Alert
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { Z_INDEX_AUTOMATE_MODAL } from '../../../styles/zIndex';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
@@ -103,6 +104,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel,
|
|||||||
}
|
}
|
||||||
size="lg"
|
size="lg"
|
||||||
centered
|
centered
|
||||||
|
zIndex={Z_INDEX_AUTOMATE_MODAL}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import CloseIcon from "@mui/icons-material/Close";
|
|||||||
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
|
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
|
||||||
import { AutomationTool } from "../../../types/automation";
|
import { AutomationTool } from "../../../types/automation";
|
||||||
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
|
import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
|
||||||
|
import { ToolId } from "../../../types/toolId";
|
||||||
import ToolSelector from "./ToolSelector";
|
import ToolSelector from "./ToolSelector";
|
||||||
import AutomationEntry from "./AutomationEntry";
|
import AutomationEntry from "./AutomationEntry";
|
||||||
|
|
||||||
interface ToolListProps {
|
interface ToolListProps {
|
||||||
tools: AutomationTool[];
|
tools: AutomationTool[];
|
||||||
toolRegistry: Record<string, ToolRegistryEntry>;
|
toolRegistry: Record<ToolId, ToolRegistryEntry>;
|
||||||
onToolUpdate: (index: number, updates: Partial<AutomationTool>) => void;
|
onToolUpdate: (index: number, updates: Partial<AutomationTool>) => void;
|
||||||
onToolRemove: (index: number) => void;
|
onToolRemove: (index: number) => void;
|
||||||
onToolConfigure: (index: number) => void;
|
onToolConfigure: (index: number) => void;
|
||||||
@@ -34,7 +35,7 @@ export default function ToolList({
|
|||||||
|
|
||||||
const handleToolSelect = (index: number, newOperation: string) => {
|
const handleToolSelect = (index: number, newOperation: string) => {
|
||||||
const defaultParams = getToolDefaultParameters(newOperation);
|
const defaultParams = getToolDefaultParameters(newOperation);
|
||||||
const toolEntry = toolRegistry[newOperation];
|
const toolEntry = toolRegistry[newOperation as ToolId];
|
||||||
// If tool has no settingsComponent, it's automatically configured
|
// If tool has no settingsComponent, it's automatically configured
|
||||||
const isConfigured = !toolEntry?.automationSettings;
|
const isConfigured = !toolEntry?.automationSettings;
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { useToolSections } from '../../../hooks/useToolSections';
|
|||||||
import { renderToolButtons } from '../shared/renderToolButtons';
|
import { renderToolButtons } from '../shared/renderToolButtons';
|
||||||
import ToolSearch from '../toolPicker/ToolSearch';
|
import ToolSearch from '../toolPicker/ToolSearch';
|
||||||
import ToolButton from '../toolPicker/ToolButton';
|
import ToolButton from '../toolPicker/ToolButton';
|
||||||
import { ToolId } from 'src/types/toolId';
|
import { ToolId } from '../../../types/toolId';
|
||||||
|
|
||||||
interface ToolSelectorProps {
|
interface ToolSelectorProps {
|
||||||
onSelect: (toolKey: string) => void;
|
onSelect: (toolKey: string) => void;
|
||||||
excludeTools?: string[];
|
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
|
selectedValue?: string; // For showing current selection when editing existing tool
|
||||||
placeholder?: string; // Custom placeholder text
|
placeholder?: string; // Custom placeholder text
|
||||||
}
|
}
|
||||||
@@ -54,16 +54,16 @@ export default function ToolSelector({
|
|||||||
|
|
||||||
// Create filtered tool registry for ToolSearch
|
// Create filtered tool registry for ToolSearch
|
||||||
const filteredToolRegistry = useMemo(() => {
|
const filteredToolRegistry = useMemo(() => {
|
||||||
const registry: Record<string, ToolRegistryEntry> = {};
|
const registry: Record<ToolId, ToolRegistryEntry> = {} as Record<ToolId, ToolRegistryEntry>;
|
||||||
baseFilteredTools.forEach(([key, tool]) => {
|
baseFilteredTools.forEach(([key, tool]) => {
|
||||||
registry[key] = tool;
|
registry[key as ToolId] = tool;
|
||||||
});
|
});
|
||||||
return registry;
|
return registry;
|
||||||
}, [baseFilteredTools]);
|
}, [baseFilteredTools]);
|
||||||
|
|
||||||
// Transform filteredTools to the expected format for useToolSections
|
// Transform filteredTools to the expected format for useToolSections
|
||||||
const transformedFilteredTools = useMemo(() => {
|
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]);
|
}, [filteredTools]);
|
||||||
|
|
||||||
// Use the same tool sections logic as the main ToolPicker
|
// Use the same tool sections logic as the main ToolPicker
|
||||||
@@ -89,7 +89,7 @@ export default function ToolSelector({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the "all" section which contains all tools without duplicates
|
// 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 || [];
|
return allSection?.subcategories || [];
|
||||||
}, [isSearching, searchGroups, sections, baseFilteredTools]);
|
}, [isSearching, searchGroups, sections, baseFilteredTools]);
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ export default function ToolSelector({
|
|||||||
|
|
||||||
const renderedTools = useMemo(() =>
|
const renderedTools = useMemo(() =>
|
||||||
displayGroups.map((subcategory) =>
|
displayGroups.map((subcategory) =>
|
||||||
renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching, true)
|
renderToolButtons(t, subcategory as any, null, handleToolSelect, !isSearching, true)
|
||||||
), [displayGroups, handleToolSelect, isSearching, t]
|
), [displayGroups, handleToolSelect, isSearching, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -143,8 +143,8 @@ export default function ToolSelector({
|
|||||||
|
|
||||||
// Get display value for selected tool
|
// Get display value for selected tool
|
||||||
const getDisplayValue = () => {
|
const getDisplayValue = () => {
|
||||||
if (selectedValue && toolRegistry[selectedValue]) {
|
if (selectedValue && toolRegistry[selectedValue as ToolId]) {
|
||||||
return toolRegistry[selectedValue].name;
|
return toolRegistry[selectedValue as ToolId].name;
|
||||||
}
|
}
|
||||||
return placeholder || t('automate.creation.tools.add', 'Add a tool...');
|
return placeholder || t('automate.creation.tools.add', 'Add a tool...');
|
||||||
};
|
};
|
||||||
@@ -153,11 +153,11 @@ export default function ToolSelector({
|
|||||||
<div ref={containerRef} className='rounded-xl'>
|
<div ref={containerRef} className='rounded-xl'>
|
||||||
{/* Always show the target - either selected tool or search input */}
|
{/* 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
|
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
|
||||||
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
||||||
borderRadius: "var(--mantine-radius-lg)" }}>
|
borderRadius: "var(--mantine-radius-lg)" }}>
|
||||||
<ToolButton id={'tool' as any /* FIX ME */} tool={toolRegistry[selectedValue]} isSelected={false}
|
<ToolButton id={'tool' as ToolId} tool={toolRegistry[selectedValue as ToolId]} isSelected={false}
|
||||||
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
|
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Tooltip } from '../../shared/Tooltip';
|
||||||
|
import HotkeyDisplay from '../../hotkeys/HotkeyDisplay';
|
||||||
|
import FavoriteStar from '../toolPicker/FavoriteStar';
|
||||||
|
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 && (
|
||||||
|
<div className="tool-panel__fullscreen-star-compact">
|
||||||
|
<FavoriteStar
|
||||||
|
isFavorite={isFav}
|
||||||
|
onToggle={toggleFavorite}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Text } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import HotkeyDisplay from '../../hotkeys/HotkeyDisplay';
|
||||||
|
import FavoriteStar from '../toolPicker/FavoriteStar';
|
||||||
|
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 && (
|
||||||
|
<div className="tool-panel__fullscreen-star">
|
||||||
|
<FavoriteStar
|
||||||
|
isFavorite={isFav}
|
||||||
|
onToggle={toggleFavorite}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DetailedToolItem;
|
||||||
|
|
||||||
|
|
||||||
@@ -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 as ToolId];
|
||||||
|
const disabled = isToolDisabled(id, tool);
|
||||||
|
|
||||||
|
return {
|
||||||
|
binding,
|
||||||
|
isFav,
|
||||||
|
toggleFavorite: () => toggleFavorite(id as ToolId),
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +15,8 @@ export const renderToolButtons = (
|
|||||||
onSelect: (id: ToolId) => void,
|
onSelect: (id: ToolId) => void,
|
||||||
showSubcategoryHeader: boolean = true,
|
showSubcategoryHeader: boolean = true,
|
||||||
disableNavigation: boolean = false,
|
disableNavigation: boolean = false,
|
||||||
searchResults?: Array<{ item: [string, any]; matchedText?: string }>
|
searchResults?: Array<{ item: [string, any]; matchedText?: string }>,
|
||||||
|
hasStars: boolean = false
|
||||||
) => {
|
) => {
|
||||||
// Create a map of matched text for quick lookup
|
// Create a map of matched text for quick lookup
|
||||||
const matchedTextMap = new Map<string, string>();
|
const matchedTextMap = new Map<string, string>();
|
||||||
@@ -43,6 +44,7 @@ export const renderToolButtons = (
|
|||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
disableNavigation={disableNavigation}
|
disableNavigation={disableNavigation}
|
||||||
matchedSynonym={matchedSynonym}
|
matchedSynonym={matchedSynonym}
|
||||||
|
hasStars={hasStars}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ActionIcon } from "@mantine/core";
|
||||||
|
import type { MantineSize } 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?: MantineSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
@@ -9,7 +9,9 @@ import { handleUnlessSpecialClick } from "../../../utils/clickHandlers";
|
|||||||
import FitText from "../../shared/FitText";
|
import FitText from "../../shared/FitText";
|
||||||
import { useHotkeys } from "../../../contexts/HotkeyContext";
|
import { useHotkeys } from "../../../contexts/HotkeyContext";
|
||||||
import HotkeyDisplay from "../../hotkeys/HotkeyDisplay";
|
import HotkeyDisplay from "../../hotkeys/HotkeyDisplay";
|
||||||
import { ToolId } from "src/types/toolId";
|
import FavoriteStar from "./FavoriteStar";
|
||||||
|
import { useToolWorkflow } from "../../../contexts/ToolWorkflowContext";
|
||||||
|
import { ToolId } from "../../../types/toolId";
|
||||||
|
|
||||||
interface ToolButtonProps {
|
interface ToolButtonProps {
|
||||||
id: ToolId;
|
id: ToolId;
|
||||||
@@ -19,15 +21,18 @@ interface ToolButtonProps {
|
|||||||
rounded?: boolean;
|
rounded?: boolean;
|
||||||
disableNavigation?: boolean;
|
disableNavigation?: boolean;
|
||||||
matchedSynonym?: string;
|
matchedSynonym?: string;
|
||||||
|
hasStars?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym }) => {
|
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym, hasStars = false }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
// Special case: read and multiTool are navigational tools that are always available
|
// Special case: read and multiTool are navigational tools that are always available
|
||||||
const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
|
const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
|
||||||
const { hotkeys } = useHotkeys();
|
const { hotkeys } = useHotkeys();
|
||||||
const binding = hotkeys[id];
|
const binding = hotkeys[id];
|
||||||
const { getToolNavigation } = useToolNavigation();
|
const { getToolNavigation } = useToolNavigation();
|
||||||
|
const { isFavorite, toggleFavorite } = useToolWorkflow();
|
||||||
|
const fav = isFavorite(id as ToolId);
|
||||||
|
|
||||||
const handleClick = (id: ToolId) => {
|
const handleClick = (id: ToolId) => {
|
||||||
if (isUnavailable) return;
|
if (isUnavailable) return;
|
||||||
@@ -108,7 +113,11 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
|||||||
justify="flex-start"
|
justify="flex-start"
|
||||||
className="tool-button"
|
className="tool-button"
|
||||||
styles={{
|
styles={{
|
||||||
root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", overflow: 'visible' },
|
root: {
|
||||||
|
borderRadius: 0,
|
||||||
|
color: "var(--tools-text-and-icon-color)",
|
||||||
|
overflow: 'visible'
|
||||||
|
},
|
||||||
label: { overflow: 'visible' }
|
label: { overflow: 'visible' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -129,7 +138,11 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
|||||||
justify="flex-start"
|
justify="flex-start"
|
||||||
className="tool-button"
|
className="tool-button"
|
||||||
styles={{
|
styles={{
|
||||||
root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", overflow: 'visible' },
|
root: {
|
||||||
|
borderRadius: 0,
|
||||||
|
color: "var(--tools-text-and-icon-color)",
|
||||||
|
overflow: 'visible'
|
||||||
|
},
|
||||||
label: { overflow: 'visible' }
|
label: { overflow: 'visible' }
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -146,16 +159,36 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
|||||||
justify="flex-start"
|
justify="flex-start"
|
||||||
className="tool-button"
|
className="tool-button"
|
||||||
aria-disabled={isUnavailable}
|
aria-disabled={isUnavailable}
|
||||||
styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)", cursor: isUnavailable ? 'not-allowed' : undefined, overflow: 'visible' }, label: { overflow: 'visible' } }}
|
styles={{
|
||||||
|
root: {
|
||||||
|
borderRadius: 0,
|
||||||
|
color: "var(--tools-text-and-icon-color)",
|
||||||
|
cursor: isUnavailable ? 'not-allowed' : undefined,
|
||||||
|
overflow: 'visible'
|
||||||
|
},
|
||||||
|
label: { overflow: 'visible' }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{buttonContent}
|
{buttonContent}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const star = hasStars && !isUnavailable ? (
|
||||||
|
<FavoriteStar
|
||||||
|
isFavorite={fav}
|
||||||
|
onToggle={() => toggleFavorite(id as ToolId)}
|
||||||
|
className="tool-button-star"
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="tool-button-container">
|
||||||
|
{star}
|
||||||
<Tooltip content={tooltipContent} position="right" arrow={true} delay={500}>
|
<Tooltip content={tooltipContent} position="right" arrow={true} delay={500}>
|
||||||
{buttonElement}
|
{buttonElement}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,24 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Container to enable hover-only favourite star overlay */
|
||||||
|
.tool-button-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-button-star {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.35rem;
|
||||||
|
right: 0.35rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
z-index: var(--z-toolpicker-star); /* lower than sticky section headers */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-button-container:hover .tool-button-star {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.search-input-container {
|
.search-input-container {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import { ToolRegistryEntry } from "../../../data/toolsTaxonomy";
|
|||||||
import { TextInput } from "../../shared/TextInput";
|
import { TextInput } from "../../shared/TextInput";
|
||||||
import "./ToolPicker.css";
|
import "./ToolPicker.css";
|
||||||
import { rankByFuzzy, idToWords } from "../../../utils/fuzzySearch";
|
import { rankByFuzzy, idToWords } from "../../../utils/fuzzySearch";
|
||||||
|
import { ToolId } from "src/types/toolId";
|
||||||
|
|
||||||
interface ToolSearchProps {
|
interface ToolSearchProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
toolRegistry: Readonly<Record<string, ToolRegistryEntry>>;
|
toolRegistry: Partial<Record<ToolId, ToolRegistryEntry>>;
|
||||||
onToolSelect?: (toolId: string) => void;
|
onToolSelect?: (toolId: ToolId) => void;
|
||||||
mode: "filter" | "dropdown" | "unstyled";
|
mode: "filter" | "dropdown" | "unstyled";
|
||||||
selectedToolKey?: string | null;
|
selectedToolKey?: string | null;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -126,7 +127,7 @@ const ToolSearch = ({
|
|||||||
key={id}
|
key={id}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onToolSelect?.(id);
|
onToolSelect?.(id as ToolId);
|
||||||
setDropdownOpen(false);
|
setDropdownOpen(false);
|
||||||
}}
|
}}
|
||||||
leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}
|
leftSection={<div style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
}, [previewFile, fileWithUrl]);
|
}, [previewFile, fileWithUrl]);
|
||||||
|
|
||||||
// Handle scroll wheel zoom with accumulator for smooth trackpad pinch
|
// Handle scroll wheel zoom with accumulator for smooth trackpad pinch
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
let accumulator = 0;
|
let accumulator = 0;
|
||||||
|
|
||||||
const handleWheel = (event: WheelEvent) => {
|
const handleWheel = (event: WheelEvent) => {
|
||||||
@@ -128,7 +128,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
}, [zoomActions]);
|
}, [zoomActions]);
|
||||||
|
|
||||||
// Handle keyboard zoom shortcuts
|
// Handle keyboard zoom shortcuts
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (!isViewerHovered) return;
|
if (!isViewerHovered) return;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Central list of formats supported by Convert operations
|
||||||
|
export const CONVERT_SUPPORTED_FORMATS = [
|
||||||
|
// Microsoft Office
|
||||||
|
'doc', 'docx', 'dot', 'dotx', 'csv', 'xls', 'xlsx', 'xlt', 'xltx', 'slk', 'dif', 'ppt', 'pptx',
|
||||||
|
// OpenDocument
|
||||||
|
'odt', 'ott', 'ods', 'ots', 'odp', 'otp', 'odg', 'otg',
|
||||||
|
// Text formats
|
||||||
|
'txt', 'text', 'xml', 'rtf', 'html', 'lwp', 'md',
|
||||||
|
// Images
|
||||||
|
'bmp', 'gif', 'jpeg', 'jpg', 'png', 'tif', 'tiff', 'pbm', 'pgm', 'ppm', 'ras', 'xbm', 'xpm', 'svg', 'svm', 'wmf', 'webp',
|
||||||
|
// StarOffice
|
||||||
|
'sda', 'sdc', 'sdd', 'sdw', 'stc', 'std', 'sti', 'stw', 'sxd', 'sxg', 'sxi', 'sxw',
|
||||||
|
// Email formats
|
||||||
|
'eml',
|
||||||
|
// Archive formats
|
||||||
|
'zip',
|
||||||
|
// Other
|
||||||
|
'dbf', 'fods', 'vsd', 'vor', 'vor3', 'vor4', 'uop', 'pct', 'ps', 'pdf',
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
@@ -4,9 +4,11 @@ import { RightRailAction, RightRailButtonConfig } from '../types/rightRail';
|
|||||||
interface RightRailContextValue {
|
interface RightRailContextValue {
|
||||||
buttons: RightRailButtonConfig[];
|
buttons: RightRailButtonConfig[];
|
||||||
actions: Record<string, RightRailAction>;
|
actions: Record<string, RightRailAction>;
|
||||||
|
allButtonsDisabled: boolean;
|
||||||
registerButtons: (buttons: RightRailButtonConfig[]) => void;
|
registerButtons: (buttons: RightRailButtonConfig[]) => void;
|
||||||
unregisterButtons: (ids: string[]) => void;
|
unregisterButtons: (ids: string[]) => void;
|
||||||
setAction: (id: string, action: RightRailAction) => void;
|
setAction: (id: string, action: RightRailAction) => void;
|
||||||
|
setAllRightRailButtonsDisabled: (disabled: boolean) => void;
|
||||||
clear: () => void;
|
clear: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ const RightRailContext = createContext<RightRailContextValue | undefined>(undefi
|
|||||||
export function RightRailProvider({ children }: { children: React.ReactNode }) {
|
export function RightRailProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [buttons, setButtons] = useState<RightRailButtonConfig[]>([]);
|
const [buttons, setButtons] = useState<RightRailButtonConfig[]>([]);
|
||||||
const [actions, setActions] = useState<Record<string, RightRailAction>>({});
|
const [actions, setActions] = useState<Record<string, RightRailAction>>({});
|
||||||
|
const [allButtonsDisabled, setAllButtonsDisabled] = useState<boolean>(false);
|
||||||
|
|
||||||
const registerButtons = useCallback((newButtons: RightRailButtonConfig[]) => {
|
const registerButtons = useCallback((newButtons: RightRailButtonConfig[]) => {
|
||||||
setButtons(prev => {
|
setButtons(prev => {
|
||||||
@@ -43,12 +46,25 @@ export function RightRailProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setActions(prev => ({ ...prev, [id]: action }));
|
setActions(prev => ({ ...prev, [id]: action }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setAllRightRailButtonsDisabled = useCallback((disabled: boolean) => {
|
||||||
|
setAllButtonsDisabled(disabled);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const clear = useCallback(() => {
|
const clear = useCallback(() => {
|
||||||
setButtons([]);
|
setButtons([]);
|
||||||
setActions({});
|
setActions({});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo<RightRailContextValue>(() => ({ buttons, actions, registerButtons, unregisterButtons, setAction, clear }), [buttons, actions, registerButtons, unregisterButtons, setAction, clear]);
|
const value = useMemo<RightRailContextValue>(() => ({
|
||||||
|
buttons,
|
||||||
|
actions,
|
||||||
|
allButtonsDisabled,
|
||||||
|
registerButtons,
|
||||||
|
unregisterButtons,
|
||||||
|
setAction,
|
||||||
|
setAllRightRailButtonsDisabled,
|
||||||
|
clear
|
||||||
|
}), [buttons, actions, allButtonsDisabled, registerButtons, unregisterButtons, setAction, setAllRightRailButtonsDisabled, clear]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RightRailContext.Provider value={value}>
|
<RightRailContext.Provider value={value}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export function SidebarProvider({ children }: SidebarProviderProps) {
|
|||||||
// All sidebar state management
|
// All sidebar state management
|
||||||
const quickAccessRef = useRef<HTMLDivElement>(null);
|
const quickAccessRef = useRef<HTMLDivElement>(null);
|
||||||
const toolPanelRef = useRef<HTMLDivElement>(null);
|
const toolPanelRef = useRef<HTMLDivElement>(null);
|
||||||
|
const rightRailRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
||||||
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
||||||
@@ -21,7 +22,8 @@ export function SidebarProvider({ children }: SidebarProviderProps) {
|
|||||||
const sidebarRefs: SidebarRefs = useMemo(() => ({
|
const sidebarRefs: SidebarRefs = useMemo(() => ({
|
||||||
quickAccessRef,
|
quickAccessRef,
|
||||||
toolPanelRef,
|
toolPanelRef,
|
||||||
}), [quickAccessRef, toolPanelRef]);
|
rightRailRef,
|
||||||
|
}), [quickAccessRef, toolPanelRef, rightRailRef]);
|
||||||
|
|
||||||
const contextValue: SidebarContextValue = useMemo(() => ({
|
const contextValue: SidebarContextValue = useMemo(() => ({
|
||||||
sidebarState,
|
sidebarState,
|
||||||
|
|||||||
@@ -3,72 +3,26 @@
|
|||||||
* Eliminates prop drilling with a single, simple context
|
* Eliminates prop drilling with a single, simple context
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
|
import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect } from 'react';
|
||||||
import { useToolManagement } from '../hooks/useToolManagement';
|
import { useToolManagement } from '../hooks/useToolManagement';
|
||||||
import { PageEditorFunctions } from '../types/pageEditor';
|
import { PageEditorFunctions } from '../types/pageEditor';
|
||||||
import { ToolRegistryEntry, ToolRegistry } from '../data/toolsTaxonomy';
|
import { ToolRegistryEntry, ToolRegistry } from '../data/toolsTaxonomy';
|
||||||
import { useNavigationActions, useNavigationState } from './NavigationContext';
|
import { useNavigationActions, useNavigationState } from './NavigationContext';
|
||||||
import { ToolId, isValidToolId } from '../types/toolId';
|
import { ToolId, isValidToolId } from '../types/toolId';
|
||||||
import { useNavigationUrlSync } from '../hooks/useUrlSync';
|
|
||||||
import { getDefaultWorkbench } from '../types/workbench';
|
import { getDefaultWorkbench } from '../types/workbench';
|
||||||
import { filterToolRegistryByQuery } from '../utils/toolSearch';
|
import { filterToolRegistryByQuery } from '../utils/toolSearch';
|
||||||
|
import { useToolHistory } from '../hooks/tools/useUserToolActivity';
|
||||||
|
import {
|
||||||
|
ToolWorkflowState,
|
||||||
|
TOOL_PANEL_MODE_STORAGE_KEY,
|
||||||
|
createInitialState,
|
||||||
|
toolWorkflowReducer,
|
||||||
|
ToolPanelMode,
|
||||||
|
} from './toolWorkflow/toolWorkflowState';
|
||||||
|
import { usePreferences } from './PreferencesContext';
|
||||||
|
|
||||||
// State interface
|
// State interface
|
||||||
interface ToolWorkflowState {
|
// Types and reducer/state moved to './toolWorkflow/state'
|
||||||
// UI State
|
|
||||||
sidebarsVisible: boolean;
|
|
||||||
leftPanelView: 'toolPicker' | 'toolContent' | 'hidden';
|
|
||||||
readerMode: boolean;
|
|
||||||
|
|
||||||
// File/Preview State
|
|
||||||
previewFile: File | null;
|
|
||||||
pageEditorFunctions: PageEditorFunctions | null;
|
|
||||||
|
|
||||||
// Search State
|
|
||||||
searchQuery: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
type ToolWorkflowAction =
|
|
||||||
| { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean }
|
|
||||||
| { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' | 'hidden' }
|
|
||||||
| { type: 'SET_READER_MODE'; payload: boolean }
|
|
||||||
| { type: 'SET_PREVIEW_FILE'; payload: File | null }
|
|
||||||
| { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null }
|
|
||||||
| { type: 'SET_SEARCH_QUERY'; payload: string }
|
|
||||||
| { type: 'RESET_UI_STATE' };
|
|
||||||
|
|
||||||
// Initial state
|
|
||||||
const initialState: ToolWorkflowState = {
|
|
||||||
sidebarsVisible: true,
|
|
||||||
leftPanelView: 'toolPicker',
|
|
||||||
readerMode: false,
|
|
||||||
previewFile: null,
|
|
||||||
pageEditorFunctions: null,
|
|
||||||
searchQuery: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reducer
|
|
||||||
function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'SET_SIDEBARS_VISIBLE':
|
|
||||||
return { ...state, sidebarsVisible: action.payload };
|
|
||||||
case 'SET_LEFT_PANEL_VIEW':
|
|
||||||
return { ...state, leftPanelView: action.payload };
|
|
||||||
case 'SET_READER_MODE':
|
|
||||||
return { ...state, readerMode: action.payload };
|
|
||||||
case 'SET_PREVIEW_FILE':
|
|
||||||
return { ...state, previewFile: action.payload };
|
|
||||||
case 'SET_PAGE_EDITOR_FUNCTIONS':
|
|
||||||
return { ...state, pageEditorFunctions: action.payload };
|
|
||||||
case 'SET_SEARCH_QUERY':
|
|
||||||
return { ...state, searchQuery: action.payload };
|
|
||||||
case 'RESET_UI_STATE':
|
|
||||||
return { ...initialState, searchQuery: state.searchQuery }; // Preserve search
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context value interface
|
// Context value interface
|
||||||
interface ToolWorkflowContextValue extends ToolWorkflowState {
|
interface ToolWorkflowContextValue extends ToolWorkflowState {
|
||||||
@@ -82,11 +36,12 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
|||||||
setSidebarsVisible: (visible: boolean) => void;
|
setSidebarsVisible: (visible: boolean) => void;
|
||||||
setLeftPanelView: (view: 'toolPicker' | 'toolContent' | 'hidden') => void;
|
setLeftPanelView: (view: 'toolPicker' | 'toolContent' | 'hidden') => void;
|
||||||
setReaderMode: (mode: boolean) => void;
|
setReaderMode: (mode: boolean) => void;
|
||||||
|
setToolPanelMode: (mode: ToolPanelMode) => void;
|
||||||
setPreviewFile: (file: File | null) => void;
|
setPreviewFile: (file: File | null) => void;
|
||||||
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
|
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
|
||||||
setSearchQuery: (query: string) => void;
|
setSearchQuery: (query: string) => void;
|
||||||
|
|
||||||
// Tool Actions
|
|
||||||
selectTool: (toolId: ToolId | null) => void;
|
selectTool: (toolId: ToolId | null) => void;
|
||||||
clearToolSelection: () => void;
|
clearToolSelection: () => void;
|
||||||
|
|
||||||
@@ -103,6 +58,11 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
|||||||
// Computed values
|
// Computed values
|
||||||
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
|
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
|
||||||
isPanelVisible: boolean;
|
isPanelVisible: boolean;
|
||||||
|
|
||||||
|
// Tool History
|
||||||
|
favoriteTools: ToolId[];
|
||||||
|
toggleFavorite: (toolId: ToolId) => void;
|
||||||
|
isFavorite: (toolId: ToolId) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToolWorkflowContext = createContext<ToolWorkflowContextValue | undefined>(undefined);
|
const ToolWorkflowContext = createContext<ToolWorkflowContextValue | undefined>(undefined);
|
||||||
@@ -113,7 +73,8 @@ interface ToolWorkflowProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||||
const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
|
const [state, dispatch] = useReducer(toolWorkflowReducer, undefined, createInitialState);
|
||||||
|
const { preferences } = usePreferences();
|
||||||
|
|
||||||
// Store reset functions for tools
|
// Store reset functions for tools
|
||||||
const [toolResetFunctions, setToolResetFunctions] = React.useState<Record<string, () => void>>({});
|
const [toolResetFunctions, setToolResetFunctions] = React.useState<Record<string, () => void>>({});
|
||||||
@@ -128,6 +89,13 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
getSelectedTool,
|
getSelectedTool,
|
||||||
} = useToolManagement();
|
} = useToolManagement();
|
||||||
|
|
||||||
|
// Tool history hook
|
||||||
|
const {
|
||||||
|
favoriteTools,
|
||||||
|
toggleFavorite,
|
||||||
|
isFavorite,
|
||||||
|
} = useToolHistory();
|
||||||
|
|
||||||
// Get selected tool from navigation context
|
// Get selected tool from navigation context
|
||||||
const selectedTool = getSelectedTool(navigationState.selectedTool);
|
const selectedTool = getSelectedTool(navigationState.selectedTool);
|
||||||
|
|
||||||
@@ -148,6 +116,11 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
dispatch({ type: 'SET_READER_MODE', payload: mode });
|
dispatch({ type: 'SET_READER_MODE', payload: mode });
|
||||||
}, [actions]);
|
}, [actions]);
|
||||||
|
|
||||||
|
const setToolPanelMode = useCallback((mode: ToolPanelMode) => {
|
||||||
|
dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: mode });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const setPreviewFile = useCallback((file: File | null) => {
|
const setPreviewFile = useCallback((file: File | null) => {
|
||||||
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
|
dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -163,6 +136,28 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
|
dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(TOOL_PANEL_MODE_STORAGE_KEY, state.toolPanelMode);
|
||||||
|
}, [state.toolPanelMode]);
|
||||||
|
|
||||||
|
// Keep tool panel mode in sync with user preference. This ensures the
|
||||||
|
// Config setting (Default tool picker mode) immediately affects the app
|
||||||
|
// and persists across reloads.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!preferences) return;
|
||||||
|
const preferredMode = preferences.defaultToolPanelMode;
|
||||||
|
if (preferredMode && preferredMode !== state.toolPanelMode) {
|
||||||
|
dispatch({ type: 'SET_TOOL_PANEL_MODE', payload: preferredMode });
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.localStorage.setItem(TOOL_PANEL_MODE_STORAGE_KEY, preferredMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [preferences.defaultToolPanelMode]);
|
||||||
|
|
||||||
// Tool reset methods
|
// Tool reset methods
|
||||||
const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => {
|
const registerToolReset = useCallback((toolId: string, resetFunction: () => void) => {
|
||||||
setToolResetFunctions(prev => ({ ...prev, [toolId]: resetFunction }));
|
setToolResetFunctions(prev => ({ ...prev, [toolId]: resetFunction }));
|
||||||
@@ -190,7 +185,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle multiTool selection - enable page editor workbench and hide left panel
|
// Handle multiTool selection - enable page editor workbench
|
||||||
if (toolId === 'multiTool') {
|
if (toolId === 'multiTool') {
|
||||||
setReaderMode(false);
|
setReaderMode(false);
|
||||||
setLeftPanelView('hidden');
|
setLeftPanelView('hidden');
|
||||||
@@ -239,15 +234,6 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
[state.sidebarsVisible, state.readerMode, state.leftPanelView]
|
[state.sidebarsVisible, state.readerMode, state.leftPanelView]
|
||||||
);
|
);
|
||||||
|
|
||||||
// URL sync for proper tool navigation
|
|
||||||
useNavigationUrlSync(
|
|
||||||
navigationState.selectedTool,
|
|
||||||
handleToolSelect,
|
|
||||||
handleBackToTools,
|
|
||||||
toolRegistry as ToolRegistry,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
// Properly memoized context value
|
// Properly memoized context value
|
||||||
const contextValue = useMemo((): ToolWorkflowContextValue => ({
|
const contextValue = useMemo((): ToolWorkflowContextValue => ({
|
||||||
// State
|
// State
|
||||||
@@ -261,6 +247,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
setSidebarsVisible,
|
setSidebarsVisible,
|
||||||
setLeftPanelView,
|
setLeftPanelView,
|
||||||
setReaderMode,
|
setReaderMode,
|
||||||
|
setToolPanelMode,
|
||||||
setPreviewFile,
|
setPreviewFile,
|
||||||
setPageEditorFunctions,
|
setPageEditorFunctions,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
@@ -280,6 +267,11 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
// Computed
|
// Computed
|
||||||
filteredTools,
|
filteredTools,
|
||||||
isPanelVisible,
|
isPanelVisible,
|
||||||
|
|
||||||
|
// Tool History
|
||||||
|
favoriteTools,
|
||||||
|
toggleFavorite,
|
||||||
|
isFavorite,
|
||||||
}), [
|
}), [
|
||||||
state,
|
state,
|
||||||
navigationState.selectedTool,
|
navigationState.selectedTool,
|
||||||
@@ -289,6 +281,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
setSidebarsVisible,
|
setSidebarsVisible,
|
||||||
setLeftPanelView,
|
setLeftPanelView,
|
||||||
setReaderMode,
|
setReaderMode,
|
||||||
|
setToolPanelMode,
|
||||||
setPreviewFile,
|
setPreviewFile,
|
||||||
setPageEditorFunctions,
|
setPageEditorFunctions,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
@@ -300,6 +293,9 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
handleReaderToggle,
|
handleReaderToggle,
|
||||||
filteredTools,
|
filteredTools,
|
||||||
isPanelVisible,
|
isPanelVisible,
|
||||||
|
favoriteTools,
|
||||||
|
toggleFavorite,
|
||||||
|
isFavorite,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -313,7 +309,6 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
export function useToolWorkflow(): ToolWorkflowContextValue {
|
export function useToolWorkflow(): ToolWorkflowContextValue {
|
||||||
const context = useContext(ToolWorkflowContext);
|
const context = useContext(ToolWorkflowContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
|
||||||
console.error('ToolWorkflowContext not found. Current stack:', new Error().stack);
|
console.error('ToolWorkflowContext not found. Current stack:', new Error().stack);
|
||||||
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
|
throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { PageEditorFunctions } from '../../types/pageEditor';
|
||||||
|
|
||||||
|
// State & Modes
|
||||||
|
export type ToolPanelMode = 'sidebar' | 'fullscreen';
|
||||||
|
|
||||||
|
export interface ToolWorkflowState {
|
||||||
|
// UI State
|
||||||
|
sidebarsVisible: boolean;
|
||||||
|
leftPanelView: 'toolPicker' | 'toolContent' | 'hidden';
|
||||||
|
readerMode: boolean;
|
||||||
|
toolPanelMode: ToolPanelMode;
|
||||||
|
|
||||||
|
previewFile: File | null;
|
||||||
|
pageEditorFunctions: PageEditorFunctions | null;
|
||||||
|
|
||||||
|
// Search State
|
||||||
|
searchQuery: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
export type ToolWorkflowAction =
|
||||||
|
| { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean }
|
||||||
|
| { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' | 'hidden' }
|
||||||
|
| { type: 'SET_READER_MODE'; payload: boolean }
|
||||||
|
| { type: 'SET_TOOL_PANEL_MODE'; payload: ToolPanelMode }
|
||||||
|
| { type: 'SET_PREVIEW_FILE'; payload: File | null }
|
||||||
|
| { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null }
|
||||||
|
| { type: 'SET_SEARCH_QUERY'; payload: string }
|
||||||
|
| { type: 'RESET_UI_STATE' };
|
||||||
|
|
||||||
|
// Storage keys
|
||||||
|
export const TOOL_PANEL_MODE_STORAGE_KEY = 'toolPanelModePreference';
|
||||||
|
|
||||||
|
export const getStoredToolPanelMode = (): ToolPanelMode => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return 'sidebar';
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored = window.localStorage.getItem(TOOL_PANEL_MODE_STORAGE_KEY);
|
||||||
|
if (stored === 'fullscreen') {
|
||||||
|
return 'fullscreen';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'sidebar';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const baseState: Omit<ToolWorkflowState, 'toolPanelMode'> = {
|
||||||
|
sidebarsVisible: true,
|
||||||
|
leftPanelView: 'toolPicker',
|
||||||
|
readerMode: false,
|
||||||
|
previewFile: null,
|
||||||
|
pageEditorFunctions: null,
|
||||||
|
searchQuery: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createInitialState = (): ToolWorkflowState => ({
|
||||||
|
...baseState,
|
||||||
|
toolPanelMode: getStoredToolPanelMode(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_SIDEBARS_VISIBLE':
|
||||||
|
return { ...state, sidebarsVisible: action.payload };
|
||||||
|
case 'SET_LEFT_PANEL_VIEW':
|
||||||
|
return { ...state, leftPanelView: action.payload };
|
||||||
|
case 'SET_READER_MODE':
|
||||||
|
return { ...state, readerMode: action.payload };
|
||||||
|
case 'SET_TOOL_PANEL_MODE':
|
||||||
|
return { ...state, toolPanelMode: action.payload };
|
||||||
|
case 'SET_PREVIEW_FILE':
|
||||||
|
return { ...state, previewFile: action.payload };
|
||||||
|
case 'SET_PAGE_EDITOR_FUNCTIONS':
|
||||||
|
return { ...state, pageEditorFunctions: action.payload };
|
||||||
|
case 'SET_SEARCH_QUERY':
|
||||||
|
return { ...state, searchQuery: action.payload };
|
||||||
|
case 'RESET_UI_STATE':
|
||||||
|
return {
|
||||||
|
...baseState,
|
||||||
|
toolPanelMode: state.toolPanelMode,
|
||||||
|
searchQuery: state.searchQuery,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -4,6 +4,17 @@ import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
|
|||||||
import { BaseToolProps } from '../types/tool';
|
import { BaseToolProps } from '../types/tool';
|
||||||
import { WorkbenchType } from '../types/workbench';
|
import { WorkbenchType } from '../types/workbench';
|
||||||
import { ToolId } from '../types/toolId';
|
import { ToolId } from '../types/toolId';
|
||||||
|
import DrawRoundedIcon from '@mui/icons-material/DrawRounded';
|
||||||
|
import SecurityRoundedIcon from '@mui/icons-material/SecurityRounded';
|
||||||
|
import VerifiedUserRoundedIcon from '@mui/icons-material/VerifiedUserRounded';
|
||||||
|
import RateReviewRoundedIcon from '@mui/icons-material/RateReviewRounded';
|
||||||
|
import ViewAgendaRoundedIcon from '@mui/icons-material/ViewAgendaRounded';
|
||||||
|
import FileDownloadRoundedIcon from '@mui/icons-material/FileDownloadRounded';
|
||||||
|
import DeleteSweepRoundedIcon from '@mui/icons-material/DeleteSweepRounded';
|
||||||
|
import SmartToyRoundedIcon from '@mui/icons-material/SmartToyRounded';
|
||||||
|
import BuildRoundedIcon from '@mui/icons-material/BuildRounded';
|
||||||
|
import TuneRoundedIcon from '@mui/icons-material/TuneRounded';
|
||||||
|
import CodeRoundedIcon from '@mui/icons-material/CodeRounded';
|
||||||
|
|
||||||
export enum SubcategoryId {
|
export enum SubcategoryId {
|
||||||
SIGNING = 'signing',
|
SIGNING = 'signing',
|
||||||
@@ -66,17 +77,46 @@ export const SUBCATEGORY_ORDER: SubcategoryId[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const SUBCATEGORY_COLOR_MAP: Record<SubcategoryId, string> = {
|
export const SUBCATEGORY_COLOR_MAP: Record<SubcategoryId, string> = {
|
||||||
[SubcategoryId.SIGNING]: '#FF7892',
|
[SubcategoryId.SIGNING]: 'var(--category-color-signing)', // Green
|
||||||
[SubcategoryId.DOCUMENT_SECURITY]: '#FF7892',
|
[SubcategoryId.DOCUMENT_SECURITY]: 'var(--category-color-security)', // Orange
|
||||||
[SubcategoryId.VERIFICATION]: '#1BB1D4',
|
[SubcategoryId.VERIFICATION]: 'var(--category-color-verification)', // Orange
|
||||||
[SubcategoryId.DOCUMENT_REVIEW]: '#48BD54',
|
[SubcategoryId.DOCUMENT_REVIEW]: 'var(--category-color-general)', // Blue
|
||||||
[SubcategoryId.PAGE_FORMATTING]: '#7882FF',
|
[SubcategoryId.PAGE_FORMATTING]: 'var(--category-color-formatting)', // Purple
|
||||||
[SubcategoryId.EXTRACTION]: '#1BB1D4',
|
[SubcategoryId.EXTRACTION]: 'var(--category-color-extraction)', // Cyan
|
||||||
[SubcategoryId.REMOVAL]: '#7882FF',
|
[SubcategoryId.REMOVAL]: 'var(--category-color-removal)', // Red
|
||||||
[SubcategoryId.AUTOMATION]: '#69DC95',
|
[SubcategoryId.AUTOMATION]: 'var(--category-color-automation)', // Pink
|
||||||
[SubcategoryId.GENERAL]: '#69DC95',
|
[SubcategoryId.GENERAL]: 'var(--category-color-general)', // Blue
|
||||||
[SubcategoryId.ADVANCED_FORMATTING]: '#F55454',
|
[SubcategoryId.ADVANCED_FORMATTING]: 'var(--category-color-formatting)', // Purple
|
||||||
[SubcategoryId.DEVELOPER_TOOLS]: '#F55454',
|
[SubcategoryId.DEVELOPER_TOOLS]: 'var(--category-color-developer)', // Gray
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSubcategoryIcon = (subcategory: SubcategoryId): React.ReactNode => {
|
||||||
|
switch (subcategory) {
|
||||||
|
case SubcategoryId.SIGNING:
|
||||||
|
return React.createElement(DrawRoundedIcon);
|
||||||
|
case SubcategoryId.DOCUMENT_SECURITY:
|
||||||
|
return React.createElement(SecurityRoundedIcon);
|
||||||
|
case SubcategoryId.VERIFICATION:
|
||||||
|
return React.createElement(VerifiedUserRoundedIcon);
|
||||||
|
case SubcategoryId.DOCUMENT_REVIEW:
|
||||||
|
return React.createElement(RateReviewRoundedIcon);
|
||||||
|
case SubcategoryId.PAGE_FORMATTING:
|
||||||
|
return React.createElement(ViewAgendaRoundedIcon);
|
||||||
|
case SubcategoryId.EXTRACTION:
|
||||||
|
return React.createElement(FileDownloadRoundedIcon);
|
||||||
|
case SubcategoryId.REMOVAL:
|
||||||
|
return React.createElement(DeleteSweepRoundedIcon);
|
||||||
|
case SubcategoryId.AUTOMATION:
|
||||||
|
return React.createElement(SmartToyRoundedIcon);
|
||||||
|
case SubcategoryId.GENERAL:
|
||||||
|
return React.createElement(BuildRoundedIcon);
|
||||||
|
case SubcategoryId.ADVANCED_FORMATTING:
|
||||||
|
return React.createElement(TuneRoundedIcon);
|
||||||
|
case SubcategoryId.DEVELOPER_TOOLS:
|
||||||
|
return React.createElement(CodeRoundedIcon);
|
||||||
|
default:
|
||||||
|
return React.createElement(BuildRoundedIcon);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategoryLabel = (t: TFunction, id: ToolCategoryId): string => t(`toolPicker.categories.${id}`, id);
|
export const getCategoryLabel = (t: TFunction, id: ToolCategoryId): string => t(`toolPicker.categories.${id}`, id);
|
||||||
|
|||||||
@@ -112,85 +112,7 @@ import OverlayPdfsSettings from "../components/tools/overlayPdfs/OverlayPdfsSett
|
|||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||||
|
|
||||||
// Convert tool supported file formats
|
// Convert tool supported file formats
|
||||||
export const CONVERT_SUPPORTED_FORMATS = [
|
import { CONVERT_SUPPORTED_FORMATS } from "../constants/convertSupportedFornats";
|
||||||
// Microsoft Office
|
|
||||||
"doc",
|
|
||||||
"docx",
|
|
||||||
"dot",
|
|
||||||
"dotx",
|
|
||||||
"csv",
|
|
||||||
"xls",
|
|
||||||
"xlsx",
|
|
||||||
"xlt",
|
|
||||||
"xltx",
|
|
||||||
"slk",
|
|
||||||
"dif",
|
|
||||||
"ppt",
|
|
||||||
"pptx",
|
|
||||||
// OpenDocument
|
|
||||||
"odt",
|
|
||||||
"ott",
|
|
||||||
"ods",
|
|
||||||
"ots",
|
|
||||||
"odp",
|
|
||||||
"otp",
|
|
||||||
"odg",
|
|
||||||
"otg",
|
|
||||||
// Text formats
|
|
||||||
"txt",
|
|
||||||
"text",
|
|
||||||
"xml",
|
|
||||||
"rtf",
|
|
||||||
"html",
|
|
||||||
"lwp",
|
|
||||||
"md",
|
|
||||||
// Images
|
|
||||||
"bmp",
|
|
||||||
"gif",
|
|
||||||
"jpeg",
|
|
||||||
"jpg",
|
|
||||||
"png",
|
|
||||||
"tif",
|
|
||||||
"tiff",
|
|
||||||
"pbm",
|
|
||||||
"pgm",
|
|
||||||
"ppm",
|
|
||||||
"ras",
|
|
||||||
"xbm",
|
|
||||||
"xpm",
|
|
||||||
"svg",
|
|
||||||
"svm",
|
|
||||||
"wmf",
|
|
||||||
"webp",
|
|
||||||
// StarOffice
|
|
||||||
"sda",
|
|
||||||
"sdc",
|
|
||||||
"sdd",
|
|
||||||
"sdw",
|
|
||||||
"stc",
|
|
||||||
"std",
|
|
||||||
"sti",
|
|
||||||
"stw",
|
|
||||||
"sxd",
|
|
||||||
"sxg",
|
|
||||||
"sxi",
|
|
||||||
"sxw",
|
|
||||||
// Email formats
|
|
||||||
"eml",
|
|
||||||
// Archive formats
|
|
||||||
"zip",
|
|
||||||
// Other
|
|
||||||
"dbf",
|
|
||||||
"fods",
|
|
||||||
"vsd",
|
|
||||||
"vor",
|
|
||||||
"vor3",
|
|
||||||
"vor4",
|
|
||||||
"uop",
|
|
||||||
"pct",
|
|
||||||
"ps",
|
|
||||||
"pdf",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Hook to get the translated tool registry
|
// Hook to get the translated tool registry
|
||||||
export function useFlatToolRegistry(): ToolRegistry {
|
export function useFlatToolRegistry(): ToolRegistry {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { ToolId } from '../../types/toolId';
|
||||||
|
import { ToolRegistryEntry } from '../../data/toolsTaxonomy';
|
||||||
|
|
||||||
|
export function useFavoriteToolItems(
|
||||||
|
favoriteTools: ToolId[],
|
||||||
|
toolRegistry: Partial<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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useState, useEffect, Dispatch, SetStateAction } from 'react';
|
||||||
|
|
||||||
|
export function useLocalStorageState<T>(key: string, defaultValue: T): [T, Dispatch<SetStateAction<T>>] {
|
||||||
|
const [state, setState] = useState<T>(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored = window.localStorage.getItem(key);
|
||||||
|
if (stored === null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(stored) as T;
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(state));
|
||||||
|
}, [key, state]);
|
||||||
|
|
||||||
|
return [state, setState];
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { useLayoutEffect, useState, RefObject, useRef } from 'react';
|
||||||
|
|
||||||
|
export interface ToolPanelGeometry {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
setGeometry(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelEl = toolPanelRef.current;
|
||||||
|
if (!panelEl) {
|
||||||
|
setGeometry(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rightRailEl = () => (rightRailRef?.current ?? null);
|
||||||
|
|
||||||
|
let rafId: number | null = null;
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleUpdate = () => {
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
|
rafId = requestAnimationFrame(() => {
|
||||||
|
computeAndSetGeometry();
|
||||||
|
rafId = null;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
scheduleUpdateRef.current = scheduleUpdate;
|
||||||
|
|
||||||
|
// Initial geometry calculation (no debounce)
|
||||||
|
computeAndSetGeometry();
|
||||||
|
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
|
resizeObserver = new ResizeObserver(() => scheduleUpdate());
|
||||||
|
resizeObserver.observe(panelEl);
|
||||||
|
if (quickAccessRef.current) {
|
||||||
|
resizeObserver.observe(quickAccessRef.current);
|
||||||
|
}
|
||||||
|
const rail = rightRailEl();
|
||||||
|
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 (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
|
scheduleUpdateRef.current = () => {};
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
};
|
||||||
|
}, [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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { ToolId } from '../../types/toolId';
|
||||||
|
|
||||||
|
const RECENT_TOOLS_KEY = 'stirlingpdf.recentTools';
|
||||||
|
const FAVORITE_TOOLS_KEY = 'stirlingpdf.favoriteTools';
|
||||||
|
|
||||||
|
export function useToolHistory() {
|
||||||
|
const [recentTools, setRecentTools] = useState<ToolId[]>([]);
|
||||||
|
const [favoriteTools, setFavoriteTools] = useState<ToolId[]>([]);
|
||||||
|
|
||||||
|
// Load from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentStr = window.localStorage.getItem(RECENT_TOOLS_KEY);
|
||||||
|
const favoritesStr = window.localStorage.getItem(FAVORITE_TOOLS_KEY);
|
||||||
|
|
||||||
|
if (recentStr) {
|
||||||
|
try {
|
||||||
|
const recent = JSON.parse(recentStr) as ToolId[];
|
||||||
|
setRecentTools(recent);
|
||||||
|
} catch {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (favoritesStr) {
|
||||||
|
try {
|
||||||
|
const favorites = JSON.parse(favoritesStr) as ToolId[];
|
||||||
|
setFavoriteTools(favorites);
|
||||||
|
} catch {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
// Toggle favorite status
|
||||||
|
const toggleFavorite = useCallback((toolId: ToolId) => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFavoriteTools((prev) => {
|
||||||
|
const isFavorite = prev.includes(toolId);
|
||||||
|
const updated = isFavorite
|
||||||
|
? prev.filter((id) => id !== toolId)
|
||||||
|
: [...prev, toolId];
|
||||||
|
window.localStorage.setItem(FAVORITE_TOOLS_KEY, JSON.stringify(updated));
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Check if a tool is favorited
|
||||||
|
const isFavorite = useCallback(
|
||||||
|
(toolId: ToolId): boolean => {
|
||||||
|
return favoriteTools.includes(toolId);
|
||||||
|
},
|
||||||
|
[favoriteTools]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
recentTools,
|
||||||
|
favoriteTools,
|
||||||
|
toggleFavorite,
|
||||||
|
isFavorite,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { useEffect, RefObject } from 'react';
|
||||||
|
|
||||||
|
const FOCUSABLE_ELEMENTS = [
|
||||||
|
'a[href]',
|
||||||
|
'button:not([disabled])',
|
||||||
|
'input:not([disabled])',
|
||||||
|
'select:not([disabled])',
|
||||||
|
'textarea:not([disabled])',
|
||||||
|
'[tabindex]:not([tabindex="-1"])',
|
||||||
|
].join(', ');
|
||||||
|
|
||||||
|
export function useFocusTrap(containerRef: RefObject<HTMLElement | null>, enabled: boolean = true) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled || !containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
const getFocusableElements = () =>
|
||||||
|
Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_ELEMENTS));
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key !== 'Tab') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusableElements = getFocusableElements();
|
||||||
|
if (focusableElements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstElement = focusableElements[0];
|
||||||
|
const lastElement = focusableElements[focusableElements.length - 1];
|
||||||
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
|
|
||||||
|
// Check if focus is within the container
|
||||||
|
if (!container.contains(activeElement)) {
|
||||||
|
event.preventDefault();
|
||||||
|
firstElement.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift + Tab (backwards)
|
||||||
|
if (event.shiftKey) {
|
||||||
|
if (activeElement === firstElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
lastElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tab (forwards)
|
||||||
|
else {
|
||||||
|
if (activeElement === lastElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
firstElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Focus first element on mount
|
||||||
|
const focusableElements = getFocusableElements();
|
||||||
|
if (focusableElements.length > 0) {
|
||||||
|
// Small delay to ensure the element is fully rendered
|
||||||
|
setTimeout(() => {
|
||||||
|
focusableElements[0]?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [containerRef, enabled]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export const useToolManagement = (): ToolManagementResult => {
|
|||||||
(Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
|
(Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
|
||||||
if (isToolAvailable(toolKey)) {
|
if (isToolAvailable(toolKey)) {
|
||||||
const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
|
const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
|
||||||
availableToolRegistry[toolKey] = {
|
availableToolRegistry[toolKey as ToolId] = {
|
||||||
...baseTool,
|
...baseTool,
|
||||||
name: baseTool.name,
|
name: baseTool.name,
|
||||||
description: baseTool.description,
|
description: baseTool.description,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ export function useToolSections(
|
|||||||
seen.add(toolId);
|
seen.add(toolId);
|
||||||
const sub = tool.subcategoryId;
|
const sub = tool.subcategoryId;
|
||||||
if (!subMap[sub]) subMap[sub] = [];
|
if (!subMap[sub]) subMap[sub] = [];
|
||||||
subMap[sub].push({ id: toolId, tool });
|
subMap[sub].push({ id: toolId as ToolId, tool });
|
||||||
});
|
});
|
||||||
const entries = Object.entries(subMap);
|
const entries = Object.entries(subMap);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import FileManager from "../components/FileManager";
|
|||||||
import LocalIcon from "../components/shared/LocalIcon";
|
import LocalIcon from "../components/shared/LocalIcon";
|
||||||
import { useFilesModalContext } from "../contexts/FilesModalContext";
|
import { useFilesModalContext } from "../contexts/FilesModalContext";
|
||||||
import AppConfigModal from "../components/shared/AppConfigModal";
|
import AppConfigModal from "../components/shared/AppConfigModal";
|
||||||
|
import ToolPanelModePrompt from "../components/tools/ToolPanelModePrompt";
|
||||||
|
|
||||||
import "./HomePage.css";
|
import "./HomePage.css";
|
||||||
|
|
||||||
@@ -30,7 +31,14 @@ export default function HomePage() {
|
|||||||
|
|
||||||
const { quickAccessRef } = sidebarRefs;
|
const { quickAccessRef } = sidebarRefs;
|
||||||
|
|
||||||
const { selectedTool, selectedToolKey, handleToolSelect, handleBackToTools } = useToolWorkflow();
|
const {
|
||||||
|
selectedTool,
|
||||||
|
selectedToolKey,
|
||||||
|
handleToolSelect,
|
||||||
|
handleBackToTools,
|
||||||
|
readerMode,
|
||||||
|
setLeftPanelView,
|
||||||
|
} = useToolWorkflow();
|
||||||
|
|
||||||
const { openFilesModal } = useFilesModalContext();
|
const { openFilesModal } = useFilesModalContext();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
@@ -110,6 +118,23 @@ export default function HomePage() {
|
|||||||
};
|
};
|
||||||
}, [isMobile]);
|
}, [isMobile]);
|
||||||
|
|
||||||
|
// Automatically switch to workbench when read mode or multiTool is activated in mobile
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMobile && (readerMode || selectedToolKey === 'multiTool')) {
|
||||||
|
setActiveMobileView('workbench');
|
||||||
|
}
|
||||||
|
}, [isMobile, readerMode, selectedToolKey]);
|
||||||
|
|
||||||
|
// When navigating back to tools view in mobile with a workbench-only tool, show tool picker
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMobile && activeMobileView === 'tools' && selectedTool) {
|
||||||
|
// Check if this is a workbench-only tool (has workbench but no component)
|
||||||
|
if (selectedTool.workbench && !selectedTool.component) {
|
||||||
|
setLeftPanelView('toolPicker');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isMobile, activeMobileView, selectedTool, setLeftPanelView]);
|
||||||
|
|
||||||
const baseUrl = getBaseUrl();
|
const baseUrl = getBaseUrl();
|
||||||
|
|
||||||
// Update document meta when tool changes
|
// Update document meta when tool changes
|
||||||
@@ -126,6 +151,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen overflow-hidden">
|
<div className="h-screen overflow-hidden">
|
||||||
|
<ToolPanelModePrompt />
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<div className="mobile-layout">
|
<div className="mobile-layout">
|
||||||
<div className="mobile-toggle">
|
<div className="mobile-toggle">
|
||||||
@@ -231,8 +257,7 @@ export default function HomePage() {
|
|||||||
h="100%"
|
h="100%"
|
||||||
className="flex-nowrap flex"
|
className="flex-nowrap flex"
|
||||||
>
|
>
|
||||||
<QuickAccessBar
|
<QuickAccessBar ref={quickAccessRef} />
|
||||||
ref={quickAccessRef} />
|
|
||||||
<ToolPanel />
|
<ToolPanel />
|
||||||
<Workbench />
|
<Workbench />
|
||||||
<RightRail />
|
<RightRail />
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
|
import { ToolPanelMode } from 'src/contexts/toolWorkflow/toolWorkflowState';
|
||||||
import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager';
|
import { indexedDBManager, DATABASE_CONFIGS } from './indexedDBManager';
|
||||||
|
|
||||||
export interface UserPreferences {
|
export interface UserPreferences {
|
||||||
autoUnzip: boolean;
|
autoUnzip: boolean;
|
||||||
autoUnzipFileLimit: number;
|
autoUnzipFileLimit: number;
|
||||||
|
defaultToolPanelMode: ToolPanelMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_PREFERENCES: UserPreferences = {
|
export const DEFAULT_PREFERENCES: UserPreferences = {
|
||||||
autoUnzip: true,
|
autoUnzip: true,
|
||||||
autoUnzipFileLimit: 4,
|
autoUnzipFileLimit: 4,
|
||||||
|
defaultToolPanelMode: 'sidebar',
|
||||||
};
|
};
|
||||||
|
|
||||||
class PreferencesService {
|
class PreferencesService {
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
/* CSS variables for Tailwind + Mantine integration */
|
/* CSS variables for Tailwind + Mantine integration */
|
||||||
|
|
||||||
:root {
|
: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 */
|
/* Standard gray scale */
|
||||||
--gray-50: 249 250 251;
|
--gray-50: 249 250 251;
|
||||||
--gray-100: 243 244 246;
|
--gray-100: 243 244 246;
|
||||||
@@ -48,6 +59,22 @@
|
|||||||
--color-yellow-200: #fef08a;
|
--color-yellow-200: #fef08a;
|
||||||
--color-yellow-300: #fde047;
|
--color-yellow-300: #fde047;
|
||||||
--color-yellow-400: #facc15;
|
--color-yellow-400: #facc15;
|
||||||
|
|
||||||
|
/* Category colors - consistent across light and dark modes */
|
||||||
|
--category-color-removal: #ef4444; /* Red for removal tools */
|
||||||
|
--category-color-security: #f59e0b; /* Orange for security tools */
|
||||||
|
--category-color-formatting: #8b5cf6; /* Purple for formatting tools */
|
||||||
|
--category-color-extraction: #06b6d4; /* Cyan for extraction tools */
|
||||||
|
--category-color-signing: #10b981; /* Green for signing tools */
|
||||||
|
--category-color-general: #3b82f6; /* Blue for general tools */
|
||||||
|
--category-color-verification: #f97316; /* Orange for verification tools */
|
||||||
|
--category-color-automation: #ec4899; /* Pink for automation tools */
|
||||||
|
--category-color-developer: #6b7280; /* Gray for developer tools */
|
||||||
|
--category-color-default: #6b7280; /* Default gray */
|
||||||
|
|
||||||
|
/* Special section colors - consistent across light and dark modes */
|
||||||
|
--special-color-favorites: #FFC107; /* Yellow/gold for favorites */
|
||||||
|
--special-color-recommended: #1BB1D4; /* Cyan for recommended */
|
||||||
--color-yellow-500: #eab308;
|
--color-yellow-500: #eab308;
|
||||||
--color-yellow-600: #ca8a04;
|
--color-yellow-600: #ca8a04;
|
||||||
--color-yellow-700: #a16207;
|
--color-yellow-700: #a16207;
|
||||||
@@ -277,6 +304,22 @@
|
|||||||
--color-gray-800: #e5e7eb;
|
--color-gray-800: #e5e7eb;
|
||||||
--color-gray-900: #f3f4f6;
|
--color-gray-900: #f3f4f6;
|
||||||
|
|
||||||
|
/* Category colors - same as light mode for consistency */
|
||||||
|
--category-color-removal: #ef4444; /* Red for removal tools */
|
||||||
|
--category-color-security: #f59e0b; /* Orange for security tools */
|
||||||
|
--category-color-formatting: #8b5cf6; /* Purple for formatting tools */
|
||||||
|
--category-color-extraction: #06b6d4; /* Cyan for extraction tools */
|
||||||
|
--category-color-signing: #10b981; /* Green for signing tools */
|
||||||
|
--category-color-general: #3b82f6; /* Blue for general tools */
|
||||||
|
--category-color-verification: #f97316; /* Orange for verification tools */
|
||||||
|
--category-color-automation: #ec4899; /* Pink for automation tools */
|
||||||
|
--category-color-developer: #6b7280; /* Gray for developer tools */
|
||||||
|
--category-color-default: #6b7280; /* Default gray */
|
||||||
|
|
||||||
|
/* Special section colors - same as light mode for consistency */
|
||||||
|
--special-color-favorites: #FFC107; /* Yellow/gold for favorites */
|
||||||
|
--special-color-recommended: #1BB1D4; /* Cyan for recommended */
|
||||||
|
|
||||||
/* Success (green) - dark */
|
/* Success (green) - dark */
|
||||||
--color-green-50: #052e16;
|
--color-green-50: #052e16;
|
||||||
--color-green-100: #064e3b;
|
--color-green-100: #064e3b;
|
||||||
@@ -446,3 +489,10 @@
|
|||||||
* {
|
* {
|
||||||
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
:root {
|
||||||
|
--shadow-color: rgba(15, 23, 42, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Centralized z-index constants for new usages added in this branch.
|
||||||
|
// Keep values identical to their original inline usages.
|
||||||
|
|
||||||
|
export const Z_INDEX_FULLSCREEN_SURFACE = 1000;
|
||||||
|
export const Z_INDEX_AUTOMATE_MODAL = 1100;
|
||||||
|
export const Z_INDEX_FILE_MANAGER_MODAL = 1200;
|
||||||
|
export const Z_INDEX_OVER_FULLSCREEN_SURFACE = 1300;
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useFileSelection } from "../contexts/FileContext";
|
import { useFileSelection } from "../contexts/FileContext";
|
||||||
import { useNavigationActions } from "../contexts/NavigationContext";
|
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
|
// Register reset function with the tool workflow context - only once on mount
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
const stableResetFunction = () => {
|
const stableResetFunction = () => {
|
||||||
if (resetFunctionRef.current) {
|
if (resetFunctionRef.current) {
|
||||||
resetFunctionRef.current();
|
resetFunctionRef.current();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface SidebarState {
|
|||||||
export interface SidebarRefs {
|
export interface SidebarRefs {
|
||||||
quickAccessRef: React.RefObject<HTMLDivElement | null>;
|
quickAccessRef: React.RefObject<HTMLDivElement | null>;
|
||||||
toolPanelRef: React.RefObject<HTMLDivElement | null>;
|
toolPanelRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
rightRailRef: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarInfo {
|
export interface SidebarInfo {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface RankedToolItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function filterToolRegistryByQuery(
|
export function filterToolRegistryByQuery(
|
||||||
toolRegistry: Record<string, ToolRegistryEntry>,
|
toolRegistry: Record<ToolId, ToolRegistryEntry>,
|
||||||
query: string
|
query: string
|
||||||
): RankedToolItem[] {
|
): RankedToolItem[] {
|
||||||
const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][];
|
const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][];
|
||||||
@@ -85,10 +85,10 @@ export function filterToolRegistryByQuery(
|
|||||||
ordered.push({ item: [id, tool], matchedText });
|
ordered.push({ item: [id, tool], matchedText });
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const { id, tool } of exactName) push(id, tool, tool.name);
|
for (const { id, tool } of exactName) push(id as ToolId, tool, tool.name);
|
||||||
for (const { id, tool, text } of exactSyn) push(id, tool, text);
|
for (const { id, tool, text } of exactSyn) push(id as ToolId, tool, text);
|
||||||
for (const { id, tool, text } of fuzzyName) push(id, tool, text);
|
for (const { id, tool, text } of fuzzyName) push(id as ToolId, tool, text);
|
||||||
for (const { id, tool, text } of fuzzySyn) push(id, tool, text);
|
for (const { id, tool, text } of fuzzySyn) push(id as ToolId, tool, text);
|
||||||
|
|
||||||
if (ordered.length > 0) return ordered;
|
if (ordered.length > 0) return ordered;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user