mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Cleanup/v2/text editor (#5169)
# Description of Changes <img width="2056" height="1075" alt="Screenshot 2025-12-04 at 8 16 28 PM" src="https://github.com/user-attachments/assets/9d2deacb-e471-4711-b9ac-9ef5a64b6f23" /> <img width="2056" height="1070" alt="Screenshot 2025-12-04 at 8 16 51 PM" src="https://github.com/user-attachments/assets/07f07835-c973-4f56-80fb-5efc00ff1651" /> <img width="728" height="517" alt="Screenshot 2025-12-04 at 8 17 04 PM" src="https://github.com/user-attachments/assets/8c59fae0-93a8-4da5-b2e4-9a89be651b7f" /> <img width="388" height="218" alt="Screenshot 2025-12-04 at 8 17 29 PM" src="https://github.com/user-attachments/assets/997e9a68-6a3b-4033-bc45-04e3945f23d5" /> --- ## 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:
@@ -6163,7 +6163,7 @@ title = "Authentication Failed"
|
|||||||
message = "Authentication was not successful. You can close this window and try again."
|
message = "Authentication was not successful. You can close this window and try again."
|
||||||
|
|
||||||
[pdfTextEditor]
|
[pdfTextEditor]
|
||||||
title = "PDF JSON Editor"
|
title = "PDF Text Editor"
|
||||||
viewLabel = "PDF Editor"
|
viewLabel = "PDF Editor"
|
||||||
converting = "Converting PDF to editable format..."
|
converting = "Converting PDF to editable format..."
|
||||||
conversionFailed = "Failed to convert PDF. Please try again."
|
conversionFailed = "Failed to convert PDF. Please try again."
|
||||||
@@ -6192,6 +6192,8 @@ reset = "Reset Changes"
|
|||||||
downloadJson = "Download JSON"
|
downloadJson = "Download JSON"
|
||||||
generatePdf = "Generate PDF"
|
generatePdf = "Generate PDF"
|
||||||
saveChanges = "Save Changes"
|
saveChanges = "Save Changes"
|
||||||
|
applyChanges = "Apply Changes"
|
||||||
|
downloadCopy = "Download Copy"
|
||||||
|
|
||||||
[pdfTextEditor.options.autoScaleText]
|
[pdfTextEditor.options.autoScaleText]
|
||||||
title = "Auto-scale text to fit boxes"
|
title = "Auto-scale text to fit boxes"
|
||||||
@@ -6210,6 +6212,24 @@ descriptionInline = "Tip: Hold Ctrl (Cmd) or Shift to multi-select text boxes. A
|
|||||||
title = "Lock edited text to a single PDF element"
|
title = "Lock edited text to a single PDF element"
|
||||||
description = "When enabled, the editor exports each edited text box as one PDF text element to avoid overlapping glyphs or mixed fonts."
|
description = "When enabled, the editor exports each edited text box as one PDF text element to avoid overlapping glyphs or mixed fonts."
|
||||||
|
|
||||||
|
[pdfTextEditor.options.advanced]
|
||||||
|
title = "Advanced Settings"
|
||||||
|
|
||||||
|
[pdfTextEditor.tooltip.header]
|
||||||
|
title = "Preview Limitations"
|
||||||
|
|
||||||
|
[pdfTextEditor.tooltip.textFocus]
|
||||||
|
title = "Text and Image Focus"
|
||||||
|
text = "This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here."
|
||||||
|
|
||||||
|
[pdfTextEditor.tooltip.previewVariance]
|
||||||
|
title = "Preview Variance"
|
||||||
|
text = "Some visuals (such as table borders, shapes, or annotation appearances) may not display exactly in the preview. The exported PDF keeps the original drawing commands whenever possible."
|
||||||
|
|
||||||
|
[pdfTextEditor.tooltip.alpha]
|
||||||
|
title = "Alpha Viewer"
|
||||||
|
text = "This alpha viewer is still evolving—certain fonts, colours, transparency effects, and layout details may shift slightly. Please double-check the generated PDF before sharing."
|
||||||
|
|
||||||
[pdfTextEditor.manual]
|
[pdfTextEditor.manual]
|
||||||
mergeTooltip = "Merge selected boxes"
|
mergeTooltip = "Merge selected boxes"
|
||||||
merge = "Merge selection"
|
merge = "Merge selection"
|
||||||
@@ -6229,8 +6249,8 @@ alpha = "This alpha viewer is still evolving—certain fonts, colours, transpare
|
|||||||
[pdfTextEditor.empty]
|
[pdfTextEditor.empty]
|
||||||
title = "No document loaded"
|
title = "No document loaded"
|
||||||
subtitle = "Load a PDF or JSON file to begin editing text content."
|
subtitle = "Load a PDF or JSON file to begin editing text content."
|
||||||
dropzone = "Drag and drop a PDF or JSON file here, or click to browse"
|
dropzone = "Drag and drop a PDF here, or click to browse"
|
||||||
dropzoneWithFiles = "Select a file from the Files tab, or drag and drop a PDF or JSON file here, or click to browse"
|
dropzoneWithFiles = "Select a file from the Files tab, or drag and drop a PDF here, or click to browse"
|
||||||
|
|
||||||
[pdfTextEditor.welcomeBanner]
|
[pdfTextEditor.welcomeBanner]
|
||||||
title = "Welcome to PDF Text Editor (Early Access)"
|
title = "Welcome to PDF Text Editor (Early Access)"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Code,
|
Code,
|
||||||
Collapse,
|
Collapse,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
List,
|
List,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -29,10 +30,14 @@ import {
|
|||||||
getFontStatusColor,
|
getFontStatusColor,
|
||||||
getFontStatusDescription,
|
getFontStatusDescription,
|
||||||
} from '@app/tools/pdfTextEditor/fontAnalysis';
|
} from '@app/tools/pdfTextEditor/fontAnalysis';
|
||||||
|
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||||
|
import { Tooltip as CustomTooltip } from '@app/components/shared/Tooltip';
|
||||||
|
|
||||||
interface FontStatusPanelProps {
|
interface FontStatusPanelProps {
|
||||||
document: PdfJsonDocument | null;
|
document: PdfJsonDocument | null;
|
||||||
pageIndex?: number;
|
pageIndex?: number;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
onCollapsedChange?: (collapsed: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FontStatusBadge = ({ analysis }: { analysis: FontAnalysis }) => {
|
const FontStatusBadge = ({ analysis }: { analysis: FontAnalysis }) => {
|
||||||
@@ -74,25 +79,31 @@ const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => {
|
|||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper withBorder p="xs" style={{ cursor: 'pointer' }} onClick={() => setExpanded(!expanded)}>
|
<Paper withBorder px="sm" py="md" style={{ cursor: 'pointer' }} onClick={() => setExpanded(!expanded)}>
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
<Group justify="space-between">
|
<Flex align="center" justify="space-between" wrap="nowrap">
|
||||||
<Group gap={4}>
|
<Group gap={4} wrap="nowrap" style={{ flex: 1, minWidth: 0 }}>
|
||||||
<FontDownloadIcon sx={{ fontSize: 16 }} />
|
<FontDownloadIcon sx={{ fontSize: 16, flexShrink: 0 }} />
|
||||||
<Text size="xs" fw={500} lineClamp={1}>
|
<CustomTooltip
|
||||||
{analysis.baseName}
|
sidebarTooltip={false}
|
||||||
</Text>
|
content={analysis.baseName}
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
<Text size="xs" fw={500} lineClamp={1} style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
{analysis.baseName}
|
||||||
|
</Text>
|
||||||
|
</CustomTooltip>
|
||||||
{analysis.isSubset && (
|
{analysis.isSubset && (
|
||||||
<Badge size="xs" color="gray" variant="outline">
|
<Badge size="xs" color="gray" variant="outline" style={{ flexShrink: 0 }}>
|
||||||
subset
|
subset
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap={4}>
|
<Group gap={4} wrap="nowrap" style={{ flexShrink: 0 }}>
|
||||||
<FontStatusBadge analysis={analysis} />
|
<FontStatusBadge analysis={analysis} />
|
||||||
{expanded ? <ExpandLessIcon sx={{ fontSize: 16 }} /> : <ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
{expanded ? <ExpandLessIcon sx={{ fontSize: 16 }} /> : <ExpandMoreIcon sx={{ fontSize: 16 }} />}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Flex>
|
||||||
|
|
||||||
<Collapse in={expanded}>
|
<Collapse in={expanded}>
|
||||||
<Stack gap={4} mt={4}>
|
<Stack gap={4} mt={4}>
|
||||||
@@ -165,7 +176,12 @@ const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }) => {
|
const FontStatusPanel: React.FC<FontStatusPanelProps> = ({
|
||||||
|
document,
|
||||||
|
pageIndex,
|
||||||
|
isCollapsed = false,
|
||||||
|
onCollapsedChange
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const fontAnalysis: DocumentFontAnalysis = useMemo(
|
const fontAnalysis: DocumentFontAnalysis = useMemo(
|
||||||
@@ -175,16 +191,6 @@ const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }
|
|||||||
|
|
||||||
const { canReproducePerfectly, hasWarnings, summary, fonts } = fontAnalysis;
|
const { canReproducePerfectly, hasWarnings, summary, fonts } = fontAnalysis;
|
||||||
|
|
||||||
const statusIcon = useMemo(() => {
|
|
||||||
if (canReproducePerfectly) {
|
|
||||||
return <CheckCircleIcon sx={{ fontSize: 16 }} />;
|
|
||||||
}
|
|
||||||
if (hasWarnings) {
|
|
||||||
return <WarningIcon sx={{ fontSize: 16 }} />;
|
|
||||||
}
|
|
||||||
return <InfoIcon sx={{ fontSize: 16 }} />;
|
|
||||||
}, [canReproducePerfectly, hasWarnings]);
|
|
||||||
|
|
||||||
// Early return AFTER all hooks are declared
|
// Early return AFTER all hooks are declared
|
||||||
if (!document || fontAnalysis.fonts.length === 0) {
|
if (!document || fontAnalysis.fonts.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -197,37 +203,46 @@ const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }
|
|||||||
: t('pdfTextEditor.fontAnalysis.allFonts', 'All fonts');
|
: t('pdfTextEditor.fontAnalysis.allFonts', 'All fonts');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion variant="contained" defaultValue={hasWarnings ? 'fonts' : undefined}>
|
<div>
|
||||||
<Accordion.Item value="fonts">
|
<div
|
||||||
<Accordion.Control>
|
style={{
|
||||||
<Group gap="xs" wrap="wrap" style={{ flex: 1 }}>
|
padding: '0.5rem',
|
||||||
<Group gap="xs" wrap="nowrap">
|
opacity: isCollapsed ? 0.8 : 1,
|
||||||
{statusIcon}
|
color: isCollapsed ? 'var(--mantine-color-dimmed)' : 'inherit',
|
||||||
<Text size="sm" fw={500}>
|
transition: 'opacity 0.2s ease, color 0.2s ease'
|
||||||
{pageLabel}
|
}}
|
||||||
</Text>
|
>
|
||||||
<Badge size="xs" color={statusColor} variant="dot">
|
{/* Header - matches ToolStep style */}
|
||||||
{fonts.length}
|
<Flex
|
||||||
</Badge>
|
align="center"
|
||||||
</Group>
|
justify="space-between"
|
||||||
|
mb={isCollapsed ? 0 : 'sm'}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => onCollapsedChange?.(!isCollapsed)}
|
||||||
|
>
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
{pageLabel}
|
||||||
|
</Text>
|
||||||
|
<Badge size="xs" color={statusColor} variant="dot">
|
||||||
|
{fonts.length}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{/* Warning badges BEFORE expansion */}
|
{isCollapsed ? (
|
||||||
<Group gap={4} wrap="wrap">
|
<LocalIcon icon="chevron-right-rounded" width="1.2rem" height="1.2rem" style={{
|
||||||
{summary.systemFallback > 0 && (
|
color: 'var(--mantine-color-dimmed)'
|
||||||
<Badge size="xs" color="yellow" variant="filled" leftSection={<WarningIcon sx={{ fontSize: 12 }} />}>
|
}} />
|
||||||
{summary.systemFallback} {t('pdfTextEditor.fontAnalysis.fallback', 'fallback')}
|
) : (
|
||||||
</Badge>
|
<LocalIcon icon="expand-more-rounded" width="1.2rem" height="1.2rem" style={{
|
||||||
)}
|
color: 'var(--mantine-color-dimmed)'
|
||||||
{summary.missing > 0 && (
|
}} />
|
||||||
<Badge size="xs" color="red" variant="filled" leftSection={<ErrorIcon sx={{ fontSize: 12 }} />}>
|
)}
|
||||||
{summary.missing} {t('pdfTextEditor.fontAnalysis.missing', 'missing')}
|
</Flex>
|
||||||
</Badge>
|
|
||||||
)}
|
{/* Content */}
|
||||||
</Group>
|
{!isCollapsed && (
|
||||||
</Group>
|
<Stack gap="xs" pl="sm">
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<Stack gap="xs">
|
|
||||||
{/* Overall Status Message */}
|
{/* Overall Status Message */}
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c="dimmed">
|
||||||
{canReproducePerfectly
|
{canReproducePerfectly
|
||||||
@@ -277,9 +292,10 @@ const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }
|
|||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Accordion.Panel>
|
)}
|
||||||
</Accordion.Item>
|
</div>
|
||||||
</Accordion>
|
<Divider style={{ color: '#E2E8F0', marginLeft: '1rem', marginRight: '-0.5rem' }} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
Menu,
|
||||||
|
Modal,
|
||||||
|
ScrollArea,
|
||||||
|
SegmentedControl,
|
||||||
|
Stack,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import AutorenewIcon from '@mui/icons-material/Autorenew';
|
||||||
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
|
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||||
|
import FileDownloadIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||||
|
|
||||||
|
import { PdfTextEditorViewData, TextGroup } from '@app/tools/pdfTextEditor/pdfTextEditorTypes';
|
||||||
|
import { pageDimensions } from '@app/tools/pdfTextEditor/pdfTextEditorUtils';
|
||||||
|
import FontStatusPanel from '@app/components/tools/pdfTextEditor/FontStatusPanel';
|
||||||
|
import ToolStep from '@app/components/tools/shared/ToolStep';
|
||||||
|
import { usePdfTextEditorTips } from '@app/components/tooltips/usePdfTextEditorTips';
|
||||||
|
import { Tooltip } from '@app/components/shared/Tooltip';
|
||||||
|
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||||
|
|
||||||
|
type GroupingMode = 'auto' | 'paragraph' | 'singleLine';
|
||||||
|
|
||||||
|
interface PdfTextEditorSidebarProps {
|
||||||
|
data: PdfTextEditorViewData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze page content to determine if it's paragraph-heavy
|
||||||
|
const analyzePageContentType = (
|
||||||
|
groups: TextGroup[],
|
||||||
|
pageWidth: number,
|
||||||
|
): boolean => {
|
||||||
|
if (groups.length < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widths = groups.map((g) => Math.max(g.bounds.right - g.bounds.left, 1));
|
||||||
|
const avgWidth = widths.reduce((sum, w) => sum + w, 0) / widths.length;
|
||||||
|
const stdDev = Math.sqrt(
|
||||||
|
widths.reduce((sum, w) => sum + Math.pow(w - avgWidth, 2), 0) / widths.length,
|
||||||
|
);
|
||||||
|
const coefficientOfVariation = avgWidth > 0 ? stdDev / avgWidth : 0;
|
||||||
|
const fullWidthRatio = widths.filter((w) => w > pageWidth * 0.65).length / widths.length;
|
||||||
|
|
||||||
|
const criterion1 = groups.length >= 3;
|
||||||
|
const criterion2 = avgWidth > pageWidth * 0.3;
|
||||||
|
const criterion3 = coefficientOfVariation > 0.5 || fullWidthRatio > 0.6;
|
||||||
|
|
||||||
|
return criterion1 && criterion2 && criterion3;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PdfTextEditorSidebar = ({ data }: PdfTextEditorSidebarProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [pendingModeChange, setPendingModeChange] = useState<GroupingMode | null>(null);
|
||||||
|
const [advancedSettingsCollapsed, setAdvancedSettingsCollapsed] = useState(false);
|
||||||
|
const [fontsCollapsed, setFontsCollapsed] = useState(false);
|
||||||
|
const pdfTextEditorTips = usePdfTextEditorTips();
|
||||||
|
|
||||||
|
const {
|
||||||
|
document: pdfDocument,
|
||||||
|
groupsByPage,
|
||||||
|
hasDocument,
|
||||||
|
hasChanges,
|
||||||
|
fileName,
|
||||||
|
isGeneratingPdf,
|
||||||
|
isSavingToWorkbench,
|
||||||
|
isConverting,
|
||||||
|
forceSingleTextElement,
|
||||||
|
groupingMode: externalGroupingMode,
|
||||||
|
autoScaleText,
|
||||||
|
selectedPage,
|
||||||
|
onReset,
|
||||||
|
onGeneratePdf,
|
||||||
|
onSaveToWorkbench,
|
||||||
|
onForceSingleTextElementChange,
|
||||||
|
onGroupingModeChange,
|
||||||
|
onAutoScaleTextChange,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
// Get page dimensions
|
||||||
|
const pages = pdfDocument?.pages ?? [];
|
||||||
|
const currentPage = pages[selectedPage] ?? null;
|
||||||
|
const { width: pageWidth } = pageDimensions(currentPage);
|
||||||
|
const pageGroups = groupsByPage[selectedPage] ?? [];
|
||||||
|
|
||||||
|
// Detect if current page contains paragraph-heavy content
|
||||||
|
const isParagraphPage = useMemo(() => {
|
||||||
|
return analyzePageContentType(pageGroups, pageWidth);
|
||||||
|
}, [pageGroups, pageWidth]);
|
||||||
|
|
||||||
|
const handleModeChangeRequest = useCallback((newMode: GroupingMode) => {
|
||||||
|
if (hasChanges && newMode !== externalGroupingMode) {
|
||||||
|
setPendingModeChange(newMode);
|
||||||
|
} else {
|
||||||
|
onGroupingModeChange(newMode);
|
||||||
|
}
|
||||||
|
}, [hasChanges, externalGroupingMode, onGroupingModeChange]);
|
||||||
|
|
||||||
|
const handleConfirmModeChange = useCallback(() => {
|
||||||
|
if (pendingModeChange) {
|
||||||
|
onGroupingModeChange(pendingModeChange);
|
||||||
|
setPendingModeChange(null);
|
||||||
|
}
|
||||||
|
}, [pendingModeChange, onGroupingModeChange]);
|
||||||
|
|
||||||
|
const handleCancelModeChange = useCallback(() => {
|
||||||
|
setPendingModeChange(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack style={{ height: '100%', display: 'flex' }} gap={0}>
|
||||||
|
<ScrollArea style={{ flex: 1 }} offsetScrollbars>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Stack gap="xs" pl="md" pr={0} pt="md">
|
||||||
|
{/* Title row with ALPHA badge and info tooltip */}
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Flex align="center" gap="xs">
|
||||||
|
<Text fw={600} size="sm">
|
||||||
|
{t('pdfTextEditor.title', 'PDF Text Editor')}
|
||||||
|
</Text>
|
||||||
|
<Badge size="xs" variant="light" color="orange">
|
||||||
|
{t('toolPanel.alpha', 'Alpha')}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Tooltip
|
||||||
|
sidebarTooltip={true}
|
||||||
|
tips={pdfTextEditorTips.tips}
|
||||||
|
header={pdfTextEditorTips.header}
|
||||||
|
pinOnClick
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" color="blue" size="sm">
|
||||||
|
<LocalIcon icon="info-outline-rounded" width="1.25rem" height="1.25rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{fileName && (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('pdfTextEditor.currentFile', 'Current file: {{name}}', { name: fileName })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<ToolStep
|
||||||
|
title={t('pdfTextEditor.options.advanced.title', 'Advanced Settings')}
|
||||||
|
isCollapsed={advancedSettingsCollapsed}
|
||||||
|
onCollapsedClick={() => setAdvancedSettingsCollapsed(!advancedSettingsCollapsed)}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Divider />
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Group gap={4} align="center" style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Tooltip
|
||||||
|
sidebarTooltip={false}
|
||||||
|
content={t(
|
||||||
|
'pdfTextEditor.options.autoScaleText.description',
|
||||||
|
'Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF.'
|
||||||
|
)}
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" color="gray" size="sm" style={{ flexShrink: 0 }}>
|
||||||
|
<InfoOutlinedIcon fontSize="small" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Text fw={500} size="sm" style={{ flex: 1 }}>
|
||||||
|
{t('pdfTextEditor.options.autoScaleText.title', 'Auto-scale text to fit boxes')}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
checked={autoScaleText}
|
||||||
|
onChange={(event) => onAutoScaleTextChange(event.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group gap={4} align="center">
|
||||||
|
<Text fw={500} size="sm">
|
||||||
|
{t('pdfTextEditor.options.groupingMode.title', 'Text Grouping Mode')}
|
||||||
|
</Text>
|
||||||
|
{externalGroupingMode === 'auto' && isParagraphPage && (
|
||||||
|
<Badge size="xs" color="blue" variant="light" key={`para-${selectedPage}`}>
|
||||||
|
{t('pdfTextEditor.pageType.paragraph', 'Paragraph page')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{externalGroupingMode === 'auto' && !isParagraphPage && hasDocument && (
|
||||||
|
<Badge size="xs" color="gray" variant="light" key={`sparse-${selectedPage}`}>
|
||||||
|
{t('pdfTextEditor.pageType.sparse', 'Sparse text')}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{externalGroupingMode === 'auto'
|
||||||
|
? t(
|
||||||
|
'pdfTextEditor.options.groupingMode.autoDescription',
|
||||||
|
'Automatically detects page type and groups text appropriately.'
|
||||||
|
)
|
||||||
|
: externalGroupingMode === 'paragraph'
|
||||||
|
? t(
|
||||||
|
'pdfTextEditor.options.groupingMode.paragraphDescription',
|
||||||
|
'Groups aligned lines into multi-line paragraph text boxes.'
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
'pdfTextEditor.options.groupingMode.singleLineDescription',
|
||||||
|
'Keeps each PDF text line as a separate text box.'
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<SegmentedControl
|
||||||
|
value={externalGroupingMode}
|
||||||
|
onChange={(value) => handleModeChangeRequest(value as GroupingMode)}
|
||||||
|
data={[
|
||||||
|
{ label: t('pdfTextEditor.groupingMode.auto', 'Auto'), value: 'auto' },
|
||||||
|
{ label: t('pdfTextEditor.groupingMode.paragraph', 'Paragraph'), value: 'paragraph' },
|
||||||
|
{ label: t('pdfTextEditor.groupingMode.singleLine', 'Single Line'), value: 'singleLine' },
|
||||||
|
]}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center">
|
||||||
|
<Group gap={4} align="center" style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Tooltip
|
||||||
|
sidebarTooltip={false}
|
||||||
|
content={t(
|
||||||
|
'pdfTextEditor.options.forceSingleElement.description',
|
||||||
|
'When enabled, the editor exports each edited text box as one PDF text element to avoid overlapping glyphs or mixed fonts.'
|
||||||
|
)}
|
||||||
|
position="top"
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" color="gray" size="sm" style={{ flexShrink: 0 }}>
|
||||||
|
<InfoOutlinedIcon fontSize="small" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Text fw={500} size="sm" style={{ flex: 1 }}>
|
||||||
|
{t('pdfTextEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
checked={forceSingleTextElement}
|
||||||
|
onChange={(event) => onForceSingleTextElementChange(event.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</ToolStep>
|
||||||
|
|
||||||
|
{hasDocument && (
|
||||||
|
<FontStatusPanel
|
||||||
|
document={pdfDocument}
|
||||||
|
pageIndex={selectedPage}
|
||||||
|
isCollapsed={fontsCollapsed}
|
||||||
|
onCollapsedChange={setFontsCollapsed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<Group gap="xs" wrap="nowrap" p="md">
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
onClick={onSaveToWorkbench}
|
||||||
|
loading={isSavingToWorkbench}
|
||||||
|
disabled={!hasDocument || !hasChanges || isConverting}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
{t('pdfTextEditor.actions.applyChanges', 'Apply Changes')}
|
||||||
|
</Button>
|
||||||
|
<Menu position="bottom-end" withinPortal>
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
size="lg"
|
||||||
|
disabled={!hasDocument || isConverting}
|
||||||
|
>
|
||||||
|
<MoreHorizIcon fontSize="small" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<FileDownloadIcon fontSize="small" />}
|
||||||
|
onClick={() => onGeneratePdf()}
|
||||||
|
disabled={!hasChanges || isGeneratingPdf}
|
||||||
|
>
|
||||||
|
{t('pdfTextEditor.actions.downloadCopy', 'Download Copy')}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<AutorenewIcon fontSize="small" />}
|
||||||
|
onClick={onReset}
|
||||||
|
color="red"
|
||||||
|
>
|
||||||
|
{t('pdfTextEditor.actions.reset', 'Reset Changes')}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Mode Change Confirmation Modal */}
|
||||||
|
<Modal
|
||||||
|
opened={pendingModeChange !== null}
|
||||||
|
onClose={handleCancelModeChange}
|
||||||
|
title={t('pdfTextEditor.modeChange.title', 'Confirm Mode Change')}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.modeChange.warning',
|
||||||
|
'Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?'
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end" gap="sm">
|
||||||
|
<Button variant="default" onClick={handleCancelModeChange}>
|
||||||
|
{t('pdfTextEditor.modeChange.cancel', 'Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button color="red" onClick={handleConfirmModeChange}>
|
||||||
|
{t('pdfTextEditor.modeChange.confirm', 'Reset and Change Mode')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PdfTextEditorSidebar;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -14,18 +13,12 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
Progress,
|
Progress,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
SegmentedControl,
|
|
||||||
Stack,
|
Stack,
|
||||||
Switch,
|
|
||||||
Text,
|
Text,
|
||||||
Title,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import DescriptionIcon from '@mui/icons-material/DescriptionOutlined';
|
|
||||||
import FileDownloadIcon from '@mui/icons-material/FileDownloadOutlined';
|
|
||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdfOutlined';
|
|
||||||
import AutorenewIcon from '@mui/icons-material/Autorenew';
|
import AutorenewIcon from '@mui/icons-material/Autorenew';
|
||||||
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
||||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
@@ -34,7 +27,6 @@ import MergeTypeIcon from '@mui/icons-material/MergeType';
|
|||||||
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
import CallSplitIcon from '@mui/icons-material/CallSplit';
|
||||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFileOutlined';
|
import UploadFileIcon from '@mui/icons-material/UploadFileOutlined';
|
||||||
import SaveIcon from '@mui/icons-material/SaveOutlined';
|
|
||||||
import { Rnd } from 'react-rnd';
|
import { Rnd } from 'react-rnd';
|
||||||
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
|
import NavigationWarningModal from '@app/components/shared/NavigationWarningModal';
|
||||||
|
|
||||||
@@ -46,7 +38,6 @@ import {
|
|||||||
TextGroup,
|
TextGroup,
|
||||||
} from '@app/tools/pdfTextEditor/pdfTextEditorTypes';
|
} from '@app/tools/pdfTextEditor/pdfTextEditorTypes';
|
||||||
import { getImageBounds, pageDimensions } from '@app/tools/pdfTextEditor/pdfTextEditorUtils';
|
import { getImageBounds, pageDimensions } from '@app/tools/pdfTextEditor/pdfTextEditorUtils';
|
||||||
import FontStatusPanel from '@app/components/tools/pdfTextEditor/FontStatusPanel';
|
|
||||||
|
|
||||||
const MAX_RENDER_WIDTH = 820;
|
const MAX_RENDER_WIDTH = 820;
|
||||||
const MIN_BOX_SIZE = 18;
|
const MIN_BOX_SIZE = 18;
|
||||||
@@ -313,8 +304,6 @@ const analyzePageContentType = (groups: TextGroup[], pageWidth: number): boolean
|
|||||||
return isParagraphPage;
|
return isParagraphPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GroupingMode = 'auto' | 'paragraph' | 'singleLine';
|
|
||||||
|
|
||||||
const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { activeFiles } = useFileContext();
|
const { activeFiles } = useFileContext();
|
||||||
@@ -327,9 +316,7 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
|||||||
const rndRefs = useRef<Map<string, any>>(new Map());
|
const rndRefs = useRef<Map<string, any>>(new Map());
|
||||||
const pendingDragUpdateRef = useRef<number | null>(null);
|
const pendingDragUpdateRef = useRef<number | null>(null);
|
||||||
const [fontFamilies, setFontFamilies] = useState<Map<string, string>>(new Map());
|
const [fontFamilies, setFontFamilies] = useState<Map<string, string>>(new Map());
|
||||||
const [autoScaleText, setAutoScaleText] = useState(true);
|
|
||||||
const [textScales, setTextScales] = useState<Map<string, number>>(new Map());
|
const [textScales, setTextScales] = useState<Map<string, number>>(new Map());
|
||||||
const [pendingModeChange, setPendingModeChange] = useState<GroupingMode | null>(null);
|
|
||||||
const measurementKeyRef = useRef<string>('');
|
const measurementKeyRef = useRef<string>('');
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const editorRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
const editorRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||||
@@ -378,27 +365,27 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
|||||||
dirtyPages,
|
dirtyPages,
|
||||||
hasDocument,
|
hasDocument,
|
||||||
hasVectorPreview,
|
hasVectorPreview,
|
||||||
fileName,
|
fileName: _fileName,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
isGeneratingPdf,
|
isGeneratingPdf: _isGeneratingPdf,
|
||||||
isSavingToWorkbench,
|
isSavingToWorkbench: _isSavingToWorkbench,
|
||||||
isConverting,
|
isConverting,
|
||||||
conversionProgress,
|
conversionProgress,
|
||||||
hasChanges,
|
hasChanges: _hasChanges,
|
||||||
forceSingleTextElement,
|
forceSingleTextElement: _forceSingleTextElement,
|
||||||
groupingMode: externalGroupingMode,
|
groupingMode: externalGroupingMode,
|
||||||
|
autoScaleText,
|
||||||
requestPagePreview,
|
requestPagePreview,
|
||||||
onSelectPage,
|
onSelectPage,
|
||||||
onGroupEdit,
|
onGroupEdit,
|
||||||
onGroupDelete,
|
onGroupDelete,
|
||||||
onImageTransform,
|
onImageTransform,
|
||||||
onImageReset,
|
onImageReset,
|
||||||
onReset,
|
onReset: _onReset,
|
||||||
onDownloadJson,
|
onGeneratePdf: _onGeneratePdf,
|
||||||
onGeneratePdf,
|
|
||||||
onSaveToWorkbench,
|
onSaveToWorkbench,
|
||||||
onForceSingleTextElementChange,
|
onForceSingleTextElementChange: _onForceSingleTextElementChange,
|
||||||
onGroupingModeChange,
|
onGroupingModeChange: _onGroupingModeChange,
|
||||||
onMergeGroups,
|
onMergeGroups,
|
||||||
onUngroupGroup,
|
onUngroupGroup,
|
||||||
onLoadFile,
|
onLoadFile,
|
||||||
@@ -428,27 +415,6 @@ const PdfTextEditorView = ({ data }: PdfTextEditorViewProps) => {
|
|||||||
} : null,
|
} : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleModeChangeRequest = useCallback((newMode: GroupingMode) => {
|
|
||||||
if (hasChanges && newMode !== externalGroupingMode) {
|
|
||||||
// Show confirmation dialog
|
|
||||||
setPendingModeChange(newMode);
|
|
||||||
} else {
|
|
||||||
// No changes, switch immediately
|
|
||||||
onGroupingModeChange(newMode);
|
|
||||||
}
|
|
||||||
}, [hasChanges, externalGroupingMode, onGroupingModeChange]);
|
|
||||||
|
|
||||||
const handleConfirmModeChange = useCallback(() => {
|
|
||||||
if (pendingModeChange) {
|
|
||||||
onGroupingModeChange(pendingModeChange);
|
|
||||||
setPendingModeChange(null);
|
|
||||||
}
|
|
||||||
}, [pendingModeChange, onGroupingModeChange]);
|
|
||||||
|
|
||||||
const handleCancelModeChange = useCallback(() => {
|
|
||||||
setPendingModeChange(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clearSelection = useCallback(() => {
|
const clearSelection = useCallback(() => {
|
||||||
setSelectedGroupIds(new Set());
|
setSelectedGroupIds(new Set());
|
||||||
lastSelectedGroupIdRef.current = null;
|
lastSelectedGroupIdRef.current = null;
|
||||||
@@ -1448,223 +1414,16 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
padding: '1.5rem',
|
padding: '1.5rem',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
display: 'grid',
|
display: 'flex',
|
||||||
gridTemplateColumns: 'minmax(0, 1fr) 320px',
|
flexDirection: 'column',
|
||||||
gridTemplateRows: '1fr',
|
|
||||||
alignItems: hasDocument ? 'start' : 'stretch',
|
|
||||||
gap: '1.5rem',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
|
||||||
withBorder
|
|
||||||
radius="md"
|
|
||||||
shadow="xs"
|
|
||||||
padding="md"
|
|
||||||
style={{
|
|
||||||
gridColumn: '2 / 3',
|
|
||||||
gridRow: 1,
|
|
||||||
maxHeight: 'calc(100vh - 3rem)',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ScrollArea style={{ flex: 1 }} offsetScrollbars>
|
|
||||||
<Stack gap="sm" pb="md">
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Group gap="xs" align="center">
|
|
||||||
<DescriptionIcon fontSize="small" />
|
|
||||||
<Title order={3}>{t('pdfTextEditor.title', 'PDF JSON Editor')}</Title>
|
|
||||||
{hasChanges && <Badge color="orange" variant="light" size="sm">{t('pdfTextEditor.badges.unsaved', 'Edited')}</Badge>}
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
leftSection={<AutorenewIcon fontSize="small" />}
|
|
||||||
onClick={onReset}
|
|
||||||
disabled={!hasDocument || isConverting}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{t('pdfTextEditor.actions.reset', 'Reset Changes')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
leftSection={<FileDownloadIcon fontSize="small" />}
|
|
||||||
onClick={onDownloadJson}
|
|
||||||
disabled={!hasDocument || isConverting}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{t('pdfTextEditor.actions.downloadJson', 'Download JSON')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leftSection={<PictureAsPdfIcon fontSize="small" />}
|
|
||||||
onClick={onGeneratePdf}
|
|
||||||
loading={isGeneratingPdf}
|
|
||||||
disabled={!hasDocument || !hasChanges || isConverting}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{t('pdfTextEditor.actions.generatePdf', 'Generate PDF')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="filled"
|
|
||||||
color="green"
|
|
||||||
leftSection={<SaveIcon fontSize="small" />}
|
|
||||||
onClick={onSaveToWorkbench}
|
|
||||||
loading={isSavingToWorkbench}
|
|
||||||
disabled={!hasDocument || !hasChanges || isConverting}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
{t('pdfTextEditor.actions.saveChanges', 'Save Changes')}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{fileName && (
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
{t('pdfTextEditor.currentFile', 'Current file: {{name}}', { name: fileName })}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Divider my="xs" />
|
|
||||||
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<div>
|
|
||||||
<Text fw={500} size="sm">
|
|
||||||
{t('pdfTextEditor.options.autoScaleText.title', 'Auto-scale text to fit boxes')}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="dimmed" mt={4}>
|
|
||||||
{t(
|
|
||||||
'pdfTextEditor.options.autoScaleText.description',
|
|
||||||
'Automatically scales text horizontally to fit within its original bounding box when font rendering differs from PDF.'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
size="md"
|
|
||||||
checked={autoScaleText}
|
|
||||||
onChange={(event) => setAutoScaleText(event.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Group gap={4} align="center">
|
|
||||||
<Text fw={500} size="sm">
|
|
||||||
{t('pdfTextEditor.options.groupingMode.title', 'Text Grouping Mode')}
|
|
||||||
</Text>
|
|
||||||
{externalGroupingMode === 'auto' && isParagraphPage && (
|
|
||||||
<Badge size="xs" color="blue" variant="light" key={`para-${selectedPage}`}>
|
|
||||||
{t('pdfTextEditor.pageType.paragraph', 'Paragraph page')}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{externalGroupingMode === 'auto' && !isParagraphPage && hasDocument && (
|
|
||||||
<Badge size="xs" color="gray" variant="light" key={`sparse-${selectedPage}`}>
|
|
||||||
{t('pdfTextEditor.pageType.sparse', 'Sparse text')}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{externalGroupingMode === 'auto'
|
|
||||||
? t(
|
|
||||||
'pdfTextEditor.options.groupingMode.autoDescription',
|
|
||||||
'Automatically detects page type and groups text appropriately.'
|
|
||||||
)
|
|
||||||
: externalGroupingMode === 'paragraph'
|
|
||||||
? t(
|
|
||||||
'pdfTextEditor.options.groupingMode.paragraphDescription',
|
|
||||||
'Groups aligned lines into multi-line paragraph text boxes.'
|
|
||||||
)
|
|
||||||
: t(
|
|
||||||
'pdfTextEditor.options.groupingMode.singleLineDescription',
|
|
||||||
'Keeps each PDF text line as a separate text box.'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<SegmentedControl
|
|
||||||
value={externalGroupingMode}
|
|
||||||
onChange={(value) => handleModeChangeRequest(value as GroupingMode)}
|
|
||||||
data={[
|
|
||||||
{ label: t('pdfTextEditor.groupingMode.auto', 'Auto'), value: 'auto' },
|
|
||||||
{ label: t('pdfTextEditor.groupingMode.paragraph', 'Paragraph'), value: 'paragraph' },
|
|
||||||
{ label: t('pdfTextEditor.groupingMode.singleLine', 'Single Line'), value: 'singleLine' },
|
|
||||||
]}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{t(
|
|
||||||
'pdfTextEditor.options.manualGrouping.descriptionInline',
|
|
||||||
'Tip: Hold Ctrl (Cmd) or Shift to multi-select text boxes. A floating toolbar will appear above the selection so you can merge, ungroup, or adjust widths.',
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<div>
|
|
||||||
<Text fw={500} size="sm">
|
|
||||||
{t('pdfTextEditor.options.forceSingleElement.title', 'Lock edited text to a single PDF element')}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="dimmed" mt={4}>
|
|
||||||
{t(
|
|
||||||
'pdfTextEditor.options.forceSingleElement.description',
|
|
||||||
'When enabled, the editor exports each edited text box as one PDF text element to avoid overlapping glyphs or mixed fonts.'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
size="md"
|
|
||||||
checked={forceSingleTextElement}
|
|
||||||
onChange={(event) => onForceSingleTextElementChange(event.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Divider my="xs" />
|
|
||||||
|
|
||||||
<Accordion variant="contained">
|
|
||||||
<Accordion.Item value="disclaimer">
|
|
||||||
<Accordion.Control>
|
|
||||||
<Group gap="xs" wrap="nowrap">
|
|
||||||
<InfoOutlinedIcon fontSize="small" />
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{t('pdfTextEditor.disclaimer.heading', 'Preview Limitations')}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Accordion.Control>
|
|
||||||
<Accordion.Panel>
|
|
||||||
<Stack gap={4}>
|
|
||||||
<Text size="xs">
|
|
||||||
{t(
|
|
||||||
'pdfTextEditor.disclaimer.textFocus',
|
|
||||||
'This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here.'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs">
|
|
||||||
{t(
|
|
||||||
'pdfTextEditor.disclaimer.previewVariance',
|
|
||||||
'Some visuals (such as table borders, shapes, or annotation appearances) may not display exactly in the preview. The exported PDF keeps the original drawing commands whenever possible.'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs">
|
|
||||||
{t(
|
|
||||||
'pdfTextEditor.disclaimer.alpha',
|
|
||||||
'This alpha viewer is still evolving—certain fonts, colours, transparency effects, and layout details may shift slightly. Please double-check the generated PDF before sharing.'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Accordion.Panel>
|
|
||||||
</Accordion.Item>
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
{hasDocument && <FontStatusPanel document={pdfDocument} pageIndex={selectedPage} />}
|
|
||||||
</Stack>
|
|
||||||
</ScrollArea>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<Alert
|
<Alert
|
||||||
icon={<WarningAmberIcon fontSize="small" />}
|
icon={<WarningAmberIcon fontSize="small" />}
|
||||||
color="red"
|
color="red"
|
||||||
radius="md"
|
radius="md"
|
||||||
style={{ gridColumn: '2 / 3' }}
|
mb="md"
|
||||||
>
|
>
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -1674,7 +1433,7 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
<Stack
|
<Stack
|
||||||
align="center"
|
align="center"
|
||||||
justify="center"
|
justify="center"
|
||||||
style={{ gridColumn: '1 / 2', gridRow: 1, height: '100%' }}
|
style={{ flex: 1, height: '100%' }}
|
||||||
>
|
>
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
@@ -1704,8 +1463,8 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed" ta="center" maw={420}>
|
<Text size="sm" c="dimmed" ta="center" maw={420}>
|
||||||
{activeFiles.length > 0
|
{activeFiles.length > 0
|
||||||
? t('pdfTextEditor.empty.dropzoneWithFiles', 'Select a file from the Files tab, or drag and drop a PDF or JSON file here, or click to browse')
|
? t('pdfTextEditor.empty.dropzoneWithFiles', 'Select a file from the Files tab, or drag and drop a PDF here, or click to browse')
|
||||||
: t('pdfTextEditor.empty.dropzone', 'Drag and drop a PDF or JSON file here, or click to browse')}
|
: t('pdfTextEditor.empty.dropzone', 'Drag and drop a PDF here, or click to browse')}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
@@ -1713,7 +1472,7 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isConverting && (
|
{isConverting && (
|
||||||
<Card withBorder radius="md" padding="xl" style={{ gridColumn: '1 / 2', gridRow: 1 }}>
|
<Card withBorder radius="md" padding="xl" style={{ flex: 1 }}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group justify="space-between" align="flex-start">
|
<Group justify="space-between" align="flex-start">
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
@@ -1748,10 +1507,8 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
gap="lg"
|
gap="lg"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
style={{
|
style={{
|
||||||
gridColumn: '1 / 2',
|
|
||||||
gridRow: 1,
|
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
height: 'calc(100vh - 3rem)',
|
flex: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1788,65 +1545,208 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
title={
|
title={
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<InfoOutlinedIcon fontSize="small" />
|
<InfoOutlinedIcon fontSize="small" />
|
||||||
<Text fw={600}>{t('pdfTextEditor.welcomeBanner.title', 'Welcome to PDF Text Editor (Early Access)')}</Text>
|
<Text fw={600}>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.title',
|
||||||
|
'Welcome to PDF Text Editor (Early Access)',
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
centered
|
centered
|
||||||
size="lg"
|
size="lg"
|
||||||
|
scrollAreaComponent={Box}
|
||||||
>
|
>
|
||||||
<ScrollArea style={{ maxHeight: '70vh' }} offsetScrollbars>
|
<div
|
||||||
<Stack gap="sm">
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
maxHeight: '70vh',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header (fixed) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.5rem',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Alert color="orange" variant="light" radius="md">
|
<Alert color="orange" variant="light" radius="md">
|
||||||
<Text size="sm" fw={500}>
|
<Text size="sm" fw={500}>
|
||||||
{t('pdfTextEditor.welcomeBanner.experimental', 'This is an experimental feature in active development. Expect some instability and issues during use.')}
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.experimental',
|
||||||
|
'This is an experimental feature in active development. Expect some instability and issues during use.',
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{t('pdfTextEditor.welcomeBanner.howItWorks', 'This tool converts your PDF to an editable format where you can modify text content and reposition images. Changes are saved back as a new PDF.')}
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.howItWorks',
|
||||||
|
'This tool converts your PDF to an editable format where you can modify text content and reposition images. Changes are saved back as a new PDF.',
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body (scrollable) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingRight: '0.25rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Divider />
|
||||||
|
<Text size="sm" fw={500} c="green.7">
|
||||||
|
{t('pdfTextEditor.welcomeBanner.bestFor', 'Works Best With:')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
component="ul"
|
||||||
|
style={{ marginLeft: '1rem', marginTop: '0.25rem' }}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.bestFor1',
|
||||||
|
'Simple PDFs containing primarily text and images',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.bestFor2',
|
||||||
|
'Documents with standard paragraph formatting',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.bestFor3',
|
||||||
|
'Letters, essays, reports, and basic documents',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</Text>
|
||||||
|
<Divider />
|
||||||
|
<Text size="sm" fw={500} c="orange.7">
|
||||||
|
{t('pdfTextEditor.welcomeBanner.notIdealFor', 'Not Ideal For:')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
component="ul"
|
||||||
|
style={{ marginLeft: '1rem', marginTop: '0.25rem' }}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.notIdealFor1',
|
||||||
|
'PDFs with special formatting like bullet points, tables, or multi-column layouts',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.notIdealFor2',
|
||||||
|
'Magazines, brochures, or heavily designed documents',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.notIdealFor3',
|
||||||
|
'Instruction manuals with complex layouts',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</Text>
|
||||||
|
<Divider />
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t('pdfTextEditor.welcomeBanner.limitations', 'Current Limitations:')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
component="ul"
|
||||||
|
style={{ marginLeft: '1rem', marginTop: '0.25rem' }}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.limitation1',
|
||||||
|
'Font rendering may differ slightly from the original PDF',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.limitation2',
|
||||||
|
'Complex graphics, form fields, and annotations are preserved but not editable',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.limitation3',
|
||||||
|
'Large files may take time to convert and process',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</Text>
|
||||||
|
<Divider />
|
||||||
|
<Text size="sm" fw={500}>
|
||||||
|
{t('pdfTextEditor.welcomeBanner.knownIssues', 'Known Issues (Being Fixed):')}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
component="ul"
|
||||||
|
style={{ marginLeft: '1rem', marginTop: '0.25rem' }}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.issue1',
|
||||||
|
'Text colour is not currently preserved (will be added soon)',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.issue2',
|
||||||
|
'Paragraph mode has more alignment and spacing issues - Single Line mode recommended',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.issue3',
|
||||||
|
'The preview display differs from the exported PDF - exported PDFs are closer to the original',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.issue4',
|
||||||
|
'Rotated text alignment may need manual adjustment',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.issue5',
|
||||||
|
'Transparency and layering effects may vary from original',
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer (fixed) */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '0.75rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Text size="sm" fw={500} c="green.7">
|
<Text size="xs" c="dimmed" mt="xs">
|
||||||
{t('pdfTextEditor.welcomeBanner.bestFor', 'Works Best With:')}
|
{t(
|
||||||
|
'pdfTextEditor.welcomeBanner.feedback',
|
||||||
|
'This is an early access feature. Please report any issues you encounter to help us improve!',
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
|
<Group justify="flex-end" gap="sm" mt="xs">
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.bestFor1', 'Simple PDFs containing primarily text and images')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.bestFor2', 'Documents with standard paragraph formatting')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.bestFor3', 'Letters, essays, reports, and basic documents')}</li>
|
|
||||||
</Text>
|
|
||||||
<Divider />
|
|
||||||
<Text size="sm" fw={500} c="orange.7">
|
|
||||||
{t('pdfTextEditor.welcomeBanner.notIdealFor', 'Not Ideal For:')}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor1', 'PDFs with special formatting like bullet points, tables, or multi-column layouts')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor2', 'Magazines, brochures, or heavily designed documents')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.notIdealFor3', 'Instruction manuals with complex layouts')}</li>
|
|
||||||
</Text>
|
|
||||||
<Divider />
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{t('pdfTextEditor.welcomeBanner.limitations', 'Current Limitations:')}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.limitation1', 'Font rendering may differ slightly from the original PDF')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.limitation2', 'Complex graphics, form fields, and annotations are preserved but not editable')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.limitation3', 'Large files may take time to convert and process')}</li>
|
|
||||||
</Text>
|
|
||||||
<Divider />
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{t('pdfTextEditor.welcomeBanner.knownIssues', 'Known Issues (Being Fixed):')}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" component="ul" style={{ marginLeft: '1rem', marginTop: '0.25rem' }}>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.issue1', 'Text colour is not currently preserved (will be added soon)')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.issue2', 'Paragraph mode has more alignment and spacing issues - Single Line mode recommended')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.issue3', 'The preview display differs from the exported PDF - exported PDFs are closer to the original')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.issue4', 'Rotated text alignment may need manual adjustment')}</li>
|
|
||||||
<li>{t('pdfTextEditor.welcomeBanner.issue5', 'Transparency and layering effects may vary from original')}</li>
|
|
||||||
</Text>
|
|
||||||
<Divider />
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{t('pdfTextEditor.welcomeBanner.feedback', 'This is an early access feature. Please report any issues you encounter to help us improve!')}
|
|
||||||
</Text>
|
|
||||||
<Group justify="flex-end" gap="sm" mt="md">
|
|
||||||
<Button variant="default" onClick={handleDismissWelcomeBanner}>
|
<Button variant="default" onClick={handleDismissWelcomeBanner}>
|
||||||
{t('pdfTextEditor.welcomeBanner.gotIt', 'Got it')}
|
{t('pdfTextEditor.welcomeBanner.gotIt', 'Got it')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1854,8 +1754,8 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
{t('pdfTextEditor.welcomeBanner.dontShowAgain', "Don't show again")}
|
{t('pdfTextEditor.welcomeBanner.dontShowAgain', "Don't show again")}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</div>
|
||||||
</ScrollArea>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
@@ -2485,31 +2385,6 @@ const selectionToolbarPosition = useMemo(() => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mode Change Confirmation Modal */}
|
|
||||||
<Modal
|
|
||||||
opened={pendingModeChange !== null}
|
|
||||||
onClose={handleCancelModeChange}
|
|
||||||
title={t('pdfTextEditor.modeChange.title', 'Confirm Mode Change')}
|
|
||||||
centered
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Text>
|
|
||||||
{t(
|
|
||||||
'pdfTextEditor.modeChange.warning',
|
|
||||||
'Changing the text grouping mode will reset all unsaved changes. Are you sure you want to continue?'
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<Group justify="flex-end" gap="sm">
|
|
||||||
<Button variant="default" onClick={handleCancelModeChange}>
|
|
||||||
{t('pdfTextEditor.modeChange.cancel', 'Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button color="red" onClick={handleConfirmModeChange}>
|
|
||||||
{t('pdfTextEditor.modeChange.confirm', 'Reset and Change Mode')}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
{/* Navigation Warning Modal */}
|
{/* Navigation Warning Modal */}
|
||||||
<NavigationWarningModal
|
<NavigationWarningModal
|
||||||
onApplyAndContinue={onSaveToWorkbench}
|
onApplyAndContinue={onSaveToWorkbench}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent } from '@app/types/tips';
|
||||||
|
|
||||||
|
export const usePdfTextEditorTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t('pdfTextEditor.tooltip.header.title', 'Preview Limitations'),
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t('pdfTextEditor.tooltip.textFocus.title', 'Text and Image Focus'),
|
||||||
|
description: t(
|
||||||
|
'pdfTextEditor.tooltip.textFocus.text',
|
||||||
|
'This workspace focuses on editing text and repositioning embedded images. Complex page artwork, form widgets, and layered graphics are preserved for export but are not fully editable here.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('pdfTextEditor.tooltip.previewVariance.title', 'Preview Variance'),
|
||||||
|
description: t(
|
||||||
|
'pdfTextEditor.tooltip.previewVariance.text',
|
||||||
|
'Some visuals (such as table borders, shapes, or annotation appearances) may not display exactly in the preview. The exported PDF keeps the original drawing commands whenever possible.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('pdfTextEditor.tooltip.alpha.title', 'Alpha Viewer'),
|
||||||
|
description: t(
|
||||||
|
'pdfTextEditor.tooltip.alpha.text',
|
||||||
|
'This alpha viewer is still evolving—certain fonts, colours, transparency effects, and layout details may shift slightly. Please double-check the generated PDF before sharing.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
valueOr,
|
valueOr,
|
||||||
} from '@app/tools/pdfTextEditor/pdfTextEditorUtils';
|
} from '@app/tools/pdfTextEditor/pdfTextEditorUtils';
|
||||||
import PdfTextEditorView from '@app/components/tools/pdfTextEditor/PdfTextEditorView';
|
import PdfTextEditorView from '@app/components/tools/pdfTextEditor/PdfTextEditorView';
|
||||||
|
import PdfTextEditorSidebar from '@app/components/tools/pdfTextEditor/PdfTextEditorSidebar';
|
||||||
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
import type { PDFDocumentProxy } from 'pdfjs-dist';
|
||||||
|
|
||||||
const WORKBENCH_VIEW_ID = 'pdfTextEditorWorkbench';
|
const WORKBENCH_VIEW_ID = 'pdfTextEditorWorkbench';
|
||||||
@@ -228,6 +229,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
const [groupingMode, setGroupingMode] = useState<'auto' | 'paragraph' | 'singleLine'>('auto');
|
const [groupingMode, setGroupingMode] = useState<'auto' | 'paragraph' | 'singleLine'>('auto');
|
||||||
const [hasVectorPreview, setHasVectorPreview] = useState(false);
|
const [hasVectorPreview, setHasVectorPreview] = useState(false);
|
||||||
const [pagePreviews, setPagePreviews] = useState<Map<number, string>>(new Map());
|
const [pagePreviews, setPagePreviews] = useState<Map<number, string>>(new Map());
|
||||||
|
const [autoScaleText, setAutoScaleText] = useState(true);
|
||||||
|
|
||||||
// Lazy loading state
|
// Lazy loading state
|
||||||
const [isLazyMode, setIsLazyMode] = useState(false);
|
const [isLazyMode, setIsLazyMode] = useState(false);
|
||||||
@@ -1565,6 +1567,8 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
hasChanges,
|
hasChanges,
|
||||||
forceSingleTextElement,
|
forceSingleTextElement,
|
||||||
groupingMode,
|
groupingMode,
|
||||||
|
autoScaleText,
|
||||||
|
onAutoScaleTextChange: setAutoScaleText,
|
||||||
requestPagePreview,
|
requestPagePreview,
|
||||||
onSelectPage: handleSelectPage,
|
onSelectPage: handleSelectPage,
|
||||||
onGroupEdit: handleGroupTextChange,
|
onGroupEdit: handleGroupTextChange,
|
||||||
@@ -1613,6 +1617,7 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
selectedPage,
|
selectedPage,
|
||||||
forceSingleTextElement,
|
forceSingleTextElement,
|
||||||
groupingMode,
|
groupingMode,
|
||||||
|
autoScaleText,
|
||||||
requestPagePreview,
|
requestPagePreview,
|
||||||
setForceSingleTextElement,
|
setForceSingleTextElement,
|
||||||
handleLoadFileFromDropzone,
|
handleLoadFileFromDropzone,
|
||||||
@@ -1683,12 +1688,17 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
icon: <DescriptionIcon fontSize="small" />,
|
icon: <DescriptionIcon fontSize="small" />,
|
||||||
component: PdfTextEditorView,
|
component: PdfTextEditorView,
|
||||||
});
|
});
|
||||||
setLeftPanelView('hidden');
|
|
||||||
setCustomWorkbenchViewData(WORKBENCH_VIEW_ID, latestViewDataRef.current);
|
setCustomWorkbenchViewData(WORKBENCH_VIEW_ID, latestViewDataRef.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Clear backend cache if we were using lazy loading
|
||||||
|
clearCachedJob(cachedJobIdRef.current);
|
||||||
|
clearCustomWorkbenchViewData(WORKBENCH_VIEW_ID);
|
||||||
|
unregisterCustomWorkbenchView(WORKBENCH_VIEW_ID);
|
||||||
|
};
|
||||||
}, [
|
}, [
|
||||||
registerCustomWorkbenchView,
|
registerCustomWorkbenchView,
|
||||||
setCustomWorkbenchViewData,
|
setCustomWorkbenchViewData,
|
||||||
setLeftPanelView,
|
|
||||||
viewLabel,
|
viewLabel,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1723,12 +1733,12 @@ const PdfTextEditor = ({ onComplete, onError }: BaseToolProps) => {
|
|||||||
setCustomWorkbenchViewData(WORKBENCH_VIEW_ID, viewData);
|
setCustomWorkbenchViewData(WORKBENCH_VIEW_ID, viewData);
|
||||||
}, [setCustomWorkbenchViewData, viewData]);
|
}, [setCustomWorkbenchViewData, viewData]);
|
||||||
|
|
||||||
// All editing happens in the custom workbench view.
|
// Render the sidebar with settings while editing happens in the custom workbench view.
|
||||||
return null;
|
return <PdfTextEditorSidebar data={viewData} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
(PdfTextEditor as ToolComponent).tool = () => {
|
(PdfTextEditor as ToolComponent).tool = () => {
|
||||||
throw new Error('PDF JSON Editor does not support automation operations.');
|
throw new Error('PDF Text Editor does not support automation operations.');
|
||||||
};
|
};
|
||||||
|
|
||||||
(PdfTextEditor as ToolComponent).getDefaultParameters = () => ({
|
(PdfTextEditor as ToolComponent).getDefaultParameters = () => ({
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ export interface PdfTextEditorViewData {
|
|||||||
hasChanges: boolean;
|
hasChanges: boolean;
|
||||||
forceSingleTextElement: boolean;
|
forceSingleTextElement: boolean;
|
||||||
groupingMode: 'auto' | 'paragraph' | 'singleLine';
|
groupingMode: 'auto' | 'paragraph' | 'singleLine';
|
||||||
|
autoScaleText: boolean;
|
||||||
|
onAutoScaleTextChange: (value: boolean) => void;
|
||||||
requestPagePreview: (pageIndex: number, scale: number) => void;
|
requestPagePreview: (pageIndex: number, scale: number) => void;
|
||||||
onSelectPage: (pageIndex: number) => void;
|
onSelectPage: (pageIndex: number) => void;
|
||||||
onGroupEdit: (pageIndex: number, groupId: string, value: string) => void;
|
onGroupEdit: (pageIndex: number, groupId: string, value: string) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user