@atom63/resume 0.1.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/chunk-FL25EF7U.js +384 -0
- package/dist/chunk-FL25EF7U.js.map +1 -0
- package/dist/editor/index.css +225 -0
- package/dist/editor/index.css.map +1 -0
- package/dist/editor/index.d.ts +35 -0
- package/dist/editor/index.js +127 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index.css +334 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.js +638 -0
- package/dist/index.js.map +1 -0
- package/dist/vite/index.d.ts +26 -0
- package/dist/vite/index.js +73 -0
- package/dist/vite/index.js.map +1 -0
- package/package.json +108 -0
- package/src/styles/css-modules.d.ts +5 -0
- package/src/styles/document.css +170 -0
- package/src/styles/styles.css +9 -0
- package/src/styles/styles.d.ts +3 -0
- package/src/styles/tokens.css +48 -0
- package/src/styles/tokens.d.ts +3 -0
- package/src/styles/viewer.css +205 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { PAGE, ResumeFontFamilyContext, PaginationReportContext, resumeMdxComponents, WRAPPER_PADDING_X } from './chunk-FL25EF7U.js';
|
|
2
|
+
export { BLOCK_GAP_PX, Columns, Entry, Footer, Group, Header, Main, PAGE, PaginatedResume, PaginationReportContext, ResumeFontFamilyContext, Rule, Section, Sidebar, packIntoPages, resumeMdxComponents, usePaginationReport, useResumeFontFamily } from './chunk-FL25EF7U.js';
|
|
3
|
+
import { MDXProvider } from '@mdx-js/react';
|
|
4
|
+
import clsx from 'clsx';
|
|
5
|
+
import { Maximize2, Minus, Plus, ClipboardCopy } from 'lucide-react';
|
|
6
|
+
import { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
|
|
7
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
function useIsMobile(breakpointPx = 768) {
|
|
10
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const mql = window.matchMedia(`(max-width: ${breakpointPx}px)`);
|
|
13
|
+
const update = () => setIsMobile(mql.matches);
|
|
14
|
+
update();
|
|
15
|
+
mql.addEventListener("change", update);
|
|
16
|
+
return () => mql.removeEventListener("change", update);
|
|
17
|
+
}, [breakpointPx]);
|
|
18
|
+
return isMobile;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/viewer/print.ts
|
|
22
|
+
function createPrintClone(source) {
|
|
23
|
+
const clone = source.cloneNode(true);
|
|
24
|
+
clone.id = "resume-print-clone";
|
|
25
|
+
clone.style.cssText = "background:white;color:black";
|
|
26
|
+
clone.querySelector("[data-resume-measure]")?.remove();
|
|
27
|
+
return clone;
|
|
28
|
+
}
|
|
29
|
+
function hideBodyChildren() {
|
|
30
|
+
const hidden = [];
|
|
31
|
+
const preserveTags = /* @__PURE__ */ new Set(["STYLE", "LINK", "SCRIPT"]);
|
|
32
|
+
for (const child of Array.from(document.body.children)) {
|
|
33
|
+
if (!preserveTags.has(child.tagName)) {
|
|
34
|
+
child.style.setProperty("display", "none", "important");
|
|
35
|
+
hidden.push(child);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return hidden;
|
|
39
|
+
}
|
|
40
|
+
function registerPrintController(opts) {
|
|
41
|
+
let hiddenElements = [];
|
|
42
|
+
let savedTitle = "";
|
|
43
|
+
const beforePrint = () => {
|
|
44
|
+
if (!opts.shouldOwnPrint()) return;
|
|
45
|
+
const target = opts.getTarget();
|
|
46
|
+
if (!target) return;
|
|
47
|
+
savedTitle = document.title;
|
|
48
|
+
document.title = opts.getFilename();
|
|
49
|
+
const clone = createPrintClone(target);
|
|
50
|
+
hiddenElements = hideBodyChildren();
|
|
51
|
+
document.body.appendChild(clone);
|
|
52
|
+
};
|
|
53
|
+
const afterPrint = () => {
|
|
54
|
+
const clone = document.getElementById("resume-print-clone");
|
|
55
|
+
if (clone) clone.remove();
|
|
56
|
+
document.title = savedTitle;
|
|
57
|
+
for (const el of hiddenElements) {
|
|
58
|
+
el.style.removeProperty("display");
|
|
59
|
+
}
|
|
60
|
+
hiddenElements = [];
|
|
61
|
+
opts.onAfterPrint?.();
|
|
62
|
+
};
|
|
63
|
+
window.addEventListener("beforeprint", beforePrint);
|
|
64
|
+
window.addEventListener("afterprint", afterPrint);
|
|
65
|
+
return () => {
|
|
66
|
+
window.removeEventListener("beforeprint", beforePrint);
|
|
67
|
+
window.removeEventListener("afterprint", afterPrint);
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/viewer/viewport.ts
|
|
72
|
+
var INTERACTIVE_PAN_SELECTOR = 'a, button, [role="button"], input, textarea, select';
|
|
73
|
+
var PAN_THRESHOLD_PX = 6;
|
|
74
|
+
function clampZoom(value, min, max) {
|
|
75
|
+
return Math.max(min, Math.min(value, max));
|
|
76
|
+
}
|
|
77
|
+
function isInteractivePanTarget(target) {
|
|
78
|
+
return target instanceof HTMLElement && Boolean(target.closest(INTERACTIVE_PAN_SELECTOR));
|
|
79
|
+
}
|
|
80
|
+
function getTouchDistance(touches) {
|
|
81
|
+
if (touches.length < 2) {
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
const first = touches[0];
|
|
85
|
+
const second = touches[1];
|
|
86
|
+
if (!first || !second) {
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
const deltaX = first.clientX - second.clientX;
|
|
90
|
+
const deltaY = first.clientY - second.clientY;
|
|
91
|
+
return Math.hypot(deltaX, deltaY);
|
|
92
|
+
}
|
|
93
|
+
function clampScrollPosition(element) {
|
|
94
|
+
const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
|
|
95
|
+
const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
|
96
|
+
element.scrollLeft = Math.min(Math.max(0, element.scrollLeft), maxScrollLeft);
|
|
97
|
+
element.scrollTop = Math.min(Math.max(0, element.scrollTop), maxScrollTop);
|
|
98
|
+
}
|
|
99
|
+
function hasExceededPanThreshold(startX, startY, currentX, currentY, threshold = PAN_THRESHOLD_PX) {
|
|
100
|
+
return Math.abs(currentX - startX) > threshold || Math.abs(currentY - startY) > threshold;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/viewer/use-resume-viewport.ts
|
|
104
|
+
var SCALE_STEP = 0.1;
|
|
105
|
+
var MIN_SCALE = 0.25;
|
|
106
|
+
var MAX_SCALE = 2;
|
|
107
|
+
var WHEEL_ZOOM_SENSITIVITY = 0.01;
|
|
108
|
+
var WHEEL_ZOOM_CAP = 0.03;
|
|
109
|
+
var clampResumeZoom = (value) => clampZoom(value, MIN_SCALE, MAX_SCALE);
|
|
110
|
+
function isInputFocused() {
|
|
111
|
+
const tag = document.activeElement?.tagName;
|
|
112
|
+
return tag === "INPUT" || tag === "TEXTAREA";
|
|
113
|
+
}
|
|
114
|
+
function useResumeViewport({
|
|
115
|
+
scrollRef,
|
|
116
|
+
contentRef,
|
|
117
|
+
isMobile,
|
|
118
|
+
isActive,
|
|
119
|
+
pdfFilename,
|
|
120
|
+
onCopy,
|
|
121
|
+
onError
|
|
122
|
+
}) {
|
|
123
|
+
const isActiveRef = useRef(isActive);
|
|
124
|
+
isActiveRef.current = isActive;
|
|
125
|
+
const printRequestedRef = useRef(false);
|
|
126
|
+
const requestPrint = useCallback(() => {
|
|
127
|
+
printRequestedRef.current = true;
|
|
128
|
+
window.print();
|
|
129
|
+
}, []);
|
|
130
|
+
const zoomAnchorRef = useRef(null);
|
|
131
|
+
const [pageCount, setPageCount] = useState(1);
|
|
132
|
+
const [zoom, setZoom] = useState(1);
|
|
133
|
+
const [fitScale, setFitScale] = useState(1);
|
|
134
|
+
const [isFitToWidth, setIsFitToWidth] = useState(true);
|
|
135
|
+
const [, forceRender] = useState(0);
|
|
136
|
+
const dragRef = useRef(null);
|
|
137
|
+
const touchPanRef = useRef(null);
|
|
138
|
+
const pinchRef = useRef(null);
|
|
139
|
+
const effectiveScale = isFitToWidth ? fitScale : zoom;
|
|
140
|
+
const effectiveScaleRef = useRef(effectiveScale);
|
|
141
|
+
const isFitToWidthRef = useRef(isFitToWidth);
|
|
142
|
+
const fitScaleRef = useRef(fitScale);
|
|
143
|
+
effectiveScaleRef.current = effectiveScale;
|
|
144
|
+
isFitToWidthRef.current = isFitToWidth;
|
|
145
|
+
fitScaleRef.current = fitScale;
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (isFitToWidth) setZoom(fitScale);
|
|
148
|
+
}, [isFitToWidth, fitScale]);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const viewport = scrollRef.current;
|
|
151
|
+
if (!viewport) return;
|
|
152
|
+
const updateFitScale = () => {
|
|
153
|
+
const available = Math.max(0, viewport.clientWidth - WRAPPER_PADDING_X);
|
|
154
|
+
setFitScale(clampResumeZoom(Math.min(1, available / PAGE.widthPx)));
|
|
155
|
+
};
|
|
156
|
+
updateFitScale();
|
|
157
|
+
const observer = new ResizeObserver(updateFitScale);
|
|
158
|
+
observer.observe(viewport);
|
|
159
|
+
window.visualViewport?.addEventListener("resize", updateFitScale);
|
|
160
|
+
return () => {
|
|
161
|
+
observer.disconnect();
|
|
162
|
+
window.visualViewport?.removeEventListener("resize", updateFitScale);
|
|
163
|
+
};
|
|
164
|
+
}, [scrollRef]);
|
|
165
|
+
const captureViewportCenter = useCallback(() => {
|
|
166
|
+
const el = scrollRef.current;
|
|
167
|
+
if (!el) return;
|
|
168
|
+
zoomAnchorRef.current = {
|
|
169
|
+
pointX: el.scrollLeft + el.clientWidth / 2,
|
|
170
|
+
pointY: el.scrollTop + el.clientHeight / 2,
|
|
171
|
+
viewportX: el.clientWidth / 2,
|
|
172
|
+
viewportY: el.clientHeight / 2,
|
|
173
|
+
prevScale: effectiveScaleRef.current
|
|
174
|
+
};
|
|
175
|
+
}, [scrollRef]);
|
|
176
|
+
const handleZoomIn = useCallback(() => {
|
|
177
|
+
captureViewportCenter();
|
|
178
|
+
setIsFitToWidth(false);
|
|
179
|
+
setZoom((prev) => clampResumeZoom(prev + SCALE_STEP));
|
|
180
|
+
}, [captureViewportCenter]);
|
|
181
|
+
const handleZoomOut = useCallback(() => {
|
|
182
|
+
captureViewportCenter();
|
|
183
|
+
setIsFitToWidth(false);
|
|
184
|
+
setZoom((prev) => clampResumeZoom(prev - SCALE_STEP));
|
|
185
|
+
}, [captureViewportCenter]);
|
|
186
|
+
const handleActualSize = useCallback(() => {
|
|
187
|
+
captureViewportCenter();
|
|
188
|
+
setIsFitToWidth(false);
|
|
189
|
+
setZoom(1);
|
|
190
|
+
}, [captureViewportCenter]);
|
|
191
|
+
const handleFitToWidth = useCallback(() => {
|
|
192
|
+
setIsFitToWidth(true);
|
|
193
|
+
}, []);
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const el = scrollRef.current;
|
|
196
|
+
if (!el) return;
|
|
197
|
+
let pendingDelta = 0;
|
|
198
|
+
let rafId = 0;
|
|
199
|
+
let lastCursorX = 0;
|
|
200
|
+
let lastCursorY = 0;
|
|
201
|
+
const flush = () => {
|
|
202
|
+
rafId = 0;
|
|
203
|
+
const delta = pendingDelta;
|
|
204
|
+
pendingDelta = 0;
|
|
205
|
+
const cursorX = lastCursorX;
|
|
206
|
+
const cursorY = lastCursorY;
|
|
207
|
+
setZoom((prev) => {
|
|
208
|
+
const next = clampResumeZoom(prev + delta);
|
|
209
|
+
if (next === prev) return prev;
|
|
210
|
+
zoomAnchorRef.current = {
|
|
211
|
+
pointX: el.scrollLeft + cursorX,
|
|
212
|
+
pointY: el.scrollTop + cursorY,
|
|
213
|
+
viewportX: cursorX,
|
|
214
|
+
viewportY: cursorY,
|
|
215
|
+
prevScale: isFitToWidthRef.current ? fitScaleRef.current : prev
|
|
216
|
+
};
|
|
217
|
+
return next;
|
|
218
|
+
});
|
|
219
|
+
setIsFitToWidth(false);
|
|
220
|
+
};
|
|
221
|
+
const onWheel = (e) => {
|
|
222
|
+
if (!e.ctrlKey) return;
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
pendingDelta = Math.max(
|
|
225
|
+
-WHEEL_ZOOM_CAP,
|
|
226
|
+
Math.min(pendingDelta + -e.deltaY * WHEEL_ZOOM_SENSITIVITY, WHEEL_ZOOM_CAP)
|
|
227
|
+
);
|
|
228
|
+
const rect = el.getBoundingClientRect();
|
|
229
|
+
lastCursorX = e.clientX - rect.left;
|
|
230
|
+
lastCursorY = e.clientY - rect.top;
|
|
231
|
+
if (rafId === 0) {
|
|
232
|
+
rafId = requestAnimationFrame(flush);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
236
|
+
return () => {
|
|
237
|
+
el.removeEventListener("wheel", onWheel);
|
|
238
|
+
if (rafId !== 0) cancelAnimationFrame(rafId);
|
|
239
|
+
};
|
|
240
|
+
}, [scrollRef]);
|
|
241
|
+
useLayoutEffect(() => {
|
|
242
|
+
const anchor = zoomAnchorRef.current;
|
|
243
|
+
if (!anchor) return;
|
|
244
|
+
const el = scrollRef.current;
|
|
245
|
+
if (!el) return;
|
|
246
|
+
const ratio = effectiveScale / anchor.prevScale;
|
|
247
|
+
el.scrollLeft = anchor.pointX * ratio - anchor.viewportX;
|
|
248
|
+
el.scrollTop = anchor.pointY * ratio - anchor.viewportY;
|
|
249
|
+
zoomAnchorRef.current = null;
|
|
250
|
+
}, [effectiveScale, scrollRef]);
|
|
251
|
+
useEffect(
|
|
252
|
+
() => registerPrintController({
|
|
253
|
+
getTarget: () => contentRef.current,
|
|
254
|
+
getFilename: () => pdfFilename ?? "resume",
|
|
255
|
+
shouldOwnPrint: () => printRequestedRef.current || isActiveRef.current,
|
|
256
|
+
onAfterPrint: () => {
|
|
257
|
+
printRequestedRef.current = false;
|
|
258
|
+
forceRender((n) => n + 1);
|
|
259
|
+
}
|
|
260
|
+
}),
|
|
261
|
+
[pdfFilename, contentRef]
|
|
262
|
+
);
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const keyActions = {
|
|
265
|
+
"+": handleZoomIn,
|
|
266
|
+
"=": handleZoomIn,
|
|
267
|
+
"-": handleZoomOut,
|
|
268
|
+
_: handleZoomOut
|
|
269
|
+
};
|
|
270
|
+
const modKeyActions = {
|
|
271
|
+
"0": handleFitToWidth,
|
|
272
|
+
"1": handleActualSize,
|
|
273
|
+
p: requestPrint
|
|
274
|
+
};
|
|
275
|
+
const handleKeyDown = (e) => {
|
|
276
|
+
if (!isActiveRef.current || isInputFocused()) return;
|
|
277
|
+
const mod = e.ctrlKey || e.metaKey;
|
|
278
|
+
const action = mod ? modKeyActions[e.key] : keyActions[e.key];
|
|
279
|
+
if (action) {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
action();
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
globalThis.addEventListener("keydown", handleKeyDown);
|
|
285
|
+
return () => globalThis.removeEventListener("keydown", handleKeyDown);
|
|
286
|
+
}, [handleZoomIn, handleZoomOut, handleFitToWidth, handleActualSize, requestPrint]);
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
const el = scrollRef.current;
|
|
289
|
+
if (!el) return;
|
|
290
|
+
const setPanActive = (active) => {
|
|
291
|
+
if (isMobile) {
|
|
292
|
+
el.dataset.panActive = active ? "" : void 0;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const moveDrag = (clientX, clientY) => {
|
|
296
|
+
const drag = dragRef.current;
|
|
297
|
+
if (!drag) return;
|
|
298
|
+
el.scrollLeft = drag.scrollX - (clientX - drag.startX);
|
|
299
|
+
el.scrollTop = drag.scrollY - (clientY - drag.startY);
|
|
300
|
+
clampScrollPosition(el);
|
|
301
|
+
};
|
|
302
|
+
const beginDrag = (clientX, clientY) => {
|
|
303
|
+
dragRef.current = {
|
|
304
|
+
startX: clientX,
|
|
305
|
+
startY: clientY,
|
|
306
|
+
scrollX: el.scrollLeft,
|
|
307
|
+
scrollY: el.scrollTop
|
|
308
|
+
};
|
|
309
|
+
setPanActive(true);
|
|
310
|
+
if (!isMobile) {
|
|
311
|
+
el.classList.replace("resume-viewer-scroll-grab", "resume-viewer-scroll-grabbing");
|
|
312
|
+
el.classList.replace("cursor-grab", "cursor-grabbing");
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const endDrag = () => {
|
|
316
|
+
touchPanRef.current = null;
|
|
317
|
+
pinchRef.current = null;
|
|
318
|
+
if (!dragRef.current) return;
|
|
319
|
+
dragRef.current = null;
|
|
320
|
+
setPanActive(false);
|
|
321
|
+
if (!isMobile) {
|
|
322
|
+
el.classList.replace("resume-viewer-scroll-grabbing", "resume-viewer-scroll-grab");
|
|
323
|
+
el.classList.replace("cursor-grabbing", "cursor-grab");
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const captureZoomAnchor = (clientX, clientY) => {
|
|
327
|
+
const rect = el.getBoundingClientRect();
|
|
328
|
+
zoomAnchorRef.current = {
|
|
329
|
+
pointX: el.scrollLeft + clientX - rect.left,
|
|
330
|
+
pointY: el.scrollTop + clientY - rect.top,
|
|
331
|
+
viewportX: clientX - rect.left,
|
|
332
|
+
viewportY: clientY - rect.top,
|
|
333
|
+
prevScale: effectiveScaleRef.current
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
const onMouseDown = (e) => {
|
|
337
|
+
if (isMobile || e.button !== 0) return;
|
|
338
|
+
if (isInteractivePanTarget(e.target)) return;
|
|
339
|
+
beginDrag(e.clientX, e.clientY);
|
|
340
|
+
e.preventDefault();
|
|
341
|
+
};
|
|
342
|
+
const onMouseMove = (e) => {
|
|
343
|
+
if (isMobile) return;
|
|
344
|
+
moveDrag(e.clientX, e.clientY);
|
|
345
|
+
};
|
|
346
|
+
const onTouchStart = (e) => {
|
|
347
|
+
if (!isMobile) return;
|
|
348
|
+
if (e.touches.length === 2) {
|
|
349
|
+
touchPanRef.current = null;
|
|
350
|
+
dragRef.current = null;
|
|
351
|
+
const distance = getTouchDistance(e.touches);
|
|
352
|
+
if (distance <= 0) return;
|
|
353
|
+
pinchRef.current = {
|
|
354
|
+
startDistance: distance,
|
|
355
|
+
startScale: effectiveScaleRef.current
|
|
356
|
+
};
|
|
357
|
+
setIsFitToWidth(false);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (e.touches.length !== 1 || pinchRef.current) return;
|
|
361
|
+
if (isInteractivePanTarget(e.target)) return;
|
|
362
|
+
const touch = e.touches[0];
|
|
363
|
+
if (!touch) return;
|
|
364
|
+
touchPanRef.current = {
|
|
365
|
+
startX: touch.clientX,
|
|
366
|
+
startY: touch.clientY,
|
|
367
|
+
scrollX: el.scrollLeft,
|
|
368
|
+
scrollY: el.scrollTop
|
|
369
|
+
};
|
|
370
|
+
};
|
|
371
|
+
const onTouchMove = (e) => {
|
|
372
|
+
if (!isMobile) return;
|
|
373
|
+
if (pinchRef.current && e.touches.length === 2) {
|
|
374
|
+
const distance = getTouchDistance(e.touches);
|
|
375
|
+
if (distance <= 0 || pinchRef.current.startDistance <= 0) return;
|
|
376
|
+
const first = e.touches[0];
|
|
377
|
+
const second = e.touches[1];
|
|
378
|
+
if (!first || !second) return;
|
|
379
|
+
const centerX = (first.clientX + second.clientX) / 2;
|
|
380
|
+
const centerY = (first.clientY + second.clientY) / 2;
|
|
381
|
+
const nextScale = clampResumeZoom(
|
|
382
|
+
pinchRef.current.startScale * (distance / pinchRef.current.startDistance)
|
|
383
|
+
);
|
|
384
|
+
captureZoomAnchor(centerX, centerY);
|
|
385
|
+
setZoom(nextScale);
|
|
386
|
+
setIsFitToWidth(false);
|
|
387
|
+
e.preventDefault();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const pendingPan = touchPanRef.current;
|
|
391
|
+
const touch = e.touches[0];
|
|
392
|
+
if (!pendingPan || !touch) return;
|
|
393
|
+
if (!dragRef.current) {
|
|
394
|
+
if (!hasExceededPanThreshold(
|
|
395
|
+
pendingPan.startX,
|
|
396
|
+
pendingPan.startY,
|
|
397
|
+
touch.clientX,
|
|
398
|
+
touch.clientY
|
|
399
|
+
)) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
dragRef.current = {
|
|
403
|
+
startX: pendingPan.startX,
|
|
404
|
+
startY: pendingPan.startY,
|
|
405
|
+
scrollX: pendingPan.scrollX,
|
|
406
|
+
scrollY: pendingPan.scrollY
|
|
407
|
+
};
|
|
408
|
+
setPanActive(true);
|
|
409
|
+
}
|
|
410
|
+
moveDrag(touch.clientX, touch.clientY);
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
};
|
|
413
|
+
el.addEventListener("mousedown", onMouseDown);
|
|
414
|
+
window.addEventListener("mousemove", onMouseMove);
|
|
415
|
+
window.addEventListener("mouseup", endDrag);
|
|
416
|
+
if (isMobile) {
|
|
417
|
+
el.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
418
|
+
el.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
419
|
+
el.addEventListener("touchend", endDrag);
|
|
420
|
+
el.addEventListener("touchcancel", endDrag);
|
|
421
|
+
}
|
|
422
|
+
return () => {
|
|
423
|
+
el.removeEventListener("mousedown", onMouseDown);
|
|
424
|
+
window.removeEventListener("mousemove", onMouseMove);
|
|
425
|
+
window.removeEventListener("mouseup", endDrag);
|
|
426
|
+
delete el.dataset.panActive;
|
|
427
|
+
if (isMobile) {
|
|
428
|
+
el.removeEventListener("touchstart", onTouchStart);
|
|
429
|
+
el.removeEventListener("touchmove", onTouchMove);
|
|
430
|
+
el.removeEventListener("touchend", endDrag);
|
|
431
|
+
el.removeEventListener("touchcancel", endDrag);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}, [isMobile, scrollRef]);
|
|
435
|
+
const handleCopyText = useCallback(async () => {
|
|
436
|
+
const content = contentRef.current;
|
|
437
|
+
if (!content) return;
|
|
438
|
+
try {
|
|
439
|
+
await navigator.clipboard.writeText(content.innerText);
|
|
440
|
+
onCopy?.();
|
|
441
|
+
} catch (error) {
|
|
442
|
+
onError?.(error);
|
|
443
|
+
}
|
|
444
|
+
}, [contentRef, onCopy, onError]);
|
|
445
|
+
const totalHeight = pageCount * PAGE.heightPx + Math.max(0, pageCount - 1) * PAGE.gapPx;
|
|
446
|
+
return {
|
|
447
|
+
pageCount,
|
|
448
|
+
setPageCount,
|
|
449
|
+
effectiveScale,
|
|
450
|
+
isFitToWidth,
|
|
451
|
+
totalHeight,
|
|
452
|
+
handleZoomIn,
|
|
453
|
+
handleZoomOut,
|
|
454
|
+
handleActualSize,
|
|
455
|
+
handleFitToWidth,
|
|
456
|
+
requestPrint,
|
|
457
|
+
handleCopyText,
|
|
458
|
+
minScale: MIN_SCALE,
|
|
459
|
+
maxScale: MAX_SCALE
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function ResumeViewer({
|
|
463
|
+
Content,
|
|
464
|
+
components,
|
|
465
|
+
fontFamily,
|
|
466
|
+
isActive = true,
|
|
467
|
+
pdfFilename,
|
|
468
|
+
onCopy,
|
|
469
|
+
onError,
|
|
470
|
+
className,
|
|
471
|
+
toolbarStart,
|
|
472
|
+
toolbarEnd
|
|
473
|
+
}) {
|
|
474
|
+
const isMobile = useIsMobile();
|
|
475
|
+
const scrollRef = useRef(null);
|
|
476
|
+
const contentRef = useRef(null);
|
|
477
|
+
const {
|
|
478
|
+
pageCount,
|
|
479
|
+
setPageCount,
|
|
480
|
+
effectiveScale,
|
|
481
|
+
isFitToWidth,
|
|
482
|
+
totalHeight,
|
|
483
|
+
handleZoomIn,
|
|
484
|
+
handleZoomOut,
|
|
485
|
+
handleActualSize,
|
|
486
|
+
handleFitToWidth,
|
|
487
|
+
requestPrint,
|
|
488
|
+
handleCopyText,
|
|
489
|
+
minScale,
|
|
490
|
+
maxScale
|
|
491
|
+
} = useResumeViewport({
|
|
492
|
+
scrollRef,
|
|
493
|
+
contentRef,
|
|
494
|
+
isMobile,
|
|
495
|
+
isActive,
|
|
496
|
+
pdfFilename,
|
|
497
|
+
onCopy,
|
|
498
|
+
onError
|
|
499
|
+
});
|
|
500
|
+
return /* @__PURE__ */ jsxs("div", { className: "resume-viewer", children: [
|
|
501
|
+
/* @__PURE__ */ jsxs("div", { className: "resume-viewer-toolbar", children: [
|
|
502
|
+
toolbarStart,
|
|
503
|
+
/* @__PURE__ */ jsxs("span", { className: "resume-viewer-pagecount", children: [
|
|
504
|
+
pageCount,
|
|
505
|
+
" page",
|
|
506
|
+
pageCount > 1 ? "s" : ""
|
|
507
|
+
] }),
|
|
508
|
+
/* @__PURE__ */ jsxs("div", { className: "resume-viewer-controls", children: [
|
|
509
|
+
/* @__PURE__ */ jsx(
|
|
510
|
+
"button",
|
|
511
|
+
{
|
|
512
|
+
"aria-label": "Fit resume to width",
|
|
513
|
+
"aria-pressed": isFitToWidth,
|
|
514
|
+
className: clsx("resume-viewer-btn", isFitToWidth && "resume-viewer-btn-active"),
|
|
515
|
+
onClick: handleFitToWidth,
|
|
516
|
+
title: "Fit to width",
|
|
517
|
+
type: "button",
|
|
518
|
+
children: /* @__PURE__ */ jsx(Maximize2, { className: "resume-viewer-icon" })
|
|
519
|
+
}
|
|
520
|
+
),
|
|
521
|
+
/* @__PURE__ */ jsxs("div", { className: "resume-viewer-btn-group", children: [
|
|
522
|
+
/* @__PURE__ */ jsx(
|
|
523
|
+
"button",
|
|
524
|
+
{
|
|
525
|
+
"aria-label": "Zoom out",
|
|
526
|
+
className: "resume-viewer-btn",
|
|
527
|
+
disabled: !isFitToWidth && effectiveScale <= minScale,
|
|
528
|
+
onClick: handleZoomOut,
|
|
529
|
+
title: "Zoom out",
|
|
530
|
+
type: "button",
|
|
531
|
+
children: /* @__PURE__ */ jsx(Minus, { className: "resume-viewer-icon" })
|
|
532
|
+
}
|
|
533
|
+
),
|
|
534
|
+
/* @__PURE__ */ jsxs(
|
|
535
|
+
"button",
|
|
536
|
+
{
|
|
537
|
+
className: "resume-viewer-zoom-label",
|
|
538
|
+
onClick: handleActualSize,
|
|
539
|
+
title: "Actual size (100%)",
|
|
540
|
+
type: "button",
|
|
541
|
+
children: [
|
|
542
|
+
Math.round(effectiveScale * 100),
|
|
543
|
+
"%"
|
|
544
|
+
]
|
|
545
|
+
}
|
|
546
|
+
),
|
|
547
|
+
/* @__PURE__ */ jsx(
|
|
548
|
+
"button",
|
|
549
|
+
{
|
|
550
|
+
"aria-label": "Zoom in",
|
|
551
|
+
className: "resume-viewer-btn",
|
|
552
|
+
disabled: effectiveScale >= maxScale,
|
|
553
|
+
onClick: handleZoomIn,
|
|
554
|
+
title: "Zoom in",
|
|
555
|
+
type: "button",
|
|
556
|
+
children: /* @__PURE__ */ jsx(Plus, { className: "resume-viewer-icon" })
|
|
557
|
+
}
|
|
558
|
+
)
|
|
559
|
+
] }),
|
|
560
|
+
/* @__PURE__ */ jsx(
|
|
561
|
+
"button",
|
|
562
|
+
{
|
|
563
|
+
"aria-label": "Copy resume text",
|
|
564
|
+
className: "resume-viewer-btn",
|
|
565
|
+
onClick: handleCopyText,
|
|
566
|
+
title: "Copy resume text",
|
|
567
|
+
type: "button",
|
|
568
|
+
children: /* @__PURE__ */ jsx(ClipboardCopy, { className: "resume-viewer-icon" })
|
|
569
|
+
}
|
|
570
|
+
),
|
|
571
|
+
/* @__PURE__ */ jsx(
|
|
572
|
+
"button",
|
|
573
|
+
{
|
|
574
|
+
className: "resume-viewer-btn resume-viewer-btn-text",
|
|
575
|
+
onClick: requestPrint,
|
|
576
|
+
type: "button",
|
|
577
|
+
children: "Save to PDF"
|
|
578
|
+
}
|
|
579
|
+
),
|
|
580
|
+
toolbarEnd
|
|
581
|
+
] })
|
|
582
|
+
] }),
|
|
583
|
+
/* @__PURE__ */ jsx(
|
|
584
|
+
"div",
|
|
585
|
+
{
|
|
586
|
+
className: clsx(
|
|
587
|
+
"resume-viewer-scroll",
|
|
588
|
+
isMobile ? "resume-viewer-scroll-mobile" : "resume-viewer-scroll-grab",
|
|
589
|
+
className
|
|
590
|
+
),
|
|
591
|
+
ref: scrollRef,
|
|
592
|
+
children: /* @__PURE__ */ jsx(
|
|
593
|
+
"section",
|
|
594
|
+
{
|
|
595
|
+
"aria-label": isMobile ? "Resume preview. Drag with one finger to pan, pinch to zoom." : "Resume preview. Drag to pan, Ctrl+scroll to zoom.",
|
|
596
|
+
className: "resume-viewer-stage",
|
|
597
|
+
children: /* @__PURE__ */ jsx(
|
|
598
|
+
"div",
|
|
599
|
+
{
|
|
600
|
+
className: "resume-viewer-page-box",
|
|
601
|
+
style: {
|
|
602
|
+
width: PAGE.widthPx * effectiveScale,
|
|
603
|
+
height: totalHeight * effectiveScale
|
|
604
|
+
},
|
|
605
|
+
children: /* @__PURE__ */ jsx(
|
|
606
|
+
"div",
|
|
607
|
+
{
|
|
608
|
+
className: "resume-viewer-scale-root",
|
|
609
|
+
"data-resume-scale-root": "",
|
|
610
|
+
style: {
|
|
611
|
+
width: PAGE.widthPx,
|
|
612
|
+
height: totalHeight,
|
|
613
|
+
transform: effectiveScale === 1 ? void 0 : `scale(${effectiveScale})`
|
|
614
|
+
},
|
|
615
|
+
children: /* @__PURE__ */ jsx(ResumeFontFamilyContext.Provider, { value: fontFamily, children: /* @__PURE__ */ jsx(PaginationReportContext.Provider, { value: setPageCount, children: /* @__PURE__ */ jsx(MDXProvider, { components: components ?? resumeMdxComponents, children: /* @__PURE__ */ jsx(
|
|
616
|
+
"div",
|
|
617
|
+
{
|
|
618
|
+
className: "resume-viewer-content",
|
|
619
|
+
id: "resume-print-target",
|
|
620
|
+
ref: contentRef,
|
|
621
|
+
style: { zIndex: 1 },
|
|
622
|
+
children: /* @__PURE__ */ jsx(Content, {})
|
|
623
|
+
}
|
|
624
|
+
) }) }) })
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
)
|
|
633
|
+
] });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export { MAX_SCALE, MIN_SCALE, ResumeViewer, SCALE_STEP, useIsMobile as useResumeIsMobile, useResumeViewport };
|
|
637
|
+
//# sourceMappingURL=index.js.map
|
|
638
|
+
//# sourceMappingURL=index.js.map
|