mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-11 13:48:37 +02:00
Merge b6d56ba587
into 768ece6921
This commit is contained in:
commit
be4dfdb0cb
@ -1,4 +1,4 @@
|
||||
<svg width="146" height="157" viewBox="0 0 146 157" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.77397 72.5462L93.6741 23.0462L94.7739 70.0462L3.77395 119.046L3.77397 72.5462Z" fill="#E6E6E6" fill-opacity="0.9"/>
|
||||
<path d="M50.774 73.5735L96.387 50.2673L142 26.961L142 71.687L50.7739 122.046L50.774 73.5735Z" fill="#E6E6E6" fill-opacity="0.8"/>
|
||||
<path d="M3.77397 72.5462L93.6741 23.0462L94.7739 70.0462L3.77395 119.046L3.77397 72.5462Z" fill="#E6E6E6" fill-opacity="0.4"/>
|
||||
<path d="M50.774 73.5735L96.387 50.2673L142 26.961L142 71.687L50.7739 122.046L50.774 73.5735Z" fill="#E6E6E6" fill-opacity="0.7"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 366 B After Width: | Height: | Size: 366 B |
@ -367,6 +367,10 @@
|
||||
"title": "Add image",
|
||||
"desc": "Adds a image onto a set location on the PDF"
|
||||
},
|
||||
"attachments": {
|
||||
"title": "Add Attachments",
|
||||
"desc": "Add or remove embedded files (attachments) to/from a PDF"
|
||||
},
|
||||
"watermark": {
|
||||
"title": "Add Watermark",
|
||||
"desc": "Add a custom watermark to your PDF document."
|
||||
@ -586,6 +590,34 @@
|
||||
"replaceColorPdf": {
|
||||
"title": "Advanced Colour options",
|
||||
"desc": "Replace colour for text and background in PDF and invert full colour of pdf to reduce file size"
|
||||
},
|
||||
"EMLToPDF": {
|
||||
"title": "Email to PDF",
|
||||
"desc": "Converts email (EML) files to PDF format including headers, body, and inline images"
|
||||
},
|
||||
"fakeScan": {
|
||||
"title": "Fake Scan",
|
||||
"desc": "Create a PDF that looks like it was scanned"
|
||||
},
|
||||
"editTableOfContents": {
|
||||
"title": "Edit Table of Contents",
|
||||
"desc": "Add or edit bookmarks and table of contents in PDF documents"
|
||||
},
|
||||
"automate": {
|
||||
"title": "Automate",
|
||||
"desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
|
||||
},
|
||||
"manageCertificates": {
|
||||
"title": "Manage Certificates",
|
||||
"desc": "Import, export, or delete digital certificate files used for signing PDFs."
|
||||
},
|
||||
"read": {
|
||||
"title": "Read",
|
||||
"desc": "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."
|
||||
},
|
||||
"reorganizePages": {
|
||||
"title": "Reorganize Pages",
|
||||
"desc": "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
|
||||
}
|
||||
},
|
||||
"viewPdf": {
|
||||
@ -766,6 +798,15 @@
|
||||
"upload": "Add image",
|
||||
"submit": "Add image"
|
||||
},
|
||||
"attachments": {
|
||||
"tags": "attachments,add,remove,embed,file",
|
||||
"title": "Add Attachments",
|
||||
"header": "Add Attachments",
|
||||
"add": "Add Attachment",
|
||||
"remove": "Remove Attachment",
|
||||
"embed": "Embed Attachment",
|
||||
"submit": "Add Attachments"
|
||||
},
|
||||
"watermark": {
|
||||
"tags": "Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo",
|
||||
"title": "Add Watermark",
|
||||
@ -1607,7 +1648,7 @@
|
||||
"title": "How we use Cookies",
|
||||
"description": {
|
||||
"1": "We use cookies and other technologies to make Stirling PDF work better for you—helping us improve our tools and keep building features you'll love.",
|
||||
"2": "If you’d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly."
|
||||
"2": "If you'd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly."
|
||||
},
|
||||
"acceptAllBtn": "Okay",
|
||||
"acceptNecessaryBtn": "No Thanks",
|
||||
@ -1631,7 +1672,7 @@
|
||||
"1": "Strictly Necessary Cookies",
|
||||
"2": "Always Enabled"
|
||||
},
|
||||
"description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off."
|
||||
"description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can't be turned off."
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
|
@ -25,31 +25,6 @@ function NavHeader({
|
||||
const { handleReaderToggle, handleBackToTools } = useToolWorkflow();
|
||||
return (
|
||||
<>
|
||||
<div className="nav-header">
|
||||
<Tooltip label="User Profile" position="right">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
className="action-icon-style"
|
||||
>
|
||||
<PersonIcon sx={{ fontSize: "1rem" }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Notifications" position="right">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
className="action-icon-style"
|
||||
>
|
||||
<NotificationsIcon sx={{ fontSize: "1rem" }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/* Divider after top icons */}
|
||||
<Divider
|
||||
size="xs"
|
||||
className="nav-header-divider"
|
||||
/>
|
||||
{/* All Tools button below divider */}
|
||||
<Tooltip label="View all available tools" position="right">
|
||||
<div className="flex flex-col items-center gap-1 mt-4 mb-2">
|
||||
@ -126,7 +101,10 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
|
||||
{
|
||||
id: 'automate',
|
||||
name: 'Automate',
|
||||
icon: <AutoAwesomeIcon sx={{ fontSize: "1.5rem" }} />,
|
||||
icon:
|
||||
<span className="material-symbols-rounded font-size-20">
|
||||
automation
|
||||
</span>,
|
||||
tooltip: 'Automate workflows',
|
||||
size: 'lg',
|
||||
isRound: false,
|
||||
@ -210,6 +188,9 @@ const QuickAccessBar = forwardRef<HTMLDivElement>(({
|
||||
ref={ref}
|
||||
data-sidebar="quick-access"
|
||||
className={`h-screen flex flex-col w-20 quick-access-bar-main ${isRainbowMode ? 'rainbow-mode' : ''}`}
|
||||
style={{
|
||||
borderRight: '1px solid var(--border-default)'
|
||||
}}
|
||||
>
|
||||
{/* Fixed header outside scrollable area */}
|
||||
<div className="quick-access-header">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { MantineProvider, ColorSchemeScript } from '@mantine/core';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { useRainbowTheme } from '../../hooks/useRainbowTheme';
|
||||
import { mantineTheme } from '../../theme/mantineTheme';
|
||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||
@ -34,8 +34,6 @@ export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) {
|
||||
const mantineColorScheme = rainbowTheme.themeMode === 'rainbow' ? 'dark' : rainbowTheme.themeMode;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ColorSchemeScript defaultColorScheme={mantineColorScheme} />
|
||||
<RainbowThemeContext.Provider value={rainbowTheme}>
|
||||
<MantineProvider
|
||||
theme={mantineTheme}
|
||||
@ -50,6 +48,5 @@ export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) {
|
||||
</div>
|
||||
</MantineProvider>
|
||||
</RainbowThemeContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,43 +1,282 @@
|
||||
import React from "react";
|
||||
import { Box, Text, Stack, Button } from "@mantine/core";
|
||||
import React, { useMemo, useRef, useLayoutEffect, useState } from "react";
|
||||
import { Box, Text, Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ToolRegistry } from "../../types/tool";
|
||||
import { baseToolRegistry, type ToolRegistryEntry } from "../../data/toolRegistry";
|
||||
import ToolButton from "./toolPicker/ToolButton";
|
||||
import "./toolPicker/ToolPicker.css";
|
||||
|
||||
interface ToolPickerProps {
|
||||
selectedToolKey: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
/** Pre-filtered tools to display */
|
||||
filteredTools: [string, ToolRegistry[string]][];
|
||||
filteredTools: [string, ToolRegistryEntry][];
|
||||
}
|
||||
|
||||
interface GroupedTools {
|
||||
[category: string]: {
|
||||
[subcategory: string]: Array<{ id: string; tool: ToolRegistryEntry }>;
|
||||
};
|
||||
}
|
||||
|
||||
const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [quickHeaderHeight, setQuickHeaderHeight] = useState(0);
|
||||
const [allHeaderHeight, setAllHeaderHeight] = useState(0);
|
||||
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
const quickHeaderRef = useRef<HTMLDivElement>(null);
|
||||
const allHeaderRef = useRef<HTMLDivElement>(null);
|
||||
const quickAccessRef = useRef<HTMLDivElement>(null);
|
||||
const allToolsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const update = () => {
|
||||
if (quickHeaderRef.current) {
|
||||
setQuickHeaderHeight(quickHeaderRef.current.offsetHeight);
|
||||
}
|
||||
if (allHeaderRef.current) {
|
||||
setAllHeaderHeight(allHeaderRef.current.offsetHeight);
|
||||
}
|
||||
};
|
||||
update();
|
||||
window.addEventListener("resize", update);
|
||||
return () => window.removeEventListener("resize", update);
|
||||
}, []);
|
||||
|
||||
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";
|
||||
if (!grouped[category]) grouped[category] = {};
|
||||
if (!grouped[category][subcategory]) grouped[category][subcategory] = [];
|
||||
grouped[category][subcategory].push({ id, tool });
|
||||
});
|
||||
return grouped;
|
||||
}, [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 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;
|
||||
Object.entries(subs).forEach(([sub, tools]) => {
|
||||
if (!target[sub]) target[sub] = [];
|
||||
target[sub].push(...tools);
|
||||
});
|
||||
});
|
||||
|
||||
const sortSubs = (obj: Record<string, Array<{ id: string; tool: ToolRegistryEntry }>>) =>
|
||||
Object.entries(obj)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([subcategory, tools]) => ({
|
||||
subcategory,
|
||||
tools: tools.sort((a, b) => a.tool.name.localeCompare(b.tool.name))
|
||||
}));
|
||||
|
||||
return [
|
||||
{ title: "QUICK ACCESS", ref: quickAccessRef, subcategories: sortSubs(quick) },
|
||||
{ title: "ALL TOOLS", ref: allToolsRef, subcategories: sortSubs(all) }
|
||||
];
|
||||
}, [groupedTools]);
|
||||
|
||||
const visibleSections = sections;
|
||||
|
||||
const quickSection = useMemo(
|
||||
() => visibleSections.find(s => s.title === "QUICK ACCESS"),
|
||||
[visibleSections]
|
||||
);
|
||||
const allSection = useMemo(
|
||||
() => visibleSections.find(s => s.title === "ALL TOOLS"),
|
||||
[visibleSections]
|
||||
);
|
||||
|
||||
const scrollTo = (ref: React.RefObject<HTMLDivElement | null>) => {
|
||||
const container = scrollableRef.current;
|
||||
const target = ref.current;
|
||||
if (container && target) {
|
||||
const stackedOffset = ref === allToolsRef
|
||||
? (quickHeaderHeight + allHeaderHeight)
|
||||
: quickHeaderHeight;
|
||||
const top = target.offsetTop - container.offsetTop - (stackedOffset || 0);
|
||||
container.scrollTo({
|
||||
top: Math.max(0, top),
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack align="flex-start">
|
||||
{filteredTools.length === 0 ? (
|
||||
<Text c="dimmed" size="sm">
|
||||
<Box
|
||||
h="100vh"
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "var(--bg-toolbar)"
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={scrollableRef}
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
minHeight: 0,
|
||||
height: "100%"
|
||||
}}
|
||||
className="tool-picker-scrollable"
|
||||
>
|
||||
{quickSection && (
|
||||
<>
|
||||
<div
|
||||
ref={quickHeaderRef}
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
borderTop: `1px solid var(--tool-header-border)`,
|
||||
borderBottom: `1px solid var(--tool-header-border)`,
|
||||
marginBottom: -1,
|
||||
padding: "0.5rem 1rem",
|
||||
fontWeight: 700,
|
||||
background: "var(--tool-header-bg)",
|
||||
color: "var(--tool-header-text)",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
onClick={() => scrollTo(quickAccessRef)}
|
||||
>
|
||||
<span>QUICK ACCESS</span>
|
||||
<span
|
||||
style={{
|
||||
background: "var(--tool-header-badge-bg)",
|
||||
color: "var(--tool-header-badge-text)",
|
||||
borderRadius: 8,
|
||||
padding: "2px 8px",
|
||||
fontSize: 12,
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{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 => (
|
||||
<Box key={sc.subcategory} w="100%">
|
||||
{quickSection.subcategories.length > 1 && (
|
||||
<Text
|
||||
size="sm"
|
||||
fw={500}
|
||||
mb="0.25rem"
|
||||
mt="1rem"
|
||||
className="tool-subcategory-title"
|
||||
>
|
||||
{sc.subcategory}
|
||||
</Text>
|
||||
)}
|
||||
<Stack gap="xs">
|
||||
{sc.tools.map(({ id, tool }) => (
|
||||
<ToolButton
|
||||
key={id}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{allSection && (
|
||||
<>
|
||||
<div
|
||||
ref={allHeaderRef}
|
||||
style={{
|
||||
position: "sticky",
|
||||
top: quickSection ? quickHeaderHeight - 1: 0,
|
||||
zIndex: 2,
|
||||
borderTop: `1px solid var(--tool-header-border)`,
|
||||
borderBottom: `1px solid var(--tool-header-border)`,
|
||||
padding: "0.5rem 1rem",
|
||||
fontWeight: 700,
|
||||
background: "var(--tool-header-bg)",
|
||||
color: "var(--tool-header-text)",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
onClick={() => scrollTo(allToolsRef)}
|
||||
>
|
||||
<span>ALL TOOLS</span>
|
||||
<span
|
||||
style={{
|
||||
background: "var(--tool-header-badge-bg)",
|
||||
color: "var(--tool-header-badge-text)",
|
||||
borderRadius: 8,
|
||||
padding: "2px 8px",
|
||||
fontSize: 12,
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{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 => (
|
||||
<Box key={sc.subcategory} w="100%">
|
||||
{allSection.subcategories.length > 1 && (
|
||||
<Text
|
||||
size="sm"
|
||||
fw={500}
|
||||
mb="0.25rem"
|
||||
mt="1rem"
|
||||
className="tool-subcategory-title"
|
||||
>
|
||||
{sc.subcategory}
|
||||
</Text>
|
||||
)}
|
||||
<Stack gap="xs">
|
||||
{sc.tools.map(({ id, tool }) => (
|
||||
<ToolButton
|
||||
key={id}
|
||||
id={id}
|
||||
tool={tool}
|
||||
isSelected={selectedToolKey === id}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!quickSection && !allSection && (
|
||||
<Text c="dimmed" size="sm" p="sm">
|
||||
{t("toolPicker.noToolsFound", "No tools found")}
|
||||
</Text>
|
||||
) : (
|
||||
filteredTools.map(([id, { icon, name }]) => (
|
||||
<Button
|
||||
key={id}
|
||||
data-testid={`tool-${id}`}
|
||||
variant={selectedToolKey === id ? "filled" : "subtle"}
|
||||
onClick={() => onSelect(id)}
|
||||
size="md"
|
||||
radius="md"
|
||||
leftSection={icon}
|
||||
fullWidth
|
||||
justify="flex-start"
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
32
frontend/src/components/tools/toolPicker/ToolButton.tsx
Normal file
32
frontend/src/components/tools/toolPicker/ToolButton.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { Button, Tooltip } from "@mantine/core";
|
||||
import { type ToolRegistryEntry } from "../../../data/toolRegistry";
|
||||
|
||||
interface ToolButtonProps {
|
||||
id: string;
|
||||
tool: ToolRegistryEntry;
|
||||
isSelected: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
|
||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect }) => {
|
||||
return (
|
||||
<Tooltip key={id} label={tool.description} withArrow openDelay={500}>
|
||||
<Button
|
||||
variant={isSelected ? "filled" : "subtle"}
|
||||
onClick={() => onSelect(id)}
|
||||
size="md"
|
||||
radius="md"
|
||||
leftSection={<div className="tool-button-icon" style={{ color: "var(--tools-text-and-icon-color)" }}>{tool.icon}</div>}
|
||||
fullWidth
|
||||
justify="flex-start"
|
||||
className="tool-button"
|
||||
styles={{ root: { borderRadius: 0, color: "var(--tools-text-and-icon-color)" } }}
|
||||
>
|
||||
{tool.name}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolButton;
|
52
frontend/src/components/tools/toolPicker/ToolPicker.css
Normal file
52
frontend/src/components/tools/toolPicker/ToolPicker.css
Normal file
@ -0,0 +1,52 @@
|
||||
.tool-picker-scrollable {
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--mantine-color-gray-4) transparent;
|
||||
}
|
||||
|
||||
.tool-picker-scrollable::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.tool-picker-scrollable::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tool-picker-scrollable::-webkit-scrollbar-thumb {
|
||||
background-color: var(--mantine-color-gray-4);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.tool-picker-scrollable::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--mantine-color-gray-5);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.tool-subcategory-title {
|
||||
text-transform: uppercase;
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--tool-subcategory-text-color);
|
||||
/* Align the text with tool labels to account for icon gutter */
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
/* Compact tool buttons */
|
||||
.tool-button {
|
||||
font-size: 0.875rem; /* default 1rem - 0.125rem? We'll apply exact -0.25rem via calc below */
|
||||
padding-top: 0.375rem;
|
||||
padding-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.tool-button .mantine-Button-label {
|
||||
font-size: .85rem;
|
||||
}
|
||||
|
||||
.tool-button-icon {
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
128
frontend/src/components/tools/toolPicker/ToolSearch.tsx
Normal file
128
frontend/src/components/tools/toolPicker/ToolSearch.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { TextInput, Stack, Button, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { type ToolRegistryEntry } from "../../../data/toolRegistry";
|
||||
|
||||
interface ToolSearchProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
toolRegistry: Readonly<Record<string, ToolRegistryEntry>>;
|
||||
onToolSelect?: (toolId: string) => void;
|
||||
mode: 'filter' | 'dropdown';
|
||||
selectedToolKey?: string | null;
|
||||
}
|
||||
|
||||
const ToolSearch = ({
|
||||
value,
|
||||
onChange,
|
||||
toolRegistry,
|
||||
onToolSelect,
|
||||
mode = 'filter',
|
||||
selectedToolKey
|
||||
}: ToolSearchProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const filteredTools = useMemo(() => {
|
||||
if (!value.trim()) return [];
|
||||
return Object.entries(toolRegistry)
|
||||
.filter(([id, tool]) => {
|
||||
if (mode === 'dropdown' && id === selectedToolKey) return false;
|
||||
return tool.name.toLowerCase().includes(value.toLowerCase()) ||
|
||||
tool.description.toLowerCase().includes(value.toLowerCase());
|
||||
})
|
||||
.slice(0, 6)
|
||||
.map(([id, tool]) => ({ id, tool }));
|
||||
}, [value, toolRegistry, mode, selectedToolKey]);
|
||||
|
||||
const handleSearchChange = (searchValue: string) => {
|
||||
onChange(searchValue);
|
||||
if (mode === 'dropdown') {
|
||||
setDropdownOpen(searchValue.trim().length > 0 && filteredTools.length > 0);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const searchInput = (
|
||||
<TextInput
|
||||
ref={searchRef}
|
||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
value={value}
|
||||
radius="md"
|
||||
onChange={(e) => handleSearchChange(e.currentTarget.value)}
|
||||
autoComplete="off"
|
||||
className="search-input rounded-lg"
|
||||
leftSection={<SearchIcon sx={{ fontSize: 16, color: 'var(--tools-text-and-icon-color)' }} />}
|
||||
/>
|
||||
);
|
||||
|
||||
if (mode === 'filter') {
|
||||
return searchInput;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={searchRef} style={{ position: 'relative' }}>
|
||||
{searchInput}
|
||||
{dropdownOpen && filteredTools.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
border: '1px solid var(--border-default)',
|
||||
borderRadius: '8px',
|
||||
marginTop: '4px',
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
|
||||
}}
|
||||
>
|
||||
<Stack gap="xs" style={{ padding: '8px' }}>
|
||||
{filteredTools.map(({ id, tool }) => (
|
||||
<Button
|
||||
key={id}
|
||||
variant="subtle"
|
||||
onClick={() => onToolSelect && onToolSelect(id)}
|
||||
leftSection={
|
||||
<div style={{ color: 'var(--tools-text-and-icon-color)' }}>
|
||||
{tool.icon}
|
||||
</div>
|
||||
}
|
||||
fullWidth
|
||||
justify="flex-start"
|
||||
style={{
|
||||
borderRadius: '6px',
|
||||
color: 'var(--tools-text-and-icon-color)',
|
||||
padding: '8px 12px'
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'left' }}>
|
||||
<div style={{ fontWeight: 500 }}>{tool.name}</div>
|
||||
<Text size="xs" c="dimmed" style={{ marginTop: '2px' }}>
|
||||
{tool.description}
|
||||
</Text>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolSearch;
|
529
frontend/src/data/toolRegistry.tsx
Normal file
529
frontend/src/data/toolRegistry.tsx
Normal file
@ -0,0 +1,529 @@
|
||||
import React from 'react';
|
||||
import SplitPdfPanel from "../tools/Split";
|
||||
import CompressPdfPanel from "../tools/Compress";
|
||||
import MergePdfPanel from "../tools/Merge";
|
||||
import OCRPanel from '../tools/OCR';
|
||||
import ConvertPanel from '../tools/Convert';
|
||||
|
||||
export type ToolRegistryEntry = {
|
||||
icon: React.ReactNode;
|
||||
name: string;
|
||||
component: React.ComponentType<any> | null;
|
||||
view: string;
|
||||
description: string;
|
||||
category: string;
|
||||
subcategory: string | null;
|
||||
};
|
||||
|
||||
export type ToolRegistry = {
|
||||
[key: string]: ToolRegistryEntry;
|
||||
};
|
||||
|
||||
export const baseToolRegistry: ToolRegistry = {
|
||||
"add-attachments": {
|
||||
icon: <span className="material-symbols-rounded">attachment</span>,
|
||||
name: "home.attachments.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.attachments.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting",
|
||||
},
|
||||
"add-image": {
|
||||
icon: <span className="material-symbols-rounded">image</span>,
|
||||
name: "home.addImage.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.addImage.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"add-page-numbers": {
|
||||
icon: <span className="material-symbols-rounded">123</span>,
|
||||
name: "home.add-page-numbers.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.add-page-numbers.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting"
|
||||
},
|
||||
"add-password": {
|
||||
icon: <span className="material-symbols-rounded">password</span>,
|
||||
name: "home.addPassword.title",
|
||||
component: null,
|
||||
view: "security",
|
||||
description: "home.addPassword.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Security"
|
||||
},
|
||||
"add-stamp": {
|
||||
icon: <span className="material-symbols-rounded">approval</span>,
|
||||
name: "home.AddStampRequest.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.AddStampRequest.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Security"
|
||||
},
|
||||
"add-watermark": {
|
||||
icon: <span className="material-symbols-rounded">branding_watermark</span>,
|
||||
name: "home.watermark.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.watermark.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Security"
|
||||
},
|
||||
"adjust-colors-contrast": {
|
||||
icon: <span className="material-symbols-rounded">palette</span>,
|
||||
name: "home.adjust-contrast.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.adjust-contrast.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"adjust-page-size-scale": {
|
||||
icon: <span className="material-symbols-rounded">crop_free</span>,
|
||||
name: "home.scalePages.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.scalePages.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting"
|
||||
},
|
||||
"auto-rename-pdf-file": {
|
||||
icon: <span className="material-symbols-rounded">match_word</span>,
|
||||
name: "home.auto-rename.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.auto-rename.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Automation"
|
||||
},
|
||||
"auto-split-by-size-count": {
|
||||
icon: <span className="material-symbols-rounded">content_cut</span>,
|
||||
name: "home.autoSizeSplitPDF.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.autoSizeSplitPDF.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Automation"
|
||||
},
|
||||
"auto-split-pages": {
|
||||
icon: <span className="material-symbols-rounded">split_scene_right</span>,
|
||||
name: "home.autoSplitPDF.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.autoSplitPDF.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Automation"
|
||||
},
|
||||
"automate": {
|
||||
icon: <span className="material-symbols-rounded">automation</span>,
|
||||
name: "home.automate.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.automate.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Automation"
|
||||
},
|
||||
"certSign": {
|
||||
icon: <span className="material-symbols-rounded">workspace_premium</span>,
|
||||
name: "home.certSign.title",
|
||||
component: null,
|
||||
view: "sign",
|
||||
description: "home.certSign.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Signing"
|
||||
},
|
||||
"change-metadata": {
|
||||
icon: <span className="material-symbols-rounded">assignment</span>,
|
||||
name: "home.changeMetadata.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.changeMetadata.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Review"
|
||||
},
|
||||
"change-permissions": {
|
||||
icon: <span className="material-symbols-rounded">admin_panel_settings</span>,
|
||||
name: "home.permissions.title",
|
||||
component: null,
|
||||
view: "security",
|
||||
description: "home.permissions.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Review"
|
||||
},
|
||||
"compare": {
|
||||
icon: <span className="material-symbols-rounded">compare</span>,
|
||||
name: "home.compare.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.compare.desc",
|
||||
category: "Recommended Tools",
|
||||
subcategory: null
|
||||
},
|
||||
"compressPdfs": {
|
||||
icon: <span className="material-symbols-rounded">zoom_in_map</span>,
|
||||
name: "home.compressPdfs.title",
|
||||
component: CompressPdfPanel,
|
||||
view: "compress",
|
||||
description: "home.compressPdfs.desc",
|
||||
category: "Recommended Tools",
|
||||
subcategory: null
|
||||
},
|
||||
"convert": {
|
||||
icon: <span className="material-symbols-rounded">sync_alt</span>,
|
||||
name: "home.fileToPDF.title",
|
||||
component: ConvertPanel,
|
||||
view: "convert",
|
||||
description: "home.fileToPDF.desc",
|
||||
category: "Recommended Tools",
|
||||
subcategory: null
|
||||
},
|
||||
"cropPdf": {
|
||||
icon: <span className="material-symbols-rounded">crop</span>,
|
||||
name: "home.crop.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.crop.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting"
|
||||
},
|
||||
"detect-split-scanned-photos": {
|
||||
icon: <span className="material-symbols-rounded">scanner</span>,
|
||||
name: "home.ScannerImageSplit.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.ScannerImageSplit.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"edit-table-of-contents": {
|
||||
icon: <span className="material-symbols-rounded">bookmark_add</span>,
|
||||
name: "home.editTableOfContents.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.editTableOfContents.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"extract-images": {
|
||||
icon: <span className="material-symbols-rounded">filter</span>,
|
||||
name: "home.extractImages.title",
|
||||
component: null,
|
||||
view: "extract",
|
||||
description: "home.extractImages.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Extraction"
|
||||
},
|
||||
"extract-pages": {
|
||||
icon: <span className="material-symbols-rounded">upload</span>,
|
||||
name: "home.extractPage.title",
|
||||
component: null,
|
||||
view: "extract",
|
||||
description: "home.extractPage.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Extraction"
|
||||
},
|
||||
"flatten": {
|
||||
icon: <span className="material-symbols-rounded">layers_clear</span>,
|
||||
name: "home.flatten.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.flatten.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Security"
|
||||
},
|
||||
"get-all-info-on-pdf": {
|
||||
icon: <span className="material-symbols-rounded">fact_check</span>,
|
||||
name: "home.getPdfInfo.title",
|
||||
component: null,
|
||||
view: "extract",
|
||||
description: "home.getPdfInfo.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Verification"
|
||||
},
|
||||
"manage-certificates": {
|
||||
icon: <span className="material-symbols-rounded">license</span>,
|
||||
name: "home.manageCertificates.title",
|
||||
component: null,
|
||||
view: "security",
|
||||
description: "home.manageCertificates.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Security"
|
||||
},
|
||||
"mergePdfs": {
|
||||
icon: <span className="material-symbols-rounded">library_add</span>,
|
||||
name: "home.merge.title",
|
||||
component: MergePdfPanel,
|
||||
view: "merge",
|
||||
description: "home.merge.desc",
|
||||
category: "Recommended Tools",
|
||||
subcategory: null
|
||||
},
|
||||
"multi-page-layout": {
|
||||
icon: <span className="material-symbols-rounded">dashboard</span>,
|
||||
name: "home.pageLayout.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.pageLayout.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting"
|
||||
},
|
||||
"multi-tool": {
|
||||
icon: <span className="material-symbols-rounded">dashboard_customize</span>,
|
||||
name: "home.multiTool.title",
|
||||
component: null,
|
||||
view: "pageEditor",
|
||||
description: "home.multiTool.desc",
|
||||
category: "Recommended Tools",
|
||||
subcategory: null
|
||||
},
|
||||
"ocr": {
|
||||
icon: <span className="material-symbols-rounded">quick_reference_all</span>,
|
||||
name: "home.ocr.title",
|
||||
component: OCRPanel,
|
||||
view: "convert",
|
||||
description: "home.ocr.desc",
|
||||
category: "Recommended Tools",
|
||||
subcategory: null
|
||||
},
|
||||
"overlay-pdfs": {
|
||||
icon: <span className="material-symbols-rounded">layers</span>,
|
||||
name: "home.overlay-pdfs.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.overlay-pdfs.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"read": {
|
||||
icon: <span className="material-symbols-rounded">article</span>,
|
||||
name: "home.read.title",
|
||||
component: null,
|
||||
view: "view",
|
||||
description: "home.read.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Review"
|
||||
},
|
||||
"redact": {
|
||||
icon: <span className="material-symbols-rounded">visibility_off</span>,
|
||||
name: "home.redact.title",
|
||||
component: null,
|
||||
view: "redact",
|
||||
description: "home.redact.desc",
|
||||
category: "Recommended Tools",
|
||||
subcategory: null
|
||||
},
|
||||
"remove": {
|
||||
icon: <span className="material-symbols-rounded">delete</span>,
|
||||
name: "home.removePages.title",
|
||||
component: null,
|
||||
view: "remove",
|
||||
description: "home.removePages.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Removal"
|
||||
},
|
||||
"remove-annotations": {
|
||||
icon: <span className="material-symbols-rounded">thread_unread</span>,
|
||||
name: "home.removeAnnotations.title",
|
||||
component: null,
|
||||
view: "remove",
|
||||
description: "home.removeAnnotations.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Removal"
|
||||
},
|
||||
"remove-blank-pages": {
|
||||
icon: <span className="material-symbols-rounded">scan_delete</span>,
|
||||
name: "home.removeBlanks.title",
|
||||
component: null,
|
||||
view: "remove",
|
||||
description: "home.removeBlanks.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Removal"
|
||||
},
|
||||
"remove-certificate-sign": {
|
||||
icon: <span className="material-symbols-rounded">remove_moderator</span>,
|
||||
name: "home.removeCertSign.title",
|
||||
component: null,
|
||||
view: "security",
|
||||
description: "home.removeCertSign.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Removal"
|
||||
},
|
||||
"remove-image": {
|
||||
icon: <span className="material-symbols-rounded">remove_selection</span>,
|
||||
name: "home.removeImagePdf.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.removeImagePdf.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Removal"
|
||||
},
|
||||
"remove-password": {
|
||||
icon: <span className="material-symbols-rounded">lock_open_right</span>,
|
||||
name: "home.removePassword.title",
|
||||
component: null,
|
||||
view: "security",
|
||||
description: "home.removePassword.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Removal"
|
||||
},
|
||||
"repair": {
|
||||
icon: <span className="material-symbols-rounded">build</span>,
|
||||
name: "home.repair.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.repair.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"replace-and-invert-color": {
|
||||
icon: <span className="material-symbols-rounded">format_color_fill</span>,
|
||||
name: "home.replaceColorPdf.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.replaceColorPdf.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"reorganize-pages": {
|
||||
icon: <span className="material-symbols-rounded">move_down</span>,
|
||||
name: "home.reorganizePages.title",
|
||||
component: null,
|
||||
view: "pageEditor",
|
||||
description: "home.reorganizePages.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting"
|
||||
},
|
||||
"rotate": {
|
||||
icon: <span className="material-symbols-rounded">rotate_right</span>,
|
||||
name: "home.rotate.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.rotate.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting"
|
||||
},
|
||||
"sanitize": {
|
||||
icon: <span className="material-symbols-rounded">sanitizer</span>,
|
||||
name: "home.sanitizePdf.title",
|
||||
component: null,
|
||||
view: "security",
|
||||
description: "home.sanitizePdf.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Security"
|
||||
},
|
||||
"scanner-effect": {
|
||||
icon: <span className="material-symbols-rounded">scanner</span>,
|
||||
name: "home.fakeScan.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.fakeScan.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"show-javascript": {
|
||||
icon: <span className="material-symbols-rounded">javascript</span>,
|
||||
name: "home.showJS.title",
|
||||
component: null,
|
||||
view: "extract",
|
||||
description: "home.showJS.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Developer Tools"
|
||||
},
|
||||
"sign": {
|
||||
icon: <span className="material-symbols-rounded">signature</span>,
|
||||
name: "home.sign.title",
|
||||
component: null,
|
||||
view: "sign",
|
||||
description: "home.sign.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Signing"
|
||||
},
|
||||
"single-large-page": {
|
||||
icon: <span className="material-symbols-rounded">looks_one</span>,
|
||||
name: "home.PdfToSinglePage.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.PdfToSinglePage.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting"
|
||||
},
|
||||
"split": {
|
||||
icon: <span className="material-symbols-rounded">content_cut</span>,
|
||||
name: "home.split.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.split.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting"
|
||||
},
|
||||
"split-by-chapters": {
|
||||
icon: <span className="material-symbols-rounded">collections_bookmark</span>,
|
||||
name: "home.splitPdfByChapters.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.splitPdfByChapters.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"split-by-sections": {
|
||||
icon: <span className="material-symbols-rounded">grid_on</span>,
|
||||
name: "home.split-by-sections.title",
|
||||
component: null,
|
||||
view: "format",
|
||||
description: "home.split-by-sections.desc",
|
||||
category: "Advanced Tools",
|
||||
subcategory: "Advanced Formatting"
|
||||
},
|
||||
"splitPdf": {
|
||||
icon: <span className="material-symbols-rounded">content_cut</span>,
|
||||
name: "home.split.title",
|
||||
component: SplitPdfPanel,
|
||||
view: "split",
|
||||
description: "home.split.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Page Formatting"
|
||||
},
|
||||
"unlock-pdf-forms": {
|
||||
icon: <span className="material-symbols-rounded">preview_off</span>,
|
||||
name: "home.unlockPDFForms.title",
|
||||
component: null,
|
||||
view: "security",
|
||||
description: "home.unlockPDFForms.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Document Security"
|
||||
},
|
||||
"validate-pdf-signature": {
|
||||
icon: <span className="material-symbols-rounded">verified</span>,
|
||||
name: "home.validateSignature.title",
|
||||
component: null,
|
||||
view: "security",
|
||||
description: "home.validateSignature.desc",
|
||||
category: "Standard Tools",
|
||||
subcategory: "Verification"
|
||||
},
|
||||
"view-pdf": {
|
||||
icon: <span className="material-symbols-rounded">article</span>,
|
||||
name: "home.viewPdf.title",
|
||||
component: null,
|
||||
view: "view",
|
||||
description: "home.viewPdf.desc",
|
||||
category: "Recommended Tools",
|
||||
subcategory: null
|
||||
}
|
||||
};
|
||||
|
||||
export const toolEndpoints: Record<string,
|
||||
string[]> = {
|
||||
split: ["split-pages",
|
||||
"split-pdf-by-sections",
|
||||
"split-by-size-or-count",
|
||||
"split-pdf-by-chapters"],
|
||||
compressPdfs: ["compress-pdf"],
|
||||
merge: ["merge-pdfs"],
|
||||
// Add more endpoint mappings as needed
|
||||
};
|
@ -18,7 +18,13 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb
|
||||
if (stored && ['light', 'dark', 'rainbow'].includes(stored)) {
|
||||
return stored as ThemeMode;
|
||||
}
|
||||
try {
|
||||
// Fallback to OS preference if available
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
return prefersDark ? 'dark' : initialTheme;
|
||||
} catch {
|
||||
return initialTheme;
|
||||
}
|
||||
});
|
||||
|
||||
// Track rapid toggles for easter egg
|
||||
|
@ -1,88 +1,13 @@
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||
import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
||||
import ApiIcon from "@mui/icons-material/Api";
|
||||
import { baseToolRegistry, toolEndpoints, type ToolRegistryEntry } from "../data/toolRegistry";
|
||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
|
||||
|
||||
|
||||
// Add entry here with maxFiles, endpoints, and lazy component
|
||||
const toolDefinitions: Record<string, ToolDefinition> = {
|
||||
split: {
|
||||
id: "split",
|
||||
icon: <ContentCutIcon />,
|
||||
component: React.lazy(() => import("../tools/Split")),
|
||||
maxFiles: 1,
|
||||
category: "manipulation",
|
||||
description: "Split PDF files into smaller parts",
|
||||
endpoints: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"]
|
||||
},
|
||||
compress: {
|
||||
id: "compress",
|
||||
icon: <ZoomInMapIcon />,
|
||||
component: React.lazy(() => import("../tools/Compress")),
|
||||
maxFiles: -1,
|
||||
category: "optimization",
|
||||
description: "Reduce PDF file size",
|
||||
endpoints: ["compress-pdf"]
|
||||
},
|
||||
convert: {
|
||||
id: "convert",
|
||||
icon: <SwapHorizIcon />,
|
||||
component: React.lazy(() => import("../tools/Convert")),
|
||||
maxFiles: -1,
|
||||
category: "manipulation",
|
||||
description: "Change to and from PDF and other formats",
|
||||
endpoints: ["pdf-to-img", "img-to-pdf", "pdf-to-word", "pdf-to-presentation", "pdf-to-text", "pdf-to-html", "pdf-to-xml", "html-to-pdf", "markdown-to-pdf", "file-to-pdf"],
|
||||
supportedFormats: [
|
||||
// Microsoft Office
|
||||
"doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx",
|
||||
// OpenDocument
|
||||
"odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg",
|
||||
// Text formats
|
||||
"txt", "text", "xml", "rtf", "html", "lwp", "md",
|
||||
// Images
|
||||
"bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp",
|
||||
// StarOffice
|
||||
"sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw",
|
||||
// Email formats
|
||||
"eml",
|
||||
// Archive formats
|
||||
"zip",
|
||||
// Other
|
||||
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
|
||||
]
|
||||
},
|
||||
swagger: {
|
||||
id: "swagger",
|
||||
icon: <ApiIcon />,
|
||||
component: React.lazy(() => import("../tools/SwaggerUI")),
|
||||
maxFiles: 0,
|
||||
category: "utility",
|
||||
description: "Open API documentation",
|
||||
endpoints: ["swagger-ui"]
|
||||
},
|
||||
ocr: {
|
||||
id: "ocr",
|
||||
icon: <span className="material-symbols-rounded font-size-20">
|
||||
quick_reference_all
|
||||
</span>,
|
||||
component: React.lazy(() => import("../tools/OCR")),
|
||||
maxFiles: -1,
|
||||
category: "utility",
|
||||
description: "Extract text from images using OCR",
|
||||
endpoints: ["ocr-pdf"]
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
interface ToolManagementResult {
|
||||
selectedToolKey: string | null;
|
||||
selectedTool: Tool | null;
|
||||
selectedTool: ToolRegistryEntry | null;
|
||||
toolSelectedFileIds: string[];
|
||||
toolRegistry: ToolRegistry;
|
||||
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||
selectTool: (toolKey: string) => void;
|
||||
clearToolSelection: () => void;
|
||||
setToolSelectedFileIds: (fileIds: string[]) => void;
|
||||
@ -95,30 +20,30 @@ export const useToolManagement = (): ToolManagementResult => {
|
||||
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
|
||||
|
||||
const allEndpoints = Array.from(new Set(
|
||||
Object.values(toolDefinitions).flatMap(tool => tool.endpoints || [])
|
||||
Object.values(toolEndpoints).flat() as string[]
|
||||
));
|
||||
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
||||
|
||||
const isToolAvailable = useCallback((toolKey: string): boolean => {
|
||||
if (endpointsLoading) return true;
|
||||
const tool = toolDefinitions[toolKey];
|
||||
if (!tool?.endpoints) return true;
|
||||
return tool.endpoints.some(endpoint => endpointStatus[endpoint] === true);
|
||||
const endpoints = toolEndpoints[toolKey] || [];
|
||||
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
|
||||
}, [endpointsLoading, endpointStatus]);
|
||||
|
||||
const toolRegistry: ToolRegistry = useMemo(() => {
|
||||
const availableTools: ToolRegistry = {};
|
||||
Object.keys(toolDefinitions).forEach(toolKey => {
|
||||
const toolRegistry: Record<string, ToolRegistryEntry> = useMemo(() => {
|
||||
const availableToolRegistry: Record<string, ToolRegistryEntry> = {};
|
||||
Object.keys(baseToolRegistry).forEach(toolKey => {
|
||||
if (isToolAvailable(toolKey)) {
|
||||
const toolDef = toolDefinitions[toolKey];
|
||||
availableTools[toolKey] = {
|
||||
...toolDef,
|
||||
name: t(`home.${toolKey}.title`, toolKey.charAt(0).toUpperCase() + toolKey.slice(1))
|
||||
const baseTool = baseToolRegistry[toolKey as keyof typeof baseToolRegistry];
|
||||
availableToolRegistry[toolKey] = {
|
||||
...baseTool,
|
||||
name: t(baseTool.name),
|
||||
description: t(baseTool.description)
|
||||
};
|
||||
}
|
||||
});
|
||||
return availableTools;
|
||||
}, [t, isToolAvailable]);
|
||||
return availableToolRegistry;
|
||||
}, [isToolAvailable, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) {
|
||||
|
@ -2,11 +2,22 @@ import '@mantine/core/styles.css';
|
||||
import './index.css'; // Import Tailwind CSS
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@mantine/core';
|
||||
import { ColorSchemeScript } from '@mantine/core';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import './i18n'; // Initialize i18next
|
||||
|
||||
// Compute initial color scheme
|
||||
function getInitialScheme(): 'light' | 'dark' {
|
||||
const stored = localStorage.getItem('stirling-theme');
|
||||
if (stored === 'light' || stored === 'dark') return stored;
|
||||
try {
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
return prefersDark ? 'dark' : 'light';
|
||||
} catch {
|
||||
return 'light';
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.getElementById('root');
|
||||
if (!container) {
|
||||
@ -15,12 +26,10 @@ if (!container) {
|
||||
const root = ReactDOM.createRoot(container); // Finds the root DOM element
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ColorSchemeScript />
|
||||
<MantineProvider defaultColorScheme="auto">
|
||||
<ColorSchemeScript defaultColorScheme={getInitialScheme()} />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</MantineProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
@ -81,7 +81,7 @@
|
||||
--text-secondary: #4b5563;
|
||||
--text-muted: #6b7280;
|
||||
--border-subtle: #e5e7eb;
|
||||
--border-default: #d1d5db;
|
||||
--border-default: #E2E8F0;
|
||||
--border-strong: #9ca3af;
|
||||
--hover-bg: #f9fafb;
|
||||
--active-bg: #f3f4f6;
|
||||
@ -117,6 +117,18 @@
|
||||
--icon-inactive-bg: #9CA3AF;
|
||||
--icon-inactive-color: #FFFFFF;
|
||||
|
||||
/* New theme colors for text and icons */
|
||||
--tools-text-and-icon-color: #374151;
|
||||
|
||||
/* Tool picker sticky header variables (light mode) */
|
||||
--tool-header-bg: #DBEFFF;
|
||||
--tool-header-border: #BEE2FF;
|
||||
--tool-header-text: #1E88E5;
|
||||
--tool-header-badge-bg: #C0DDFF;
|
||||
--tool-header-badge-text: #004E99;
|
||||
|
||||
/* Subcategory title styling (light mode) */
|
||||
--tool-subcategory-text-color: #6B7280;
|
||||
--accent-interactive: #4A90E2;
|
||||
--text-instruction: #4A90E2;
|
||||
--text-brand: var(--color-gray-700);
|
||||
@ -177,7 +189,7 @@
|
||||
--bg-raised: #1F2329;
|
||||
--bg-muted: #1F2329;
|
||||
--bg-background: #2A2F36;
|
||||
--bg-toolbar: #272A2E;
|
||||
--bg-toolbar: #1F2329;
|
||||
--bg-file-manager: #1F2329;
|
||||
--bg-file-list: #2A2F36;
|
||||
--btn-open-file: #0A8BFF;
|
||||
@ -185,7 +197,7 @@
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--border-subtle: #2A2F36;
|
||||
--border-default: #374151;
|
||||
--border-default: #3A4047;
|
||||
--border-strong: #4b5563;
|
||||
--hover-bg: #374151;
|
||||
--active-bg: #4b5563;
|
||||
@ -251,6 +263,18 @@
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4);
|
||||
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.4);
|
||||
|
||||
--tools-text-and-icon-color: #D0D6DC;
|
||||
|
||||
/* Tool picker sticky header variables (dark mode) */
|
||||
--tool-header-bg: #2A2F36;
|
||||
--tool-header-border: #3A4047;
|
||||
--tool-header-text: #D0D6DC;
|
||||
--tool-header-badge-bg: #4B525A;
|
||||
--tool-header-badge-text: #FFFFFF;
|
||||
|
||||
/* Subcategory title styling (dark mode) */
|
||||
--tool-subcategory-text-color: #6B7280;
|
||||
}
|
||||
|
||||
/* Dropzone drop state styling */
|
||||
|
Loading…
Reference in New Issue
Block a user