mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-22 04:09:22 +00:00
re-structure baseToolRegistry to be a nested shape, preserving the order of tools and links
This commit is contained in:
parent
b6d56ba587
commit
8581829f6e
@ -22,6 +22,7 @@ export interface TooltipProps {
|
||||
title: string;
|
||||
logo?: React.ReactNode;
|
||||
};
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export const Tooltip: React.FC<TooltipProps> = ({
|
||||
@ -37,12 +38,20 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
arrow = false,
|
||||
portalTarget,
|
||||
header,
|
||||
delay = 0,
|
||||
}) => {
|
||||
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;
|
||||
@ -52,6 +61,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
const open = isControlled ? controlledOpen : internalOpen;
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
clearTimers();
|
||||
if (isControlled) {
|
||||
onOpenChange?.(newOpen);
|
||||
} else {
|
||||
@ -62,6 +72,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
if (!newOpen) {
|
||||
setIsPinned(false);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const handleTooltipClick = (e: React.MouseEvent) => {
|
||||
@ -96,6 +107,13 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
}
|
||||
}, [isPinned]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimers();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const getArrowClass = () => {
|
||||
// No arrow for sidebar tooltips
|
||||
if (sidebarTooltip) return null;
|
||||
@ -175,27 +193,27 @@ 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;
|
||||
if (openTimeoutRef.current) {
|
||||
clearTimeout(openTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Only show on hover if not pinned
|
||||
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
|
||||
if (openTimeoutRef.current) {
|
||||
clearTimeout(openTimeoutRef.current);
|
||||
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);
|
||||
@ -206,6 +224,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
if (open) {
|
||||
setIsPinned(!isPinned);
|
||||
} else {
|
||||
clearTimers();
|
||||
handleOpenChange(true);
|
||||
setIsPinned(true);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ A flexible, accessible tooltip component that supports both regular positioning
|
||||
- 📌 **Click-to-Pin**: Click to pin tooltips open, click outside or the close button to unpin
|
||||
- 🔗 **Link Support**: Full support for clickable links in descriptions, bullets, and body content
|
||||
- 🎮 **Controlled by Default**: Always uses controlled state management for consistent behavior
|
||||
- ⏱️ **Hover Timing Controls**: Optional long-hover requirement via `delayAppearance` and `delay`
|
||||
|
||||
## Behavior
|
||||
|
||||
@ -61,6 +62,7 @@ function MyComponent() {
|
||||
| `arrow` | `boolean` | `false` | Shows a small triangular arrow pointing to the trigger element |
|
||||
| `portalTarget` | `HTMLElement` | `undefined` | DOM node to portal the tooltip into |
|
||||
| `header` | `{ title: string; logo?: ReactNode }` | - | Optional header with title and logo |
|
||||
| `delay` | `number` | `0` | Optional hover-open delay (ms). If omitted or 0, opens immediately |
|
||||
|
||||
### TooltipTip Interface
|
||||
|
||||
@ -106,6 +108,17 @@ interface TooltipTip {
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
### Optional Hover Delay
|
||||
|
||||
```tsx
|
||||
// Show after a 1s hover
|
||||
<Tooltip content="Appears after a long hover" delay={1000} />
|
||||
|
||||
// Custom long-hover duration (2 seconds)
|
||||
<Tooltip content="Appears after 2s" delay={2000} />
|
||||
```
|
||||
|
||||
|
||||
### Custom JSX Content
|
||||
|
||||
```tsx
|
||||
@ -214,6 +227,12 @@ Links automatically get proper styling with hover states and open in new tabs wh
|
||||
- Calculates optimal position based on trigger element's `getBoundingClientRect()`
|
||||
- **Dynamic arrow positioning**: Arrow stays aligned with trigger even when tooltip is clamped
|
||||
|
||||
## Timing Details
|
||||
|
||||
- Opening uses `delay` (ms) if provided; otherwise opens immediately. Closing occurs immediately when the cursor leaves (unless pinned).
|
||||
- All internal timers are cleared on state changes, mouse transitions, and unmount to avoid overlaps.
|
||||
- Only one tooltip can be open at a time; hovering a new trigger closes others immediately.
|
||||
|
||||
### Sidebar Tooltips
|
||||
- When `sidebarTooltip={true}`, horizontal positioning is locked to the right of the sidebar
|
||||
- Vertical positioning follows the trigger but clamps to viewport
|
||||
|
69
frontend/src/components/tools/SearchResults.tsx
Normal file
69
frontend/src/components/tools/SearchResults.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Stack, Text } from '@mantine/core';
|
||||
import { type ToolRegistryEntry } from '../../data/toolRegistry';
|
||||
import ToolButton from './toolPicker/ToolButton';
|
||||
|
||||
interface SearchResultsProps {
|
||||
filteredTools: [string, ToolRegistryEntry][];
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
const SearchResults: React.FC<SearchResultsProps> = ({ filteredTools, onSelect }) => {
|
||||
const groups = useMemo(() => {
|
||||
const subMap: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {};
|
||||
const seen = new Set<string>();
|
||||
|
||||
filteredTools.forEach(([id, tool]) => {
|
||||
if (seen.has(id)) return;
|
||||
seen.add(id);
|
||||
const sub = tool?.subcategory || 'General';
|
||||
if (!subMap[sub]) subMap[sub] = [];
|
||||
subMap[sub].push({ id, tool });
|
||||
});
|
||||
|
||||
return Object.entries(subMap)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([subcategory, tools]) => ({
|
||||
subcategory,
|
||||
tools
|
||||
}));
|
||||
}, [filteredTools]);
|
||||
|
||||
if (groups.length === 0) {
|
||||
return (
|
||||
<Text c="dimmed" size="sm" p="sm">
|
||||
No tools found
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack p="sm" gap="xs">
|
||||
{groups.map(group => (
|
||||
<Box key={group.subcategory} w="100%">
|
||||
<Text size="sm" fw={500} mb="0.25rem" mt="1rem" className="tool-subcategory-title">
|
||||
{group.subcategory}
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
{group.tools.map(({ id, tool }) => (
|
||||
<ToolButton
|
||||
key={id}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={false}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
{/* bottom spacer within each group not strictly required, outer list can add a spacer if needed */}
|
||||
</Box>
|
||||
))}
|
||||
{/* global spacer to allow scrolling past last row in search mode */}
|
||||
<div aria-hidden style={{ height: 44 * 4 }} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResults;
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { TextInput, useMantineColorScheme } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||
import { useToolPanelState, useToolSelection, useWorkbenchState } from '../../contexts/ToolWorkflowContext';
|
||||
import ToolPicker from './ToolPicker';
|
||||
import SearchResults from './SearchResults';
|
||||
import ToolRenderer from './ToolRenderer';
|
||||
import { useSidebarContext } from "../../contexts/SidebarContext";
|
||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||
@ -13,6 +14,7 @@ import rainbowStyles from '../../styles/rainbow.module.css';
|
||||
export default function ToolPanel() {
|
||||
const { t } = useTranslation();
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const { sidebarRefs } = useSidebarContext();
|
||||
const { toolPanelRef } = sidebarRefs;
|
||||
|
||||
@ -37,10 +39,7 @@ export default function ToolPanel() {
|
||||
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={{
|
||||
width: isPanelVisible ? '20rem' : '0',
|
||||
padding: isPanelVisible ? '0.5rem' : '0'
|
||||
}}
|
||||
style={{width: isPanelVisible ? '20rem' : '0'}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
@ -52,23 +51,57 @@ export default function ToolPanel() {
|
||||
}}
|
||||
>
|
||||
{/* Search Bar - Always visible at the top */}
|
||||
<div className="mb-4">
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: colorScheme === 'dark' ? '#1F2329' : '#EFF1F4',
|
||||
padding: '0.75rem 1rem',
|
||||
marginBottom: (leftPanelView === 'toolContent') ? '1rem' : 0,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.currentTarget.value)}
|
||||
autoComplete="off"
|
||||
size="sm"
|
||||
styles={{
|
||||
root: {
|
||||
marginTop: '0.5rem',
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
input: {
|
||||
backgroundColor: colorScheme === 'dark' ? '#4B525A' : '#FFFFFF',
|
||||
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
borderBottom: leftPanelView === 'toolContent' ? `1px solid ${colorScheme === 'dark' ? '#3A4047' : '#E0E0E0'}` : 'none',
|
||||
},
|
||||
section: {
|
||||
color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382',
|
||||
}
|
||||
}}
|
||||
leftSection={<span className="material-symbols-rounded" style={{ fontSize: 16, color: colorScheme === 'dark' ? '#FFFFFF' : '#6B7382' }}>search</span>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{leftPanelView === 'toolPicker' ? (
|
||||
{searchQuery.trim().length > 0 ? (
|
||||
// Searching view (replaces both picker and content)
|
||||
<div className="flex-1 flex flex-col">
|
||||
<div className="flex-1 min-h-0">
|
||||
<SearchResults
|
||||
filteredTools={filteredTools}
|
||||
onSelect={handleToolSelect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : leftPanelView === 'toolPicker' ? (
|
||||
// Tool Picker View
|
||||
<div className="flex-1 flex flex-col">
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
onSelect={handleToolSelect}
|
||||
filteredTools={filteredTools}
|
||||
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useMemo, useRef, useLayoutEffect, useState } from "react";
|
||||
import { Box, Text, Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { baseToolRegistry, type ToolRegistryEntry } from "../../data/toolRegistry";
|
||||
import { type ToolRegistryEntry, SUBCATEGORY_ORDER } from "../../data/toolRegistry";
|
||||
import ToolButton from "./toolPicker/ToolButton";
|
||||
import "./toolPicker/ToolPicker.css";
|
||||
|
||||
@ -9,6 +9,7 @@ interface ToolPickerProps {
|
||||
selectedToolKey: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
filteredTools: [string, ToolRegistryEntry][];
|
||||
isSearching?: boolean;
|
||||
}
|
||||
|
||||
interface GroupedTools {
|
||||
@ -17,7 +18,7 @@ interface GroupedTools {
|
||||
};
|
||||
}
|
||||
|
||||
const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProps) => {
|
||||
const ToolPicker = ({ selectedToolKey, onSelect, filteredTools, isSearching = false }: ToolPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [quickHeaderHeight, setQuickHeaderHeight] = useState(0);
|
||||
const [allHeaderHeight, setAllHeaderHeight] = useState(0);
|
||||
@ -45,9 +46,8 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProp
|
||||
const groupedTools = useMemo(() => {
|
||||
const grouped: GroupedTools = {};
|
||||
filteredTools.forEach(([id, tool]) => {
|
||||
const baseTool = baseToolRegistry[id as keyof typeof baseToolRegistry];
|
||||
const category = baseTool?.category || "OTHER";
|
||||
const subcategory = baseTool?.subcategory || "General";
|
||||
const category = tool?.category || "OTHER";
|
||||
const subcategory = tool?.subcategory || "General";
|
||||
if (!grouped[category]) grouped[category] = {};
|
||||
if (!grouped[category][subcategory]) grouped[category][subcategory] = [];
|
||||
grouped[category][subcategory].push({ id, tool });
|
||||
@ -56,34 +56,54 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProp
|
||||
}, [filteredTools]);
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const mapping: Record<string, "QUICK ACCESS" | "ALL TOOLS"> = {
|
||||
"RECOMMENDED TOOLS": "QUICK ACCESS",
|
||||
"STANDARD TOOLS": "ALL TOOLS",
|
||||
"ADVANCED TOOLS": "ALL TOOLS"
|
||||
|
||||
const getOrderIndex = (name: string) => {
|
||||
const idx = SUBCATEGORY_ORDER.indexOf(name);
|
||||
return idx === -1 ? Number.MAX_SAFE_INTEGER : idx;
|
||||
};
|
||||
// Build two buckets: Quick includes only Recommended; All includes all categories (including Recommended)
|
||||
const quick: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {};
|
||||
const all: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {};
|
||||
|
||||
Object.entries(groupedTools).forEach(([origCat, subs]) => {
|
||||
const bucket = mapping[origCat.toUpperCase()] || "ALL TOOLS";
|
||||
const target = bucket === "QUICK ACCESS" ? quick : all;
|
||||
const upperCat = origCat.toUpperCase();
|
||||
|
||||
// Always add to ALL
|
||||
Object.entries(subs).forEach(([sub, tools]) => {
|
||||
if (!target[sub]) target[sub] = [];
|
||||
target[sub].push(...tools);
|
||||
if (!all[sub]) all[sub] = [];
|
||||
all[sub].push(...tools);
|
||||
});
|
||||
|
||||
// Add Recommended to QUICK ACCESS
|
||||
if (upperCat === 'RECOMMENDED TOOLS') {
|
||||
Object.entries(subs).forEach(([sub, tools]) => {
|
||||
if (!quick[sub]) quick[sub] = [];
|
||||
quick[sub].push(...tools);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sortSubs = (obj: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>>) =>
|
||||
Object.entries(obj)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.sort(([a], [b]) => {
|
||||
const ai = getOrderIndex(a);
|
||||
const bi = getOrderIndex(b);
|
||||
if (ai !== bi) return ai - bi;
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map(([subcategory, tools]) => ({
|
||||
subcategory,
|
||||
tools: tools.sort((a, b) => a.tool.name.localeCompare(b.tool.name))
|
||||
// preserve original insertion order coming from filteredTools
|
||||
tools
|
||||
}));
|
||||
|
||||
return [
|
||||
// Build sections and filter out any with no tools (avoids empty headers during search)
|
||||
const built = [
|
||||
{ title: "QUICK ACCESS", ref: quickAccessRef, subcategories: sortSubs(quick) },
|
||||
{ title: "ALL TOOLS", ref: allToolsRef, subcategories: sortSubs(all) }
|
||||
];
|
||||
|
||||
return built.filter(section => section.subcategories.some(sc => sc.tools.length > 0));
|
||||
}, [groupedTools]);
|
||||
|
||||
const visibleSections = sections;
|
||||
@ -112,6 +132,27 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProp
|
||||
}
|
||||
};
|
||||
|
||||
// Build flat list by subcategory for search mode
|
||||
const searchGroups = useMemo(() => {
|
||||
if (!isSearching) return [] as Array<{ subcategory: string; tools: Array<{ id: string; tool: ToolRegistryEntry }> }>;
|
||||
const subMap: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>> = {};
|
||||
const seen = new Set<string>();
|
||||
filteredTools.forEach(([id, tool]) => {
|
||||
if (seen.has(id)) return;
|
||||
seen.add(id);
|
||||
const sub = tool?.subcategory || 'General';
|
||||
if (!subMap[sub]) subMap[sub] = [];
|
||||
subMap[sub].push({ id, tool });
|
||||
});
|
||||
return Object.entries(subMap)
|
||||
.sort(([a],[b]) => a.localeCompare(b))
|
||||
.map(([subcategory, tools]) => ({
|
||||
subcategory,
|
||||
// preserve insertion order
|
||||
tools
|
||||
}));
|
||||
}, [isSearching, filteredTools]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
h="100vh"
|
||||
@ -132,6 +173,35 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProp
|
||||
}}
|
||||
className="tool-picker-scrollable"
|
||||
>
|
||||
{isSearching ? (
|
||||
<Stack p="sm" gap="xs">
|
||||
{searchGroups.length === 0 ? (
|
||||
<Text c="dimmed" size="sm" p="sm">
|
||||
{t("toolPicker.noToolsFound", "No tools found")}
|
||||
</Text>
|
||||
) : (
|
||||
searchGroups.map(group => (
|
||||
<Box key={group.subcategory} w="100%">
|
||||
<Text size="sm" fw={500} mb="0.25rem" mt="1rem" className="tool-subcategory-title">
|
||||
{group.subcategory}
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
{group.tools.map(({ id, tool }) => (
|
||||
<ToolButton
|
||||
key={id}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
) : (
|
||||
<>
|
||||
{quickSection && (
|
||||
<>
|
||||
<div
|
||||
@ -165,15 +235,15 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProp
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{quickSection.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
|
||||
{quickSection?.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Box ref={quickAccessRef} w="100%">
|
||||
<Stack p="sm" gap="xs">
|
||||
{quickSection.subcategories.map(sc => (
|
||||
{quickSection?.subcategories.map(sc => (
|
||||
<Box key={sc.subcategory} w="100%">
|
||||
{quickSection.subcategories.length > 1 && (
|
||||
{quickSection?.subcategories.length > 1 && (
|
||||
<Text
|
||||
size="sm"
|
||||
fw={500}
|
||||
@ -234,15 +304,15 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProp
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{allSection.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
|
||||
{allSection?.subcategories.reduce((acc, sc) => acc + sc.tools.length, 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Box ref={allToolsRef} w="100%">
|
||||
<Stack p="sm" gap="xs">
|
||||
{allSection.subcategories.map(sc => (
|
||||
{allSection?.subcategories.map(sc => (
|
||||
<Box key={sc.subcategory} w="100%">
|
||||
{allSection.subcategories.length > 1 && (
|
||||
{allSection?.subcategories.length > 1 && (
|
||||
<Text
|
||||
size="sm"
|
||||
fw={500}
|
||||
@ -276,6 +346,11 @@ const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProp
|
||||
{t("toolPicker.noToolsFound", "No tools found")}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* bottom spacer to allow scrolling past the last row */}
|
||||
<div aria-hidden style={{ height: 200 }} />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Button, Tooltip } from "@mantine/core";
|
||||
import { Button } from "@mantine/core";
|
||||
import { Tooltip } from "../../shared/Tooltip";
|
||||
import { type ToolRegistryEntry } from "../../../data/toolRegistry";
|
||||
|
||||
interface ToolButtonProps {
|
||||
@ -11,7 +12,7 @@ interface ToolButtonProps {
|
||||
|
||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => {
|
||||
return (
|
||||
<Tooltip key={id} label={tool.description} withArrow openDelay={500}>
|
||||
<Tooltip content={tool.description} position="right" arrow={true} delay={500}>
|
||||
<Button
|
||||
variant={isSelected ? "filled" : "subtle"}
|
||||
onClick={() => onSelect(id)}
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react';
|
||||
import { useToolManagement } from '../hooks/useToolManagement';
|
||||
import { ToolConfiguration } from '../types/tool';
|
||||
import { type ToolRegistryEntry } from '../data/toolRegistry';
|
||||
import { PageEditorFunctions } from '../types/pageEditor';
|
||||
|
||||
// State interface
|
||||
@ -69,8 +69,8 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio
|
||||
interface ToolWorkflowContextValue extends ToolWorkflowState {
|
||||
// Tool management (from hook)
|
||||
selectedToolKey: string | null;
|
||||
selectedTool: ToolConfiguration | null;
|
||||
toolRegistry: any; // From useToolManagement
|
||||
selectedTool: ToolRegistryEntry | null;
|
||||
toolRegistry: Record<string, ToolRegistryEntry>; // From useToolManagement
|
||||
|
||||
// UI Actions
|
||||
setSidebarsVisible: (visible: boolean) => void;
|
||||
@ -90,7 +90,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
||||
handleReaderToggle: () => void;
|
||||
|
||||
// Computed values
|
||||
filteredTools: [string, any][]; // Filtered by search
|
||||
filteredTools: [string, ToolRegistryEntry][]; // Filtered by search
|
||||
isPanelVisible: boolean;
|
||||
}
|
||||
|
||||
@ -146,7 +146,9 @@ export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowPro
|
||||
onViewChange?.('fileEditor');
|
||||
setLeftPanelView('toolContent');
|
||||
setReaderMode(false);
|
||||
}, [selectTool, onViewChange, setLeftPanelView, setReaderMode]);
|
||||
// Clear search so the tool content becomes visible immediately
|
||||
setSearchQuery('');
|
||||
}, [selectTool, onViewChange, setLeftPanelView, setReaderMode, setSearchQuery]);
|
||||
|
||||
const handleBackToTools = useCallback(() => {
|
||||
setLeftPanelView('toolPicker');
|
||||
|
@ -19,7 +19,32 @@ export type ToolRegistry = {
|
||||
[key: string]: ToolRegistryEntry;
|
||||
};
|
||||
|
||||
export const baseToolRegistry: ToolRegistry = {
|
||||
/**
|
||||
* Shape overview:
|
||||
* - flatToolRegistryMap: { [toolId]: ToolRegistryEntry }
|
||||
* - buildStructuredRegistry(): {
|
||||
* QUICK_ACCESS: Array<ToolRegistryEntry & { id: string }>,
|
||||
* ALL_TOOLS: { [category]: { [subcategory]: Array<ToolRegistryEntry & { id: string }> } }
|
||||
* }
|
||||
* - baseToolRegistry: [ { QUICK_ACCESS }, { ALL_TOOLS } ]
|
||||
* Quick reference helpers are provided below for convenience.
|
||||
*/
|
||||
// Ordered list used elsewhere for display ordering
|
||||
export const SUBCATEGORY_ORDER: string[] = [
|
||||
'Signing',
|
||||
'Document Security',
|
||||
'Verification',
|
||||
'Document Review',
|
||||
'Page Formatting',
|
||||
'Extraction',
|
||||
'Removal',
|
||||
'Automation',
|
||||
'General', // for now, this is the same as quick access, until we add suggested tools & favorites & whatnot
|
||||
'Advanced Formatting',
|
||||
'Developer Tools',
|
||||
];
|
||||
|
||||
export const flatToolRegistryMap: ToolRegistry = {
|
||||
"add-attachments": {
|
||||
icon: <span className="material-symbols-rounded">attachment</span>,
|
||||
name: "home.attachments.title",
|
||||
@ -434,6 +459,43 @@ export const baseToolRegistry: ToolRegistry = {
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Developer Tools"
|
||||
},
|
||||
// External Developer Resources (open in new tab)
|
||||
"dev-api": {
|
||||
icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
|
||||
name: "API",
|
||||
component: null,
|
||||
view: "external",
|
||||
description: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Developer Tools"
|
||||
},
|
||||
"dev-folder-scanning": {
|
||||
icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
|
||||
name: "Automated Folder Scanning",
|
||||
component: null,
|
||||
view: "external",
|
||||
description: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Developer Tools"
|
||||
},
|
||||
"dev-sso-guide": {
|
||||
icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
|
||||
name: "SSO Guide",
|
||||
component: null,
|
||||
view: "external",
|
||||
description: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Developer Tools"
|
||||
},
|
||||
"dev-airgapped": {
|
||||
icon: <span className="material-symbols-rounded" style={{ color: '#2F7BF6' }}>open_in_new</span>,
|
||||
name: "Air-gapped Setup",
|
||||
component: null,
|
||||
view: "external",
|
||||
description: "https://docs.stirlingpdf.com/Pro/#activation",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Developer Tools"
|
||||
},
|
||||
"sign": {
|
||||
icon: <span className="material-symbols-rounded">signature</span>,
|
||||
name: "home.sign.title",
|
||||
@ -517,8 +579,66 @@ export const baseToolRegistry: ToolRegistry = {
|
||||
}
|
||||
};
|
||||
|
||||
export const toolEndpoints: Record<string,
|
||||
string[]> = {
|
||||
// Build structured registry that preserves order for sections
|
||||
export type ToolConfig = ToolRegistryEntry & { id: string };
|
||||
export type ToolRegistryStructured = {
|
||||
QUICK_ACCESS: ToolConfig[];
|
||||
ALL_TOOLS: Record<string, Record<string, ToolConfig[]>>;
|
||||
};
|
||||
|
||||
function buildStructuredRegistry(): ToolRegistryStructured {
|
||||
const entries: Array<[string, ToolRegistryEntry]> = Object.entries(flatToolRegistryMap);
|
||||
const quick: ToolConfig[] = [];
|
||||
const all: Record<string, Record<string, ToolConfig[]>> = {};
|
||||
|
||||
for (const [id, tool] of entries) {
|
||||
const sub = tool.subcategory ?? 'General';
|
||||
const cat = tool.category ?? 'OTHER';
|
||||
// Quick access: use the existing "Recommended Tools" category
|
||||
if (tool.category === 'Recommended Tools') {
|
||||
quick.push({ id, ...tool });
|
||||
}
|
||||
if (!all[cat]) all[cat] = {};
|
||||
if (!all[cat][sub]) all[cat][sub] = [];
|
||||
all[cat][sub].push({ id, ...tool });
|
||||
}
|
||||
|
||||
// Preserve subcategory ordering within each category
|
||||
for (const cat of Object.keys(all)) {
|
||||
const subcats = all[cat];
|
||||
const ordered: Record<string, ToolConfig[]> = {};
|
||||
SUBCATEGORY_ORDER.forEach(orderName => {
|
||||
if (subcats[orderName]) ordered[orderName] = subcats[orderName];
|
||||
});
|
||||
// Append any remaining subcategories not in the predefined order
|
||||
Object.keys(subcats)
|
||||
.filter(name => !(name in ordered))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.forEach(name => (ordered[name] = subcats[name]));
|
||||
all[cat] = ordered;
|
||||
}
|
||||
|
||||
return { QUICK_ACCESS: quick, ALL_TOOLS: all };
|
||||
}
|
||||
|
||||
export const baseToolRegistry: [
|
||||
{ QUICK_ACCESS: ToolConfig[] },
|
||||
{ ALL_TOOLS: Record<string, Record<string, ToolConfig[]>> }
|
||||
] = [
|
||||
{ QUICK_ACCESS: buildStructuredRegistry().QUICK_ACCESS },
|
||||
{ ALL_TOOLS: buildStructuredRegistry().ALL_TOOLS }
|
||||
];
|
||||
|
||||
// Convenience accessors for the structured shape
|
||||
export const getQuickAccessTools = (): ToolConfig[] => baseToolRegistry[0].QUICK_ACCESS;
|
||||
export const getAllToolsStructured = (): Record<string, Record<string, ToolConfig[]>> => baseToolRegistry[1].ALL_TOOLS;
|
||||
|
||||
// Compatibility: provide a flat registry for existing hooks/components
|
||||
export function getFlatToolRegistry(): ToolRegistry {
|
||||
return flatToolRegistryMap;
|
||||
}
|
||||
|
||||
export const toolEndpoints: Record<string, string[]> = {
|
||||
split: ["split-pages",
|
||||
"split-pdf-by-sections",
|
||||
"split-by-size-or-count",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { baseToolRegistry, toolEndpoints, type ToolRegistryEntry } from "../data/toolRegistry";
|
||||
import { getFlatToolRegistry, toolEndpoints, type ToolRegistryEntry } from "../data/toolRegistry";
|
||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||
|
||||
interface ToolManagementResult {
|
||||
@ -32,9 +32,10 @@ export const useToolManagement = (): ToolManagementResult => {
|
||||
|
||||
const toolRegistry: Record<string, ToolRegistryEntry> = useMemo(() => {
|
||||
const availableToolRegistry: Record<string, ToolRegistryEntry> = {};
|
||||
Object.keys(baseToolRegistry).forEach(toolKey => {
|
||||
const base = getFlatToolRegistry();
|
||||
Object.keys(base).forEach(toolKey => {
|
||||
if (isToolAvailable(toolKey)) {
|
||||
const baseTool = baseToolRegistry[toolKey as keyof typeof baseToolRegistry];
|
||||
const baseTool = base[toolKey as keyof typeof base];
|
||||
availableToolRegistry[toolKey] = {
|
||||
...baseTool,
|
||||
name: t(baseTool.name),
|
||||
|
Loading…
x
Reference in New Issue
Block a user