@alliance-droid/status-feedback-system 2.0.0 → 2.2.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 +7 -0
- package/dist/components/DevFeedback.svelte +97 -58
- package/dist/components/DevFeedback.svelte.d.ts +3 -7
- package/dist/components/shared/PasteImageInput.svelte +169 -0
- package/dist/components/shared/PasteImageInput.svelte.d.ts +14 -0
- package/dist/components/shared/index.d.ts +1 -0
- package/dist/components/shared/index.js +1 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/package.json +64 -62
- package/dist/components/dev/screenshot.d.ts +0 -36
- package/dist/components/dev/screenshot.js +0 -94
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `<DevFeedback>` *Report Bug* form: paste a screenshot from clipboard (Cmd/Ctrl+V) into the description textarea. Pasted images attach to the resulting ADO work item. When both a pasted image and a server-side capture are available, the pasted image wins.
|
|
8
|
+
- `<PasteImageInput>` shared component exported from the package root for consumers that want to build their own paste-to-attach inputs.
|
|
9
|
+
|
|
3
10
|
## 2.0.0
|
|
4
11
|
|
|
5
12
|
### Breaking Changes
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount, onDestroy } from 'svelte';
|
|
3
3
|
import { createStatusClient, type StatusClient } from '../api.js';
|
|
4
|
+
import PasteImageInput from './shared/PasteImageInput.svelte';
|
|
4
5
|
import {
|
|
5
6
|
Card,
|
|
6
7
|
Button,
|
|
@@ -11,7 +12,6 @@
|
|
|
11
12
|
} from '@alliance-droid/svelte-component-library';
|
|
12
13
|
import DevContextMenu from './dev/DevContextMenu.svelte';
|
|
13
14
|
import DevOnboardingToast from './dev/DevOnboardingToast.svelte';
|
|
14
|
-
import { captureScreenshot, type ScreenshotProvider } from './dev/screenshot.js';
|
|
15
15
|
import {
|
|
16
16
|
detectEnvironment,
|
|
17
17
|
getEnvironmentName,
|
|
@@ -40,13 +40,10 @@
|
|
|
40
40
|
*/
|
|
41
41
|
userEmail: string;
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* screenshot capture to a server endpoint (e.g. Puppeteer / screenshot-service).
|
|
46
|
-
*
|
|
47
|
-
* See `createServerScreenshotProvider()` for a ready-made helper.
|
|
43
|
+
* Endpoint on the consumer app that issues render tokens.
|
|
44
|
+
* Default: '/svc/internal/issue-render-token'
|
|
48
45
|
*/
|
|
49
|
-
|
|
46
|
+
renderTokenEndpoint?: string;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
49
|
let {
|
|
@@ -57,7 +54,7 @@
|
|
|
57
54
|
showOnboarding = true,
|
|
58
55
|
boardUrl,
|
|
59
56
|
userEmail,
|
|
60
|
-
|
|
57
|
+
renderTokenEndpoint = '/svc/internal/issue-render-token',
|
|
61
58
|
}: Props = $props();
|
|
62
59
|
|
|
63
60
|
// Dev-mode warning when userEmail is empty
|
|
@@ -146,14 +143,18 @@
|
|
|
146
143
|
let formType = $state<'bug' | 'feature'>('bug');
|
|
147
144
|
let formTitle = $state('');
|
|
148
145
|
let formMessage = $state('');
|
|
146
|
+
let pastedScreenshot = $state<string | null>(null);
|
|
147
|
+
let pastedFilename = $state('');
|
|
148
|
+
let messageTextareaWrapperEl: HTMLDivElement | undefined = $state();
|
|
149
|
+
let messageTextareaEl: HTMLTextAreaElement | undefined = $state();
|
|
149
150
|
|
|
150
151
|
|
|
151
152
|
let submitting = $state(false);
|
|
152
153
|
let submitResult = $state<'success' | 'error' | null>(null);
|
|
153
154
|
|
|
154
|
-
let
|
|
155
|
-
let
|
|
156
|
-
let
|
|
155
|
+
let renderContext = $state<{ url: string; scrollX: number; scrollY: number; renderToken: string; issuer: string } | null>(null);
|
|
156
|
+
let fetchingRenderToken = $state(false);
|
|
157
|
+
let renderTokenFailed = $state(false);
|
|
157
158
|
|
|
158
159
|
// Drag state
|
|
159
160
|
let isDragging = $state(false);
|
|
@@ -249,22 +250,52 @@
|
|
|
249
250
|
if (e.key === 'Escape') {
|
|
250
251
|
menuOpen = false;
|
|
251
252
|
formOpen = false;
|
|
252
|
-
|
|
253
|
+
renderContext = null;
|
|
254
|
+
pastedScreenshot = null;
|
|
255
|
+
pastedFilename = '';
|
|
253
256
|
}
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
async function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
async function fetchRenderToken() {
|
|
260
|
+
fetchingRenderToken = true;
|
|
261
|
+
renderTokenFailed = false;
|
|
262
|
+
try {
|
|
263
|
+
const url = window.location.href;
|
|
264
|
+
const scrollX = Math.round(window.scrollX);
|
|
265
|
+
const scrollY = Math.round(window.scrollY);
|
|
266
|
+
const res = await fetch(renderTokenEndpoint, {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
credentials: 'include',
|
|
269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
270
|
+
body: JSON.stringify({ url, scrollX, scrollY }),
|
|
271
|
+
});
|
|
272
|
+
if (!res.ok) {
|
|
273
|
+
renderTokenFailed = true;
|
|
274
|
+
console.warn('[DevFeedback] Render token fetch failed:', res.status);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const { token } = (await res.json()) as { token: string };
|
|
278
|
+
renderContext = { url, scrollX, scrollY, renderToken: token, issuer: new URL(url).origin };
|
|
279
|
+
} catch (err) {
|
|
280
|
+
renderTokenFailed = true;
|
|
281
|
+
console.warn('[DevFeedback] Render token fetch error:', err);
|
|
282
|
+
} finally {
|
|
283
|
+
fetchingRenderToken = false;
|
|
284
|
+
}
|
|
262
285
|
}
|
|
263
286
|
|
|
287
|
+
$effect(() => {
|
|
288
|
+
if (messageTextareaWrapperEl) {
|
|
289
|
+
messageTextareaEl = messageTextareaWrapperEl.querySelector('textarea') ?? undefined;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
264
293
|
function openForm(type: 'bug' | 'feature') {
|
|
265
294
|
formType = type;
|
|
266
295
|
formTitle = '';
|
|
267
296
|
formMessage = '';
|
|
297
|
+
pastedScreenshot = null;
|
|
298
|
+
pastedFilename = '';
|
|
268
299
|
submitResult = null;
|
|
269
300
|
menuOpen = false;
|
|
270
301
|
formOpen = true;
|
|
@@ -273,9 +304,9 @@
|
|
|
273
304
|
modalY = 0;
|
|
274
305
|
|
|
275
306
|
if (type === 'bug') {
|
|
276
|
-
|
|
307
|
+
fetchRenderToken();
|
|
277
308
|
} else {
|
|
278
|
-
|
|
309
|
+
renderContext = null;
|
|
279
310
|
}
|
|
280
311
|
}
|
|
281
312
|
|
|
@@ -302,8 +333,16 @@
|
|
|
302
333
|
metadata.consoleErrors = [...consoleErrors];
|
|
303
334
|
}
|
|
304
335
|
|
|
305
|
-
if (
|
|
306
|
-
metadata.
|
|
336
|
+
if (pastedScreenshot) {
|
|
337
|
+
metadata.pastedScreenshot = pastedScreenshot;
|
|
338
|
+
metadata.pastedScreenshotFilename = pastedFilename;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (renderContext) {
|
|
342
|
+
metadata.renderToken = renderContext.renderToken;
|
|
343
|
+
metadata.issuer = renderContext.issuer;
|
|
344
|
+
metadata.scrollX = renderContext.scrollX;
|
|
345
|
+
metadata.scrollY = renderContext.scrollY;
|
|
307
346
|
}
|
|
308
347
|
|
|
309
348
|
const gitCommit = getEnvVar('VERCEL_GIT_COMMIT_SHA');
|
|
@@ -326,7 +365,9 @@
|
|
|
326
365
|
setTimeout(() => {
|
|
327
366
|
formOpen = false;
|
|
328
367
|
submitResult = null;
|
|
329
|
-
|
|
368
|
+
renderContext = null;
|
|
369
|
+
pastedScreenshot = null;
|
|
370
|
+
pastedFilename = '';
|
|
330
371
|
}, 2000);
|
|
331
372
|
} catch {
|
|
332
373
|
submitResult = 'error';
|
|
@@ -415,13 +456,21 @@
|
|
|
415
456
|
label="Title"
|
|
416
457
|
/>
|
|
417
458
|
|
|
418
|
-
<
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
459
|
+
<div bind:this={messageTextareaWrapperEl}>
|
|
460
|
+
<Textarea
|
|
461
|
+
value={formMessage} oninput={(e) => (formMessage = (e.currentTarget as HTMLTextAreaElement).value)}
|
|
462
|
+
placeholder={formType === 'bug'
|
|
463
|
+
? 'What happened? What did you expect?'
|
|
464
|
+
: 'Describe the feature you\'d like...'}
|
|
465
|
+
label={formType === 'bug' ? 'Description' : 'Details'}
|
|
466
|
+
rows={4}
|
|
467
|
+
/>
|
|
468
|
+
</div>
|
|
469
|
+
<PasteImageInput
|
|
470
|
+
textareaEl={messageTextareaEl}
|
|
471
|
+
bind:image={pastedScreenshot}
|
|
472
|
+
bind:filename={pastedFilename}
|
|
473
|
+
previewClass="mt-2"
|
|
425
474
|
/>
|
|
426
475
|
|
|
427
476
|
<!-- Auto-captured context (bugs only) -->
|
|
@@ -451,40 +500,30 @@
|
|
|
451
500
|
{/if}
|
|
452
501
|
</div>
|
|
453
502
|
|
|
454
|
-
<!--
|
|
455
|
-
{#if
|
|
503
|
+
<!-- Render token status -->
|
|
504
|
+
{#if fetchingRenderToken}
|
|
456
505
|
<div class="flex items-center gap-2 text-sm text-foreground-tertiary">
|
|
457
|
-
<Spinner />
|
|
506
|
+
<Spinner /> Preparing screenshot...
|
|
458
507
|
</div>
|
|
459
|
-
{:else if
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
<button
|
|
467
|
-
class="text-xs text-primary hover:underline bg-transparent border-none cursor-pointer p-0"
|
|
468
|
-
onclick={doScreenshot}
|
|
469
|
-
>
|
|
470
|
-
Retry
|
|
471
|
-
</button>
|
|
472
|
-
</div>
|
|
473
|
-
{:else if screenshotData}
|
|
474
|
-
<div class="relative">
|
|
475
|
-
<img
|
|
476
|
-
src={screenshotData}
|
|
477
|
-
alt="Screenshot"
|
|
478
|
-
class="w-full rounded-md border border-border max-h-40 object-cover object-top"
|
|
479
|
-
/>
|
|
508
|
+
{:else if renderTokenFailed && !pastedScreenshot}
|
|
509
|
+
<div class="rounded-md border border-border bg-surface p-3">
|
|
510
|
+
<div class="flex items-center gap-2 text-sm text-foreground-secondary mb-1">
|
|
511
|
+
<i class="fa-solid fa-image-slash" aria-hidden="true"></i>
|
|
512
|
+
<span>Screenshot setup failed</span>
|
|
513
|
+
</div>
|
|
514
|
+
<p class="text-xs text-foreground-tertiary mb-2">Your feedback will be submitted without a screenshot.</p>
|
|
480
515
|
<button
|
|
481
|
-
class="
|
|
482
|
-
onclick={
|
|
483
|
-
aria-label="Remove screenshot"
|
|
516
|
+
class="text-xs text-primary hover:underline bg-transparent border-none cursor-pointer p-0"
|
|
517
|
+
onclick={fetchRenderToken}
|
|
484
518
|
>
|
|
485
|
-
|
|
519
|
+
Retry
|
|
486
520
|
</button>
|
|
487
521
|
</div>
|
|
522
|
+
{:else if renderContext && !pastedScreenshot}
|
|
523
|
+
<div class="rounded-md border border-border bg-surface-secondary p-2 flex items-center gap-2 text-sm text-foreground-secondary">
|
|
524
|
+
<i class="fa-solid fa-camera" aria-hidden="true"></i>
|
|
525
|
+
<span>Screenshot will be captured server-side on submit</span>
|
|
526
|
+
</div>
|
|
488
527
|
{/if}
|
|
489
528
|
</div>
|
|
490
529
|
{/if}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type ScreenshotProvider } from './dev/screenshot.js';
|
|
2
1
|
interface Props {
|
|
3
2
|
apiUrl: string;
|
|
4
3
|
apiKey: string;
|
|
@@ -19,13 +18,10 @@ interface Props {
|
|
|
19
18
|
*/
|
|
20
19
|
userEmail: string;
|
|
21
20
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* screenshot capture to a server endpoint (e.g. Puppeteer / screenshot-service).
|
|
25
|
-
*
|
|
26
|
-
* See `createServerScreenshotProvider()` for a ready-made helper.
|
|
21
|
+
* Endpoint on the consumer app that issues render tokens.
|
|
22
|
+
* Default: '/svc/internal/issue-render-token'
|
|
27
23
|
*/
|
|
28
|
-
|
|
24
|
+
renderTokenEndpoint?: string;
|
|
29
25
|
}
|
|
30
26
|
declare const DevFeedback: import("svelte").Component<Props, {}, "">;
|
|
31
27
|
type DevFeedback = ReturnType<typeof DevFeedback>;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
textareaEl: HTMLTextAreaElement | undefined;
|
|
6
|
+
image?: string | null;
|
|
7
|
+
filename?: string;
|
|
8
|
+
maxBytes?: number;
|
|
9
|
+
allowedMimeTypes?: string[];
|
|
10
|
+
onimage?: (value: string | null) => void;
|
|
11
|
+
previewClass?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
textareaEl,
|
|
16
|
+
image = $bindable(null),
|
|
17
|
+
filename = $bindable(''),
|
|
18
|
+
maxBytes = 5 * 1024 * 1024,
|
|
19
|
+
allowedMimeTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
|
|
20
|
+
onimage,
|
|
21
|
+
previewClass = '',
|
|
22
|
+
}: Props = $props();
|
|
23
|
+
|
|
24
|
+
let errorMessage = $state('');
|
|
25
|
+
let errorTimer: ReturnType<typeof setTimeout> | null = null;
|
|
26
|
+
let attachedTextarea: HTMLTextAreaElement | null = null;
|
|
27
|
+
|
|
28
|
+
function setError(msg: string) {
|
|
29
|
+
errorMessage = msg;
|
|
30
|
+
if (errorTimer) clearTimeout(errorTimer);
|
|
31
|
+
errorTimer = setTimeout(() => { errorMessage = ''; }, 4000);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function handlePaste(e: ClipboardEvent) {
|
|
35
|
+
const items = Array.from(e.clipboardData?.items ?? []);
|
|
36
|
+
const imageItem = items.find((i) => i.kind === 'file' && i.type.startsWith('image/'));
|
|
37
|
+
if (!imageItem) return;
|
|
38
|
+
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
const file = imageItem.getAsFile();
|
|
41
|
+
if (!file) return;
|
|
42
|
+
|
|
43
|
+
if (!allowedMimeTypes.includes(file.type)) {
|
|
44
|
+
setError('Unsupported image type \u2014 PNG, JPEG, GIF, or WebP only');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (file.size > maxBytes) {
|
|
48
|
+
const limitMb = Math.round(maxBytes / (1024 * 1024));
|
|
49
|
+
setError(`Image too large \u2014 max ${limitMb} MB`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dataUrl = await readFileAsDataUrl(file);
|
|
54
|
+
image = dataUrl;
|
|
55
|
+
filename = generateFilename(file.type);
|
|
56
|
+
errorMessage = '';
|
|
57
|
+
onimage?.(dataUrl);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readFileAsDataUrl(file: File): Promise<string> {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const reader = new FileReader();
|
|
63
|
+
reader.onload = () => resolve(reader.result as string);
|
|
64
|
+
reader.onerror = () => reject(reader.error);
|
|
65
|
+
reader.readAsDataURL(file);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function generateFilename(mime: string): string {
|
|
70
|
+
const ext = mime.split('/')[1] || 'png';
|
|
71
|
+
const now = new Date();
|
|
72
|
+
const stamp = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
73
|
+
return `pasted-image-${stamp}.${ext}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function removeImage() {
|
|
77
|
+
image = null;
|
|
78
|
+
filename = '';
|
|
79
|
+
onimage?.(null);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatSize(dataUrl: string): string {
|
|
83
|
+
const commaIdx = dataUrl.indexOf(',');
|
|
84
|
+
if (commaIdx === -1) return '';
|
|
85
|
+
const bytes = Math.round((dataUrl.length - commaIdx - 1) * 0.75);
|
|
86
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
87
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
88
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
$effect(() => {
|
|
92
|
+
if (attachedTextarea === textareaEl) return;
|
|
93
|
+
if (attachedTextarea) attachedTextarea.removeEventListener('paste', handlePaste);
|
|
94
|
+
attachedTextarea = textareaEl ?? null;
|
|
95
|
+
if (attachedTextarea) attachedTextarea.addEventListener('paste', handlePaste);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
onDestroy(() => {
|
|
99
|
+
if (attachedTextarea) attachedTextarea.removeEventListener('paste', handlePaste);
|
|
100
|
+
if (errorTimer) clearTimeout(errorTimer);
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
{#if image}
|
|
105
|
+
<div class="sf-paste-preview {previewClass}">
|
|
106
|
+
<img src={image} alt={filename} class="sf-paste-thumb" />
|
|
107
|
+
<div class="sf-paste-meta">
|
|
108
|
+
<span class="sf-paste-name">{filename}</span>
|
|
109
|
+
<span class="sf-paste-size">{formatSize(image)}</span>
|
|
110
|
+
</div>
|
|
111
|
+
<button type="button" class="sf-paste-remove" onclick={removeImage} aria-label="Remove image">
|
|
112
|
+
×
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
{/if}
|
|
116
|
+
|
|
117
|
+
{#if errorMessage}
|
|
118
|
+
<p class="sf-paste-error">{errorMessage}</p>
|
|
119
|
+
{/if}
|
|
120
|
+
|
|
121
|
+
<style>
|
|
122
|
+
.sf-paste-preview {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 0.75rem;
|
|
126
|
+
padding: 0.5rem;
|
|
127
|
+
border: 1px solid var(--color-border, #2a2f3a);
|
|
128
|
+
border-radius: 0.5rem;
|
|
129
|
+
background: var(--color-surface-secondary, rgba(255, 255, 255, 0.04));
|
|
130
|
+
}
|
|
131
|
+
.sf-paste-thumb {
|
|
132
|
+
width: 120px;
|
|
133
|
+
height: 80px;
|
|
134
|
+
object-fit: cover;
|
|
135
|
+
border-radius: 0.25rem;
|
|
136
|
+
flex-shrink: 0;
|
|
137
|
+
}
|
|
138
|
+
.sf-paste-meta {
|
|
139
|
+
flex: 1;
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
min-width: 0;
|
|
143
|
+
}
|
|
144
|
+
.sf-paste-name {
|
|
145
|
+
font-size: 0.8125rem;
|
|
146
|
+
color: var(--color-foreground, #e5e7eb);
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
text-overflow: ellipsis;
|
|
150
|
+
}
|
|
151
|
+
.sf-paste-size {
|
|
152
|
+
font-size: 0.75rem;
|
|
153
|
+
color: var(--color-foreground-tertiary, #9ca3af);
|
|
154
|
+
}
|
|
155
|
+
.sf-paste-remove {
|
|
156
|
+
background: transparent;
|
|
157
|
+
border: none;
|
|
158
|
+
color: var(--color-foreground-tertiary, #9ca3af);
|
|
159
|
+
font-size: 1.25rem;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
padding: 0.25rem 0.5rem;
|
|
162
|
+
}
|
|
163
|
+
.sf-paste-remove:hover { color: var(--color-danger, #ef4444); }
|
|
164
|
+
.sf-paste-error {
|
|
165
|
+
margin-top: 0.5rem;
|
|
166
|
+
font-size: 0.8125rem;
|
|
167
|
+
color: var(--color-danger, #ef4444);
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
textareaEl: HTMLTextAreaElement | undefined;
|
|
3
|
+
image?: string | null;
|
|
4
|
+
filename?: string;
|
|
5
|
+
maxBytes?: number;
|
|
6
|
+
allowedMimeTypes?: string[];
|
|
7
|
+
onimage?: (value: string | null) => void;
|
|
8
|
+
previewClass?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const PasteImageInput: import("svelte").Component<Props, {
|
|
11
|
+
removeImage: () => void;
|
|
12
|
+
}, "image" | "filename">;
|
|
13
|
+
type PasteImageInput = ReturnType<typeof PasteImageInput>;
|
|
14
|
+
export default PasteImageInput;
|
|
@@ -2,3 +2,4 @@ export { default as StatusIndicator } from './StatusIndicator.svelte';
|
|
|
2
2
|
export { default as UptimeBar } from './UptimeBar.svelte';
|
|
3
3
|
export { default as IncidentTimeline } from './IncidentTimeline.svelte';
|
|
4
4
|
export { default as SystemCard } from './SystemCard.svelte';
|
|
5
|
+
export { default as PasteImageInput } from './PasteImageInput.svelte';
|
|
@@ -2,3 +2,4 @@ export { default as StatusIndicator } from './StatusIndicator.svelte';
|
|
|
2
2
|
export { default as UptimeBar } from './UptimeBar.svelte';
|
|
3
3
|
export { default as IncidentTimeline } from './IncidentTimeline.svelte';
|
|
4
4
|
export { default as SystemCard } from './SystemCard.svelte';
|
|
5
|
+
export { default as PasteImageInput } from './PasteImageInput.svelte';
|
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';
|
|
@@ -25,3 +23,4 @@ export { default as StatusIndicator } from './components/shared/StatusIndicator.
|
|
|
25
23
|
export { default as UptimeBar } from './components/shared/UptimeBar.svelte';
|
|
26
24
|
export { default as IncidentTimeline } from './components/shared/IncidentTimeline.svelte';
|
|
27
25
|
export { default as SystemCard } from './components/shared/SystemCard.svelte';
|
|
26
|
+
export { default as PasteImageInput } from './components/shared/PasteImageInput.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 ──
|
|
@@ -26,3 +25,4 @@ export { default as StatusIndicator } from './components/shared/StatusIndicator.
|
|
|
26
25
|
export { default as UptimeBar } from './components/shared/UptimeBar.svelte';
|
|
27
26
|
export { default as IncidentTimeline } from './components/shared/IncidentTimeline.svelte';
|
|
28
27
|
export { default as SystemCard } from './components/shared/SystemCard.svelte';
|
|
28
|
+
export { default as PasteImageInput } from './components/shared/PasteImageInput.svelte';
|
package/package.json
CHANGED
|
@@ -1,64 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
2
|
+
"name": "@alliance-droid/status-feedback-system",
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
|
|
6
|
+
"svelte": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"svelte": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"!dist/**/*.test.*",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "svelte-package -o dist && publint",
|
|
21
|
+
"lint": "eslint src",
|
|
22
|
+
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@alliance-droid/svelte-component-library": ">=1.0.0",
|
|
29
|
+
"svelte": "^5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@alliance-droid/svelte-component-library": "^1.0.5",
|
|
33
|
+
"@eslint/js": "^10.0.1",
|
|
34
|
+
"@sveltejs/package": "^2.3.0",
|
|
35
|
+
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
|
36
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
37
|
+
"@testing-library/svelte": "^5.3.1",
|
|
38
|
+
"eslint": "^10.4.0",
|
|
39
|
+
"eslint-plugin-svelte": "^3.17.1",
|
|
40
|
+
"globals": "^17.6.0",
|
|
41
|
+
"jsdom": "^29.0.0",
|
|
42
|
+
"publint": "^0.3.0",
|
|
43
|
+
"svelte": "^5.48.0",
|
|
44
|
+
"svelte-check": "^4.3.0",
|
|
45
|
+
"typescript": "^5.9.0",
|
|
46
|
+
"typescript-eslint": "^8.59.4",
|
|
47
|
+
"vite": "^7.3.0",
|
|
48
|
+
"vitest": "^4.0.0"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"svelte",
|
|
52
|
+
"sveltekit",
|
|
53
|
+
"status-page",
|
|
54
|
+
"feedback",
|
|
55
|
+
"uptime"
|
|
56
|
+
],
|
|
57
|
+
"author": "Alliance Technical Group",
|
|
58
|
+
"license": "UNLICENSED",
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "restricted"
|
|
61
|
+
},
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "git+https://alliance.ghe.com/alliance/status-feedback-system.git"
|
|
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
|
-
}
|