@alliance-droid/status-feedback-system 1.4.0 → 2.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/CHANGELOG.md +12 -0
- package/dist/components/DevFeedback.svelte +87 -61
- package/dist/components/DevFeedback.svelte.d.ts +10 -8
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- package/package.json +8 -6
- package/dist/components/dev/screenshot.d.ts +0 -36
- package/dist/components/dev/screenshot.js +0 -94
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- `userEmail` prop on `DevFeedback` is now **required** (was optional). Pass the authenticated user's email; pass an empty string when no user is signed in.
|
|
8
|
+
- Widget renders nothing (no bubble, no context menu) when `userEmail` is empty. The previous lock-screen UI for unauthenticated users is removed.
|
|
9
|
+
- Anonymous feedback submission is no longer possible through the widget.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Dev-mode console warning when `userEmail` is empty, to surface misconfigurations in consumer apps.
|
|
14
|
+
|
|
3
15
|
## 1.3.0
|
|
4
16
|
|
|
5
17
|
- Add composable FeedbackBoard components (FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm)
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
} from '@alliance-droid/svelte-component-library';
|
|
12
12
|
import DevContextMenu from './dev/DevContextMenu.svelte';
|
|
13
13
|
import DevOnboardingToast from './dev/DevOnboardingToast.svelte';
|
|
14
|
-
import { captureScreenshot, type ScreenshotProvider } from './dev/screenshot.js';
|
|
15
14
|
import {
|
|
16
15
|
detectEnvironment,
|
|
17
16
|
getEnvironmentName,
|
|
@@ -31,16 +30,19 @@
|
|
|
31
30
|
showOnboarding?: boolean;
|
|
32
31
|
/** URL to the feedback board page. If omitted, "View Open Issues" is hidden from the context menu. */
|
|
33
32
|
boardUrl?: string;
|
|
34
|
-
/** Authenticated user email. Required for submitting feedback. */
|
|
35
|
-
userEmail?: string;
|
|
36
33
|
/**
|
|
37
|
-
*
|
|
38
|
-
* the default html2canvas client-side capture. Use this to delegate
|
|
39
|
-
* screenshot capture to a server endpoint (e.g. Puppeteer / screenshot-service).
|
|
34
|
+
* Authenticated user email. Required.
|
|
40
35
|
*
|
|
41
|
-
*
|
|
36
|
+
* Pass an empty string when the user is not signed in — the widget will
|
|
37
|
+
* render nothing (no floating bubble, no menu, no form). Anonymous
|
|
38
|
+
* feedback is not supported.
|
|
42
39
|
*/
|
|
43
|
-
|
|
40
|
+
userEmail: string;
|
|
41
|
+
/**
|
|
42
|
+
* Endpoint on the consumer app that issues render tokens.
|
|
43
|
+
* Default: '/svc/internal/issue-render-token'
|
|
44
|
+
*/
|
|
45
|
+
renderTokenEndpoint?: string;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
let {
|
|
@@ -51,9 +53,24 @@
|
|
|
51
53
|
showOnboarding = true,
|
|
52
54
|
boardUrl,
|
|
53
55
|
userEmail,
|
|
54
|
-
|
|
56
|
+
renderTokenEndpoint = '/svc/internal/issue-render-token',
|
|
55
57
|
}: Props = $props();
|
|
56
58
|
|
|
59
|
+
// Dev-mode warning when userEmail is empty
|
|
60
|
+
$effect(() => {
|
|
61
|
+
if (!userEmail && typeof console !== 'undefined') {
|
|
62
|
+
// eslint-disable-next-line no-restricted-globals -- safe typeof guard for universal code
|
|
63
|
+
const proc = typeof globalThis !== 'undefined' ? (globalThis as Record<string, unknown>).process as { env?: Record<string, string> } | undefined : undefined;
|
|
64
|
+
const env = proc?.env?.NODE_ENV || 'development';
|
|
65
|
+
if (env !== 'production') {
|
|
66
|
+
console.warn(
|
|
67
|
+
'[status-feedback-system] <DevFeedback> was rendered with an empty userEmail. ' +
|
|
68
|
+
"The widget will not appear. Pass the authenticated user's email, or do not render <DevFeedback> when no user is signed in.",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
57
74
|
// --- Client (lazy, stable) ---
|
|
58
75
|
let client: StatusClient | null = $state(null);
|
|
59
76
|
let lastApiUrl = '';
|
|
@@ -70,6 +87,7 @@
|
|
|
70
87
|
|
|
71
88
|
// --- Environment gate ---
|
|
72
89
|
let isEnabled = $state(false);
|
|
90
|
+
let hasUserEmail = $derived(!!userEmail);
|
|
73
91
|
|
|
74
92
|
// --- State ---
|
|
75
93
|
let menuOpen = $state(false);
|
|
@@ -125,13 +143,13 @@
|
|
|
125
143
|
let formTitle = $state('');
|
|
126
144
|
let formMessage = $state('');
|
|
127
145
|
|
|
128
|
-
|
|
146
|
+
|
|
129
147
|
let submitting = $state(false);
|
|
130
148
|
let submitResult = $state<'success' | 'error' | null>(null);
|
|
131
149
|
|
|
132
|
-
let
|
|
133
|
-
let
|
|
134
|
-
let
|
|
150
|
+
let renderContext = $state<{ url: string; scrollX: number; scrollY: number; renderToken: string; issuer: string } | null>(null);
|
|
151
|
+
let fetchingRenderToken = $state(false);
|
|
152
|
+
let renderTokenFailed = $state(false);
|
|
135
153
|
|
|
136
154
|
// Drag state
|
|
137
155
|
let isDragging = $state(false);
|
|
@@ -171,7 +189,7 @@
|
|
|
171
189
|
const ONBOARDING_KEY = 'sf-devfeedback-onboarded';
|
|
172
190
|
|
|
173
191
|
onMount(() => {
|
|
174
|
-
isEnabled = detectEnvironment(enabled);
|
|
192
|
+
isEnabled = detectEnvironment(enabled) && !!userEmail;
|
|
175
193
|
if (!isEnabled) return;
|
|
176
194
|
|
|
177
195
|
// Hook console.error
|
|
@@ -227,16 +245,36 @@
|
|
|
227
245
|
if (e.key === 'Escape') {
|
|
228
246
|
menuOpen = false;
|
|
229
247
|
formOpen = false;
|
|
230
|
-
|
|
248
|
+
renderContext = null;
|
|
231
249
|
}
|
|
232
250
|
}
|
|
233
251
|
|
|
234
|
-
async function
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
252
|
+
async function fetchRenderToken() {
|
|
253
|
+
fetchingRenderToken = true;
|
|
254
|
+
renderTokenFailed = false;
|
|
255
|
+
try {
|
|
256
|
+
const url = window.location.href;
|
|
257
|
+
const scrollX = Math.round(window.scrollX);
|
|
258
|
+
const scrollY = Math.round(window.scrollY);
|
|
259
|
+
const res = await fetch(renderTokenEndpoint, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
credentials: 'include',
|
|
262
|
+
headers: { 'Content-Type': 'application/json' },
|
|
263
|
+
body: JSON.stringify({ url, scrollX, scrollY }),
|
|
264
|
+
});
|
|
265
|
+
if (!res.ok) {
|
|
266
|
+
renderTokenFailed = true;
|
|
267
|
+
console.warn('[DevFeedback] Render token fetch failed:', res.status);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const { token } = (await res.json()) as { token: string };
|
|
271
|
+
renderContext = { url, scrollX, scrollY, renderToken: token, issuer: new URL(url).origin };
|
|
272
|
+
} catch (err) {
|
|
273
|
+
renderTokenFailed = true;
|
|
274
|
+
console.warn('[DevFeedback] Render token fetch error:', err);
|
|
275
|
+
} finally {
|
|
276
|
+
fetchingRenderToken = false;
|
|
277
|
+
}
|
|
240
278
|
}
|
|
241
279
|
|
|
242
280
|
function openForm(type: 'bug' | 'feature') {
|
|
@@ -251,14 +289,14 @@
|
|
|
251
289
|
modalY = 0;
|
|
252
290
|
|
|
253
291
|
if (type === 'bug') {
|
|
254
|
-
|
|
292
|
+
fetchRenderToken();
|
|
255
293
|
} else {
|
|
256
|
-
|
|
294
|
+
renderContext = null;
|
|
257
295
|
}
|
|
258
296
|
}
|
|
259
297
|
|
|
260
298
|
async function handleSubmit() {
|
|
261
|
-
if (!formMessage.trim() || !
|
|
299
|
+
if (!formMessage.trim() || !hasUserEmail) return;
|
|
262
300
|
submitting = true;
|
|
263
301
|
submitResult = null;
|
|
264
302
|
|
|
@@ -280,8 +318,11 @@
|
|
|
280
318
|
metadata.consoleErrors = [...consoleErrors];
|
|
281
319
|
}
|
|
282
320
|
|
|
283
|
-
if (
|
|
284
|
-
metadata.
|
|
321
|
+
if (renderContext) {
|
|
322
|
+
metadata.renderToken = renderContext.renderToken;
|
|
323
|
+
metadata.issuer = renderContext.issuer;
|
|
324
|
+
metadata.scrollX = renderContext.scrollX;
|
|
325
|
+
metadata.scrollY = renderContext.scrollY;
|
|
285
326
|
}
|
|
286
327
|
|
|
287
328
|
const gitCommit = getEnvVar('VERCEL_GIT_COMMIT_SHA');
|
|
@@ -304,7 +345,7 @@
|
|
|
304
345
|
setTimeout(() => {
|
|
305
346
|
formOpen = false;
|
|
306
347
|
submitResult = null;
|
|
307
|
-
|
|
348
|
+
renderContext = null;
|
|
308
349
|
}, 2000);
|
|
309
350
|
} catch {
|
|
310
351
|
submitResult = 'error';
|
|
@@ -381,11 +422,6 @@
|
|
|
381
422
|
{formType === 'bug' ? "We'll look into this." : "Thanks for the suggestion!"}
|
|
382
423
|
</p>
|
|
383
424
|
</div>
|
|
384
|
-
{:else if !isAuthenticated}
|
|
385
|
-
<div class="text-center py-6 text-foreground-tertiary">
|
|
386
|
-
<i class="fa-solid fa-lock text-2xl" aria-hidden="true"></i>
|
|
387
|
-
<p class="mt-2 text-sm">Sign in to submit feedback</p>
|
|
388
|
-
</div>
|
|
389
425
|
{:else}
|
|
390
426
|
<div class="flex items-center gap-1.5 text-xs text-foreground-tertiary">
|
|
391
427
|
<i class="fa-solid fa-user" aria-hidden="true"></i>
|
|
@@ -434,40 +470,30 @@
|
|
|
434
470
|
{/if}
|
|
435
471
|
</div>
|
|
436
472
|
|
|
437
|
-
<!--
|
|
438
|
-
{#if
|
|
473
|
+
<!-- Render token status -->
|
|
474
|
+
{#if fetchingRenderToken}
|
|
439
475
|
<div class="flex items-center gap-2 text-sm text-foreground-tertiary">
|
|
440
|
-
<Spinner />
|
|
476
|
+
<Spinner /> Preparing screenshot...
|
|
441
477
|
</div>
|
|
442
|
-
{:else if
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
<button
|
|
450
|
-
class="text-xs text-primary hover:underline bg-transparent border-none cursor-pointer p-0"
|
|
451
|
-
onclick={doScreenshot}
|
|
452
|
-
>
|
|
453
|
-
Retry
|
|
454
|
-
</button>
|
|
455
|
-
</div>
|
|
456
|
-
{:else if screenshotData}
|
|
457
|
-
<div class="relative">
|
|
458
|
-
<img
|
|
459
|
-
src={screenshotData}
|
|
460
|
-
alt="Screenshot"
|
|
461
|
-
class="w-full rounded-md border border-border max-h-40 object-cover object-top"
|
|
462
|
-
/>
|
|
478
|
+
{:else if renderTokenFailed}
|
|
479
|
+
<div class="rounded-md border border-border bg-surface p-3">
|
|
480
|
+
<div class="flex items-center gap-2 text-sm text-foreground-secondary mb-1">
|
|
481
|
+
<i class="fa-solid fa-image-slash" aria-hidden="true"></i>
|
|
482
|
+
<span>Screenshot setup failed</span>
|
|
483
|
+
</div>
|
|
484
|
+
<p class="text-xs text-foreground-tertiary mb-2">Your feedback will be submitted without a screenshot.</p>
|
|
463
485
|
<button
|
|
464
|
-
class="
|
|
465
|
-
onclick={
|
|
466
|
-
aria-label="Remove screenshot"
|
|
486
|
+
class="text-xs text-primary hover:underline bg-transparent border-none cursor-pointer p-0"
|
|
487
|
+
onclick={fetchRenderToken}
|
|
467
488
|
>
|
|
468
|
-
|
|
489
|
+
Retry
|
|
469
490
|
</button>
|
|
470
491
|
</div>
|
|
492
|
+
{:else if renderContext}
|
|
493
|
+
<div class="rounded-md border border-border bg-surface-secondary p-2 flex items-center gap-2 text-sm text-foreground-secondary">
|
|
494
|
+
<i class="fa-solid fa-camera" aria-hidden="true"></i>
|
|
495
|
+
<span>Screenshot will be captured server-side on submit</span>
|
|
496
|
+
</div>
|
|
471
497
|
{/if}
|
|
472
498
|
</div>
|
|
473
499
|
{/if}
|
|
@@ -482,7 +508,7 @@
|
|
|
482
508
|
</div>
|
|
483
509
|
|
|
484
510
|
<!-- Footer -->
|
|
485
|
-
{#if submitResult !== 'success' &&
|
|
511
|
+
{#if submitResult !== 'success' && hasUserEmail}
|
|
486
512
|
<div class="flex justify-end gap-2 px-5 py-4 border-t border-border shrink-0">
|
|
487
513
|
<Button variant="neutral" style="outline" onclick={() => (formOpen = false)}>
|
|
488
514
|
Cancel
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type ScreenshotProvider } from './dev/screenshot.js';
|
|
2
1
|
interface Props {
|
|
3
2
|
apiUrl: string;
|
|
4
3
|
apiKey: string;
|
|
@@ -10,16 +9,19 @@ interface Props {
|
|
|
10
9
|
showOnboarding?: boolean;
|
|
11
10
|
/** URL to the feedback board page. If omitted, "View Open Issues" is hidden from the context menu. */
|
|
12
11
|
boardUrl?: string;
|
|
13
|
-
/** Authenticated user email. Required for submitting feedback. */
|
|
14
|
-
userEmail?: string;
|
|
15
12
|
/**
|
|
16
|
-
*
|
|
17
|
-
* the default html2canvas client-side capture. Use this to delegate
|
|
18
|
-
* screenshot capture to a server endpoint (e.g. Puppeteer / screenshot-service).
|
|
13
|
+
* Authenticated user email. Required.
|
|
19
14
|
*
|
|
20
|
-
*
|
|
15
|
+
* Pass an empty string when the user is not signed in — the widget will
|
|
16
|
+
* render nothing (no floating bubble, no menu, no form). Anonymous
|
|
17
|
+
* feedback is not supported.
|
|
21
18
|
*/
|
|
22
|
-
|
|
19
|
+
userEmail: string;
|
|
20
|
+
/**
|
|
21
|
+
* Endpoint on the consumer app that issues render tokens.
|
|
22
|
+
* Default: '/svc/internal/issue-render-token'
|
|
23
|
+
*/
|
|
24
|
+
renderTokenEndpoint?: string;
|
|
23
25
|
}
|
|
24
26
|
declare const DevFeedback: import("svelte").Component<Props, {}, "">;
|
|
25
27
|
type DevFeedback = ReturnType<typeof DevFeedback>;
|
package/dist/index.d.ts
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export type { Project, System, Incident, IncidentUpdate, Feedback, FeedbackResponse, FeedbackVisibility, StatusHistoryEntry, SystemStatus, IncidentStatus, IncidentSeverity, FeedbackStatus, ApiResponse, } from './types.js';
|
|
11
11
|
export { deriveOverallStatus } from './utils.js';
|
|
12
|
-
export { createServerScreenshotProvider } from './components/dev/screenshot.js';
|
|
13
|
-
export type { ScreenshotProvider } from './components/dev/screenshot.js';
|
|
14
12
|
export { createStatusClient } from './api.js';
|
|
15
13
|
export type { StatusClient, StatusClientConfig, StatusData, IncidentData, UptimeData, BoardData, BoardItem, BoardCategory } from './api.js';
|
|
16
14
|
export { default as StatusPage } from './components/StatusPage.svelte';
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
// ── Utilities ──
|
|
11
11
|
export { deriveOverallStatus } from './utils.js';
|
|
12
|
-
export { createServerScreenshotProvider } from './components/dev/screenshot.js';
|
|
13
12
|
// ── API Client ──
|
|
14
13
|
export { createStatusClient } from './api.js';
|
|
15
14
|
// ── Client Components ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alliance-droid/status-feedback-system",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -18,30 +18,32 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "svelte-package -o dist && publint",
|
|
21
|
-
"lint": "
|
|
21
|
+
"lint": "eslint src",
|
|
22
22
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
23
23
|
"test": "vitest run",
|
|
24
24
|
"test:watch": "vitest",
|
|
25
25
|
"prepublishOnly": "npm run build"
|
|
26
26
|
},
|
|
27
|
-
"dependencies": {
|
|
28
|
-
"html2canvas": "^1.4.1"
|
|
29
|
-
},
|
|
30
27
|
"peerDependencies": {
|
|
31
28
|
"@alliance-droid/svelte-component-library": ">=1.0.0",
|
|
32
29
|
"svelte": "^5.0.0"
|
|
33
30
|
},
|
|
34
31
|
"devDependencies": {
|
|
35
32
|
"@alliance-droid/svelte-component-library": "^1.0.5",
|
|
33
|
+
"@eslint/js": "^10.0.1",
|
|
36
34
|
"@sveltejs/package": "^2.3.0",
|
|
37
35
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
|
38
36
|
"@testing-library/jest-dom": "^6.9.1",
|
|
39
37
|
"@testing-library/svelte": "^5.3.1",
|
|
38
|
+
"eslint": "^10.4.0",
|
|
39
|
+
"eslint-plugin-svelte": "^3.17.1",
|
|
40
|
+
"globals": "^17.6.0",
|
|
40
41
|
"jsdom": "^29.0.0",
|
|
41
42
|
"publint": "^0.3.0",
|
|
42
43
|
"svelte": "^5.48.0",
|
|
43
44
|
"svelte-check": "^4.3.0",
|
|
44
45
|
"typescript": "^5.9.0",
|
|
46
|
+
"typescript-eslint": "^8.59.4",
|
|
45
47
|
"vite": "^7.3.0",
|
|
46
48
|
"vitest": "^4.0.0"
|
|
47
49
|
},
|
|
@@ -61,4 +63,4 @@
|
|
|
61
63
|
"type": "git",
|
|
62
64
|
"url": "git+https://alliance.ghe.com/alliance/status-feedback-system.git"
|
|
63
65
|
}
|
|
64
|
-
}
|
|
66
|
+
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Screenshot capture utility for DevFeedback.
|
|
3
|
-
*
|
|
4
|
-
* Supports two modes:
|
|
5
|
-
* 1. Custom provider — consumer supplies a `ScreenshotProvider` callback
|
|
6
|
-
* (e.g. server-side capture via Puppeteer / screenshot-service).
|
|
7
|
-
* 2. html2canvas fallback — dynamically imports html2canvas if no provider
|
|
8
|
-
* is given (html2canvas is an optional peer dependency).
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* A function that captures a screenshot and returns a data URL (or null on failure).
|
|
12
|
-
* Consumers can implement this however they like — server endpoint, different
|
|
13
|
-
* client library, canvas API, etc.
|
|
14
|
-
*/
|
|
15
|
-
export type ScreenshotProvider = () => Promise<string | null>;
|
|
16
|
-
/**
|
|
17
|
-
* Capture a screenshot using the given provider, or fall back to html2canvas.
|
|
18
|
-
*/
|
|
19
|
-
export declare function captureScreenshot(provider?: ScreenshotProvider): Promise<string | null>;
|
|
20
|
-
/**
|
|
21
|
-
* Create a screenshot provider that POSTs to a server endpoint.
|
|
22
|
-
*
|
|
23
|
-
* The endpoint receives `{ url, viewport }` and should return
|
|
24
|
-
* `{ screenshot: "data:image/..." }` (or any JSON with a `screenshot` string field).
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* <DevFeedback
|
|
29
|
-
* screenshotProvider={createServerScreenshotProvider('/api/screenshot')}
|
|
30
|
-
* ...
|
|
31
|
-
* />
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
|
-
export declare function createServerScreenshotProvider(endpoint: string, options?: {
|
|
35
|
-
headers?: Record<string, string>;
|
|
36
|
-
}): ScreenshotProvider;
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Screenshot capture utility for DevFeedback.
|
|
3
|
-
*
|
|
4
|
-
* Supports two modes:
|
|
5
|
-
* 1. Custom provider — consumer supplies a `ScreenshotProvider` callback
|
|
6
|
-
* (e.g. server-side capture via Puppeteer / screenshot-service).
|
|
7
|
-
* 2. html2canvas fallback — dynamically imports html2canvas if no provider
|
|
8
|
-
* is given (html2canvas is an optional peer dependency).
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Capture a screenshot using the given provider, or fall back to html2canvas.
|
|
12
|
-
*/
|
|
13
|
-
export async function captureScreenshot(provider) {
|
|
14
|
-
if (provider) {
|
|
15
|
-
try {
|
|
16
|
-
return await provider();
|
|
17
|
-
}
|
|
18
|
-
catch (err) {
|
|
19
|
-
console.error('[DevFeedback] Screenshot provider failed:', err);
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return captureWithHtml2Canvas();
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Create a screenshot provider that POSTs to a server endpoint.
|
|
27
|
-
*
|
|
28
|
-
* The endpoint receives `{ url, viewport }` and should return
|
|
29
|
-
* `{ screenshot: "data:image/..." }` (or any JSON with a `screenshot` string field).
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* ```ts
|
|
33
|
-
* <DevFeedback
|
|
34
|
-
* screenshotProvider={createServerScreenshotProvider('/api/screenshot')}
|
|
35
|
-
* ...
|
|
36
|
-
* />
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
39
|
-
export function createServerScreenshotProvider(endpoint, options) {
|
|
40
|
-
return async () => {
|
|
41
|
-
const res = await fetch(endpoint, {
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: {
|
|
44
|
-
'Content-Type': 'application/json',
|
|
45
|
-
...options?.headers,
|
|
46
|
-
},
|
|
47
|
-
body: JSON.stringify({
|
|
48
|
-
url: window.location.href,
|
|
49
|
-
viewport: {
|
|
50
|
-
width: window.innerWidth,
|
|
51
|
-
height: window.innerHeight,
|
|
52
|
-
},
|
|
53
|
-
}),
|
|
54
|
-
});
|
|
55
|
-
if (!res.ok)
|
|
56
|
-
return null;
|
|
57
|
-
const data = await res.json();
|
|
58
|
-
return data.screenshot ?? null;
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
/** html2canvas fallback — gracefully degrades if not installed. */
|
|
62
|
-
async function captureWithHtml2Canvas() {
|
|
63
|
-
try {
|
|
64
|
-
const html2canvas = (await import('html2canvas')).default;
|
|
65
|
-
const canvas = await html2canvas(document.body, {
|
|
66
|
-
logging: false,
|
|
67
|
-
useCORS: true,
|
|
68
|
-
scale: 1,
|
|
69
|
-
width: window.innerWidth,
|
|
70
|
-
height: window.innerHeight,
|
|
71
|
-
windowWidth: window.innerWidth,
|
|
72
|
-
windowHeight: window.innerHeight,
|
|
73
|
-
ignoreElements: (element) => {
|
|
74
|
-
// Skip elements that might have unsupported CSS (oklab, oklch, color-mix)
|
|
75
|
-
const style = window.getComputedStyle(element);
|
|
76
|
-
const hasUnsupportedColor = [
|
|
77
|
-
style.color,
|
|
78
|
-
style.backgroundColor,
|
|
79
|
-
style.borderColor,
|
|
80
|
-
].some(color => color && (color.includes('oklab') || color.includes('oklch') || color.includes('color-mix')));
|
|
81
|
-
return hasUnsupportedColor;
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
return canvas.toDataURL('image/jpeg', 0.7);
|
|
85
|
-
}
|
|
86
|
-
catch (err) {
|
|
87
|
-
console.error('[DevFeedback] Screenshot capture failed:', err);
|
|
88
|
-
if (err instanceof Error && err.message.includes('oklab')) {
|
|
89
|
-
console.warn('[DevFeedback] Your CSS uses oklab/oklch colors which html2canvas doesn\'t support. Screenshot skipped.');
|
|
90
|
-
}
|
|
91
|
-
// html2canvas failed (unsupported CSS, cross-origin iframe, etc.)
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|