diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx
index 229da12ff..8ce2d87f2 100644
--- a/frontend/src/components/layout/Workbench.tsx
+++ b/frontend/src/components/layout/Workbench.tsx
@@ -2,8 +2,7 @@ import React from 'react';
import { Box } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
-import { ToolConfiguration } from '../../types/tool';
-import { PageEditorFunctions } from '../../types/pageEditor';
+import { useWorkbenchState, useToolSelection } from '../../contexts/ToolWorkflowContext';
import TopControls from '../shared/TopControls';
import FileEditor from '../fileEditor/FileEditor';
@@ -18,28 +17,8 @@ interface WorkbenchProps {
activeFiles: File[];
/** Current view mode */
currentView: string;
- /** Currently selected tool key */
- selectedToolKey: string | null;
- /** Selected tool configuration */
- selectedTool: ToolConfiguration | null;
- /** Whether sidebars are visible */
- sidebarsVisible: boolean;
- /** Function to set sidebars visibility */
- setSidebarsVisible: (visible: boolean) => void;
- /** File to preview */
- previewFile: File | null;
- /** Function to clear preview file */
- setPreviewFile: (file: File | null) => void;
- /** Page editor functions */
- pageEditorFunctions: PageEditorFunctions | null;
- /** Function to set page editor functions */
- setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
/** Handler for view changes */
onViewChange: (view: string) => void;
- /** Handler for tool selection */
- onToolSelect: (toolId: string) => void;
- /** Handler for setting left panel view */
- onSetLeftPanelView: (view: 'toolPicker' | 'toolContent') => void;
/** Handler for adding files to active files */
onAddToActiveFiles: (file: File) => void;
}
@@ -47,39 +26,36 @@ interface WorkbenchProps {
export default function Workbench({
activeFiles,
currentView,
- selectedToolKey,
- selectedTool,
- sidebarsVisible,
- setSidebarsVisible,
- previewFile,
- setPreviewFile,
- pageEditorFunctions,
- setPageEditorFunctions,
onViewChange,
- onToolSelect,
- onSetLeftPanelView,
onAddToActiveFiles
}: WorkbenchProps) {
const { t } = useTranslation();
const { isRainbowMode } = useRainbowThemeContext();
+
+ // Use context-based hooks to eliminate prop drilling
+ const {
+ previewFile,
+ pageEditorFunctions,
+ sidebarsVisible,
+ setPreviewFile,
+ setPageEditorFunctions,
+ setSidebarsVisible
+ } = useWorkbenchState();
+
+ const { selectedToolKey, selectedTool, handleToolSelect } = useToolSelection();
const handlePreviewClose = () => {
setPreviewFile(null);
const previousMode = sessionStorage.getItem('previousMode');
if (previousMode === 'split') {
- onToolSelect('split');
- onViewChange('split');
- onSetLeftPanelView('toolContent');
+ // Use context's handleToolSelect which coordinates tool selection and view changes
+ handleToolSelect('split');
sessionStorage.removeItem('previousMode');
} else if (previousMode === 'compress') {
- onToolSelect('compress');
- onViewChange('compress');
- onSetLeftPanelView('toolContent');
+ handleToolSelect('compress');
sessionStorage.removeItem('previousMode');
} else if (previousMode === 'convert') {
- onToolSelect('convert');
- onViewChange('convert');
- onSetLeftPanelView('toolContent');
+ handleToolSelect('convert');
sessionStorage.removeItem('previousMode');
} else {
onViewChange('fileEditor');
@@ -124,9 +100,7 @@ export default function Workbench({
sidebarsVisible={sidebarsVisible}
setSidebarsVisible={setSidebarsVisible}
previewFile={previewFile}
- {...(previewFile && {
- onClose: handlePreviewClose
- })}
+ onClose={handlePreviewClose}
/>
);
diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx
index 2f78a0a9f..feef717ea 100644
--- a/frontend/src/components/shared/QuickAccessBar.tsx
+++ b/frontend/src/components/shared/QuickAccessBar.tsx
@@ -12,16 +12,10 @@ import rainbowStyles from '../../styles/rainbow.module.css';
import AppConfigModal from './AppConfigModal';
import { useIsOverflowing } from '../../hooks/useIsOverflowing';
import { useFilesModalContext } from '../../contexts/FilesModalContext';
+import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import './QuickAccessBar.css';
-interface QuickAccessBarProps {
- onToolsClick: () => void;
- onReaderToggle: () => void;
- selectedToolKey?: string;
- toolRegistry: any;
- leftPanelView: 'toolPicker' | 'toolContent';
- readerMode: boolean;
-}
+// No props needed - component uses context
interface ButtonConfig {
id: string;
@@ -36,15 +30,12 @@ interface ButtonConfig {
function NavHeader({
activeButton,
- setActiveButton,
- onReaderToggle,
- onToolsClick
+ setActiveButton
}: {
activeButton: string;
setActiveButton: (id: string) => void;
- onReaderToggle: () => void;
- onToolsClick: () => void;
}) {
+ const { handleReaderToggle, handleBackToTools } = useToolWorkflow();
return (
<>
@@ -80,8 +71,8 @@ function NavHeader({
variant="subtle"
onClick={() => {
setActiveButton('tools');
- onReaderToggle();
- onToolsClick();
+ handleReaderToggle();
+ handleBackToTools();
}}
style={{
backgroundColor: activeButton === 'tools' ? 'var(--icon-tools-bg)' : 'var(--icon-inactive-bg)',
@@ -104,16 +95,10 @@ function NavHeader({
);
}
-const QuickAccessBar = ({
- onToolsClick,
- onReaderToggle,
- selectedToolKey,
- toolRegistry,
- leftPanelView,
- readerMode,
-}: QuickAccessBarProps) => {
+const QuickAccessBar = () => {
const { isRainbowMode } = useRainbowThemeContext();
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
+ const { handleReaderToggle } = useToolWorkflow();
const [configModalOpen, setConfigModalOpen] = useState(false);
const [activeButton, setActiveButton] = useState
('tools');
const scrollableRef = useRef(null);
@@ -134,7 +119,7 @@ const QuickAccessBar = ({
type: 'navigation',
onClick: () => {
setActiveButton('read');
- onReaderToggle();
+ handleReaderToggle();
}
},
{
@@ -240,9 +225,7 @@ const QuickAccessBar = ({
diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx
index a8005f2d5..eeedd8f3f 100644
--- a/frontend/src/components/tools/ToolPanel.tsx
+++ b/frontend/src/components/tools/ToolPanel.tsx
@@ -1,52 +1,30 @@
-import React, { useState } from 'react';
-import { Button, TextInput } from '@mantine/core';
+import React from 'react';
+import { TextInput } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
-import { ToolRegistry, ToolConfiguration } from '../../types/tool';
+import { useToolPanelState, useToolSelection, useWorkbenchState } from '../../contexts/ToolWorkflowContext';
import ToolPicker from './ToolPicker';
import ToolRenderer from './ToolRenderer';
import rainbowStyles from '../../styles/rainbow.module.css';
-interface ToolPanelProps {
- /** Whether the tool panel is visible */
- visible: boolean;
- /** Whether reader mode is active (hides the panel) */
- readerMode: boolean;
- /** Current view mode: 'toolPicker' or 'toolContent' */
- leftPanelView: 'toolPicker' | 'toolContent';
- /** Currently selected tool key */
- selectedToolKey: string | null;
- /** Selected tool configuration */
- selectedTool: ToolConfiguration | null;
- /** Tool registry with all available tools */
- toolRegistry: ToolRegistry;
- /** Handler for tool selection */
- onToolSelect: (toolId: string) => void;
- /** Handler for back to tools navigation */
- onBackToTools: () => void;
- /** Handler for file preview */
- onPreviewFile?: (file: File | null) => void;
-}
+// No props needed - component uses context
-export default function ToolPanel({
- visible,
- readerMode,
- leftPanelView,
- selectedToolKey,
- selectedTool,
- toolRegistry,
- onToolSelect,
- onBackToTools,
- onPreviewFile
-}: ToolPanelProps) {
+export default function ToolPanel() {
const { t } = useTranslation();
const { isRainbowMode } = useRainbowThemeContext();
- const [search, setSearch] = useState("");
-
- // Filter tools based on search
- const filteredTools = Object.entries(toolRegistry).filter(([_, { name }]) =>
- name.toLowerCase().includes(search.toLowerCase())
- );
+
+ // Use context-based hooks to eliminate prop drilling
+ const {
+ leftPanelView,
+ isPanelVisible,
+ searchQuery,
+ filteredTools,
+ setSearchQuery,
+ handleBackToTools
+ } = useToolPanelState();
+
+ const { selectedToolKey, handleToolSelect } = useToolSelection();
+ const { setPreviewFile } = useWorkbenchState();
return (
setSearch(e.currentTarget.value)}
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.currentTarget.value)}
autoComplete="off"
size="sm"
/>
@@ -83,7 +61,7 @@ export default function ToolPanel({
@@ -94,7 +72,7 @@ export default function ToolPanel({
diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx
new file mode 100644
index 000000000..b6e480a7d
--- /dev/null
+++ b/frontend/src/contexts/ToolWorkflowContext.tsx
@@ -0,0 +1,299 @@
+/**
+ * ToolWorkflowContext - Manages tool selection, UI state, and workflow coordination
+ * Reduces prop drilling and improves performance through selective subscriptions
+ */
+
+import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
+import { useToolManagement } from '../hooks/useToolManagement';
+import { ToolConfiguration } from '../types/tool';
+import { PageEditorFunctions } from '../types/pageEditor';
+
+// State interface
+interface ToolWorkflowState {
+ // UI State
+ sidebarsVisible: boolean;
+ leftPanelView: 'toolPicker' | 'toolContent';
+ 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' }
+ | { 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
+interface ToolWorkflowContextValue extends ToolWorkflowState {
+ // Tool management (from hook)
+ selectedToolKey: string | null;
+ selectedTool: ToolConfiguration | null;
+ toolRegistry: any; // From useToolManagement
+
+ // UI Actions
+ setSidebarsVisible: (visible: boolean) => void;
+ setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void;
+ setReaderMode: (mode: boolean) => void;
+ setPreviewFile: (file: File | null) => void;
+ setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
+ setSearchQuery: (query: string) => void;
+
+ // Tool Actions
+ selectTool: (toolId: string) => void;
+ clearToolSelection: () => void;
+
+ // Workflow Actions (compound actions)
+ handleToolSelect: (toolId: string) => void;
+ handleBackToTools: () => void;
+ handleReaderToggle: () => void;
+
+ // Computed values
+ filteredTools: [string, any][]; // Filtered by search
+ isPanelVisible: boolean;
+}
+
+const ToolWorkflowContext = createContext
(undefined);
+
+// Provider component
+interface ToolWorkflowProviderProps {
+ children: React.ReactNode;
+ /** Handler for view changes (passed from parent) */
+ onViewChange?: (view: string) => void;
+}
+
+export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowProviderProps) {
+ const [state, dispatch] = useReducer(toolWorkflowReducer, initialState);
+
+ // Tool management hook
+ const {
+ selectedToolKey,
+ selectedTool,
+ toolRegistry,
+ selectTool,
+ clearToolSelection,
+ } = useToolManagement();
+
+ // UI Action creators
+ const setSidebarsVisible = useCallback((visible: boolean) => {
+ dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
+ }, []);
+
+ const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent') => {
+ dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view });
+ }, []);
+
+ const setReaderMode = useCallback((mode: boolean) => {
+ dispatch({ type: 'SET_READER_MODE', payload: mode });
+ }, []);
+
+ const setPreviewFile = useCallback((file: File | null) => {
+ dispatch({ type: 'SET_PREVIEW_FILE', payload: file });
+ }, []);
+
+ const setPageEditorFunctions = useCallback((functions: PageEditorFunctions | null) => {
+ dispatch({ type: 'SET_PAGE_EDITOR_FUNCTIONS', payload: functions });
+ }, []);
+
+ const setSearchQuery = useCallback((query: string) => {
+ dispatch({ type: 'SET_SEARCH_QUERY', payload: query });
+ }, []);
+
+ // Workflow actions (compound actions that coordinate multiple state changes)
+ const handleToolSelect = useCallback((toolId: string) => {
+ selectTool(toolId);
+ onViewChange?.('fileEditor');
+ setLeftPanelView('toolContent');
+ setReaderMode(false);
+ }, [selectTool, onViewChange, setLeftPanelView, setReaderMode]);
+
+ const handleBackToTools = useCallback(() => {
+ setLeftPanelView('toolPicker');
+ setReaderMode(false);
+ clearToolSelection();
+ }, [setLeftPanelView, setReaderMode, clearToolSelection]);
+
+ const handleReaderToggle = useCallback(() => {
+ setReaderMode(true);
+ }, [setReaderMode]);
+
+ // Computed values (memoized to prevent unnecessary recalculations)
+ // Separate filteredTools memoization to avoid context re-renders
+ const filteredTools = useMemo(() => {
+ if (!toolRegistry) return [];
+ return Object.entries(toolRegistry).filter(([_, { name }]) =>
+ name.toLowerCase().includes(state.searchQuery.toLowerCase())
+ );
+ }, [toolRegistry, state.searchQuery]);
+
+ const isPanelVisible = useMemo(() =>
+ state.sidebarsVisible && !state.readerMode,
+ [state.sidebarsVisible, state.readerMode]
+ );
+
+ // Context value (memoized to prevent unnecessary rerenders)
+ const contextValue = useMemo((): ToolWorkflowContextValue => ({
+ // State - destructure to avoid object reference changes
+ sidebarsVisible: state.sidebarsVisible,
+ leftPanelView: state.leftPanelView,
+ readerMode: state.readerMode,
+ previewFile: state.previewFile,
+ pageEditorFunctions: state.pageEditorFunctions,
+ searchQuery: state.searchQuery,
+
+ // Tool state
+ selectedToolKey,
+ selectedTool,
+ toolRegistry,
+
+ // Actions
+ setSidebarsVisible,
+ setLeftPanelView,
+ setReaderMode,
+ setPreviewFile,
+ setPageEditorFunctions,
+ setSearchQuery,
+ selectTool,
+ clearToolSelection,
+
+ // Workflow Actions
+ handleToolSelect,
+ handleBackToTools,
+ handleReaderToggle,
+
+ // Computed
+ filteredTools,
+ isPanelVisible,
+ }), [
+ // State values (not the state object itself)
+ state.sidebarsVisible,
+ state.leftPanelView,
+ state.readerMode,
+ state.previewFile,
+ state.pageEditorFunctions,
+ state.searchQuery,
+
+ // Tool state (toolRegistry removed from deps - it's passed through but doesn't affect memoization)
+ selectedToolKey,
+ selectedTool,
+
+ // Actions are stable due to useCallback
+ setSidebarsVisible,
+ setLeftPanelView,
+ setReaderMode,
+ setPreviewFile,
+ setPageEditorFunctions,
+ setSearchQuery,
+ selectTool,
+ clearToolSelection,
+ handleToolSelect,
+ handleBackToTools,
+ handleReaderToggle,
+
+ // Computed values
+ filteredTools,
+ isPanelVisible,
+ ]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Custom hook to use the context
+export function useToolWorkflow(): ToolWorkflowContextValue {
+ const context = useContext(ToolWorkflowContext);
+ if (!context) {
+ throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider');
+ }
+ return context;
+}
+
+// Selective hooks for performance (only subscribe to specific parts of state)
+export function useToolSelection() {
+ const { selectedToolKey, selectedTool, selectTool, clearToolSelection, handleToolSelect } = useToolWorkflow();
+ return { selectedToolKey, selectedTool, selectTool, clearToolSelection, handleToolSelect };
+}
+
+export function useToolPanelState() {
+ const {
+ leftPanelView,
+ isPanelVisible,
+ searchQuery,
+ filteredTools,
+ setLeftPanelView,
+ setSearchQuery,
+ handleBackToTools
+ } = useToolWorkflow();
+ return {
+ leftPanelView,
+ isPanelVisible,
+ searchQuery,
+ filteredTools,
+ setLeftPanelView,
+ setSearchQuery,
+ handleBackToTools
+ };
+}
+
+export function useWorkbenchState() {
+ const {
+ previewFile,
+ pageEditorFunctions,
+ sidebarsVisible,
+ setPreviewFile,
+ setPageEditorFunctions,
+ setSidebarsVisible
+ } = useToolWorkflow();
+ return {
+ previewFile,
+ pageEditorFunctions,
+ sidebarsVisible,
+ setPreviewFile,
+ setPageEditorFunctions,
+ setSidebarsVisible
+ };
+}
\ No newline at end of file
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx
index 6df32611f..5e01df805 100644
--- a/frontend/src/pages/HomePage.tsx
+++ b/frontend/src/pages/HomePage.tsx
@@ -1,11 +1,10 @@
-import React, { useState, useCallback, useEffect, useRef } from "react";
+import React, { useCallback, useEffect } from "react";
import { useTranslation } from 'react-i18next';
import { useFileContext } from "../contexts/FileContext";
import { FileSelectionProvider, useFileSelection } from "../contexts/FileSelectionContext";
-import { useToolManagement } from "../hooks/useToolManagement";
+import { ToolWorkflowProvider, useToolSelection } from "../contexts/ToolWorkflowContext";
import { useFileHandler } from "../hooks/useFileHandler";
import { Group } from "@mantine/core";
-import { PageEditorFunctions } from "../types/pageEditor";
import ToolPanel from "../components/tools/ToolPanel";
import Workbench from "../components/layout/Workbench";
@@ -20,19 +19,7 @@ function HomePageContent() {
const { setMaxFiles, setIsToolMode, setSelectedFiles } = useFileSelection();
const { addToActiveFiles } = useFileHandler();
- const {
- selectedToolKey,
- selectedTool,
- toolRegistry,
- selectTool,
- clearToolSelection,
- } = useToolManagement();
-
- const [sidebarsVisible, setSidebarsVisible] = useState(true);
- const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
- const [readerMode, setReaderMode] = useState(false);
- const [pageEditorFunctions, setPageEditorFunctions] = useState(null);
- const [previewFile, setPreviewFile] = useState(null);
+ const { selectedTool, selectedToolKey } = useToolSelection();
// Update file selection context when tool changes
useEffect(() => {
@@ -46,25 +33,8 @@ function HomePageContent() {
}
}, [selectedTool, setMaxFiles, setIsToolMode, setSelectedFiles]);
- const handleToolSelect = useCallback(
- (id: string) => {
- selectTool(id);
- setCurrentView('fileEditor'); // Tools use fileEditor view for file selection
- setLeftPanelView('toolContent');
- setReaderMode(false);
- },
- [selectTool, setCurrentView]
- );
-
- const handleQuickAccessTools = useCallback(() => {
- setLeftPanelView('toolPicker');
- setReaderMode(false);
- clearToolSelection();
- }, [clearToolSelection]);
-
- const handleReaderToggle = useCallback(() => {
- setReaderMode(true);
- }, [readerMode]);
+ // These handlers are now provided by the context
+ // The context handles the coordination between tool selection and UI state
const handleViewChange = useCallback((view: string) => {
setCurrentView(view as any);
@@ -76,41 +46,14 @@ function HomePageContent() {
gap={0}
className="min-h-screen w-screen overflow-hidden flex-nowrap flex"
>
-
+
-
+
@@ -120,11 +63,22 @@ function HomePageContent() {
);
}
-// Main HomePage component wrapped with FileSelectionProvider
+// HomePage wrapper that connects context to file context
+function HomePageWrapper() {
+ const { setCurrentView } = useFileContext();
+
+ return (
+
+
+
+ );
+}
+
+// Main HomePage component wrapped with providers
export default function HomePage() {
return (
-
+
);
}