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
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
export interface SVGRenderResult {
|
|
2
|
-
success: boolean;
|
|
3
|
-
error?: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Serialize SVG element to string
|
|
8
|
-
*/
|
|
9
|
-
function serializeSVG(svg: SVGElement): string {
|
|
10
|
-
const serializer = new XMLSerializer();
|
|
11
|
-
let svgString = serializer.serializeToString(svg);
|
|
12
|
-
|
|
13
|
-
// Ensure xmlns is present
|
|
14
|
-
if (!svgString.includes('xmlns=')) {
|
|
15
|
-
svgString = svgString.replace(
|
|
16
|
-
'<svg',
|
|
17
|
-
'<svg xmlns="http://www.w3.org/2000/svg"'
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return svgString;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Render SVG element to canvas
|
|
26
|
-
*/
|
|
27
|
-
export async function renderSVG(
|
|
28
|
-
svg: SVGElement,
|
|
29
|
-
ctx: CanvasRenderingContext2D
|
|
30
|
-
): Promise<SVGRenderResult> {
|
|
31
|
-
try {
|
|
32
|
-
const rect = svg.getBoundingClientRect();
|
|
33
|
-
|
|
34
|
-
// Use viewport coordinates directly
|
|
35
|
-
const x = rect.left;
|
|
36
|
-
const y = rect.top;
|
|
37
|
-
const width = rect.width;
|
|
38
|
-
const height = rect.height;
|
|
39
|
-
|
|
40
|
-
// Skip very small SVGs
|
|
41
|
-
if (width < 1 || height < 1) {
|
|
42
|
-
return { success: true };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Serialize SVG to string
|
|
46
|
-
const svgString = serializeSVG(svg);
|
|
47
|
-
|
|
48
|
-
// Create blob URL
|
|
49
|
-
const blob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
50
|
-
const url = URL.createObjectURL(blob);
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
// Load as image
|
|
54
|
-
const img = new Image();
|
|
55
|
-
await new Promise<void>((resolve, reject) => {
|
|
56
|
-
img.onload = () => resolve();
|
|
57
|
-
img.onerror = () => reject(new Error('SVG image load failed'));
|
|
58
|
-
img.src = url;
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
// Draw to canvas
|
|
62
|
-
ctx.drawImage(img, x, y, width, height);
|
|
63
|
-
|
|
64
|
-
return { success: true };
|
|
65
|
-
} finally {
|
|
66
|
-
// Clean up blob URL
|
|
67
|
-
URL.revokeObjectURL(url);
|
|
68
|
-
}
|
|
69
|
-
} catch (error) {
|
|
70
|
-
return {
|
|
71
|
-
success: false,
|
|
72
|
-
error: error instanceof Error ? error.message : 'SVG render failed'
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Render all SVG elements
|
|
79
|
-
*/
|
|
80
|
-
export async function renderSVGs(
|
|
81
|
-
svgs: SVGElement[],
|
|
82
|
-
ctx: CanvasRenderingContext2D,
|
|
83
|
-
onProgress?: (current: number, total: number) => void
|
|
84
|
-
): Promise<void> {
|
|
85
|
-
for (let i = 0; i < svgs.length; i++) {
|
|
86
|
-
await renderSVG(svgs[i], ctx);
|
|
87
|
-
|
|
88
|
-
if (onProgress) {
|
|
89
|
-
onProgress(i + 1, svgs.length);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Check if element is MathJax rendered
|
|
96
|
-
*/
|
|
97
|
-
export function isMathJaxElement(element: Element): boolean {
|
|
98
|
-
return (
|
|
99
|
-
element.classList.contains('MathJax') ||
|
|
100
|
-
element.classList.contains('MathJax_Display') ||
|
|
101
|
-
element.classList.contains('MJX-TeXAtom-ORD') ||
|
|
102
|
-
element.hasAttribute('data-mathml')
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Get all SVG elements from the document
|
|
108
|
-
*/
|
|
109
|
-
export function getSVGElements(elements: Element[]): SVGElement[] {
|
|
110
|
-
const svgs: SVGElement[] = [];
|
|
111
|
-
|
|
112
|
-
for (const element of elements) {
|
|
113
|
-
if (element instanceof SVGElement) {
|
|
114
|
-
svgs.push(element);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return svgs;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Get all MathJax elements from the document
|
|
123
|
-
*/
|
|
124
|
-
export function getMathJaxElements(elements: Element[]): Element[] {
|
|
125
|
-
return elements.filter(element => isMathJaxElement(element));
|
|
126
|
-
}
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text Content Renderer - Layer 4
|
|
3
|
-
* Renders HTML text elements to canvas using SVG foreignObject technique
|
|
4
|
-
* This preserves exact browser rendering including fonts, wrapping, and nested styles
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface TextRenderResult {
|
|
8
|
-
success: boolean;
|
|
9
|
-
error?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Render HTML element as image using SVG foreignObject
|
|
14
|
-
* This technique captures the exact browser rendering of text with all styles
|
|
15
|
-
*/
|
|
16
|
-
export async function renderTextElement(
|
|
17
|
-
element: HTMLElement,
|
|
18
|
-
ctx: CanvasRenderingContext2D
|
|
19
|
-
): Promise<TextRenderResult> {
|
|
20
|
-
try {
|
|
21
|
-
const rect = element.getBoundingClientRect();
|
|
22
|
-
|
|
23
|
-
// Use viewport coordinates directly (canvas is already translated)
|
|
24
|
-
const x = rect.left;
|
|
25
|
-
const y = rect.top;
|
|
26
|
-
const width = rect.width;
|
|
27
|
-
const height = rect.height;
|
|
28
|
-
|
|
29
|
-
// Skip elements with no dimensions
|
|
30
|
-
if (width < 1 || height < 1) {
|
|
31
|
-
return { success: true };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Get computed styles to preserve appearance
|
|
35
|
-
const styles = window.getComputedStyle(element);
|
|
36
|
-
|
|
37
|
-
// Clone element to avoid modifying original
|
|
38
|
-
const clonedElement = element.cloneNode(true) as HTMLElement;
|
|
39
|
-
|
|
40
|
-
// Get all relevant styles
|
|
41
|
-
const relevantStyles = [
|
|
42
|
-
'color',
|
|
43
|
-
'font-family',
|
|
44
|
-
'font-size',
|
|
45
|
-
'font-weight',
|
|
46
|
-
'font-style',
|
|
47
|
-
'line-height',
|
|
48
|
-
'text-align',
|
|
49
|
-
'text-decoration',
|
|
50
|
-
'text-transform',
|
|
51
|
-
'letter-spacing',
|
|
52
|
-
'word-spacing',
|
|
53
|
-
'white-space',
|
|
54
|
-
'padding',
|
|
55
|
-
'margin',
|
|
56
|
-
'display',
|
|
57
|
-
'flex-direction',
|
|
58
|
-
'align-items',
|
|
59
|
-
'justify-content'
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
// Build inline styles from computed styles
|
|
63
|
-
let inlineStyle = '';
|
|
64
|
-
relevantStyles.forEach(prop => {
|
|
65
|
-
const value = styles.getPropertyValue(prop);
|
|
66
|
-
if (value) {
|
|
67
|
-
inlineStyle += `${prop}: ${value}; `;
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Apply styles to cloned element
|
|
72
|
-
clonedElement.setAttribute('style', inlineStyle);
|
|
73
|
-
|
|
74
|
-
// Wrap in foreignObject SVG
|
|
75
|
-
const foreignObject = `
|
|
76
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
77
|
-
<foreignObject width="100%" height="100%">
|
|
78
|
-
<div xmlns="http://www.w3.org/1999/xhtml" style="width: ${width}px; height: ${height}px; overflow: hidden;">
|
|
79
|
-
${clonedElement.outerHTML}
|
|
80
|
-
</div>
|
|
81
|
-
</foreignObject>
|
|
82
|
-
</svg>
|
|
83
|
-
`;
|
|
84
|
-
|
|
85
|
-
// Create blob URL
|
|
86
|
-
const blob = new Blob([foreignObject], { type: 'image/svg+xml;charset=utf-8' });
|
|
87
|
-
const url = URL.createObjectURL(blob);
|
|
88
|
-
|
|
89
|
-
try {
|
|
90
|
-
// Load as image with timeout
|
|
91
|
-
const img = new Image();
|
|
92
|
-
await new Promise<void>((resolve, reject) => {
|
|
93
|
-
const timeout = setTimeout(() => {
|
|
94
|
-
reject(new Error('Text element render timeout'));
|
|
95
|
-
}, 1000); // 1 second timeout
|
|
96
|
-
|
|
97
|
-
img.onload = () => {
|
|
98
|
-
clearTimeout(timeout);
|
|
99
|
-
resolve();
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
img.onerror = () => {
|
|
103
|
-
clearTimeout(timeout);
|
|
104
|
-
reject(new Error('Text element image load failed'));
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
img.src = url;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Draw to canvas
|
|
111
|
-
ctx.drawImage(img, x, y, width, height);
|
|
112
|
-
|
|
113
|
-
return { success: true };
|
|
114
|
-
} finally {
|
|
115
|
-
// Clean up blob URL
|
|
116
|
-
URL.revokeObjectURL(url);
|
|
117
|
-
}
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.warn('[TextRenderer] Failed to render text element:', error);
|
|
120
|
-
return {
|
|
121
|
-
success: false,
|
|
122
|
-
error: error instanceof Error ? error.message : 'Text render failed'
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Check if element contains renderable text content
|
|
129
|
-
*/
|
|
130
|
-
function hasTextContent(element: HTMLElement): boolean {
|
|
131
|
-
// Skip non-visible elements
|
|
132
|
-
const styles = window.getComputedStyle(element);
|
|
133
|
-
if (styles.display === 'none' || styles.visibility === 'hidden') {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Check if element or its children have text
|
|
138
|
-
const text = element.textContent?.trim();
|
|
139
|
-
if (!text || text.length === 0) {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get text elements suitable for rendering
|
|
148
|
-
* Selects leaf elements that contain actual text content
|
|
149
|
-
*/
|
|
150
|
-
export function getTextElements(elements: Element[]): HTMLElement[] {
|
|
151
|
-
const textElements: HTMLElement[] = [];
|
|
152
|
-
|
|
153
|
-
for (const element of elements) {
|
|
154
|
-
if (!(element instanceof HTMLElement)) continue;
|
|
155
|
-
|
|
156
|
-
const tag = element.tagName;
|
|
157
|
-
|
|
158
|
-
// Skip elements that are rendered by other layers
|
|
159
|
-
if (['IMG', 'SVG', 'CANVAS', 'VIDEO', 'IFRAME', 'SCRIPT', 'STYLE', 'NOSCRIPT'].includes(tag)) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Focus on text-containing elements
|
|
164
|
-
// Prioritize leaf elements (P, SPAN, H1-H6, A, etc.)
|
|
165
|
-
const isTextElement = [
|
|
166
|
-
'P', 'SPAN', 'DIV', 'A', 'BUTTON', 'LABEL', 'LI',
|
|
167
|
-
'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
|
168
|
-
'TD', 'TH', 'STRONG', 'EM', 'B', 'I', 'U',
|
|
169
|
-
'CODE', 'PRE', 'BLOCKQUOTE'
|
|
170
|
-
].includes(tag);
|
|
171
|
-
|
|
172
|
-
if (isTextElement && hasTextContent(element)) {
|
|
173
|
-
// Check if this is a leaf element or has only text children
|
|
174
|
-
const hasBlockChildren = Array.from(element.children).some(child => {
|
|
175
|
-
const childTag = child.tagName;
|
|
176
|
-
return ['DIV', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'TABLE'].includes(childTag);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Only add if it's a leaf element (no block children)
|
|
180
|
-
if (!hasBlockChildren) {
|
|
181
|
-
textElements.push(element);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return textElements;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Render all text elements to canvas
|
|
191
|
-
*/
|
|
192
|
-
export async function renderTextContent(
|
|
193
|
-
elements: Element[],
|
|
194
|
-
ctx: CanvasRenderingContext2D,
|
|
195
|
-
onProgress?: (current: number, total: number) => void
|
|
196
|
-
): Promise<void> {
|
|
197
|
-
const textElements = getTextElements(elements);
|
|
198
|
-
|
|
199
|
-
console.log('[TextRenderer] Rendering', textElements.length, 'text elements');
|
|
200
|
-
|
|
201
|
-
for (let i = 0; i < textElements.length; i++) {
|
|
202
|
-
const result = await renderTextElement(textElements[i], ctx);
|
|
203
|
-
|
|
204
|
-
if (!result.success) {
|
|
205
|
-
console.warn('[TextRenderer] Failed to render element:', textElements[i], result.error);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (onProgress) {
|
|
209
|
-
onProgress(i + 1, textElements.length);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export interface Rect {
|
|
2
|
-
x: number;
|
|
3
|
-
y: number;
|
|
4
|
-
width: number;
|
|
5
|
-
height: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface DeviceCapabilities {
|
|
9
|
-
maxCanvasSize: number;
|
|
10
|
-
recommendedScale: number;
|
|
11
|
-
recommendedQuality: number;
|
|
12
|
-
chunkSize: number;
|
|
13
|
-
yieldInterval: number;
|
|
14
|
-
maxTimeout: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface CaptureOptions {
|
|
18
|
-
quality: number;
|
|
19
|
-
scale: number;
|
|
20
|
-
includeMathJax: boolean;
|
|
21
|
-
timeout: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface CaptureProgress {
|
|
25
|
-
phase: 'analyzing' | 'rendering' | 'exporting';
|
|
26
|
-
progress: number; // 0-100
|
|
27
|
-
message: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface CleanupTracker {
|
|
31
|
-
objectUrls: string[];
|
|
32
|
-
tempCanvases: HTMLCanvasElement[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface CaptureBounds {
|
|
36
|
-
left: number;
|
|
37
|
-
top: number;
|
|
38
|
-
right: number;
|
|
39
|
-
bottom: number;
|
|
40
|
-
}
|
package/src/utils/screenshot.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { Rect, CaptureProgress } from './screenshot/types';
|
|
2
|
-
import { captureWithCanvas } from './screenshot/canvas-renderer';
|
|
3
|
-
import { domToPng } from 'modern-screenshot';
|
|
4
|
-
|
|
5
|
-
// Re-export Rect type for backwards compatibility
|
|
6
|
-
export type { Rect };
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Capture screenshot with 3-tier fallback strategy:
|
|
10
|
-
* 1. Canvas-based capture (primary)
|
|
11
|
-
* 2. modern-screenshot library (fallback)
|
|
12
|
-
* 3. Return null (final fallback)
|
|
13
|
-
*/
|
|
14
|
-
export const captureScreenshot = async (
|
|
15
|
-
cropRect?: Rect,
|
|
16
|
-
onProgress?: (progress: CaptureProgress) => void
|
|
17
|
-
): Promise<string | null> => {
|
|
18
|
-
console.log('[Screenshot] Starting capture with 3-tier fallback strategy');
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
// Tier 1: Try canvas-based capture
|
|
22
|
-
console.log('[Screenshot] Tier 1: Attempting canvas-based capture...');
|
|
23
|
-
const canvasResult = await captureWithCanvas(cropRect, {}, onProgress);
|
|
24
|
-
|
|
25
|
-
if (canvasResult) {
|
|
26
|
-
console.log('[Screenshot] Tier 1 successful - canvas-based capture');
|
|
27
|
-
return canvasResult;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
console.warn('[Screenshot] Tier 1 failed, falling back to Tier 2');
|
|
31
|
-
} catch (error) {
|
|
32
|
-
console.warn('[Screenshot] Tier 1 error:', error);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
// Tier 2: Fallback to modern-screenshot
|
|
37
|
-
console.log('[Screenshot] Tier 2: Attempting modern-screenshot...');
|
|
38
|
-
|
|
39
|
-
const element = document.documentElement;
|
|
40
|
-
const targetX = cropRect ? cropRect.x : 0;
|
|
41
|
-
const targetY = cropRect ? cropRect.y : 0;
|
|
42
|
-
const targetWidth = cropRect ? cropRect.width : window.innerWidth;
|
|
43
|
-
const targetHeight = cropRect ? cropRect.height : window.innerHeight;
|
|
44
|
-
|
|
45
|
-
const scrollX = window.scrollX;
|
|
46
|
-
const scrollY = window.scrollY;
|
|
47
|
-
const offsetX = scrollX + targetX;
|
|
48
|
-
const offsetY = scrollY + targetY;
|
|
49
|
-
|
|
50
|
-
const dataUrl = await domToPng(element, {
|
|
51
|
-
quality: 0.9,
|
|
52
|
-
width: targetWidth,
|
|
53
|
-
height: targetHeight,
|
|
54
|
-
style: {
|
|
55
|
-
transform: `translate(${-offsetX}px, ${-offsetY}px)`
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
console.log('[Screenshot] Tier 2 successful - modern-screenshot');
|
|
60
|
-
return dataUrl;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
console.error('[Screenshot] Tier 2 failed:', error);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Tier 3: All methods failed
|
|
66
|
-
console.error('[Screenshot] All capture methods failed');
|
|
67
|
-
return null;
|
|
68
|
-
};
|