Feature/v2/all tools sidebar (#4151)

# Description of Changes

- Added the all tools sidebar
- Added a TextFit component that shrinks text to fit containers
- Added a TopToolIcon on the nav, that animates down to give users
feedback on what tool is selected
- Added the baseToolRegistry, to replace the old pattern of listing
tools, allowing us to clean up the ToolRegistry code
- Fixed Mantine light/dark theme race condition 
- General styling tweaks

---

## 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.
This commit is contained in:
EthanHealy01
2025-08-19 13:31:09 +01:00
committed by GitHub
parent c1b7911518
commit 8f32082145
40 changed files with 2617 additions and 451 deletions
+41 -20
View File
@@ -15,6 +15,7 @@ export interface TooltipProps {
children: React.ReactElement;
offset?: number;
maxWidth?: number | string;
minWidth?: number | string;
open?: boolean;
onOpenChange?: (open: boolean) => void;
arrow?: boolean;
@@ -23,6 +24,8 @@ export interface TooltipProps {
title: string;
logo?: React.ReactNode;
};
delay?: number;
containerStyle?: React.CSSProperties;
}
export const Tooltip: React.FC<TooltipProps> = ({
@@ -32,18 +35,28 @@ export const Tooltip: React.FC<TooltipProps> = ({
tips,
children,
offset: gap = 8,
maxWidth = 280,
maxWidth,
minWidth,
open: controlledOpen,
onOpenChange,
arrow = false,
portalTarget,
header,
delay = 0,
containerStyle={},
}) => {
const [internalOpen, setInternalOpen] = useState(false);
const [isPinned, setIsPinned] = useState(false);
const triggerRef = useRef<HTMLElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const openTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const clearTimers = () => {
if (openTimeoutRef.current) {
clearTimeout(openTimeoutRef.current);
openTimeoutRef.current = null;
}
};
// Get sidebar context for tooltip positioning
const sidebarContext = sidebarTooltip ? useSidebarContext() : null;
@@ -53,6 +66,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
const open = isControlled ? controlledOpen : internalOpen;
const handleOpenChange = (newOpen: boolean) => {
clearTimers();
if (isControlled) {
onOpenChange?.(newOpen);
} else {
@@ -63,6 +77,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
if (!newOpen) {
setIsPinned(false);
}
};
const handleTooltipClick = (e: React.MouseEvent) => {
@@ -97,6 +112,13 @@ export const Tooltip: React.FC<TooltipProps> = ({
}
}, [isPinned]);
useEffect(() => {
return () => {
clearTimers();
};
}, []);
const getArrowClass = () => {
// No arrow for sidebar tooltips
if (sidebarTooltip) return null;
@@ -118,8 +140,8 @@ export const Tooltip: React.FC<TooltipProps> = ({
'';
};
// Only show tooltip when position is ready and correct
const shouldShowTooltip = open && (sidebarTooltip ? positionReady : true);
// Always mount when open so we can measure; hide until positioned to avoid flash
const shouldShowTooltip = open;
const tooltipElement = shouldShowTooltip ? (
<div
@@ -128,11 +150,13 @@ export const Tooltip: React.FC<TooltipProps> = ({
position: 'fixed',
top: coords.top,
left: coords.left,
maxWidth,
width: (maxWidth !== undefined ? maxWidth : '25rem'),
minWidth: minWidth,
zIndex: 9999,
visibility: 'visible',
opacity: 1,
visibility: positionReady ? 'visible' : 'hidden',
opacity: positionReady ? 1 : 0,
color: 'var(--text-primary)',
...containerStyle,
}}
className={`${styles['tooltip-container']} ${isPinned ? styles.pinned : ''}`}
onClick={handleTooltipClick}
@@ -176,27 +200,23 @@ export const Tooltip: React.FC<TooltipProps> = ({
) : null;
const handleMouseEnter = (e: React.MouseEvent) => {
// Clear any existing timeout
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
}
// Only show on hover if not pinned
clearTimers();
if (!isPinned) {
handleOpenChange(true);
const effectiveDelay = Math.max(0, delay || 0);
openTimeoutRef.current = setTimeout(() => {
handleOpenChange(true);
}, effectiveDelay);
}
(children.props as any)?.onMouseEnter?.(e);
};
const handleMouseLeave = (e: React.MouseEvent) => {
// Only hide on mouse leave if not pinned
clearTimers();
openTimeoutRef.current = null;
if (!isPinned) {
// Add a small delay to prevent flickering
hoverTimeoutRef.current = setTimeout(() => {
handleOpenChange(false);
}, 100);
handleOpenChange(false);
}
(children.props as any)?.onMouseLeave?.(e);
@@ -207,6 +227,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
if (open) {
setIsPinned(!isPinned);
} else {
clearTimers();
handleOpenChange(true);
setIsPinned(true);
}