pdf-smith 0.2.0 → 0.4.0
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/preview-plugin-26BUIGCE.js +199 -0
- package/dist/preview-plugin-26BUIGCE.js.map +1 -0
- package/dist/server.cjs +104 -9
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/src/preview/app.tsx +262 -34
- package/src/preview/design-tokens.ts +42 -0
- package/src/preview/home.tsx +260 -79
- package/src/preview/icons.tsx +250 -0
- package/src/preview/navigation.tsx +325 -92
- package/src/preview/preview-plugin.ts +122 -2
- package/dist/preview-plugin-QDLEMAEE.js +0 -104
- package/dist/preview-plugin-QDLEMAEE.js.map +0 -1
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { colors, fonts, radii, SIDEBAR_WIDTH } from './design-tokens';
|
|
3
|
+
import { ChevronLeftIcon, DownloadIcon, GridViewIcon } from './icons';
|
|
4
|
+
|
|
5
|
+
type ExportState = 'idle' | 'exporting' | 'error';
|
|
6
|
+
|
|
1
7
|
interface NavigationProps {
|
|
2
8
|
pages: Record<string, React.ComponentType>;
|
|
3
9
|
activePage: string | null;
|
|
@@ -5,116 +11,343 @@ interface NavigationProps {
|
|
|
5
11
|
documentSlug: string;
|
|
6
12
|
}
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
height: '100vh',
|
|
14
|
-
background: '#1a1a2e',
|
|
15
|
-
color: '#e0e0e0',
|
|
16
|
-
padding: '16px',
|
|
17
|
-
overflowY: 'auto',
|
|
18
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
19
|
-
fontSize: '14px',
|
|
20
|
-
zIndex: 1000,
|
|
21
|
-
boxSizing: 'border-box',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const titleStyle: React.CSSProperties = {
|
|
25
|
-
fontSize: '16px',
|
|
26
|
-
fontWeight: 700,
|
|
27
|
-
marginBottom: '16px',
|
|
28
|
-
color: '#ffffff',
|
|
29
|
-
letterSpacing: '0.5px',
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const backLinkStyle: React.CSSProperties = {
|
|
33
|
-
display: 'block',
|
|
34
|
-
fontSize: '13px',
|
|
35
|
-
color: '#888',
|
|
36
|
-
textDecoration: 'none',
|
|
37
|
-
marginBottom: '12px',
|
|
38
|
-
transition: 'color 0.15s',
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const buttonBaseStyle: React.CSSProperties = {
|
|
42
|
-
display: 'block',
|
|
43
|
-
width: '100%',
|
|
44
|
-
padding: '8px 12px',
|
|
45
|
-
border: 'none',
|
|
46
|
-
borderRadius: '6px',
|
|
47
|
-
cursor: 'pointer',
|
|
48
|
-
textAlign: 'left',
|
|
49
|
-
fontSize: '13px',
|
|
50
|
-
marginBottom: '4px',
|
|
51
|
-
transition: 'background 0.15s',
|
|
52
|
-
fontFamily: 'inherit',
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
function getButtonStyle(isActive: boolean): React.CSSProperties {
|
|
56
|
-
return {
|
|
57
|
-
...buttonBaseStyle,
|
|
58
|
-
background: isActive ? '#16213e' : 'transparent',
|
|
59
|
-
color: isActive ? '#ffffff' : '#b0b0b0',
|
|
60
|
-
fontWeight: isActive ? 600 : 400,
|
|
61
|
-
};
|
|
14
|
+
function formatSlug(slug: string): string {
|
|
15
|
+
return slug
|
|
16
|
+
.split('-')
|
|
17
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
18
|
+
.join(' ');
|
|
62
19
|
}
|
|
63
20
|
|
|
64
|
-
const separatorStyle: React.CSSProperties = {
|
|
65
|
-
border: 'none',
|
|
66
|
-
borderTop: '1px solid #2a2a4a',
|
|
67
|
-
margin: '12px 0',
|
|
68
|
-
};
|
|
69
|
-
|
|
70
21
|
export function Navigation({ pages, activePage, onSelectPage, documentSlug }: NavigationProps) {
|
|
71
22
|
const pageNames = Object.keys(pages);
|
|
23
|
+
const [exportState, setExportState] = useState<ExportState>('idle');
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (exportState !== 'error') return;
|
|
27
|
+
const timer = setTimeout(() => setExportState('idle'), 3000);
|
|
28
|
+
return () => clearTimeout(timer);
|
|
29
|
+
}, [exportState]);
|
|
30
|
+
|
|
31
|
+
async function handleExport() {
|
|
32
|
+
setExportState('exporting');
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`/api/export/${documentSlug}`, { method: 'POST' });
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const body = await res.json().catch(() => ({ error: 'Export failed' }));
|
|
37
|
+
throw new Error(body.error ?? 'Export failed');
|
|
38
|
+
}
|
|
39
|
+
const contentType = res.headers.get('Content-Type') ?? '';
|
|
40
|
+
if (!contentType.includes('application/pdf')) {
|
|
41
|
+
throw new Error('Server returned unexpected response');
|
|
42
|
+
}
|
|
43
|
+
const blob = await res.blob();
|
|
44
|
+
const disposition = res.headers.get('Content-Disposition') ?? '';
|
|
45
|
+
const filenameMatch = disposition.match(/filename="(.+)"/);
|
|
46
|
+
const filename = filenameMatch?.[1] ?? `${documentSlug}.pdf`;
|
|
47
|
+
const url = URL.createObjectURL(blob);
|
|
48
|
+
const a = document.createElement('a');
|
|
49
|
+
a.href = url;
|
|
50
|
+
a.download = filename;
|
|
51
|
+
document.body.appendChild(a);
|
|
52
|
+
a.click();
|
|
53
|
+
a.remove();
|
|
54
|
+
URL.revokeObjectURL(url);
|
|
55
|
+
setExportState('idle');
|
|
56
|
+
} catch {
|
|
57
|
+
setExportState('error');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
72
60
|
|
|
73
61
|
return (
|
|
74
|
-
<nav
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
62
|
+
<nav
|
|
63
|
+
data-pdf-smith-nav=""
|
|
64
|
+
style={{
|
|
65
|
+
position: 'fixed',
|
|
66
|
+
left: 0,
|
|
67
|
+
top: 0,
|
|
68
|
+
bottom: 0,
|
|
69
|
+
width: `${SIDEBAR_WIDTH}px`,
|
|
70
|
+
background: colors.stone900,
|
|
71
|
+
color: colors.stone300,
|
|
72
|
+
display: 'flex',
|
|
73
|
+
flexDirection: 'column',
|
|
74
|
+
zIndex: 100,
|
|
75
|
+
borderRight: `1px solid ${colors.stone800}`,
|
|
76
|
+
fontFamily: fonts.body,
|
|
77
|
+
boxSizing: 'border-box',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{/* Header */}
|
|
81
|
+
<div
|
|
82
|
+
style={{
|
|
83
|
+
padding: '16px 20px',
|
|
84
|
+
borderBottom: `1px solid ${colors.stone800}`,
|
|
83
85
|
}}
|
|
84
86
|
>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
<a
|
|
88
|
+
href="/"
|
|
89
|
+
style={{
|
|
90
|
+
display: 'flex',
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
gap: '6px',
|
|
93
|
+
fontSize: '13px',
|
|
94
|
+
color: colors.stone400,
|
|
95
|
+
textDecoration: 'none',
|
|
96
|
+
transition: 'color 0.15s',
|
|
97
|
+
marginBottom: '12px',
|
|
98
|
+
}}
|
|
99
|
+
onMouseEnter={(e) => {
|
|
100
|
+
e.currentTarget.style.color = colors.stone200;
|
|
101
|
+
}}
|
|
102
|
+
onMouseLeave={(e) => {
|
|
103
|
+
e.currentTarget.style.color = colors.stone400;
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<ChevronLeftIcon size={14} />
|
|
107
|
+
All Documents
|
|
108
|
+
</a>
|
|
109
|
+
<div
|
|
110
|
+
style={{
|
|
111
|
+
fontFamily: fonts.heading,
|
|
112
|
+
fontSize: '16px',
|
|
113
|
+
fontWeight: 600,
|
|
114
|
+
color: colors.stone100,
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{formatSlug(documentSlug)}
|
|
118
|
+
</div>
|
|
119
|
+
<div
|
|
120
|
+
style={{
|
|
121
|
+
fontFamily: fonts.mono,
|
|
122
|
+
fontSize: '11px',
|
|
123
|
+
color: colors.stone500,
|
|
124
|
+
marginTop: '2px',
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
pdfs/{documentSlug}
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* Page Navigation */}
|
|
132
|
+
<div style={{ flex: 1, overflowY: 'auto', padding: '12px' }}>
|
|
133
|
+
<div
|
|
134
|
+
style={{
|
|
135
|
+
fontSize: '11px',
|
|
136
|
+
fontWeight: 600,
|
|
137
|
+
textTransform: 'uppercase',
|
|
138
|
+
letterSpacing: '0.06em',
|
|
139
|
+
color: colors.stone500,
|
|
140
|
+
padding: '8px 8px 6px',
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
Pages
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* All Pages button */}
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={() => onSelectPage(null)}
|
|
150
|
+
style={{
|
|
151
|
+
display: 'flex',
|
|
152
|
+
alignItems: 'center',
|
|
153
|
+
gap: '10px',
|
|
154
|
+
padding: '8px 10px',
|
|
155
|
+
borderRadius: radii.md,
|
|
156
|
+
color: activePage === null ? colors.ember : colors.stone300,
|
|
157
|
+
fontSize: '13px',
|
|
158
|
+
fontWeight: 500,
|
|
159
|
+
fontFamily: 'inherit',
|
|
160
|
+
transition: 'all 0.12s',
|
|
161
|
+
cursor: 'pointer',
|
|
162
|
+
marginBottom: '8px',
|
|
163
|
+
border: `1px solid ${activePage === null ? colors.forge : colors.stone700}`,
|
|
164
|
+
background: activePage === null ? 'rgba(194, 65, 12, 0.1)' : 'transparent',
|
|
165
|
+
width: '100%',
|
|
166
|
+
textAlign: 'left',
|
|
167
|
+
}}
|
|
168
|
+
onMouseEnter={(e) => {
|
|
169
|
+
if (activePage !== null) {
|
|
170
|
+
e.currentTarget.style.background = 'rgba(250, 249, 247, 0.05)';
|
|
171
|
+
e.currentTarget.style.borderColor = colors.stone600;
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
onMouseLeave={(e) => {
|
|
175
|
+
if (activePage !== null) {
|
|
176
|
+
e.currentTarget.style.background = 'transparent';
|
|
177
|
+
e.currentTarget.style.borderColor = colors.stone700;
|
|
178
|
+
}
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
<GridViewIcon size={14} />
|
|
182
|
+
All Pages
|
|
183
|
+
</button>
|
|
184
|
+
|
|
185
|
+
{/* Individual page links */}
|
|
186
|
+
{pageNames.map((name, idx) => {
|
|
187
|
+
const isActive = activePage === name;
|
|
188
|
+
return (
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
key={name}
|
|
192
|
+
onClick={() => onSelectPage(name)}
|
|
193
|
+
style={{
|
|
194
|
+
display: 'flex',
|
|
195
|
+
alignItems: 'center',
|
|
196
|
+
gap: '10px',
|
|
197
|
+
padding: '8px 10px',
|
|
198
|
+
borderRadius: radii.md,
|
|
199
|
+
color: isActive ? colors.ember : colors.stone400,
|
|
200
|
+
fontSize: '13px',
|
|
201
|
+
fontFamily: 'inherit',
|
|
202
|
+
transition: 'all 0.12s',
|
|
203
|
+
cursor: 'pointer',
|
|
204
|
+
marginBottom: '2px',
|
|
205
|
+
border: 'none',
|
|
206
|
+
background: isActive ? 'rgba(194, 65, 12, 0.12)' : 'transparent',
|
|
207
|
+
width: '100%',
|
|
208
|
+
textAlign: 'left',
|
|
209
|
+
}}
|
|
210
|
+
onMouseEnter={(e) => {
|
|
211
|
+
if (!isActive) {
|
|
212
|
+
e.currentTarget.style.background = 'rgba(250, 249, 247, 0.05)';
|
|
213
|
+
e.currentTarget.style.color = colors.stone200;
|
|
214
|
+
}
|
|
215
|
+
}}
|
|
216
|
+
onMouseLeave={(e) => {
|
|
217
|
+
if (!isActive) {
|
|
218
|
+
e.currentTarget.style.background = 'transparent';
|
|
219
|
+
e.currentTarget.style.color = colors.stone400;
|
|
220
|
+
}
|
|
221
|
+
}}
|
|
222
|
+
>
|
|
223
|
+
<span
|
|
224
|
+
style={{
|
|
225
|
+
width: '22px',
|
|
226
|
+
height: '22px',
|
|
227
|
+
display: 'flex',
|
|
228
|
+
alignItems: 'center',
|
|
229
|
+
justifyContent: 'center',
|
|
230
|
+
fontFamily: fonts.mono,
|
|
231
|
+
fontSize: '11px',
|
|
232
|
+
fontWeight: 600,
|
|
233
|
+
background: isActive ? colors.forge : colors.stone800,
|
|
234
|
+
color: isActive ? 'white' : 'inherit',
|
|
235
|
+
borderRadius: '5px',
|
|
236
|
+
flexShrink: 0,
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
{idx + 1}
|
|
240
|
+
</span>
|
|
241
|
+
<span
|
|
242
|
+
style={{
|
|
243
|
+
overflow: 'hidden',
|
|
244
|
+
textOverflow: 'ellipsis',
|
|
245
|
+
whiteSpace: 'nowrap',
|
|
246
|
+
}}
|
|
247
|
+
>
|
|
248
|
+
{name}
|
|
249
|
+
</span>
|
|
250
|
+
</button>
|
|
251
|
+
);
|
|
252
|
+
})}
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Footer */}
|
|
256
|
+
<div
|
|
257
|
+
style={{
|
|
258
|
+
padding: '12px 16px',
|
|
259
|
+
borderTop: `1px solid ${colors.stone800}`,
|
|
260
|
+
display: 'flex',
|
|
261
|
+
flexDirection: 'column',
|
|
262
|
+
gap: '8px',
|
|
97
263
|
}}
|
|
98
264
|
>
|
|
99
|
-
All Pages
|
|
100
|
-
</button>
|
|
101
|
-
<hr style={separatorStyle} />
|
|
102
|
-
{pageNames.map((name) => (
|
|
103
265
|
<button
|
|
104
266
|
type="button"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
267
|
+
disabled={exportState === 'exporting'}
|
|
268
|
+
onClick={handleExport}
|
|
269
|
+
style={{
|
|
270
|
+
display: 'flex',
|
|
271
|
+
alignItems: 'center',
|
|
272
|
+
justifyContent: 'center',
|
|
273
|
+
gap: '6px',
|
|
274
|
+
padding: '8px 16px',
|
|
275
|
+
background:
|
|
276
|
+
exportState === 'error'
|
|
277
|
+
? '#DC2626'
|
|
278
|
+
: exportState === 'exporting'
|
|
279
|
+
? colors.stone600
|
|
280
|
+
: colors.forge,
|
|
281
|
+
color: 'white',
|
|
282
|
+
border: 'none',
|
|
283
|
+
borderRadius: radii.md,
|
|
284
|
+
fontFamily: fonts.body,
|
|
285
|
+
fontSize: '13px',
|
|
286
|
+
fontWeight: 500,
|
|
287
|
+
cursor: exportState === 'exporting' ? 'not-allowed' : 'pointer',
|
|
288
|
+
transition: 'background 0.15s',
|
|
289
|
+
width: '100%',
|
|
290
|
+
opacity: exportState === 'exporting' ? 0.8 : 1,
|
|
291
|
+
}}
|
|
108
292
|
onMouseEnter={(e) => {
|
|
109
|
-
if (
|
|
293
|
+
if (exportState === 'idle') e.currentTarget.style.background = colors.deepForge;
|
|
110
294
|
}}
|
|
111
295
|
onMouseLeave={(e) => {
|
|
112
|
-
if (
|
|
296
|
+
if (exportState === 'idle') e.currentTarget.style.background = colors.forge;
|
|
113
297
|
}}
|
|
114
298
|
>
|
|
115
|
-
{
|
|
299
|
+
{exportState === 'exporting' ? (
|
|
300
|
+
<>
|
|
301
|
+
<style>{`@keyframes pdf-smith-spin { to { transform: rotate(360deg) } }`}</style>
|
|
302
|
+
<span
|
|
303
|
+
style={{
|
|
304
|
+
width: '14px',
|
|
305
|
+
height: '14px',
|
|
306
|
+
border: '2px solid rgba(255,255,255,0.3)',
|
|
307
|
+
borderTopColor: 'white',
|
|
308
|
+
borderRadius: '50%',
|
|
309
|
+
animation: 'pdf-smith-spin 0.6s linear infinite',
|
|
310
|
+
flexShrink: 0,
|
|
311
|
+
}}
|
|
312
|
+
/>
|
|
313
|
+
Exporting...
|
|
314
|
+
</>
|
|
315
|
+
) : exportState === 'error' ? (
|
|
316
|
+
'Export Failed'
|
|
317
|
+
) : (
|
|
318
|
+
<>
|
|
319
|
+
<DownloadIcon size={14} />
|
|
320
|
+
Export PDF
|
|
321
|
+
</>
|
|
322
|
+
)}
|
|
116
323
|
</button>
|
|
117
|
-
|
|
324
|
+
<div
|
|
325
|
+
style={{
|
|
326
|
+
display: 'flex',
|
|
327
|
+
alignItems: 'center',
|
|
328
|
+
justifyContent: 'space-between',
|
|
329
|
+
fontSize: '11px',
|
|
330
|
+
color: colors.stone500,
|
|
331
|
+
fontFamily: fonts.mono,
|
|
332
|
+
}}
|
|
333
|
+
>
|
|
334
|
+
<span>
|
|
335
|
+
{pageNames.length} page{pageNames.length !== 1 ? 's' : ''}
|
|
336
|
+
</span>
|
|
337
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
338
|
+
<span
|
|
339
|
+
style={{
|
|
340
|
+
width: '6px',
|
|
341
|
+
height: '6px',
|
|
342
|
+
borderRadius: '50%',
|
|
343
|
+
background: colors.success,
|
|
344
|
+
display: 'inline-block',
|
|
345
|
+
}}
|
|
346
|
+
/>
|
|
347
|
+
HMR active
|
|
348
|
+
</span>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
118
351
|
</nav>
|
|
119
352
|
);
|
|
120
353
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import type { AddressInfo } from 'node:net';
|
|
3
|
+
import path from 'node:path';
|
|
1
4
|
import type { Plugin } from 'vite';
|
|
5
|
+
import type { DocumentConfig } from '../config';
|
|
2
6
|
|
|
3
7
|
const VIRTUAL_DOCUMENTS_ID = 'virtual:pdf-smith-documents';
|
|
4
8
|
const RESOLVED_VIRTUAL_DOCUMENTS_ID = `\0${VIRTUAL_DOCUMENTS_ID}`;
|
|
@@ -7,11 +11,110 @@ interface PreviewPluginOptions {
|
|
|
7
11
|
pkgSrcDir: string;
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
async function handleExportRequest(
|
|
15
|
+
server: import('vite').ViteDevServer,
|
|
16
|
+
slug: string,
|
|
17
|
+
res: import('node:http').ServerResponse,
|
|
18
|
+
) {
|
|
19
|
+
const root = server.config.root;
|
|
20
|
+
const pagesDir = path.join(root, 'pdfs', slug, 'pages');
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(pagesDir)) {
|
|
23
|
+
res.statusCode = 404;
|
|
24
|
+
res.setHeader('Content-Type', 'application/json');
|
|
25
|
+
res.end(JSON.stringify({ error: `Document "${slug}" not found` }));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let config: DocumentConfig | undefined;
|
|
30
|
+
const configPath = path.join(root, 'pdfs', slug, 'config.ts');
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const configModule = await server.ssrLoadModule(configPath);
|
|
34
|
+
config = configModule.default;
|
|
35
|
+
} catch {
|
|
36
|
+
// Config loading failed, proceed without it
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let playwright: typeof import('playwright');
|
|
41
|
+
try {
|
|
42
|
+
playwright = await import('playwright');
|
|
43
|
+
} catch {
|
|
44
|
+
res.statusCode = 500;
|
|
45
|
+
res.setHeader('Content-Type', 'application/json');
|
|
46
|
+
res.end(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
error:
|
|
49
|
+
'Playwright is required for PDF export. Install it with: npm install -D playwright && npx playwright install chromium',
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let browser: import('playwright').Browser | undefined;
|
|
56
|
+
try {
|
|
57
|
+
const address = server.httpServer?.address() as AddressInfo | null;
|
|
58
|
+
const port = address?.port;
|
|
59
|
+
if (!port) throw new Error('Could not determine server port');
|
|
60
|
+
|
|
61
|
+
browser = await playwright.chromium.launch({ headless: true });
|
|
62
|
+
const page = await browser.newPage();
|
|
63
|
+
await page.goto(`http://localhost:${port}/${slug}`, { waitUntil: 'networkidle' });
|
|
64
|
+
await page.waitForSelector('[data-pdf-smith-page]', { timeout: 30_000 });
|
|
65
|
+
|
|
66
|
+
const margin = { top: '0', bottom: '0', left: '0', right: '0' };
|
|
67
|
+
const pdfOptions: Parameters<typeof page.pdf>[0] = {
|
|
68
|
+
preferCSSPageSize: true,
|
|
69
|
+
printBackground: true,
|
|
70
|
+
margin,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (config?.pageNumbers?.enabled) {
|
|
74
|
+
pdfOptions.displayHeaderFooter = true;
|
|
75
|
+
pdfOptions.headerTemplate = '<span></span>';
|
|
76
|
+
pdfOptions.footerTemplate =
|
|
77
|
+
config.pageNumbers.template ??
|
|
78
|
+
'<div style="font-size:10px;text-align:center;width:100%;"><span class="pageNumber"></span> / <span class="totalPages"></span></div>';
|
|
79
|
+
margin.bottom = '40px';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const buffer = await page.pdf(pdfOptions);
|
|
83
|
+
await page.close();
|
|
84
|
+
|
|
85
|
+
const filename = config?.output ?? `${slug}.pdf`;
|
|
86
|
+
res.statusCode = 200;
|
|
87
|
+
res.setHeader('Content-Type', 'application/pdf');
|
|
88
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
89
|
+
res.end(buffer);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (!res.headersSent) {
|
|
92
|
+
res.statusCode = 500;
|
|
93
|
+
res.setHeader('Content-Type', 'application/json');
|
|
94
|
+
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'PDF export failed' }));
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
await browser?.close().catch(() => {});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
10
101
|
export function pdfSmithPreviewPlugin({ pkgSrcDir }: PreviewPluginOptions): Plugin {
|
|
11
102
|
return {
|
|
12
103
|
name: 'pdf-smith-preview',
|
|
13
104
|
|
|
14
105
|
configureServer(server) {
|
|
106
|
+
// Pre-middleware: Export API runs before Vite internals.
|
|
107
|
+
// Must be a sync function — async middleware returns a Promise that
|
|
108
|
+
// Connect ignores, so the pipeline can advance to the catch-all
|
|
109
|
+
// before the async work finishes.
|
|
110
|
+
server.middlewares.use((req, res, next) => {
|
|
111
|
+
if (req.method !== 'POST') return next();
|
|
112
|
+
const match = req.url?.match(/^\/api\/export\/([^/]+)$/);
|
|
113
|
+
if (!match) return next();
|
|
114
|
+
void handleExportRequest(server, match[1], res);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Post-middleware: catch-all HTML handler (after Vite internals)
|
|
15
118
|
return () => {
|
|
16
119
|
server.middlewares.use((req, res, next) => {
|
|
17
120
|
const url = req.url ?? '';
|
|
@@ -36,18 +139,35 @@ window.__vite_plugin_react_preamble_installed__ = true
|
|
|
36
139
|
<meta charset="UTF-8" />
|
|
37
140
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
38
141
|
<title>pdf-smith Preview</title>
|
|
142
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
143
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
144
|
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght,SOFT,WONK@0,9..144,100..900,0..100,0..1&family=Inter:opsz,wght@14..32,100..900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
39
145
|
<style>
|
|
146
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
147
|
+
body {
|
|
148
|
+
margin: 0;
|
|
149
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
150
|
+
-webkit-font-smoothing: antialiased;
|
|
151
|
+
}
|
|
152
|
+
.pdf-smith-search::placeholder { color: #78716C; }
|
|
40
153
|
@media print {
|
|
41
154
|
[data-pdf-smith-nav] { display: none !important; }
|
|
155
|
+
[data-pdf-smith-toolbar] { display: none !important; }
|
|
156
|
+
[data-pdf-smith-minimap] { display: none !important; }
|
|
42
157
|
[data-pdf-smith-container] {
|
|
43
158
|
margin-left: 0 !important;
|
|
44
159
|
padding: 0 !important;
|
|
160
|
+
padding-top: 0 !important;
|
|
45
161
|
background: none !important;
|
|
46
162
|
}
|
|
47
|
-
[data-pdf-smith-container] >
|
|
163
|
+
[data-pdf-smith-container] > div {
|
|
164
|
+
padding: 0 !important;
|
|
165
|
+
gap: 0 !important;
|
|
166
|
+
}
|
|
167
|
+
[data-pdf-smith-document] > div {
|
|
48
168
|
margin-bottom: 0 !important;
|
|
49
169
|
}
|
|
50
|
-
[data-pdf-smith-
|
|
170
|
+
[data-pdf-smith-document] > div > div:first-child {
|
|
51
171
|
display: none !important;
|
|
52
172
|
}
|
|
53
173
|
}
|