hocmai-feedback-widget 0.2.0 → 0.2.2
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/README.md +132 -97
- package/dist/capture/device-detection.d.ts +13 -0
- package/dist/capture/fonts.d.ts +4 -0
- package/dist/capture/index.d.ts +14 -0
- package/dist/capture/mathjax-fonts.d.ts +5 -0
- package/dist/capture/types.d.ts +25 -0
- package/dist/ckeditor.mjs +113 -0
- package/dist/ckeditor.mjs.map +1 -0
- package/dist/components/CkEditor.d.ts +14 -0
- package/dist/components/FeedbackWidget.d.ts +11 -0
- package/dist/context/FeedbackContext.d.ts +5 -0
- package/dist/hocmai-feedback-widget.mjs +7544 -0
- package/dist/hocmai-feedback-widget.mjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/package.json +32 -28
- package/src/App.tsx +143 -58
- package/src/capture/device-detection.ts +52 -0
- package/src/capture/fonts.ts +34 -0
- package/src/capture/index.ts +362 -0
- package/src/capture/mathjax-fonts.ts +143 -0
- package/src/capture/types.ts +34 -0
- package/src/ckeditor.d.ts +14 -0
- package/src/components/CkEditor.tsx +130 -0
- package/src/components/CropOverlay.tsx +29 -47
- package/src/components/FeedbackWidget.tsx +96 -90
- package/src/context/FeedbackContext.tsx +11 -4
- package/src/index.ts +9 -0
- package/src/main.tsx +7 -8
- package/src/utils/device.ts +14 -14
- package/dist/support-feedback-lib.es.js +0 -3867
- package/dist/support-feedback-lib.umd.js +0 -84
- package/dist/utils/screenshot/background-renderer.d.ts +0 -3
- package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
- package/dist/utils/screenshot/cleanup.d.ts +0 -3
- package/dist/utils/screenshot/device-detection.d.ts +0 -2
- package/dist/utils/screenshot/element-collector.d.ts +0 -10
- package/dist/utils/screenshot/image-renderer.d.ts +0 -21
- package/dist/utils/screenshot/performance.d.ts +0 -3
- package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
- package/dist/utils/screenshot/text-renderer.d.ts +0 -23
- package/dist/utils/screenshot/types.d.ts +0 -35
- package/dist/utils/screenshot.d.ts +0 -9
- package/dist/vite.svg +0 -1
- package/src/App.css +0 -42
- package/src/assets/react.svg +0 -1
- package/src/index.css +0 -68
- package/src/utils/screenshot/background-renderer.ts +0 -54
- package/src/utils/screenshot/canvas-renderer.ts +0 -217
- package/src/utils/screenshot/cleanup.ts +0 -32
- package/src/utils/screenshot/device-detection.ts +0 -38
- package/src/utils/screenshot/element-collector.ts +0 -83
- package/src/utils/screenshot/image-renderer.ts +0 -148
- package/src/utils/screenshot/performance.ts +0 -57
- package/src/utils/screenshot/svg-renderer.ts +0 -126
- package/src/utils/screenshot/text-renderer.ts +0 -212
- package/src/utils/screenshot/types.ts +0 -40
- package/src/utils/screenshot.ts +0 -68
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
export { default as FeedbackWidget } from './components/FeedbackWidget';
|
|
2
2
|
export type { FeedbackData, FeedbackProps } from './components/FeedbackWidget';
|
|
3
|
+
export type { CkEditorProps } from './components/CkEditor';
|
|
3
4
|
export { FeedbackProvider, useFeedback } from './context/FeedbackContext';
|
|
5
|
+
export { captureScreenshot, captureScreenshotWithOptions, prewarmCapture, } from './capture';
|
|
6
|
+
export type { Rect, CaptureProgress, CaptureOptions } from './capture/types';
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hocmai-feedback-widget",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
"module": "./dist/
|
|
6
|
+
"description": "Feedback/report widget with MathJax-aware screenshot capture (snapdom + embedded fonts).",
|
|
7
|
+
"module": "./dist/hocmai-feedback-widget.mjs",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
@@ -13,44 +13,48 @@
|
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
|
-
"import": "./dist/
|
|
17
|
-
|
|
16
|
+
"import": "./dist/hocmai-feedback-widget.mjs"
|
|
17
|
+
},
|
|
18
|
+
"./ckeditor": {
|
|
19
|
+
"types": "./dist/components/CkEditor.d.ts",
|
|
20
|
+
"import": "./dist/ckeditor.mjs"
|
|
18
21
|
}
|
|
19
22
|
},
|
|
20
23
|
"scripts": {
|
|
21
24
|
"dev": "vite",
|
|
22
|
-
"build": "
|
|
23
|
-
"
|
|
24
|
-
"preview": "vite preview"
|
|
25
|
+
"build": "vite build",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"preview": "vite preview",
|
|
28
|
+
"test": "playwright test"
|
|
25
29
|
},
|
|
26
30
|
"dependencies": {
|
|
31
|
+
"@zumer/snapdom": "^2.12.9",
|
|
27
32
|
"lucide-react": "^0.562.0",
|
|
28
33
|
"modern-screenshot": "^4.6.7",
|
|
29
34
|
"ua-parser-js": "^2.0.7"
|
|
30
35
|
},
|
|
31
36
|
"peerDependencies": {
|
|
32
37
|
"react": ">=16.8.0",
|
|
33
|
-
"react-dom": ">=16.8.0"
|
|
38
|
+
"react-dom": ">=16.8.0",
|
|
39
|
+
"@ckeditor/ckeditor5-react": "*",
|
|
40
|
+
"hocmai-ckeditor-custom": "*",
|
|
41
|
+
"ckeditor5-classic-with-mathtype": "*"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@ckeditor/ckeditor5-react": { "optional": true },
|
|
45
|
+
"hocmai-ckeditor-custom": { "optional": true },
|
|
46
|
+
"ckeditor5-classic-with-mathtype": { "optional": true }
|
|
34
47
|
},
|
|
35
48
|
"devDependencies": {
|
|
36
|
-
"@
|
|
37
|
-
"@
|
|
38
|
-
"@types/
|
|
39
|
-
"@types/react": "^
|
|
40
|
-
"@
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"globals": "^16.5.0",
|
|
47
|
-
"http-server": "^14.1.1",
|
|
48
|
-
"patch-package": "^8.0.1",
|
|
49
|
-
"react": "^19.2.0",
|
|
50
|
-
"react-dom": "^19.2.0",
|
|
51
|
-
"typescript": "~5.9.3",
|
|
52
|
-
"typescript-eslint": "^8.46.4",
|
|
53
|
-
"vite": "^7.2.4",
|
|
54
|
-
"vite-plugin-dts": "^4.5.4"
|
|
49
|
+
"@playwright/test": "^1.48.0",
|
|
50
|
+
"@types/node": "^22.0.0",
|
|
51
|
+
"@types/react": "^18.3.0",
|
|
52
|
+
"@types/react-dom": "^18.3.0",
|
|
53
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
54
|
+
"react": "^18.3.1",
|
|
55
|
+
"react-dom": "^18.3.1",
|
|
56
|
+
"typescript": "~5.6.0",
|
|
57
|
+
"vite": "^5.4.0",
|
|
58
|
+
"vite-plugin-dts": "^4.2.0"
|
|
55
59
|
}
|
|
56
60
|
}
|
package/src/App.tsx
CHANGED
|
@@ -1,75 +1,160 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { FeedbackProvider, useFeedback } from './
|
|
3
|
-
import { type FeedbackData } from './components/FeedbackWidget';
|
|
4
|
-
import './App.css'
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FeedbackProvider, useFeedback, captureScreenshot, type FeedbackData } from './index';
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
declare global {
|
|
5
|
+
interface Window {
|
|
6
|
+
MathJax?: {
|
|
7
|
+
typesetPromise?: (els?: Element[]) => Promise<void>;
|
|
8
|
+
startup?: { promise?: Promise<unknown> };
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
}
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// A pile of formula-heavy questions, mirroring the host app's MathJax CHTML usage.
|
|
14
|
+
const FORMULAS = [
|
|
15
|
+
String.raw`\(\displaystyle \int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}\)`,
|
|
16
|
+
String.raw`\(\displaystyle \sum_{n=1}^{\infty}\frac{1}{n^2} = \frac{\pi^2}{6}\)`,
|
|
17
|
+
String.raw`\(\displaystyle \frac{d}{dx}\left[\frac{\sin x}{1+\cos x}\right]\)`,
|
|
18
|
+
String.raw`\(\displaystyle x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}\)`,
|
|
19
|
+
String.raw`\(\displaystyle \lim_{x\to 0}\frac{\sin x}{x} = 1\)`,
|
|
20
|
+
String.raw`\(\displaystyle \binom{n}{k} = \frac{n!}{k!(n-k)!}\)`,
|
|
21
|
+
String.raw`\(\displaystyle \begin{pmatrix} a & b \\ c & d \end{pmatrix}^{-1}\)`,
|
|
22
|
+
String.raw`\(\displaystyle \sqrt[3]{\frac{a^2+b^2}{c^2}}\)`,
|
|
23
|
+
];
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
>
|
|
21
|
-
{selected ? 'Selected' : 'Select'}
|
|
22
|
-
</button>
|
|
25
|
+
const Question: React.FC<{ id: number }> = ({ id }) => {
|
|
26
|
+
const { openFeedback } = useFeedback();
|
|
27
|
+
const f1 = FORMULAS[id % FORMULAS.length];
|
|
28
|
+
const f2 = FORMULAS[(id + 3) % FORMULAS.length];
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
data-question-id={id}
|
|
33
|
+
style={{
|
|
34
|
+
border: '1px solid #e5e7eb',
|
|
35
|
+
borderRadius: 8,
|
|
36
|
+
padding: 16,
|
|
37
|
+
margin: '12px 0',
|
|
38
|
+
background: '#fff',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
42
|
+
<strong>Câu {id}</strong>
|
|
43
|
+
<button onClick={() => openFeedback(id)}>Báo lỗi</button>
|
|
44
|
+
</div>
|
|
45
|
+
<p
|
|
46
|
+
dangerouslySetInnerHTML={{
|
|
47
|
+
__html: `Cho biểu thức ${f1}. Tính giá trị của ${f2} và chứng minh kết quả.`,
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
51
|
+
{['A', 'B', 'C', 'D'].map((opt, i) => (
|
|
52
|
+
<label key={opt} style={{ display: 'flex', gap: 8 }}>
|
|
53
|
+
<input type="radio" name={`q-${id}`} />
|
|
54
|
+
<span
|
|
55
|
+
dangerouslySetInnerHTML={{
|
|
56
|
+
__html: `${opt}. ${FORMULAS[(id + i) % FORMULAS.length]}`,
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</label>
|
|
60
|
+
))}
|
|
30
61
|
</div>
|
|
31
62
|
</div>
|
|
32
63
|
);
|
|
33
64
|
};
|
|
34
65
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
// Mirror the host app's fixed full-screen watermark overlay (position:fixed; inset:0; z-index:10).
|
|
67
|
+
const Watermark: React.FC = () => {
|
|
68
|
+
const cols = 4;
|
|
69
|
+
const rows = 6;
|
|
70
|
+
const items = [];
|
|
71
|
+
for (let r = 0; r < rows; r++) {
|
|
72
|
+
for (let c = 0; c < cols; c++) {
|
|
73
|
+
items.push(
|
|
74
|
+
<div
|
|
75
|
+
key={`wm-${r}-${c}`}
|
|
76
|
+
style={{
|
|
77
|
+
position: 'absolute',
|
|
78
|
+
left: `${(c / cols) * 100 + 100 / cols / 2}%`,
|
|
79
|
+
top: `${(r / rows) * 100 + 100 / rows / 2}%`,
|
|
80
|
+
transform: 'translate(-50%, -50%) rotate(-45deg)',
|
|
81
|
+
fontSize: '20px',
|
|
82
|
+
fontWeight: 700,
|
|
83
|
+
color: 'rgba(0,0,0,0.06)',
|
|
84
|
+
whiteSpace: 'nowrap',
|
|
85
|
+
pointerEvents: 'none',
|
|
86
|
+
letterSpacing: '4px',
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
HOCMAI.VN
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
38
94
|
return (
|
|
39
|
-
<div style={{
|
|
40
|
-
|
|
41
|
-
<p style={{ marginBottom: '2rem' }}>
|
|
42
|
-
Support Package integrated via <code>FeedbackProvider</code>.
|
|
43
|
-
</p>
|
|
44
|
-
|
|
45
|
-
<img
|
|
46
|
-
style={{ width: '48px', height: '48px', borderRadius: '50%', background: '#1d4ed8', marginLeft: '8px', objectFit: 'cover', }}
|
|
47
|
-
src='https://cdn2.thecatapi.com/images/MTY3ODIyMQ.jpg'
|
|
48
|
-
/>
|
|
49
|
-
<QuestionItem id="101" content="What is the capital of Vietnam?" />
|
|
50
|
-
<QuestionItem id="102" content="Calculate the area of a circle with radius 5." />
|
|
51
|
-
<QuestionItem id="103" content="Who wrote 'Truyen Kieu'?" />
|
|
52
|
-
|
|
53
|
-
<div style={{ marginTop: '40px', padding: '20px', background: '#f9fafb', borderRadius: '8px' }}>
|
|
54
|
-
<button onClick={() => setCount(c => c + 1)}>Global Counter: {count}</button>
|
|
55
|
-
</div>
|
|
95
|
+
<div style={{ position: 'fixed', inset: 0, zIndex: 10, overflow: 'hidden', pointerEvents: 'none' }} aria-hidden>
|
|
96
|
+
{items}
|
|
56
97
|
</div>
|
|
57
|
-
)
|
|
58
|
-
}
|
|
98
|
+
);
|
|
99
|
+
};
|
|
59
100
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
101
|
+
const COUNT = 60;
|
|
102
|
+
|
|
103
|
+
const App: React.FC = () => {
|
|
104
|
+
const handleSubmit = async (data: FeedbackData) => {
|
|
105
|
+
console.log('Feedback submitted (dev):', data);
|
|
64
106
|
};
|
|
65
107
|
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
// Typeset all the math, then flag readiness for the Playwright test.
|
|
110
|
+
const run = async () => {
|
|
111
|
+
if (window.MathJax?.startup?.promise) {
|
|
112
|
+
await window.MathJax.startup.promise;
|
|
113
|
+
}
|
|
114
|
+
if (window.MathJax?.typesetPromise) {
|
|
115
|
+
await window.MathJax.typesetPromise();
|
|
116
|
+
}
|
|
117
|
+
// Expose for the Playwright capture test.
|
|
118
|
+
(window as unknown as { __captureScreenshot?: typeof captureScreenshot }).__captureScreenshot =
|
|
119
|
+
captureScreenshot;
|
|
120
|
+
document.body.setAttribute('data-math-ready', 'true');
|
|
121
|
+
};
|
|
122
|
+
run();
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
66
125
|
return (
|
|
67
|
-
<FeedbackProvider onSubmit={
|
|
68
|
-
|
|
126
|
+
<FeedbackProvider onSubmit={handleSubmit}>
|
|
127
|
+
{/* Mirror host app: normal-flow root + fixed overlay watermark + fixed opaque header */}
|
|
128
|
+
<div style={{ width: '100vw', display: 'flex', flexDirection: 'column' }}>
|
|
129
|
+
<Watermark />
|
|
130
|
+
<div
|
|
131
|
+
style={{
|
|
132
|
+
height: 114,
|
|
133
|
+
width: '100%',
|
|
134
|
+
position: 'fixed',
|
|
135
|
+
top: 0,
|
|
136
|
+
left: 0,
|
|
137
|
+
zIndex: 20,
|
|
138
|
+
background: 'linear-gradient(90deg,#1e3a8a,#2563eb)',
|
|
139
|
+
color: '#fff',
|
|
140
|
+
display: 'flex',
|
|
141
|
+
alignItems: 'center',
|
|
142
|
+
justifyContent: 'center',
|
|
143
|
+
fontWeight: 700,
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
ĐỀ THI THỬ — header cố định (fixed)
|
|
147
|
+
</div>
|
|
148
|
+
<div style={{ maxWidth: 760, margin: '0 auto', padding: 24, paddingTop: 140, fontFamily: 'system-ui, sans-serif' }}>
|
|
149
|
+
<h1>Đề thi thử (dev harness · {COUNT} câu nhiều công thức)</h1>
|
|
150
|
+
<p>Trang này tái hiện môi trường MathJax CHTML để kiểm thử chụp màn hình.</p>
|
|
151
|
+
{Array.from({ length: COUNT }, (_, i) => (
|
|
152
|
+
<Question key={i + 1} id={i + 1} />
|
|
153
|
+
))}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
69
156
|
</FeedbackProvider>
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export default App
|
|
74
|
-
|
|
157
|
+
);
|
|
158
|
+
};
|
|
75
159
|
|
|
160
|
+
export default App;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface CaptureCapabilities {
|
|
2
|
+
/** Largest canvas dimension (px) the device can reliably allocate. */
|
|
3
|
+
maxCanvasSize: number;
|
|
4
|
+
/** Recommended pixel multiplier before clamping to maxCanvasSize. */
|
|
5
|
+
recommendedScale: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function ua(): string {
|
|
9
|
+
return (typeof navigator !== 'undefined' ? navigator.userAgent : '').toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isIOSSafari(): boolean {
|
|
13
|
+
const u = ua();
|
|
14
|
+
const isIOS = /iphone|ipad|ipod/.test(u) ||
|
|
15
|
+
// iPadOS 13+ reports as desktop Safari with touch support
|
|
16
|
+
(/macintosh/.test(u) && typeof document !== 'undefined' && 'ontouchend' in document);
|
|
17
|
+
const isSafari = /safari/.test(u) && !/chrome|crios|crmo|fxios|edgios/.test(u);
|
|
18
|
+
return isIOS && isSafari;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getCaptureCapabilities(): CaptureCapabilities {
|
|
22
|
+
const u = ua();
|
|
23
|
+
const isMobile = /iphone|ipod|android.*mobile/.test(u);
|
|
24
|
+
const isTablet = /ipad|android(?!.*mobile)/.test(u);
|
|
25
|
+
const deviceMemory = (navigator as { deviceMemory?: number }).deviceMemory ?? 4;
|
|
26
|
+
const isLowMemory = deviceMemory < 4;
|
|
27
|
+
|
|
28
|
+
if (isMobile) {
|
|
29
|
+
return { maxCanvasSize: 4096, recommendedScale: isLowMemory ? 1 : 1.5 };
|
|
30
|
+
}
|
|
31
|
+
if (isTablet) {
|
|
32
|
+
return { maxCanvasSize: 8192, recommendedScale: 2 };
|
|
33
|
+
}
|
|
34
|
+
return { maxCanvasSize: 16384, recommendedScale: 2 };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Clamp a desired scale so neither dimension of the captured element exceeds
|
|
39
|
+
* the device's max canvas size. Keeps capture from silently failing on big pages.
|
|
40
|
+
*/
|
|
41
|
+
export function clampScale(
|
|
42
|
+
desiredScale: number,
|
|
43
|
+
contentWidth: number,
|
|
44
|
+
contentHeight: number,
|
|
45
|
+
maxCanvasSize: number
|
|
46
|
+
): number {
|
|
47
|
+
const maxByWidth = contentWidth > 0 ? maxCanvasSize / contentWidth : desiredScale;
|
|
48
|
+
const maxByHeight = contentHeight > 0 ? maxCanvasSize / contentHeight : desiredScale;
|
|
49
|
+
const scale = Math.min(desiredScale, maxByWidth, maxByHeight);
|
|
50
|
+
// Never go below 0.5 — a tiny screenshot is worse than a slightly-too-big one.
|
|
51
|
+
return Math.max(0.5, scale);
|
|
52
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface MathJaxLike {
|
|
2
|
+
startup?: { promise?: Promise<unknown> };
|
|
3
|
+
typesetPromise?: () => Promise<unknown>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function getMathJax(): MathJaxLike | undefined {
|
|
7
|
+
return (window as unknown as { MathJax?: MathJaxLike }).MathJax;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Wait until MathJax has finished its initial typeset so formulas have laid out. */
|
|
11
|
+
export async function waitForMathJax(): Promise<void> {
|
|
12
|
+
const MJ = getMathJax();
|
|
13
|
+
if (!MJ) return;
|
|
14
|
+
try {
|
|
15
|
+
if (MJ.startup?.promise) {
|
|
16
|
+
await MJ.startup.promise;
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
/* ignore — capture should proceed even if MathJax errored */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Two animation frames: let layout/paint settle before we snapshot. */
|
|
24
|
+
function nextFrames(): Promise<void> {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Resolve once the page is ready for a faithful snapshot. */
|
|
31
|
+
export async function ensureReadyForCapture(): Promise<void> {
|
|
32
|
+
await waitForMathJax();
|
|
33
|
+
await nextFrames();
|
|
34
|
+
}
|