@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 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
- * Custom screenshot provider. When supplied, this is called instead of
44
- * the default html2canvas client-side capture. Use this to delegate
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
- screenshotProvider?: ScreenshotProvider;
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
- screenshotProvider,
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 screenshotData = $state<string | null>(null);
155
- let capturingScreenshot_ = $state(false);
156
- let screenshotFailed = $state(false);
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
- screenshotData = null;
253
+ renderContext = null;
254
+ pastedScreenshot = null;
255
+ pastedFilename = '';
253
256
  }
254
257
  }
255
258
 
256
- async function doScreenshot() {
257
- capturingScreenshot_ = true;
258
- screenshotFailed = false;
259
- screenshotData = await captureScreenshot(screenshotProvider);
260
- if (!screenshotData) screenshotFailed = true;
261
- capturingScreenshot_ = false;
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
- doScreenshot();
307
+ fetchRenderToken();
277
308
  } else {
278
- screenshotData = null;
309
+ renderContext = null;
279
310
  }
280
311
  }
281
312
 
@@ -302,8 +333,16 @@
302
333
  metadata.consoleErrors = [...consoleErrors];
303
334
  }
304
335
 
305
- if (screenshotData) {
306
- metadata.screenshot = screenshotData;
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
- screenshotData = null;
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
- <Textarea
419
- value={formMessage} oninput={(e) => (formMessage = (e.currentTarget as HTMLTextAreaElement).value)}
420
- placeholder={formType === 'bug'
421
- ? 'What happened? What did you expect?'
422
- : 'Describe the feature you\'d like...'}
423
- label={formType === 'bug' ? 'Description' : 'Details'}
424
- rows={4}
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
- <!-- Screenshot preview -->
455
- {#if capturingScreenshot_}
503
+ <!-- Render token status -->
504
+ {#if fetchingRenderToken}
456
505
  <div class="flex items-center gap-2 text-sm text-foreground-tertiary">
457
- <Spinner /> Capturing screenshot...
506
+ <Spinner /> Preparing screenshot...
458
507
  </div>
459
- {:else if screenshotFailed}
460
- <div class="rounded-md border border-border bg-surface p-3">
461
- <div class="flex items-center gap-2 text-sm text-foreground-secondary mb-1">
462
- <i class="fa-solid fa-image-slash" aria-hidden="true"></i>
463
- <span>Screenshot capture failed</span>
464
- </div>
465
- <p class="text-xs text-foreground-tertiary mb-2">Check console for error. May need: <code class="bg-background px-1 rounded">npm install html2canvas</code></p>
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="absolute top-1 right-1 bg-black/60 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs border-none cursor-pointer hover:bg-black/80"
482
- onclick={() => (screenshotData = null)}
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
- <i class="fa-solid fa-xmark"></i>
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
- * Custom screenshot provider. When supplied, this is called instead of
23
- * the default html2canvas client-side capture. Use this to delegate
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
- screenshotProvider?: ScreenshotProvider;
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
+ &times;
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
- "name": "@alliance-droid/status-feedback-system",
3
- "version": "2.0.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": "svelte-check --tsconfig ./tsconfig.json",
22
- "check": "svelte-check --tsconfig ./tsconfig.json",
23
- "test": "vitest run",
24
- "test:watch": "vitest",
25
- "prepublishOnly": "npm run build"
26
- },
27
- "dependencies": {
28
- "html2canvas": "^1.4.1"
29
- },
30
- "peerDependencies": {
31
- "@alliance-droid/svelte-component-library": ">=1.0.0",
32
- "svelte": "^5.0.0"
33
- },
34
- "devDependencies": {
35
- "@alliance-droid/svelte-component-library": "^1.0.5",
36
- "@sveltejs/package": "^2.3.0",
37
- "@sveltejs/vite-plugin-svelte": "^6.0.0",
38
- "@testing-library/jest-dom": "^6.9.1",
39
- "@testing-library/svelte": "^5.3.1",
40
- "jsdom": "^29.0.0",
41
- "publint": "^0.3.0",
42
- "svelte": "^5.48.0",
43
- "svelte-check": "^4.3.0",
44
- "typescript": "^5.9.0",
45
- "vite": "^7.3.0",
46
- "vitest": "^4.0.0"
47
- },
48
- "keywords": [
49
- "svelte",
50
- "sveltekit",
51
- "status-page",
52
- "feedback",
53
- "uptime"
54
- ],
55
- "author": "Alliance Technical Group",
56
- "license": "UNLICENSED",
57
- "publishConfig": {
58
- "access": "restricted"
59
- },
60
- "repository": {
61
- "type": "git",
62
- "url": "git+https://alliance.ghe.com/alliance/status-feedback-system.git"
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
- }