mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
use refs rather than document query selection to find tooltip position (or just to find sidebar rect for furture features that require it)
This commit is contained in:
parent
aff93c4008
commit
eee931e4a2
@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import React, { useState, useRef, forwardRef } from "react";
|
||||
import { ActionIcon, Stack, Tooltip, Divider } from "@mantine/core";
|
||||
import MenuBookIcon from "@mui/icons-material/MenuBookRounded";
|
||||
import AppsIcon from "@mui/icons-material/AppsRounded";
|
||||
@ -8,32 +8,12 @@ import FolderIcon from "@mui/icons-material/FolderRounded";
|
||||
import PersonIcon from "@mui/icons-material/PersonRounded";
|
||||
import NotificationsIcon from "@mui/icons-material/NotificationsRounded";
|
||||
import { useRainbowThemeContext } from "./RainbowThemeProvider";
|
||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||
import AppConfigModal from './AppConfigModal';
|
||||
import { useIsOverflowing } from '../../hooks/useIsOverflowing';
|
||||
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||
import { QuickAccessBarProps, ButtonConfig } from '../../types/sidebar';
|
||||
import './QuickAccessBar.css';
|
||||
|
||||
interface QuickAccessBarProps {
|
||||
onToolsClick: () => void;
|
||||
onReaderToggle: () => void;
|
||||
selectedToolKey?: string;
|
||||
toolRegistry: any;
|
||||
leftPanelView: 'toolPicker' | 'toolContent';
|
||||
readerMode: boolean;
|
||||
}
|
||||
|
||||
interface ButtonConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: React.ReactNode;
|
||||
tooltip: string;
|
||||
isRound?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
onClick: () => void;
|
||||
type?: 'navigation' | 'modal' | 'action'; // navigation = main nav, modal = triggers modal, action = other actions
|
||||
}
|
||||
|
||||
function NavHeader({
|
||||
activeButton,
|
||||
setActiveButton,
|
||||
@ -104,14 +84,10 @@ function NavHeader({
|
||||
);
|
||||
}
|
||||
|
||||
const QuickAccessBar = ({
|
||||
const QuickAccessBar = forwardRef<HTMLDivElement, QuickAccessBarProps>(({
|
||||
onToolsClick,
|
||||
onReaderToggle,
|
||||
selectedToolKey,
|
||||
toolRegistry,
|
||||
leftPanelView,
|
||||
readerMode,
|
||||
}: QuickAccessBarProps) => {
|
||||
}, ref) => {
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
@ -234,6 +210,7 @@ const QuickAccessBar = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="quick-access"
|
||||
className={`h-screen flex flex-col w-20 quick-access-bar-main ${isRainbowMode ? 'rainbow-mode' : ''}`}
|
||||
>
|
||||
@ -336,6 +313,6 @@ const QuickAccessBar = ({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default QuickAccessBar;
|
@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
|
||||
import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils';
|
||||
import { useTooltipPosition } from '../../hooks/useTooltipPosition';
|
||||
import { TooltipContent, TooltipTip } from './tooltip/TooltipContent';
|
||||
import { useSidebarContext } from '../../contexts/SidebarContext';
|
||||
import styles from './tooltip/Tooltip.module.css'
|
||||
|
||||
export interface TooltipProps {
|
||||
@ -43,6 +44,9 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Get sidebar context for tooltip positioning
|
||||
const sidebarContext = sidebarTooltip ? useSidebarContext() : null;
|
||||
|
||||
// Always use controlled mode - if no controlled props provided, use internal state
|
||||
const isControlled = controlledOpen !== undefined;
|
||||
const open = isControlled ? controlledOpen : internalOpen;
|
||||
@ -80,7 +84,9 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
position,
|
||||
gap,
|
||||
triggerRef,
|
||||
tooltipRef
|
||||
tooltipRef,
|
||||
sidebarRefs: sidebarContext?.sidebarRefs,
|
||||
sidebarState: sidebarContext?.sidebarState
|
||||
});
|
||||
|
||||
// Add document click listener for unpinning
|
||||
|
@ -217,5 +217,7 @@ Links automatically get proper styling with hover states and open in new tabs wh
|
||||
### Sidebar Tooltips
|
||||
- When `sidebarTooltip={true}`, horizontal positioning is locked to the right of the sidebar
|
||||
- Vertical positioning follows the trigger but clamps to viewport
|
||||
- Automatically detects sidebar width or falls back to 240px
|
||||
- **Smart sidebar detection**: Uses `getSidebarInfo()` to determine which sidebar is active (tool panel vs quick access bar) and gets its exact position
|
||||
- **Dynamic positioning**: Adapts to whether the tool panel is expanded or collapsed
|
||||
- **Conditional display**: Only shows tooltips when the tool panel is active (`sidebarInfo.isToolPanelActive`)
|
||||
- **No arrows** - sidebar tooltips don't show arrows
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from './types';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const CompressTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from './types';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const OcrTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
47
frontend/src/contexts/SidebarContext.tsx
Normal file
47
frontend/src/contexts/SidebarContext.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { createContext, useContext, useState, useRef } from 'react';
|
||||
import { SidebarState, SidebarRefs, SidebarContextValue, SidebarProviderProps } from '../types/sidebar';
|
||||
|
||||
const SidebarContext = createContext<SidebarContextValue | undefined>(undefined);
|
||||
|
||||
export function SidebarProvider({ children }: SidebarProviderProps) {
|
||||
// All sidebar state management
|
||||
const quickAccessRef = useRef<HTMLDivElement>(null);
|
||||
const toolPanelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
||||
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
||||
const [readerMode, setReaderMode] = useState(false);
|
||||
|
||||
const sidebarState: SidebarState = {
|
||||
sidebarsVisible,
|
||||
leftPanelView,
|
||||
readerMode,
|
||||
};
|
||||
|
||||
const sidebarRefs: SidebarRefs = {
|
||||
quickAccessRef,
|
||||
toolPanelRef,
|
||||
};
|
||||
|
||||
const contextValue: SidebarContextValue = {
|
||||
sidebarState,
|
||||
sidebarRefs,
|
||||
setSidebarsVisible,
|
||||
setLeftPanelView,
|
||||
setReaderMode,
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSidebarContext(): SidebarContextValue {
|
||||
const context = useContext(SidebarContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useSidebarContext must be used within a SidebarProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { clamp } from '../utils/genericUtils';
|
||||
import { getSidebarRect } from '../utils/sidebarUtils';
|
||||
import { getSidebarInfo } from '../utils/sidebarUtils';
|
||||
import { SidebarRefs, SidebarState } from '../types/sidebar';
|
||||
|
||||
type Position = 'right' | 'left' | 'top' | 'bottom';
|
||||
|
||||
@ -51,7 +52,9 @@ export function useTooltipPosition({
|
||||
position,
|
||||
gap,
|
||||
triggerRef,
|
||||
tooltipRef
|
||||
tooltipRef,
|
||||
sidebarRefs,
|
||||
sidebarState
|
||||
}: {
|
||||
open: boolean;
|
||||
sidebarTooltip: boolean;
|
||||
@ -59,6 +62,8 @@ export function useTooltipPosition({
|
||||
gap: number;
|
||||
triggerRef: React.RefObject<HTMLElement | null>;
|
||||
tooltipRef: React.RefObject<HTMLDivElement | null>;
|
||||
sidebarRefs?: SidebarRefs;
|
||||
sidebarState?: SidebarState;
|
||||
}): PositionState {
|
||||
const [coords, setCoords] = useState<{ top: number; left: number; arrowOffset: number | null }>({
|
||||
top: 0,
|
||||
@ -67,12 +72,8 @@ export function useTooltipPosition({
|
||||
});
|
||||
const [positionReady, setPositionReady] = useState(false);
|
||||
|
||||
// Memoize sidebar position for performance
|
||||
const sidebarLeft = useMemo(() => {
|
||||
if (!sidebarTooltip) return 0;
|
||||
const sidebarInfo = getSidebarRect();
|
||||
return sidebarInfo.rect ? sidebarInfo.rect.right : 240;
|
||||
}, [sidebarTooltip]);
|
||||
// Fallback sidebar position (only used as last resort)
|
||||
const sidebarLeft = 240;
|
||||
|
||||
const updatePosition = () => {
|
||||
if (!triggerRef.current || !open) return;
|
||||
@ -84,18 +85,24 @@ export function useTooltipPosition({
|
||||
let arrowOffset: number | null = null;
|
||||
|
||||
if (sidebarTooltip) {
|
||||
// Get fresh sidebar position each time
|
||||
const sidebarInfo = getSidebarRect();
|
||||
const currentSidebarRight = sidebarInfo.rect ? sidebarInfo.rect.right : sidebarLeft;
|
||||
|
||||
// Only show tooltip if we have the correct sidebar (ToolPanel)
|
||||
if (!sidebarInfo.isCorrectSidebar) {
|
||||
console.log('🚫 Not showing tooltip - wrong sidebar detected');
|
||||
// Require sidebar refs and state for proper positioning
|
||||
if (!sidebarRefs || !sidebarState) {
|
||||
console.warn('⚠️ Sidebar tooltip requires sidebarRefs and sidebarState props');
|
||||
setPositionReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Position to the right of correct sidebar with 20px gap
|
||||
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
|
||||
|
||||
|
@ -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";
|
||||
@ -20,9 +22,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;
|
||||
@ -37,9 +50,6 @@ function HomePageContent() {
|
||||
clearToolSelection,
|
||||
} = useToolManagement();
|
||||
|
||||
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
||||
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
||||
const [readerMode, setReaderMode] = useState(false);
|
||||
const [pageEditorFunctions, setPageEditorFunctions] = useState<PageEditorFunctions | null>(null);
|
||||
const [previewFile, setPreviewFile] = useState<File | null>(null);
|
||||
|
||||
@ -92,16 +102,14 @@ function HomePageContent() {
|
||||
>
|
||||
{/* Quick Access Bar */}
|
||||
<QuickAccessBar
|
||||
ref={quickAccessRef}
|
||||
onToolsClick={handleQuickAccessTools}
|
||||
onReaderToggle={handleReaderToggle}
|
||||
selectedToolKey={selectedToolKey}
|
||||
toolRegistry={toolRegistry}
|
||||
leftPanelView={leftPanelView}
|
||||
readerMode={readerMode}
|
||||
/>
|
||||
|
||||
{/* Left: Tool Picker or Selected Tool Panel */}
|
||||
<div
|
||||
ref={toolPanelRef}
|
||||
data-sidebar="tool-panel"
|
||||
className={`h-screen flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${isRainbowMode ? rainbowStyles.rainbowPaper : ''}`}
|
||||
style={{
|
||||
@ -280,7 +288,9 @@ function HomePageContent() {
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<FileSelectionProvider>
|
||||
<HomePageContent />
|
||||
<SidebarProvider>
|
||||
<HomePageContent />
|
||||
</SidebarProvider>
|
||||
</FileSelectionProvider>
|
||||
);
|
||||
}
|
||||
|
46
frontend/src/types/sidebar.ts
Normal file
46
frontend/src/types/sidebar.ts
Normal file
@ -0,0 +1,46 @@
|
||||
export interface SidebarState {
|
||||
sidebarsVisible: boolean;
|
||||
leftPanelView: 'toolPicker' | 'toolContent';
|
||||
readerMode: boolean;
|
||||
}
|
||||
|
||||
export interface SidebarRefs {
|
||||
quickAccessRef: React.RefObject<HTMLDivElement | null>;
|
||||
toolPanelRef: React.RefObject<HTMLDivElement | null>;
|
||||
}
|
||||
|
||||
export interface SidebarInfo {
|
||||
rect: DOMRect | null;
|
||||
isToolPanelActive: boolean;
|
||||
sidebarState: SidebarState;
|
||||
}
|
||||
|
||||
// Context-related interfaces
|
||||
export interface SidebarContextValue {
|
||||
sidebarState: SidebarState;
|
||||
sidebarRefs: SidebarRefs;
|
||||
setSidebarsVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setLeftPanelView: React.Dispatch<React.SetStateAction<'toolPicker' | 'toolContent'>>;
|
||||
setReaderMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
@ -1,50 +1,34 @@
|
||||
import { SidebarRefs, SidebarState, SidebarInfo } from '../types/sidebar';
|
||||
|
||||
/**
|
||||
* Gets the All tools sidebar
|
||||
* @returns Object containing the sidebar rect and whether it's the correct 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 getSidebarRect(): { rect: DOMRect | null, isCorrectSidebar: boolean } {
|
||||
// Find the rightmost sidebar - this will be the "All Tools" expanded panel
|
||||
const allSidebars = [];
|
||||
export function getSidebarInfo(refs: SidebarRefs, state: SidebarState): SidebarInfo {
|
||||
const { quickAccessRef, toolPanelRef } = refs;
|
||||
const { sidebarsVisible, readerMode } = state;
|
||||
|
||||
// Find the QuickAccessBar (narrow left bar)
|
||||
const quickAccessBar = document.querySelector('[data-sidebar="quick-access"]');
|
||||
if (quickAccessBar) {
|
||||
const rect = quickAccessBar.getBoundingClientRect();
|
||||
if (rect.width > 0) {
|
||||
allSidebars.push({
|
||||
element: 'QuickAccessBar',
|
||||
selector: '[data-sidebar="quick-access"]',
|
||||
rect
|
||||
});
|
||||
}
|
||||
}
|
||||
// Determine if tool panel should be active based on state
|
||||
const isToolPanelActive = sidebarsVisible && !readerMode;
|
||||
|
||||
// Find the tool panel (the expanded "All Tools" panel)
|
||||
const toolPanel = document.querySelector('[data-sidebar="tool-panel"]');
|
||||
if (toolPanel) {
|
||||
const rect = toolPanel.getBoundingClientRect();
|
||||
if (rect.width > 0) {
|
||||
allSidebars.push({
|
||||
element: 'ToolPanel',
|
||||
selector: '[data-sidebar="tool-panel"]',
|
||||
rect
|
||||
});
|
||||
}
|
||||
}
|
||||
let rect: DOMRect | null = null;
|
||||
|
||||
// Use the rightmost sidebar (which should be the tool panel when expanded)
|
||||
if (allSidebars.length > 0) {
|
||||
const rightmostSidebar = allSidebars.reduce((rightmost, current) => {
|
||||
return current.rect.right > rightmost.rect.right ? current : rightmost;
|
||||
});
|
||||
|
||||
// Only consider it correct if we're using the ToolPanel (expanded All Tools sidebar)
|
||||
const isCorrectSidebar = rightmostSidebar.element === 'ToolPanel';
|
||||
return { rect: rightmostSidebar.rect, isCorrectSidebar };
|
||||
}
|
||||
|
||||
console.warn('⚠️ No sidebars found, using fallback positioning');
|
||||
// Final fallback
|
||||
return { rect: new DOMRect(0, 0, 280, window.innerHeight), isCorrectSidebar: false };
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user