colour and fixes

This commit is contained in:
Anthony Stirling 2025-10-05 23:00:44 +01:00
parent ac03c49abb
commit 0eefc734f5
7 changed files with 654 additions and 190 deletions

View File

@ -1977,3 +1977,7 @@ viewer.nextPage=Next Page
viewer.pageNavigation=Page Navigation
viewer.currentPage=Current Page
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

View File

@ -1,12 +1,17 @@
import React, { useMemo } from 'react';
import { Badge, Text } from '@mantine/core';
import { ActionIcon, Badge, Text } from '@mantine/core';
import { Tooltip } from '../shared/Tooltip';
import { useTranslation } from 'react-i18next';
import { ToolRegistryEntry } from '../../data/toolsTaxonomy';
import { ToolRegistryEntry, getSubcategoryLabel, getSubcategoryColor, getSubcategoryIcon } from '../../data/toolsTaxonomy';
import { ToolId } from '../../types/toolId';
import { useToolSections } from '../../hooks/useToolSections';
import { getSubcategoryLabel } from '../../data/toolsTaxonomy';
import NoToolsFound from './shared/NoToolsFound';
import { useHotkeys } from '../../contexts/HotkeyContext';
import HotkeyDisplay from '../hotkeys/HotkeyDisplay';
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import StarRoundedIcon from '@mui/icons-material/StarRounded';
import StarBorderRoundedIcon from '@mui/icons-material/StarBorderRounded';
import HistoryRoundedIcon from '@mui/icons-material/HistoryRounded';
import './ToolPanel.css';
interface LegacyToolListProps {
@ -27,11 +32,36 @@ const LegacyToolList = ({
onSelect,
}: LegacyToolListProps) => {
const { t } = useTranslation();
const { hotkeys } = useHotkeys();
const { toolRegistry, recentTools, favoriteTools, toggleFavorite, isFavorite } = useToolWorkflow();
const { sections, searchGroups } = useToolSections(filteredTools, searchQuery);
const tooltipPortalTarget = typeof document !== 'undefined' ? document.body : undefined;
// Prepare recent and favorite tool items
const recentToolItems = useMemo(() => {
return recentTools
.map((toolId) => {
const tool = toolRegistry[toolId];
return tool ? { id: toolId, tool } : null;
})
.filter(Boolean)
.slice(0, 6); // Show max 6 recent tools
}, [recentTools, toolRegistry]);
const favoriteToolItems = useMemo(() => {
return favoriteTools
.map((toolId) => {
const tool = toolRegistry[toolId];
return tool ? { id: toolId, tool } : null;
})
.filter(Boolean);
}, [favoriteTools, toolRegistry]);
// Show recent/favorites section only when not searching
const showRecentFavorites = searchQuery.trim().length === 0 && (recentToolItems.length > 0 || favoriteToolItems.length > 0);
const subcategoryGroups = useMemo(() => {
if (searchQuery.trim().length > 0) {
return searchGroups;
@ -40,7 +70,7 @@ const LegacyToolList = ({
return allSection ? allSection.subcategories : [];
}, [searchGroups, sections, searchQuery]);
if (subcategoryGroups.length === 0) {
if (subcategoryGroups.length === 0 && !showRecentFavorites) {
return (
<div className="tool-panel__legacy-empty">
<NoToolsFound />
@ -55,158 +85,296 @@ const LegacyToolList = ({
? 'tool-panel__legacy-groups tool-panel__legacy-groups--detailed'
: 'tool-panel__legacy-groups tool-panel__legacy-groups--compact';
// Helper function to render a tool item
const renderToolItem = (id: string, tool: ToolRegistryEntry) => {
const matchedText = matchedTextMap.get(id);
const isSelected = selectedToolKey === id;
const isDisabled = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
const binding = hotkeys[id];
const isFav = isFavorite(id as ToolId);
const categoryColor = getSubcategoryColor(tool.subcategoryId);
let iconNode: React.ReactNode = null;
if (React.isValidElement<{ style?: React.CSSProperties }>(tool.icon)) {
const element = tool.icon as React.ReactElement<{ style?: React.CSSProperties }>;
iconNode = React.cloneElement(element, {
style: {
...(element.props.style || {}),
fontSize: showDescriptions ? '1.75rem' : '1.5rem',
},
});
} else {
iconNode = tool.icon;
}
const handleClick = () => {
if (isDisabled) return;
if (tool.link) {
window.open(tool.link, '_blank', 'noopener,noreferrer');
return;
}
onSelect(id as ToolId);
};
const handleStarClick = (e: React.MouseEvent) => {
e.stopPropagation();
toggleFavorite(id as ToolId);
};
// Detailed view
if (showDescriptions) {
return (
<button
key={id}
type="button"
className={`tool-panel__legacy-item tool-panel__legacy-item--detailed ${isSelected ? 'tool-panel__legacy-item--selected' : ''} tool-panel__legacy-item--with-star`}
onClick={handleClick}
aria-disabled={isDisabled}
disabled={isDisabled}
>
{tool.icon ? (
<span
className="tool-panel__legacy-icon"
aria-hidden
style={{
background: `linear-gradient(135deg,
color-mix(in srgb, ${categoryColor} 18%, var(--legacy-bg-icon-detailed)),
color-mix(in srgb, ${categoryColor} 8%, var(--legacy-bg-icon-detailed))
)`,
color: categoryColor
}}
>
{iconNode}
</span>
) : null}
<span className="tool-panel__legacy-body">
<Text fw={600} size="sm" className="tool-panel__legacy-name">
{tool.name}
</Text>
<Text size="sm" c="dimmed" className="tool-panel__legacy-description">
{tool.description}
</Text>
{binding && (
<div className="tool-panel__legacy-shortcut">
<span style={{ color: 'var(--mantine-color-dimmed)', fontSize: '0.75rem' }}>
{t('settings.hotkeys.shortcut', 'Shortcut')}
</span>
<HotkeyDisplay binding={binding} size="sm" />
</div>
)}
{matchedText && (
<Text size="xs" c="dimmed" className="tool-panel__legacy-match">
{t('toolPanel.legacy.matchedSynonym', 'Matches "{{text}}"', { text: matchedText })}
</Text>
)}
</span>
{!isDisabled && (
<ActionIcon
variant="subtle"
radius="xl"
size="sm"
onClick={handleStarClick}
className="tool-panel__legacy-star"
aria-label={isFav ? t('toolPanel.legacy.unfavorite', 'Remove from favourites') : t('toolPanel.legacy.favorite', 'Add to favourites')}
>
{isFav ? (
<StarRoundedIcon fontSize="small" style={{ color: '#FFC107' }} />
) : (
<StarBorderRoundedIcon fontSize="small" />
)}
</ActionIcon>
)}
</button>
);
}
// Compact view
const compactButton = (
<button
key={id}
type="button"
className={`tool-panel__legacy-list-item ${isSelected ? 'tool-panel__legacy-list-item--selected' : ''} ${!isDisabled ? 'tool-panel__legacy-list-item--with-star' : ''}`}
onClick={handleClick}
aria-disabled={isDisabled}
disabled={isDisabled}
>
{tool.icon ? (
<span
className="tool-panel__legacy-list-icon"
aria-hidden
style={{
background: `linear-gradient(135deg,
color-mix(in srgb, ${categoryColor} 15%, var(--legacy-bg-icon-compact)),
color-mix(in srgb, ${categoryColor} 6%, var(--legacy-bg-icon-compact))
)`,
color: categoryColor
}}
>
{iconNode}
</span>
) : null}
<span className="tool-panel__legacy-list-body">
<Text fw={600} size="sm" className="tool-panel__legacy-name">
{tool.name}
</Text>
{matchedText && (
<Text size="xs" c="dimmed" className="tool-panel__legacy-match">
{t('toolPanel.legacy.matchedSynonym', 'Matches "{{text}}"', { text: matchedText})}
</Text>
)}
</span>
{!isDisabled && (
<ActionIcon
variant="subtle"
radius="xl"
size="xs"
onClick={handleStarClick}
className="tool-panel__legacy-star-compact"
aria-label={isFav ? t('toolPanel.legacy.unfavorite', 'Remove from favourites') : t('toolPanel.legacy.favorite', 'Add to favourites')}
>
{isFav ? (
<StarRoundedIcon fontSize="inherit" style={{ color: '#FFC107', fontSize: '1rem' }} />
) : (
<StarBorderRoundedIcon fontSize="inherit" style={{ fontSize: '1rem' }} />
)}
</ActionIcon>
)}
</button>
);
const tooltipContent = (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
<span>{tool.description}</span>
{binding && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem' }}>
<span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500 }}>
{t('settings.hotkeys.shortcut', 'Shortcut')}
</span>
<HotkeyDisplay binding={binding} />
</div>
)}
</div>
);
return (
<Tooltip
key={id}
content={tooltipContent}
position="top"
portalTarget={tooltipPortalTarget}
arrow
delay={80}
>
{compactButton}
</Tooltip>
);
};
return (
<div className={containerClass}>
{subcategoryGroups.map(({ subcategoryId, tools }) => (
<section
key={subcategoryId}
className={`tool-panel__legacy-group ${showDescriptions ? 'tool-panel__legacy-group--detailed' : 'tool-panel__legacy-group--compact'}`}
>
<header className="tool-panel__legacy-section-header">
<Text size="sm" fw={600} tt="uppercase" lts={0.5} c="dimmed">
{getSubcategoryLabel(t, subcategoryId)}
</Text>
<Badge size="sm" variant="light" color="gray">
{tools.length}
</Badge>
</header>
{showDescriptions ? (
<div className="tool-panel__legacy-grid tool-panel__legacy-grid--detailed">
{tools.map(({ id, tool }) => {
const matchedText = matchedTextMap.get(id);
const isSelected = selectedToolKey === id;
const isDisabled = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
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;
}
const handleClick = () => {
if (isDisabled) return;
if (tool.link) {
window.open(tool.link, '_blank', 'noopener,noreferrer');
return;
}
onSelect(id as ToolId);
};
return (
<button
key={id}
type="button"
className={`tool-panel__legacy-item tool-panel__legacy-item--detailed ${isSelected ? 'tool-panel__legacy-item--selected' : ''}`}
onClick={handleClick}
aria-disabled={isDisabled}
disabled={isDisabled}
>
{tool.icon ? (
<span className="tool-panel__legacy-icon" aria-hidden>
{iconNode}
</span>
) : null}
<span className="tool-panel__legacy-body">
<Text fw={600} size="sm" className="tool-panel__legacy-name">
{tool.name}
</Text>
<Text size="sm" c="dimmed" className="tool-panel__legacy-description">
{tool.description}
</Text>
{matchedText && (
<Text size="xs" c="dimmed" className="tool-panel__legacy-match">
{t('toolPanel.legacy.matchedSynonym', 'Matches "{{text}}"', { text: matchedText })}
</Text>
)}
</span>
</button>
);
})}
</div>
) : (
<div className="tool-panel__legacy-list">
{tools.map(({ id, tool }) => {
const matchedText = matchedTextMap.get(id);
const isSelected = selectedToolKey === id;
const isDisabled = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
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 handleClick = () => {
if (isDisabled) return;
if (tool.link) {
window.open(tool.link, '_blank', 'noopener,noreferrer');
return;
}
onSelect(id as ToolId);
};
const baseButton = (
<button
type="button"
className={`tool-panel__legacy-list-item ${isSelected ? 'tool-panel__legacy-list-item--selected' : ''}`}
onClick={handleClick}
aria-disabled={isDisabled}
disabled={isDisabled}
>
{tool.icon ? (
<span className="tool-panel__legacy-list-icon" aria-hidden>
{iconNode}
</span>
) : null}
<span className="tool-panel__legacy-list-body">
<Text fw={600} size="sm" className="tool-panel__legacy-name">
{tool.name}
</Text>
{matchedText && (
<Text size="xs" c="dimmed" className="tool-panel__legacy-match">
{t('toolPanel.legacy.matchedSynonym', 'Matches "{{text}}"', { text: matchedText })}
</Text>
)}
</span>
</button>
);
if (showDescriptions || !tool.description) {
return React.cloneElement(baseButton, { key: id });
}
return (
<Tooltip
key={id}
content={tool.description}
position="top"
portalTarget={tooltipPortalTarget}
arrow
delay={80}
>
{baseButton}
</Tooltip>
);
})}
</div>
{showRecentFavorites && (
<>
{favoriteToolItems.length > 0 && (
<section className="tool-panel__legacy-group tool-panel__legacy-group--special">
<header className="tool-panel__legacy-section-header">
<div className="tool-panel__legacy-section-title">
<span className="tool-panel__legacy-section-icon" style={{ color: '#FFC107' }} aria-hidden>
<StarRoundedIcon />
</span>
<Text size="sm" fw={600} tt="uppercase" lts={0.5} c="dimmed">
{t('toolPanel.legacy.favorites', 'Favourites')}
</Text>
</div>
<Badge size="sm" variant="light" color="yellow">
{favoriteToolItems.length}
</Badge>
</header>
{showDescriptions ? (
<div className="tool-panel__legacy-grid tool-panel__legacy-grid--detailed">
{favoriteToolItems.map((item: any) => renderToolItem(item.id, item.tool))}
</div>
) : (
<div className="tool-panel__legacy-list">
{favoriteToolItems.map((item: any) => renderToolItem(item.id, item.tool))}
</div>
)}
</section>
)}
</section>
))}
{recentToolItems.length > 0 && (
<section className="tool-panel__legacy-group tool-panel__legacy-group--special">
<header className="tool-panel__legacy-section-header">
<div className="tool-panel__legacy-section-title">
<span className="tool-panel__legacy-section-icon" style={{ color: '#1BB1D4' }} aria-hidden>
<HistoryRoundedIcon />
</span>
<Text size="sm" fw={600} tt="uppercase" lts={0.5} c="dimmed">
{t('toolPanel.legacy.recent', 'Recently used')}
</Text>
</div>
<Badge size="sm" variant="light" color="cyan">
{recentToolItems.length}
</Badge>
</header>
{showDescriptions ? (
<div className="tool-panel__legacy-grid tool-panel__legacy-grid--detailed">
{recentToolItems.map((item: any) => renderToolItem(item.id, item.tool))}
</div>
) : (
<div className="tool-panel__legacy-list">
{recentToolItems.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__legacy-group ${showDescriptions ? 'tool-panel__legacy-group--detailed' : 'tool-panel__legacy-group--compact'}`}
style={{
borderColor: `color-mix(in srgb, ${categoryColor} 25%, var(--legacy-border-subtle-65))`,
}}
>
<header className="tool-panel__legacy-section-header">
<div className="tool-panel__legacy-section-title">
<span
className="tool-panel__legacy-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="light" style={{
backgroundColor: `color-mix(in srgb, ${categoryColor} 15%, transparent)`,
color: categoryColor,
borderColor: `color-mix(in srgb, ${categoryColor} 30%, transparent)`
}}>
{tools.length}
</Badge>
</header>
{showDescriptions ? (
<div className="tool-panel__legacy-grid tool-panel__legacy-grid--detailed">
{tools.map(({ id, tool }) => renderToolItem(id, tool))}
</div>
) : (
<div className="tool-panel__legacy-list">
{tools.map(({ id, tool }) => renderToolItem(id, tool))}
</div>
)}
</section>
);
})}
</div>
);
};

View File

@ -2,9 +2,9 @@
.tool-panel__legacy-surface-inner {
--legacy-bg-surface-1: color-mix(in srgb, var(--bg-toolbar) 96%, transparent);
--legacy-bg-surface-2: color-mix(in srgb, var(--bg-background) 90%, transparent);
--legacy-bg-header: color-mix(in srgb, var(--bg-toolbar) 86%, transparent);
--legacy-bg-controls-1: color-mix(in srgb, var(--bg-toolbar) 84%, transparent);
--legacy-bg-controls-2: color-mix(in srgb, var(--bg-background) 72%, transparent);
--legacy-bg-header: var(--bg-toolbar);
--legacy-bg-controls-1: var(--bg-toolbar);
--legacy-bg-controls-2: color-mix(in srgb, var(--bg-toolbar) 95%, var(--bg-background));
--legacy-bg-body-1: color-mix(in srgb, var(--bg-background) 86%, transparent);
--legacy-bg-body-2: color-mix(in srgb, var(--bg-toolbar) 78%, transparent);
--legacy-bg-group: color-mix(in srgb, var(--bg-toolbar) 82%, transparent);
@ -87,7 +87,7 @@
height: 100%;
display: flex;
flex-direction: column;
border-radius: 0 1.25rem 1.25rem 0;
border-radius: 0;
background:
linear-gradient(
140deg,
@ -111,34 +111,36 @@
.tool-panel__legacy-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
align-items: center;
gap: 1rem;
padding: 1.25rem 1.75rem;
border-bottom: 1px solid var(--legacy-border-subtle-70);
background: linear-gradient(
180deg,
var(--legacy-bg-header),
transparent 85%
);
padding: 0.75rem 1.75rem;
border-bottom: 1px solid var(--border-subtle);
background: var(--bg-toolbar);
}
.tool-panel__legacy-heading {
.tool-panel__legacy-brand {
display: flex;
flex-direction: column;
gap: 0.3rem;
align-items: center;
gap: 0.625rem;
}
.tool-panel__legacy-brand-icon {
height: 1.75rem;
width: auto;
}
.tool-panel__legacy-brand-text {
height: 1.25rem;
width: auto;
}
.tool-panel__legacy-controls {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.75rem;
border-bottom: 1px solid var(--legacy-border-subtle-70);
background: linear-gradient(
180deg,
var(--legacy-bg-controls-1),
var(--legacy-bg-controls-2)
);
padding: 0.75rem 1.75rem;
border-bottom: 1px solid var(--tool-panel-search-border-bottom);
background: var(--tool-panel-search-bg);
}
.tool-panel__legacy-controls .search-input-container {
@ -188,10 +190,24 @@
.tool-panel__legacy-section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.1rem 0.15rem 0.35rem;
}
.tool-panel__legacy-section-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.tool-panel__legacy-section-icon {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
}
.tool-panel__legacy-grid {
display: grid;
gap: 0.75rem;
@ -244,6 +260,51 @@
min-height: 7.5rem;
}
.tool-panel__legacy-item--with-star {
position: relative;
}
.tool-panel__legacy-star {
position: absolute;
top: 0.5rem;
right: 0.5rem;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 2;
}
.tool-panel__legacy-item:hover .tool-panel__legacy-star,
.tool-panel__legacy-star:focus {
opacity: 1;
}
.tool-panel__legacy-list-item--with-star {
position: relative;
}
.tool-panel__legacy-star-compact {
position: absolute;
top: 0.35rem;
right: 0.35rem;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 2;
}
.tool-panel__legacy-list-item:hover .tool-panel__legacy-star-compact,
.tool-panel__legacy-star-compact:focus {
opacity: 1;
}
.tool-panel__legacy-group--special {
background: linear-gradient(
135deg,
color-mix(in srgb, var(--legacy-bg-group) 95%, var(--text-primary) 5%),
var(--legacy-bg-group)
);
border-width: 1.5px;
}
.tool-panel__legacy-icon {
display: flex;
align-items: center;
@ -254,10 +315,37 @@
background: var(--legacy-bg-icon-detailed);
color: var(--legacy-text-icon);
flex-shrink: 0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: hidden;
}
.tool-panel__legacy-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__legacy-item:hover:not([aria-disabled="true"]) .tool-panel__legacy-icon {
transform: scale(1.08);
box-shadow: 0 4px 12px color-mix(in srgb, var(--text-primary) 15%, transparent);
}
.tool-panel__legacy-item:hover:not([aria-disabled="true"]) .tool-panel__legacy-icon::before {
opacity: 1;
}
.tool-panel__legacy-icon svg {
font-size: 1.65rem;
position: relative;
z-index: 1;
}
.tool-panel__legacy-body {
@ -279,6 +367,13 @@
font-style: italic;
}
.tool-panel__legacy-shortcut {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.2rem;
}
.tool-panel__legacy-list {
display: flex;
flex-direction: column;
@ -298,6 +393,7 @@
border: 1px solid transparent;
width: 100%;
box-sizing: border-box;
position: relative;
}
.tool-panel__legacy-list-item[aria-disabled="true"],
@ -326,6 +422,36 @@
background: var(--legacy-bg-icon-compact);
color: var(--legacy-text-icon-compact);
flex-shrink: 0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
overflow: hidden;
}
.tool-panel__legacy-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__legacy-list-item:hover:not([aria-disabled="true"]) .tool-panel__legacy-list-icon {
transform: scale(1.06);
box-shadow: 0 2px 8px color-mix(in srgb, var(--text-primary) 12%, transparent);
}
.tool-panel__legacy-list-item:hover:not([aria-disabled="true"]) .tool-panel__legacy-list-icon::before {
opacity: 1;
}
.tool-panel__legacy-list-icon svg {
position: relative;
z-index: 1;
}
.tool-panel__legacy-list-body {

View File

@ -46,7 +46,7 @@ export default function ToolPanel() {
const legacyExpanded = isLegacyMode && leftPanelView === 'toolPicker' && !isMobile;
// Use custom hooks for state management
const [showLegacyDescriptions, setShowLegacyDescriptions] = useLocalStorageState('legacyToolDescriptions', true);
const [showLegacyDescriptions, setShowLegacyDescriptions] = useLocalStorageState('legacyToolDescriptions', false);
const legacyGeometry = useToolPanelGeometry({
enabled: legacyExpanded,
toolPanelRef,

View File

@ -12,6 +12,7 @@ import { ToolId, isValidToolId } from '../types/toolId';
import { useNavigationUrlSync } from '../hooks/useUrlSync';
import { getDefaultWorkbench } from '../types/workbench';
import { filterToolRegistryByQuery } from '../utils/toolSearch';
import { useToolHistory } from '../hooks/tools/useToolHistory';
// State interface
type ToolPanelMode = 'sidebar' | 'legacy';
@ -134,6 +135,13 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
// Computed values
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
isPanelVisible: boolean;
// Tool History
recentTools: ToolId[];
favoriteTools: ToolId[];
addToRecent: (toolId: ToolId) => void;
toggleFavorite: (toolId: ToolId) => void;
isFavorite: (toolId: ToolId) => boolean;
}
const ToolWorkflowContext = createContext<ToolWorkflowContextValue | undefined>(undefined);
@ -159,6 +167,15 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
getSelectedTool,
} = useToolManagement();
// Tool history hook
const {
recentTools,
favoriteTools,
addToRecent,
toggleFavorite,
isFavorite,
} = useToolHistory();
// Get selected tool from navigation context
const selectedTool = getSelectedTool(navigationState.selectedTool);
@ -224,19 +241,24 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Workflow actions (compound actions that coordinate multiple state changes)
const handleToolSelect = useCallback((toolId: ToolId) => {
// Track tool usage in recent history
addToRecent(toolId);
// Handle read tool selection - should behave exactly like QuickAccessBar read button
if (toolId === 'read') {
setReaderMode(true);
actions.setSelectedTool('read');
actions.setWorkbench('viewer');
setSearchQuery('');
setToolPanelMode('sidebar'); // Close legacy mode when switching to reader
setLeftPanelView('toolPicker'); // Show tool picker when navigating back to tools
return;
}
// Handle multiTool selection - enable page editor workbench and hide left panel
// Handle multiTool selection - enable page editor workbench
if (toolId === 'multiTool') {
setReaderMode(false);
setLeftPanelView('hidden');
setLeftPanelView('toolPicker'); // Show tool picker when navigating back to tools in mobile
actions.setSelectedTool('multiTool');
actions.setWorkbench('pageEditor');
setSearchQuery('');
@ -259,7 +281,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
setSearchQuery('');
setLeftPanelView('toolContent');
setReaderMode(false); // Disable read mode when selecting tools
}, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]);
}, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery, addToRecent]);
const handleBackToTools = useCallback(() => {
setLeftPanelView('toolPicker');
@ -324,6 +346,13 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
// Computed
filteredTools,
isPanelVisible,
// Tool History
recentTools,
favoriteTools,
addToRecent,
toggleFavorite,
isFavorite,
}), [
state,
navigationState.selectedTool,
@ -345,6 +374,11 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
handleReaderToggle,
filteredTools,
isPanelVisible,
recentTools,
favoriteTools,
addToRecent,
toggleFavorite,
isFavorite,
]);
return (

View File

@ -4,6 +4,17 @@ import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation';
import { BaseToolProps } from '../types/tool';
import { WorkbenchType } from '../types/workbench';
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 {
SIGNING = 'signing',
@ -66,17 +77,46 @@ export const SUBCATEGORY_ORDER: SubcategoryId[] = [
];
export const SUBCATEGORY_COLOR_MAP: Record<SubcategoryId, string> = {
[SubcategoryId.SIGNING]: '#FF7892',
[SubcategoryId.DOCUMENT_SECURITY]: '#FF7892',
[SubcategoryId.VERIFICATION]: '#1BB1D4',
[SubcategoryId.DOCUMENT_REVIEW]: '#48BD54',
[SubcategoryId.PAGE_FORMATTING]: '#7882FF',
[SubcategoryId.EXTRACTION]: '#1BB1D4',
[SubcategoryId.REMOVAL]: '#7882FF',
[SubcategoryId.AUTOMATION]: '#69DC95',
[SubcategoryId.GENERAL]: '#69DC95',
[SubcategoryId.ADVANCED_FORMATTING]: '#F55454',
[SubcategoryId.DEVELOPER_TOOLS]: '#F55454',
[SubcategoryId.SIGNING]: '#E91E63', // Rose Pink
[SubcategoryId.DOCUMENT_SECURITY]: '#D32F2F', // Deep Red
[SubcategoryId.VERIFICATION]: '#1976D2', // Medium Blue
[SubcategoryId.DOCUMENT_REVIEW]: '#388E3C', // Forest Green
[SubcategoryId.PAGE_FORMATTING]: '#5E35B1', // Deep Purple
[SubcategoryId.EXTRACTION]: '#F57C00', // Amber/Gold
[SubcategoryId.REMOVAL]: '#E64A19', // Rust Orange
[SubcategoryId.AUTOMATION]: '#00897B', // Teal
[SubcategoryId.GENERAL]: '#689F38', // Olive Green
[SubcategoryId.ADVANCED_FORMATTING]: '#8E24AA', // Deep Magenta
[SubcategoryId.DEVELOPER_TOOLS]: '#455A64', // Slate Grey
};
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);

View File

@ -0,0 +1,92 @@
import { useState, useEffect, useCallback } from 'react';
import { ToolId } from '../../types/toolId';
const RECENT_TOOLS_KEY = 'stirlingpdf.recentTools';
const FAVORITE_TOOLS_KEY = 'stirlingpdf.favoriteTools';
const MAX_RECENT_TOOLS = 10;
interface ToolHistoryData {
recentTools: ToolId[];
favoriteTools: ToolId[];
}
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
}
}
}, []);
// Add a tool to recent history
const addToRecent = useCallback((toolId: ToolId) => {
if (typeof window === 'undefined') {
return;
}
setRecentTools((prev) => {
// Remove if already exists
const filtered = prev.filter((id) => id !== toolId);
// Add to front
const updated = [toolId, ...filtered].slice(0, MAX_RECENT_TOOLS);
window.localStorage.setItem(RECENT_TOOLS_KEY, JSON.stringify(updated));
return updated;
});
}, []);
// Toggle favorite status
const toggleFavorite = useCallback((toolId: ToolId) => {
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,
addToRecent,
toggleFavorite,
isFavorite,
};
}