opensource text editor (#5146)

# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## 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:
Anthony Stirling
2025-12-03 12:55:34 +00:00
committed by GitHub
parent f902e8aca9
commit bdb3c887f3
54 changed files with 33 additions and 41 deletions
@@ -0,0 +1,286 @@
import React, { useMemo, useState } from 'react';
import {
Accordion,
Badge,
Box,
Code,
Collapse,
Group,
List,
Paper,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useTranslation } from 'react-i18next';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import WarningIcon from '@mui/icons-material/Warning';
import ErrorIcon from '@mui/icons-material/Error';
import InfoIcon from '@mui/icons-material/Info';
import FontDownloadIcon from '@mui/icons-material/FontDownload';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import { PdfJsonDocument } from '@app/tools/pdfTextEditor/pdfTextEditorTypes';
import {
analyzeDocumentFonts,
DocumentFontAnalysis,
FontAnalysis,
getFontStatusColor,
getFontStatusDescription,
} from '@app/tools/pdfTextEditor/fontAnalysis';
interface FontStatusPanelProps {
document: PdfJsonDocument | null;
pageIndex?: number;
}
const FontStatusBadge = ({ analysis }: { analysis: FontAnalysis }) => {
const color = getFontStatusColor(analysis.status);
const description = getFontStatusDescription(analysis.status);
const icon = useMemo(() => {
switch (analysis.status) {
case 'perfect':
return <CheckCircleIcon sx={{ fontSize: 14 }} />;
case 'embedded-subset':
return <InfoIcon sx={{ fontSize: 14 }} />;
case 'system-fallback':
return <WarningIcon sx={{ fontSize: 14 }} />;
case 'missing':
return <ErrorIcon sx={{ fontSize: 14 }} />;
default:
return <InfoIcon sx={{ fontSize: 14 }} />;
}
}, [analysis.status]);
return (
<Tooltip label={description} position="top" withArrow>
<Badge
size="xs"
color={color}
variant="light"
leftSection={icon}
style={{ cursor: 'help' }}
>
{analysis.status.replace('-', ' ')}
</Badge>
</Tooltip>
);
};
const FontDetailItem = ({ analysis }: { analysis: FontAnalysis }) => {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
return (
<Paper withBorder p="xs" style={{ cursor: 'pointer' }} onClick={() => setExpanded(!expanded)}>
<Stack gap={4}>
<Group justify="space-between">
<Group gap={4}>
<FontDownloadIcon sx={{ fontSize: 16 }} />
<Text size="xs" fw={500} lineClamp={1}>
{analysis.baseName}
</Text>
{analysis.isSubset && (
<Badge size="xs" color="gray" variant="outline">
subset
</Badge>
)}
</Group>
<Group gap={4}>
<FontStatusBadge analysis={analysis} />
{expanded ? <ExpandLessIcon sx={{ fontSize: 16 }} /> : <ExpandMoreIcon sx={{ fontSize: 16 }} />}
</Group>
</Group>
<Collapse in={expanded}>
<Stack gap={4} mt={4}>
{/* Font Details */}
<Box>
<Text size="xs" c="dimmed" mb={2}>
{t('pdfTextEditor.fontAnalysis.details', 'Font Details')}:
</Text>
<Stack gap={2}>
<Group gap={4}>
<Text size="xs" c="dimmed">
{t('pdfTextEditor.fontAnalysis.embedded', 'Embedded')}:
</Text>
<Code style={{ fontSize: '0.65rem', padding: '0 4px' }}>{analysis.embedded ? 'Yes' : 'No'}</Code>
</Group>
{analysis.subtype && (
<Group gap={4}>
<Text size="xs" c="dimmed">
{t('pdfTextEditor.fontAnalysis.type', 'Type')}:
</Text>
<Code style={{ fontSize: '0.65rem', padding: '0 4px' }}>{analysis.subtype}</Code>
</Group>
)}
{analysis.webFormat && (
<Group gap={4}>
<Text size="xs" c="dimmed">
{t('pdfTextEditor.fontAnalysis.webFormat', 'Web Format')}:
</Text>
<Code style={{ fontSize: '0.65rem', padding: '0 4px' }}>{analysis.webFormat}</Code>
</Group>
)}
</Stack>
</Box>
{/* Warnings */}
{analysis.warnings.length > 0 && (
<Box>
<Text size="xs" c="orange" fw={500}>
{t('pdfTextEditor.fontAnalysis.warnings', 'Warnings')}:
</Text>
<List size="xs" spacing={2} withPadding>
{analysis.warnings.map((warning, index) => (
<List.Item key={index}>
<Text size="xs">{warning}</Text>
</List.Item>
))}
</List>
</Box>
)}
{/* Suggestions */}
{analysis.suggestions.length > 0 && (
<Box>
<Text size="xs" c="blue" fw={500}>
{t('pdfTextEditor.fontAnalysis.suggestions', 'Notes')}:
</Text>
<List size="xs" spacing={2} withPadding>
{analysis.suggestions.map((suggestion, index) => (
<List.Item key={index}>
<Text size="xs">{suggestion}</Text>
</List.Item>
))}
</List>
</Box>
)}
</Stack>
</Collapse>
</Stack>
</Paper>
);
};
const FontStatusPanel: React.FC<FontStatusPanelProps> = ({ document, pageIndex }) => {
const { t } = useTranslation();
const fontAnalysis: DocumentFontAnalysis = useMemo(
() => analyzeDocumentFonts(document, pageIndex),
[document, pageIndex]
);
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
if (!document || fontAnalysis.fonts.length === 0) {
return null;
}
const statusColor = canReproducePerfectly ? 'green' : hasWarnings ? 'yellow' : 'blue';
const pageLabel = pageIndex !== undefined
? t('pdfTextEditor.fontAnalysis.currentPageFonts', 'Fonts on this page')
: t('pdfTextEditor.fontAnalysis.allFonts', 'All fonts');
return (
<Accordion variant="contained" defaultValue={hasWarnings ? 'fonts' : undefined}>
<Accordion.Item value="fonts">
<Accordion.Control>
<Group gap="xs" wrap="wrap" style={{ flex: 1 }}>
<Group gap="xs" wrap="nowrap">
{statusIcon}
<Text size="sm" fw={500}>
{pageLabel}
</Text>
<Badge size="xs" color={statusColor} variant="dot">
{fonts.length}
</Badge>
</Group>
{/* Warning badges BEFORE expansion */}
<Group gap={4} wrap="wrap">
{summary.systemFallback > 0 && (
<Badge size="xs" color="yellow" variant="filled" leftSection={<WarningIcon sx={{ fontSize: 12 }} />}>
{summary.systemFallback} {t('pdfTextEditor.fontAnalysis.fallback', 'fallback')}
</Badge>
)}
{summary.missing > 0 && (
<Badge size="xs" color="red" variant="filled" leftSection={<ErrorIcon sx={{ fontSize: 12 }} />}>
{summary.missing} {t('pdfTextEditor.fontAnalysis.missing', 'missing')}
</Badge>
)}
</Group>
</Group>
</Accordion.Control>
<Accordion.Panel>
<Stack gap="xs">
{/* Overall Status Message */}
<Text size="xs" c="dimmed">
{canReproducePerfectly
? t(
'pdfTextEditor.fontAnalysis.perfectMessage',
'All fonts can be reproduced perfectly.'
)
: hasWarnings
? t(
'pdfTextEditor.fontAnalysis.warningMessage',
'Some fonts may not render correctly.'
)
: t(
'pdfTextEditor.fontAnalysis.infoMessage',
'Font reproduction information available.'
)}
</Text>
{/* Summary Statistics */}
<Group gap={4} wrap="wrap">
{summary.perfect > 0 && (
<Badge size="xs" color="green" variant="light" leftSection={<CheckCircleIcon sx={{ fontSize: 12 }} />}>
{summary.perfect} {t('pdfTextEditor.fontAnalysis.perfect', 'perfect')}
</Badge>
)}
{summary.embeddedSubset > 0 && (
<Badge size="xs" color="blue" variant="light" leftSection={<InfoIcon sx={{ fontSize: 12 }} />}>
{summary.embeddedSubset} {t('pdfTextEditor.fontAnalysis.subset', 'subset')}
</Badge>
)}
{summary.systemFallback > 0 && (
<Badge size="xs" color="yellow" variant="light" leftSection={<WarningIcon sx={{ fontSize: 12 }} />}>
{summary.systemFallback} {t('pdfTextEditor.fontAnalysis.fallback', 'fallback')}
</Badge>
)}
{summary.missing > 0 && (
<Badge size="xs" color="red" variant="light" leftSection={<ErrorIcon sx={{ fontSize: 12 }} />}>
{summary.missing} {t('pdfTextEditor.fontAnalysis.missing', 'missing')}
</Badge>
)}
</Group>
{/* Font List */}
<Stack gap={4} mt="xs">
{fonts.map((font, index) => (
<FontDetailItem key={`${font.fontId}-${index}`} analysis={font} />
))}
</Stack>
</Stack>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
};
export default FontStatusPanel;
File diff suppressed because it is too large Load Diff