docgen-tool 6.0.3 → 6.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/pdf/pdf-generator/generate-pdf.tsx +9 -0
- package/dist/app/pdf/pdf-generator/generate-pdf.worker.tsx +56 -0
- package/dist/app/pdf/{react-pdf → pdf-generator}/pdf-page/pdf-footer/pdf-footer.tsx +6 -6
- package/dist/app/pdf/{react-pdf → pdf-generator}/pdf-page/pdf-html-block/custom-renderers/custom-renderers.tsx +22 -18
- package/dist/app/pdf/{react-pdf → pdf-generator}/pdf-page/pdf-html-block/pdf-html-block.tsx +5 -8
- package/dist/app/pdf/{react-pdf → pdf-generator}/pdf-page/pdf-page.tsx +4 -8
- package/dist/app/pdf/{react-pdf → pdf-generator}/react-pdf.tsx +16 -47
- package/dist/app/pdf/pdf-generator/use-generate.pdf.tsx +46 -0
- package/dist/app/pdf/pdf-viewer/pdf-controls/pdf-controls.module.css +61 -0
- package/dist/app/pdf/pdf-viewer/pdf-controls/pdf-controls.tsx +62 -0
- package/dist/app/pdf/pdf-viewer/pdf-display/pdf-display.module.css +22 -0
- package/dist/app/pdf/pdf-viewer/pdf-display/pdf-display.tsx +37 -0
- package/dist/app/pdf/pdf-viewer/pdf-loader/pdf-loader.tsx +11 -0
- package/dist/app/pdf/pdf-viewer/pdf-viewer.module.css +50 -0
- package/dist/app/pdf/pdf-viewer/pdf-viewer.tsx +67 -0
- package/dist/app/views/components/card/card.module.css +65 -0
- package/dist/app/views/components/card/card.tsx +53 -0
- package/dist/app/views/components/copyright/copyright.tsx +1 -3
- package/dist/app/views/components/footer/footer.tsx +6 -11
- package/dist/app/views/components/loader/loader.module.css +49 -0
- package/dist/app/views/components/loader/loader.tsx +52 -0
- package/dist/app/views/components/loader/spinner.module.css +28 -0
- package/dist/app/views/components/loader/spinner.tsx +6 -0
- package/dist/app/views/components/logo/logo.tsx +3 -11
- package/dist/app/views/components/page/page.tsx +3 -8
- package/dist/app/views/components/pdf-footer/pdf-footer.tsx +15 -16
- package/dist/app/views/components/pdf-header/pdf-header.tsx +2 -4
- package/dist/app/views/components/pdf-toggle-button/pdf-toggle-button.tsx +1 -5
- package/dist/app/views/components/side-bar/side-bar.tsx +7 -6
- package/dist/app/views/content/markdown.tsx +14 -3
- package/dist/app/views/pages/cover/cover.tsx +79 -85
- package/dist/app/views/pages/main/main.module.css +4 -0
- package/dist/app/views/pages/main/main.tsx +12 -13
- package/dist/app/views/pages/main/pdf-viewer.tsx +0 -1
- package/dist/app/views/router.tsx +24 -38
- package/dist/cli/cli.js +2 -2
- package/package.json +7 -5
- package/dist/app/views/assets/.DS_Store +0 -0
- package/dist/app/views/assets/styles/.DS_Store +0 -0
- package/dist/app/views/assets/styles/fonts/.DS_Store +0 -0
- /package/dist/app/pdf/{react-pdf → pdf-generator}/pdf-styles/pdf-admonitions-styles.ts +0 -0
- /package/dist/app/pdf/{react-pdf → pdf-generator}/pdf-styles/pdf-docs-styles.ts +0 -0
- /package/dist/app/pdf/{react-pdf → pdf-generator}/pdf-styles/pdf-styles.ts +0 -0
- /package/dist/app/pdf/{react-pdf → pdf-generator}/pdf-styles/pdf-table-styles.ts +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { pdf } from '@react-pdf/renderer';
|
|
3
|
+
import { Pdf } from './react-pdf.tsx';
|
|
4
|
+
|
|
5
|
+
export const generatePdf = async (loadedPages) => {
|
|
6
|
+
const instance = await pdf(<Pdf loadedPages={loadedPages} />);
|
|
7
|
+
const blob = await instance.toBlob();
|
|
8
|
+
return blob;
|
|
9
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { generatePdf } from './generate-pdf.tsx';
|
|
2
|
+
import type { TSection, TSortedPages } from '../../../docgen/types.ts';
|
|
3
|
+
|
|
4
|
+
declare const __BASE_PATH__: string;
|
|
5
|
+
declare const __DOCGEN_PAGES__: TSortedPages;
|
|
6
|
+
|
|
7
|
+
const loadPdfPages = async (
|
|
8
|
+
sortedPages: any,
|
|
9
|
+
): Promise<Record<string, string>> => {
|
|
10
|
+
const pages: Record<string, string> = {};
|
|
11
|
+
|
|
12
|
+
const sources = Object.values(sortedPages).flatMap((columns: TSection) =>
|
|
13
|
+
columns.flatMap((section) => section.pages.map((p: any) => p.source)),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
await Promise.all(
|
|
17
|
+
sources.map(async (filename) => {
|
|
18
|
+
const url = `${__BASE_PATH__}${filename}`;
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(url);
|
|
21
|
+
pages[filename] = res.ok
|
|
22
|
+
? await res.text()
|
|
23
|
+
: `Error loading ${filename}: ${res.status}`;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
pages[filename] = `Error loading ${filename}: ${err}`;
|
|
26
|
+
}
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return pages;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const slowTask = async () => {
|
|
34
|
+
let data;
|
|
35
|
+
try {
|
|
36
|
+
const loadedPages = await loadPdfPages(__DOCGEN_PAGES__);
|
|
37
|
+
data = await generatePdf(loadedPages);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(error);
|
|
40
|
+
}
|
|
41
|
+
self.postMessage({
|
|
42
|
+
type: 'complete',
|
|
43
|
+
payload: {
|
|
44
|
+
data,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
self.onmessage = ({ data: message }) => {
|
|
50
|
+
switch (message.type) {
|
|
51
|
+
case 'start':
|
|
52
|
+
slowTask();
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -18,18 +18,18 @@ const styles = StyleSheet.create({
|
|
|
18
18
|
width: '33%',
|
|
19
19
|
textAlign: 'center',
|
|
20
20
|
fontSize: 10,
|
|
21
|
-
}
|
|
21
|
+
},
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
export const PdfFooter = ({parameters}) => (
|
|
24
|
+
export const PdfFooter = ({ parameters }) => (
|
|
25
25
|
<View style={styles.footer} fixed>
|
|
26
26
|
<Text style={styles.column}>{parameters.title}</Text>
|
|
27
|
-
<Text style={styles.column}>{`© ${parameters.year} ${parameters.name}`}</Text>
|
|
28
27
|
<Text
|
|
29
28
|
style={styles.column}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
>{`© ${parameters.year} ${parameters.name}`}</Text>
|
|
30
|
+
<Text
|
|
31
|
+
style={styles.column}
|
|
32
|
+
render={({ pageNumber, totalPages }) => `${pageNumber} / ${totalPages}`}
|
|
33
33
|
/>
|
|
34
34
|
</View>
|
|
35
35
|
);
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
View,
|
|
5
|
-
Text,
|
|
6
|
-
Image,
|
|
7
|
-
} from '@react-pdf/renderer';
|
|
2
|
+
import { View, Text, Image } from '@react-pdf/renderer';
|
|
8
3
|
import * as cheerio from 'cheerio';
|
|
9
4
|
|
|
5
|
+
declare const __BASE_PATH__: string;
|
|
6
|
+
|
|
10
7
|
/*
|
|
11
8
|
For "default" renderers in react-pdf-html, see
|
|
12
9
|
|
|
@@ -16,17 +13,21 @@ import * as cheerio from 'cheerio';
|
|
|
16
13
|
(and renderers.tsx for more details)
|
|
17
14
|
*/
|
|
18
15
|
|
|
19
|
-
export const customRenderers = ({options}) => ({
|
|
16
|
+
export const customRenderers = ({ options }) => ({
|
|
20
17
|
div: (payload) => {
|
|
21
|
-
const {children, style, element} = payload;
|
|
18
|
+
const { children, style, element } = payload;
|
|
22
19
|
const classNames = element.classList.toString();
|
|
23
|
-
if (classNames.includes(
|
|
24
|
-
return
|
|
20
|
+
if (classNames.includes('dgPDFPageBreak')) {
|
|
21
|
+
return (
|
|
22
|
+
<View break style={style}>
|
|
23
|
+
{children}
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
25
26
|
}
|
|
26
27
|
return <View style={style}>{children}</View>;
|
|
27
28
|
},
|
|
28
29
|
pre: (payload) => {
|
|
29
|
-
const {children, element, style} = payload;
|
|
30
|
+
const { children, element, style } = payload;
|
|
30
31
|
//strip and handle code blocks
|
|
31
32
|
const $ = cheerio.load(element.content.join());
|
|
32
33
|
const code = $('code');
|
|
@@ -35,11 +36,14 @@ export const customRenderers = ({options}) => ({
|
|
|
35
36
|
}
|
|
36
37
|
return <Text style={style}>{children}</Text>;
|
|
37
38
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
img: (payload) => {
|
|
40
|
+
const { element, style } = payload;
|
|
41
|
+
// Load images from base URL
|
|
42
|
+
return (
|
|
43
|
+
<Image
|
|
44
|
+
style={style}
|
|
45
|
+
source={`${__BASE_PATH__}${element?.attributes?.src}`}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
},
|
|
45
49
|
});
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import Html from 'react-pdf-html';
|
|
2
|
+
import Html from 'react-pdf-html-simple';
|
|
3
3
|
import { fontSize, htmlStyleSheet } from '../../pdf-styles/pdf-styles.ts';
|
|
4
|
-
import { customRenderers } from
|
|
4
|
+
import { customRenderers } from './custom-renderers/custom-renderers.tsx';
|
|
5
5
|
|
|
6
|
-
export const PdfHtmlBlock = ({
|
|
7
|
-
page,
|
|
8
|
-
options
|
|
9
|
-
}) => {
|
|
6
|
+
export const PdfHtmlBlock = ({ page, options }) => {
|
|
10
7
|
return (
|
|
11
8
|
<Html
|
|
12
9
|
style={{ fontSize }}
|
|
13
10
|
stylesheet={htmlStyleSheet}
|
|
14
|
-
renderers={customRenderers({options})}
|
|
11
|
+
renderers={customRenderers({ options })}
|
|
15
12
|
>
|
|
16
13
|
{page}
|
|
17
14
|
</Html>
|
|
18
15
|
);
|
|
19
|
-
}
|
|
16
|
+
};
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Page, View, StyleSheet } from '@react-pdf/renderer';
|
|
3
|
-
import { PdfHtmlBlock } from
|
|
3
|
+
import { PdfHtmlBlock } from './pdf-html-block/pdf-html-block.tsx';
|
|
4
4
|
import { PdfFooter } from './pdf-footer/pdf-footer.tsx';
|
|
5
5
|
import { pdfStyleSheet } from '../pdf-styles/pdf-styles.ts';
|
|
6
6
|
|
|
7
7
|
export const reactPdfStyles = StyleSheet.create(pdfStyleSheet);
|
|
8
8
|
|
|
9
|
-
export const PdfPage = ({
|
|
10
|
-
page,
|
|
11
|
-
parameters,
|
|
12
|
-
options
|
|
13
|
-
}) => {
|
|
9
|
+
export const PdfPage = ({ page, parameters, options }) => {
|
|
14
10
|
return (
|
|
15
11
|
<Page size="A4" style={reactPdfStyles.page}>
|
|
16
12
|
<View>
|
|
17
13
|
<PdfHtmlBlock page={page} options={options} />
|
|
18
14
|
</View>
|
|
19
|
-
<PdfFooter parameters={parameters}/>
|
|
15
|
+
<PdfFooter parameters={parameters} />
|
|
20
16
|
</Page>
|
|
21
|
-
)
|
|
17
|
+
);
|
|
22
18
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from 'react';
|
|
2
2
|
import { Document, Font } from '@react-pdf/renderer';
|
|
3
3
|
import { PdfPage } from './pdf-page/pdf-page.tsx';
|
|
4
4
|
import { marked } from 'marked';
|
|
@@ -15,10 +15,11 @@ import spaceMonoRegular from '../../views/assets/styles/fonts/space-mono-regular
|
|
|
15
15
|
import spaceMonoItalic from '../../views/assets/styles/fonts/space-mono-italic.ttf';
|
|
16
16
|
import spaceMono700 from '../../views/assets/styles/fonts/space-mono-700.ttf';
|
|
17
17
|
import spaceMono700Italic from '../../views/assets/styles/fonts/space-mono-700-italic.ttf';
|
|
18
|
-
import type { TParameters, TSortedPages
|
|
18
|
+
import type { TParameters, TSortedPages } from '../../../docgen/types.ts';
|
|
19
19
|
import { preprocessAdmonitions } from '../../common/markdown/markdown.ts';
|
|
20
20
|
|
|
21
|
-
declare const
|
|
21
|
+
declare const __DOCGEN_PARAMETERS__: TParameters;
|
|
22
|
+
declare const __DOCGEN_PAGES__: TSortedPages;
|
|
22
23
|
|
|
23
24
|
Font.register({
|
|
24
25
|
family: 'archivo',
|
|
@@ -48,58 +49,26 @@ Font.register({
|
|
|
48
49
|
],
|
|
49
50
|
});
|
|
50
51
|
|
|
51
|
-
type PdfProps = {
|
|
52
|
-
parameters: TParameters;
|
|
53
|
-
options: any;
|
|
54
|
-
sortedPages: TSortedPages;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
52
|
// Async loader for PDF pages
|
|
58
|
-
const loadPdfPages = async (sortedPages: any): Promise<Record<string, string>> => {
|
|
59
|
-
const pages: Record<string, string> = {};
|
|
60
53
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await Promise.all(
|
|
67
|
-
sources.map(async (filename) => {
|
|
68
|
-
const url = `${__BASE_PATH__}${filename}`;
|
|
69
|
-
try {
|
|
70
|
-
const res = await fetch(url);
|
|
71
|
-
pages[filename] = res.ok ? await res.text() : `Error loading ${filename}: ${res.status}`;
|
|
72
|
-
} catch (err) {
|
|
73
|
-
pages[filename] = `Error loading ${filename}: ${err}`;
|
|
74
|
-
}
|
|
75
|
-
}),
|
|
54
|
+
export const Pdf = ({ loadedPages }) => {
|
|
55
|
+
const parameters = __DOCGEN_PARAMETERS__;
|
|
56
|
+
const options = {};
|
|
57
|
+
const allSources = Object.values(__DOCGEN_PAGES__).flatMap((columns) =>
|
|
58
|
+
columns.flatMap((section) => section.pages.map((p: any) => p.source)),
|
|
76
59
|
);
|
|
77
60
|
|
|
78
|
-
return pages;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export const Pdf = ({ parameters, options, sortedPages }: PdfProps) => {
|
|
82
|
-
const [pages, setPages] = useState<Record<string, string>>({});
|
|
83
|
-
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
const fetchPages = async () => {
|
|
86
|
-
const loaded = await loadPdfPages(sortedPages);
|
|
87
|
-
setPages(loaded);
|
|
88
|
-
};
|
|
89
|
-
fetchPages();
|
|
90
|
-
}, [sortedPages]);
|
|
91
|
-
|
|
92
|
-
const allSources = Object.values(sortedPages)
|
|
93
|
-
.flatMap((columns) =>
|
|
94
|
-
columns.flatMap((section) => section.pages.map((p: any) => p.source)),
|
|
95
|
-
);
|
|
96
|
-
|
|
97
61
|
return (
|
|
98
62
|
<Document>
|
|
99
63
|
{allSources.map((source, i) => {
|
|
100
|
-
const html = marked(preprocessAdmonitions(
|
|
64
|
+
const html = marked(preprocessAdmonitions(loadedPages[source] || ''));
|
|
101
65
|
return (
|
|
102
|
-
<PdfPage
|
|
66
|
+
<PdfPage
|
|
67
|
+
key={i}
|
|
68
|
+
page={html}
|
|
69
|
+
parameters={parameters}
|
|
70
|
+
options={options}
|
|
71
|
+
/>
|
|
103
72
|
);
|
|
104
73
|
})}
|
|
105
74
|
</Document>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import WorkerURL from './generate-pdf.worker.tsx?worker';
|
|
3
|
+
|
|
4
|
+
export type TGeneratedPdf = {
|
|
5
|
+
pdfBlob: Blob | null;
|
|
6
|
+
pdfUrl: string | null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
let cachedPdf: TGeneratedPdf | null = null;
|
|
10
|
+
|
|
11
|
+
export const useGeneratePdf = () => {
|
|
12
|
+
const [result, setResult] = useState<TGeneratedPdf>({
|
|
13
|
+
pdfBlob: null,
|
|
14
|
+
pdfUrl: null,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (cachedPdf) {
|
|
19
|
+
setResult(cachedPdf);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const worker = new WorkerURL();
|
|
24
|
+
|
|
25
|
+
worker.onmessage = (e) => {
|
|
26
|
+
if (e.data.type === 'complete') {
|
|
27
|
+
const blob: Blob | null = e.data.payload.data ?? null;
|
|
28
|
+
const url = blob ? URL.createObjectURL(blob) : null;
|
|
29
|
+
|
|
30
|
+
cachedPdf = {
|
|
31
|
+
pdfBlob: blob,
|
|
32
|
+
pdfUrl: url,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
setResult(cachedPdf);
|
|
36
|
+
worker.terminate();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
worker.postMessage({ type: 'start' });
|
|
41
|
+
|
|
42
|
+
return () => worker.terminate();
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
.pdfControlsContainer {
|
|
2
|
+
display: flex;
|
|
3
|
+
justify-content: space-between;
|
|
4
|
+
/* Push items to edges */
|
|
5
|
+
align-items: center;
|
|
6
|
+
width: 100%;
|
|
7
|
+
gap: 16px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.paginationControls {
|
|
11
|
+
display: flex;
|
|
12
|
+
gap: 4px;
|
|
13
|
+
align-items: center;
|
|
14
|
+
/* Removed flex: 1 and justify-content: center to keep it on the left */
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.actionButtons {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: 4px;
|
|
20
|
+
align-items: center;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.controlButton {
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
width: 32px;
|
|
28
|
+
height: 32px;
|
|
29
|
+
padding: 0;
|
|
30
|
+
border: none;
|
|
31
|
+
background-color: transparent;
|
|
32
|
+
color: #f1f3f4;
|
|
33
|
+
/* Light grey/white text */
|
|
34
|
+
border-radius: 16px;
|
|
35
|
+
/* Rounded for hover effect */
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
transition: background-color 0.1s ease;
|
|
38
|
+
font-size: 20px;
|
|
39
|
+
/* Icon size */
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.controlButton:hover {
|
|
43
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
44
|
+
/* Subtle grey highlight */
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.controlButton:disabled {
|
|
48
|
+
color: #5f6368;
|
|
49
|
+
/* Dimmed icon color */
|
|
50
|
+
cursor: default;
|
|
51
|
+
background-color: transparent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.pageText {
|
|
55
|
+
font-size: 13px;
|
|
56
|
+
color: #f1f3f4;
|
|
57
|
+
margin: 0 8px;
|
|
58
|
+
font-family: Roboto, Arial, sans-serif;
|
|
59
|
+
font-weight: 500;
|
|
60
|
+
user-select: none;
|
|
61
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
TbChevronLeft,
|
|
4
|
+
TbChevronRight,
|
|
5
|
+
TbDownload,
|
|
6
|
+
TbExternalLink,
|
|
7
|
+
} from 'react-icons/tb';
|
|
8
|
+
import styles from './pdf-controls.module.css';
|
|
9
|
+
|
|
10
|
+
export const PdfControls = ({
|
|
11
|
+
pageNumber,
|
|
12
|
+
numPages,
|
|
13
|
+
goToPrevPage,
|
|
14
|
+
goToNextPage,
|
|
15
|
+
onDownload,
|
|
16
|
+
onOpenNewTab,
|
|
17
|
+
}) => {
|
|
18
|
+
const mobileDevice = 'ongesturechange' in window;
|
|
19
|
+
return (
|
|
20
|
+
<div className={styles.pdfControlsContainer}>
|
|
21
|
+
<div className={styles.paginationControls}>
|
|
22
|
+
<button
|
|
23
|
+
className={styles.controlButton}
|
|
24
|
+
onClick={goToPrevPage}
|
|
25
|
+
disabled={pageNumber <= 1}
|
|
26
|
+
title="Previous page"
|
|
27
|
+
>
|
|
28
|
+
<TbChevronLeft />
|
|
29
|
+
</button>
|
|
30
|
+
<span className={styles.pageText}>
|
|
31
|
+
{pageNumber} of {numPages}
|
|
32
|
+
</span>
|
|
33
|
+
<button
|
|
34
|
+
className={styles.controlButton}
|
|
35
|
+
onClick={goToNextPage}
|
|
36
|
+
disabled={pageNumber >= numPages}
|
|
37
|
+
title="Next page"
|
|
38
|
+
>
|
|
39
|
+
<TbChevronRight />
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
<div className={styles.actionButtons}>
|
|
43
|
+
<button
|
|
44
|
+
className={styles.controlButton}
|
|
45
|
+
onClick={onDownload}
|
|
46
|
+
title="Download PDF"
|
|
47
|
+
>
|
|
48
|
+
<TbDownload />
|
|
49
|
+
</button>
|
|
50
|
+
{!mobileDevice && (
|
|
51
|
+
<button
|
|
52
|
+
className={styles.controlButton}
|
|
53
|
+
onClick={onOpenNewTab}
|
|
54
|
+
title="Open in new tab"
|
|
55
|
+
>
|
|
56
|
+
<TbExternalLink />
|
|
57
|
+
</button>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.pdfDisplayWrapper {
|
|
2
|
+
> div div {
|
|
3
|
+
/* Override global rule from page.scss that interferes with the text highlighting layer in the react-pdf viewer */
|
|
4
|
+
margin: 0;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:global(.react-pdf__Page__annotations) {
|
|
8
|
+
background: transparent !important;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:global(.react-pdf__Page__annotations) * {
|
|
12
|
+
background: transparent !important;
|
|
13
|
+
border: none !important;
|
|
14
|
+
box-shadow: none !important;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:global(.react-pdf__Page__annotations) a {
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
color: var(--color-text-link) !important;
|
|
20
|
+
text-decoration: underline !important;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useResizeDetector } from 'react-resize-detector';
|
|
3
|
+
import { Document, Page, pdfjs } from 'react-pdf';
|
|
4
|
+
import styles from './pdf-display.module.css';
|
|
5
|
+
|
|
6
|
+
import 'react-pdf/dist/Page/TextLayer.css';
|
|
7
|
+
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
|
8
|
+
import { PdfLoader } from '../pdf-loader/pdf-loader.tsx';
|
|
9
|
+
|
|
10
|
+
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
|
11
|
+
'pdfjs-dist/build/pdf.worker.min.mjs',
|
|
12
|
+
import.meta.url,
|
|
13
|
+
).toString();
|
|
14
|
+
|
|
15
|
+
export const PdfDisplay = ({ pdfUrl, pageNumber, onPdfLoadSuccess }) => {
|
|
16
|
+
const { width, ref } = useResizeDetector<HTMLDivElement>();
|
|
17
|
+
if (!pdfUrl) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const key = `${pdfUrl}-${pageNumber}`;
|
|
21
|
+
return (
|
|
22
|
+
<div ref={ref} className={styles.pdfDisplayWrapper}>
|
|
23
|
+
<Document
|
|
24
|
+
file={pdfUrl}
|
|
25
|
+
loading={<PdfLoader />}
|
|
26
|
+
onLoadSuccess={onPdfLoadSuccess}
|
|
27
|
+
>
|
|
28
|
+
<Page
|
|
29
|
+
key={key}
|
|
30
|
+
pageNumber={pageNumber}
|
|
31
|
+
width={width}
|
|
32
|
+
renderAnnotationLayer={true}
|
|
33
|
+
/>
|
|
34
|
+
</Document>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Loader } from '../../../views/components/loader/loader.tsx';
|
|
3
|
+
import { Spinner } from '../../../views/components/loader/spinner.tsx';
|
|
4
|
+
|
|
5
|
+
export const PdfLoader = () => {
|
|
6
|
+
return (
|
|
7
|
+
<Loader cover text="Loading PDF...">
|
|
8
|
+
<Spinner />
|
|
9
|
+
</Loader>
|
|
10
|
+
);
|
|
11
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
.pdfViewerOuterWrapper {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
align-items: center;
|
|
5
|
+
flex-grow: 1;
|
|
6
|
+
width: auto;
|
|
7
|
+
margin: 30px 30px 0 30px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.pdfViewerInnerWrapper {
|
|
11
|
+
flex-grow: 1;
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
min-width: 300px;
|
|
15
|
+
max-width: 800px;
|
|
16
|
+
width: 100%;
|
|
17
|
+
aspect-ratio: 210 / 297;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.card {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
height: 100%;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.cardContent {
|
|
29
|
+
flex-grow: 1;
|
|
30
|
+
overflow-y: auto;
|
|
31
|
+
scrollbar-width: none;
|
|
32
|
+
-ms-overflow-style: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.cardContent::-webkit-scrollbar {
|
|
36
|
+
display: none;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.loaderWrapper {
|
|
40
|
+
position: relative;
|
|
41
|
+
width: 100%;
|
|
42
|
+
aspect-ratio: 210 / 297;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.pdfHeader {
|
|
46
|
+
background-color: #323639 !important;
|
|
47
|
+
color: white;
|
|
48
|
+
border-bottom: 1px solid #323639;
|
|
49
|
+
padding: 4px 12px !important;
|
|
50
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useGeneratePdf } from '../pdf-generator/use-generate.pdf.tsx';
|
|
3
|
+
import { PdfDisplay } from './pdf-display/pdf-display.tsx';
|
|
4
|
+
import { PdfControls } from './pdf-controls/pdf-controls.tsx';
|
|
5
|
+
import styles from './pdf-viewer.module.css';
|
|
6
|
+
import { PdfLoader } from './pdf-loader/pdf-loader.tsx';
|
|
7
|
+
import { Card } from '../../views/components/card/card.tsx';
|
|
8
|
+
|
|
9
|
+
export const PdfViewer = () => {
|
|
10
|
+
const { pdfUrl } = useGeneratePdf();
|
|
11
|
+
const [numPages, setNumPages] = useState(0);
|
|
12
|
+
const [pageNumber, setPageNumber] = useState(1);
|
|
13
|
+
|
|
14
|
+
const loading = !pdfUrl;
|
|
15
|
+
|
|
16
|
+
const onPdfLoadSuccess = ({ numPages }: { numPages: number }) =>
|
|
17
|
+
setNumPages(numPages);
|
|
18
|
+
const goToPrevPage = () => setPageNumber((p) => Math.max(p - 1, 1));
|
|
19
|
+
const goToNextPage = () => setPageNumber((p) => Math.min(p + 1, numPages));
|
|
20
|
+
|
|
21
|
+
const onDownload = () => {
|
|
22
|
+
if (!pdfUrl) return;
|
|
23
|
+
const link = document.createElement('a');
|
|
24
|
+
link.href = pdfUrl;
|
|
25
|
+
link.download = 'document.pdf';
|
|
26
|
+
document.body.appendChild(link);
|
|
27
|
+
link.click();
|
|
28
|
+
document.body.removeChild(link);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const onOpenNewTab = () => {
|
|
32
|
+
if (!pdfUrl) return;
|
|
33
|
+
window.open(pdfUrl, '_blank');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className={styles.pdfViewerOuterWrapper}>
|
|
38
|
+
<div className={styles.pdfViewerInnerWrapper}>
|
|
39
|
+
<Card
|
|
40
|
+
padding={false}
|
|
41
|
+
headerClassName={styles.pdfHeader}
|
|
42
|
+
className={styles.card}
|
|
43
|
+
contentClassName={styles.cardContent}
|
|
44
|
+
heading={
|
|
45
|
+
<PdfControls
|
|
46
|
+
pageNumber={pageNumber}
|
|
47
|
+
numPages={numPages}
|
|
48
|
+
goToPrevPage={goToPrevPage}
|
|
49
|
+
goToNextPage={goToNextPage}
|
|
50
|
+
onDownload={onDownload}
|
|
51
|
+
onOpenNewTab={onOpenNewTab}
|
|
52
|
+
/>
|
|
53
|
+
}
|
|
54
|
+
>
|
|
55
|
+
<div className={styles.loaderWrapper}>
|
|
56
|
+
{loading && <PdfLoader />}
|
|
57
|
+
<PdfDisplay
|
|
58
|
+
pdfUrl={pdfUrl}
|
|
59
|
+
pageNumber={pageNumber}
|
|
60
|
+
onPdfLoadSuccess={onPdfLoadSuccess}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
</Card>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|