;
+ sidebarRefs?: SidebarRefs;
+ sidebarState?: SidebarState;
+}): PositionState {
+ const [coords, setCoords] = useState<{ top: number; left: number; arrowOffset: number | null }>({
+ top: 0,
+ left: 0,
+ arrowOffset: null
+ });
+ const [positionReady, setPositionReady] = useState(false);
+
+ // Fallback sidebar position (only used as last resort)
+ const sidebarLeft = 240;
+
+ const updatePosition = () => {
+ if (!triggerRef.current || !open) return;
+
+ const triggerRect = triggerRef.current.getBoundingClientRect();
+
+ let top: number;
+ let left: number;
+ let arrowOffset: number | null = null;
+
+ if (sidebarTooltip) {
+ // Require sidebar refs and state for proper positioning
+ if (!sidebarRefs || !sidebarState) {
+ console.warn('⚠️ Sidebar tooltip requires sidebarRefs and sidebarState props');
+ setPositionReady(false);
+ return;
+ }
+
+ const sidebarInfo = getSidebarInfo(sidebarRefs, sidebarState);
+ const currentSidebarRight = sidebarInfo.rect ? sidebarInfo.rect.right : sidebarLeft;
+
+ // Only show tooltip if we have the tool panel active
+ if (!sidebarInfo.isToolPanelActive) {
+ console.log('🚫 Not showing tooltip - tool panel not active');
+ setPositionReady(false);
+ return;
+ }
+
+ // Position to the right of active sidebar with 20px gap
+ left = currentSidebarRight + 20;
+ top = triggerRect.top; // Align top of tooltip with trigger element
+
+ // Only clamp if we have tooltip dimensions
+ if (tooltipRef.current) {
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
+ const maxTop = window.innerHeight - tooltipRect.height - 4;
+ const originalTop = top;
+ top = clamp(top, 4, maxTop);
+
+ // If tooltip was clamped, adjust arrow position to stay aligned with trigger
+ if (originalTop !== top) {
+ arrowOffset = triggerRect.top + triggerRect.height / 2 - top;
+ }
+ }
+
+ setCoords({ top, left, arrowOffset });
+ setPositionReady(true);
+ } else {
+ // Regular tooltip logic
+ if (!tooltipRef.current) return;
+
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
+ const placement = place(triggerRect, tooltipRect, position, gap);
+ top = placement.top;
+ left = placement.left;
+
+ // Clamp to viewport
+ top = clamp(top, 4, window.innerHeight - tooltipRect.height - 4);
+ left = clamp(left, 4, window.innerWidth - tooltipRect.width - 4);
+
+ // Calculate arrow position to stay aligned with trigger
+ if (position === 'top' || position === 'bottom') {
+ // For top/bottom arrows, adjust horizontal position
+ const triggerCenter = triggerRect.left + triggerRect.width / 2;
+ const tooltipCenter = left + tooltipRect.width / 2;
+ if (Math.abs(triggerCenter - tooltipCenter) > 4) {
+ // Arrow needs adjustment
+ arrowOffset = triggerCenter - left - 4; // 4px is half arrow width
+ }
+ } else {
+ // For left/right arrows, adjust vertical position
+ const triggerCenter = triggerRect.top + triggerRect.height / 2;
+ const tooltipCenter = top + tooltipRect.height / 2;
+ if (Math.abs(triggerCenter - tooltipCenter) > 4) {
+ // Arrow needs adjustment
+ arrowOffset = triggerCenter - top - 4; // 4px is half arrow height
+ }
+ }
+
+ setCoords({ top, left, arrowOffset });
+ setPositionReady(true);
+ }
+ };
+
+ useEffect(() => {
+ if (!open) return;
+
+ requestAnimationFrame(updatePosition);
+
+ const handleUpdate = () => requestAnimationFrame(updatePosition);
+ window.addEventListener('scroll', handleUpdate, true);
+ window.addEventListener('resize', handleUpdate);
+
+ return () => {
+ window.removeEventListener('scroll', handleUpdate, true);
+ window.removeEventListener('resize', handleUpdate);
+ };
+ }, [open, sidebarLeft, position, gap, sidebarTooltip]);
+
+ return { coords, positionReady };
+}
\ No newline at end of file
diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx
index f86d0d229..5913a84e7 100644
--- a/frontend/src/pages/HomePage.tsx
+++ b/frontend/src/pages/HomePage.tsx
@@ -2,11 +2,13 @@ import React, { useState, useCallback, useEffect, useRef } from "react";
import { useTranslation } from 'react-i18next';
import { useFileContext } from "../contexts/FileContext";
import { FileSelectionProvider, useFileSelection } from "../contexts/FileSelectionContext";
+import { SidebarProvider, useSidebarContext } from "../contexts/SidebarContext";
import { useToolManagement } from "../hooks/useToolManagement";
import { useFileHandler } from "../hooks/useFileHandler";
import { Group, Box, Button } from "@mantine/core";
import { useRainbowThemeContext } from "../components/shared/RainbowThemeProvider";
import { PageEditorFunctions } from "../types/pageEditor";
+import { SidebarRefs, SidebarState } from "../types/sidebar";
import rainbowStyles from '../styles/rainbow.module.css';
import ToolPicker from "../components/tools/ToolPicker";
@@ -21,9 +23,20 @@ import QuickAccessBar from "../components/shared/QuickAccessBar";
import LandingPage from "../components/shared/LandingPage";
import FileUploadModal from "../components/shared/FileUploadModal";
+
function HomePageContent() {
const { t } = useTranslation();
const { isRainbowMode } = useRainbowThemeContext();
+ const {
+ sidebarState,
+ sidebarRefs,
+ setSidebarsVisible,
+ setLeftPanelView,
+ setReaderMode
+ } = useSidebarContext();
+
+ const { sidebarsVisible, leftPanelView, readerMode } = sidebarState;
+ const { quickAccessRef, toolPanelRef } = sidebarRefs;
const fileContext = useFileContext();
const { activeFiles, currentView, setCurrentView } = fileContext;
@@ -101,17 +114,16 @@ function HomePageContent() {
>
{/* Quick Access Bar */}
{/* Left: Tool Picker or Selected Tool Panel */}
-
+
+
+
);
}
diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css
index cc8f8e543..88df7eeb0 100644
--- a/frontend/src/styles/theme.css
+++ b/frontend/src/styles/theme.css
@@ -103,6 +103,13 @@
--icon-config-bg: #9CA3AF;
--icon-config-color: #FFFFFF;
+ /* Colors for tooltips */
+ --tooltip-title-bg: #DBEFFF;
+ --tooltip-title-color: #31528E;
+ --tooltip-header-bg: #31528E;
+ --tooltip-header-color: white;
+ --tooltip-border: var(--border-default);
+
/* Inactive icon colors for light mode */
--icon-inactive-bg: #9CA3AF;
--icon-inactive-color: #FFFFFF;
@@ -119,6 +126,29 @@
/* Subcategory title styling (light mode) */
--tool-subcategory-text-color: #6B7280;
+ --accent-interactive: #4A90E2;
+ --text-instruction: #4A90E2;
+ --text-brand: var(--color-gray-700);
+ --text-brand-accent: #DC2626;
+
+ /* container */
+ --landing-paper-bg: var(--bg-surface);
+ --landing-inner-paper-bg: #EEF8FF;
+ --landing-inner-paper-border: #CDEAFF;
+ --landing-button-bg: var(--bg-surface);
+ --landing-button-color: var(--icon-tools-bg);
+ --landing-button-border: #E0F2F7;
+ --landing-button-hover-bg: rgb(251, 251, 251);
+
+ /* drop state */
+ --landing-drop-paper-bg: #E3F2FD;
+ --landing-drop-inner-paper-bg: #BBDEFB;
+ --landing-drop-inner-paper-border: #90CAF9;
+
+ /* shadows */
+ --drop-shadow-color: rgba(0, 0, 0, 0.08);
+ --drop-shadow-color-strong: rgba(0, 0, 0, 0.04);
+ --drop-shadow-filter: drop-shadow(0 0.2rem 0.4rem rgba(0, 0, 0, 0.08)) drop-shadow(0 0.6rem 0.6rem rgba(0, 0, 0, 0.06)) drop-shadow(0 1.2rem 1rem rgba(0, 0, 0, 0.04));
}
[data-mantine-color-scheme="dark"] {
@@ -190,6 +220,37 @@
--icon-inactive-bg: #2A2F36;
--icon-inactive-color: #6E7581;
+ /* Dark mode tooltip colors */
+ --tooltip-title-bg: #4B525A;
+ --tooltip-title-color: #fff;
+ --tooltip-header-bg: var(--bg-raised);
+ --tooltip-header-color: var(--text-primary);
+ --tooltip-border: var(--border-default);
+
+ --accent-interactive: #ffffff;
+ --text-instruction: #ffffff;
+ --text-brand: var(--color-gray-800);
+ --text-brand-accent: #EF4444;
+
+ /* container */
+ --landing-paper-bg: #171A1F;
+ --landing-inner-paper-bg: var(--bg-raised);
+ --landing-inner-paper-border: #2D3237;
+ --landing-button-bg: #2B3037;
+ --landing-button-color: #ffffff;
+ --landing-button-border: #2D3237;
+ --landing-button-hover-bg: #4c525b;
+
+ /* drop state */
+ --landing-drop-paper-bg: #1A2332;
+ --landing-drop-inner-paper-bg: #2A3441;
+ --landing-drop-inner-paper-border: #3A4451;
+
+ /* shadows */
+ --drop-shadow-color: rgba(255, 255, 255, 0.08);
+ --drop-shadow-color-strong: rgba(255, 255, 255, 0.04);
+ --drop-shadow-filter: drop-shadow(0 0.2rem 0.4rem rgba(200, 200, 200, 0.08)) drop-shadow(0 0.6rem 0.6rem rgba(200, 200, 200, 0.06)) drop-shadow(0 1.2rem 1rem rgba(200, 200, 200, 0.04));
+
/* Adjust shadows for dark mode */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4);
@@ -210,6 +271,12 @@
--tool-subcategory-text-color: #6B7280;
}
+/* Dropzone drop state styling */
+[data-accept] .dropzone-inner {
+ background-color: var(--landing-drop-inner-paper-bg) !important;
+ border-color: var(--landing-drop-inner-paper-border) !important;
+}
+
/* Smooth transitions for theme switching */
* {
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx
index cc0cd5cbc..f4b50b264 100644
--- a/frontend/src/tools/Compress.tsx
+++ b/frontend/src/tools/Compress.tsx
@@ -17,6 +17,7 @@ import CompressSettings from "../components/tools/compress/CompressSettings";
import { useCompressParameters } from "../hooks/tools/compress/useCompressParameters";
import { useCompressOperation } from "../hooks/tools/compress/useCompressOperation";
import { BaseToolProps } from "../types/tool";
+import { CompressTips } from "../components/tooltips/CompressTips";
const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
@@ -25,6 +26,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const compressParams = useCompressParameters();
const compressOperation = useCompressOperation();
+ const compressTips = CompressTips();
// Endpoint validation
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf");
@@ -104,6 +106,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
isCompleted={settingsCollapsed}
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
completedMessage={settingsCollapsed ? "Compression completed" : undefined}
+ tooltip={compressTips}
>
{
const { t } = useTranslation();
@@ -26,6 +27,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const ocrParams = useOCRParameters();
const ocrOperation = useOCROperation();
+ const ocrTips = OcrTips();
// Step expansion state management
const [expandedStep, setExpandedStep] = useState<'files' | 'settings' | 'advanced' | null>('files');
@@ -126,6 +128,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
setExpandedStep(expandedStep === 'settings' ? null : 'settings');
}}
completedMessage={hasFiles && hasValidSettings && settingsCollapsed ? "Basic settings configured" : undefined}
+ tooltip={ocrTips}
>
;
+ toolPanelRef: React.RefObject;
+}
+
+export interface SidebarInfo {
+ rect: DOMRect | null;
+ isToolPanelActive: boolean;
+ sidebarState: SidebarState;
+}
+
+// Context-related interfaces
+export interface SidebarContextValue {
+ sidebarState: SidebarState;
+ sidebarRefs: SidebarRefs;
+ setSidebarsVisible: React.Dispatch>;
+ setLeftPanelView: React.Dispatch>;
+ setReaderMode: React.Dispatch>;
+}
+
+export interface SidebarProviderProps {
+ children: React.ReactNode;
+}
+
+// QuickAccessBar related interfaces
+export interface QuickAccessBarProps {
+ onToolsClick: () => void;
+ onReaderToggle: () => void;
+}
+
+export interface ButtonConfig {
+ id: string;
+ name: string;
+ icon: React.ReactNode;
+ tooltip: string;
+ isRound?: boolean;
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ onClick: () => void;
+ type?: 'navigation' | 'modal' | 'action';
+}
diff --git a/frontend/src/types/tips.ts b/frontend/src/types/tips.ts
new file mode 100644
index 000000000..58519e114
--- /dev/null
+++ b/frontend/src/types/tips.ts
@@ -0,0 +1,13 @@
+export interface TooltipContent {
+ header?: {
+ title: string;
+ logo?: string | React.ReactNode;
+ };
+ tips?: Array<{
+ title?: string;
+ description?: string;
+ bullets?: string[];
+ body?: React.ReactNode;
+ }>;
+ content?: React.ReactNode;
+}
\ No newline at end of file
diff --git a/frontend/src/utils/genericUtils.ts b/frontend/src/utils/genericUtils.ts
new file mode 100644
index 000000000..253346292
--- /dev/null
+++ b/frontend/src/utils/genericUtils.ts
@@ -0,0 +1,42 @@
+/**
+ * DOM utility functions for common operations
+ */
+
+/**
+ * Clamps a value between a minimum and maximum
+ * @param value - The value to clamp
+ * @param min - The minimum allowed value
+ * @param max - The maximum allowed value
+ * @returns The clamped value
+ */
+export function clamp(value: number, min: number, max: number): number {
+ return Math.min(Math.max(value, min), max);
+}
+
+/**
+ * Safely adds an event listener with proper cleanup
+ * @param target - The target element or window/document
+ * @param event - The event type
+ * @param handler - The event handler function
+ * @param options - Event listener options
+ * @returns A cleanup function to remove the listener
+ */
+export function addEventListenerWithCleanup(
+ target: EventTarget,
+ event: string,
+ handler: EventListener,
+ options?: boolean | AddEventListenerOptions
+): () => void {
+ target.addEventListener(event, handler, options);
+ return () => target.removeEventListener(event, handler, options);
+}
+
+/**
+ * Checks if a click event occurred outside of a specified element
+ * @param event - The click event
+ * @param element - The element to check against
+ * @returns True if the click was outside the element
+ */
+export function isClickOutside(event: MouseEvent, element: HTMLElement | null): boolean {
+ return element ? !element.contains(event.target as Node) : true;
+}
diff --git a/frontend/src/utils/sidebarUtils.ts b/frontend/src/utils/sidebarUtils.ts
new file mode 100644
index 000000000..cef144971
--- /dev/null
+++ b/frontend/src/utils/sidebarUtils.ts
@@ -0,0 +1,34 @@
+import { SidebarRefs, SidebarState, SidebarInfo } from '../types/sidebar';
+
+/**
+ * Gets the All tools sidebar information using React refs and state
+ * @param refs - Object containing refs to sidebar elements
+ * @param state - Current sidebar state
+ * @returns Object containing the sidebar rect and whether the tool panel is active
+ */
+export function getSidebarInfo(refs: SidebarRefs, state: SidebarState): SidebarInfo {
+ const { quickAccessRef, toolPanelRef } = refs;
+ const { sidebarsVisible, readerMode } = state;
+
+ // Determine if tool panel should be active based on state
+ const isToolPanelActive = sidebarsVisible && !readerMode;
+
+ let rect: DOMRect | null = null;
+
+ if (isToolPanelActive && toolPanelRef.current) {
+ // Tool panel is expanded: use its rect
+ rect = toolPanelRef.current.getBoundingClientRect();
+ } else if (quickAccessRef.current) {
+ // Fall back to quick access bar
+ // This probably isn't needed but if we ever have tooltips or modals that need to be positioned relative to the quick access bar, we can use this
+ rect = quickAccessRef.current.getBoundingClientRect();
+ }
+
+ return {
+ rect,
+ isToolPanelActive,
+ sidebarState: state
+ };
+}
+
+
\ No newline at end of file