@alliance-droid/status-feedback-system 0.2.1 → 0.3.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.
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { createStatusClient } from '../api.js';
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
Button,
|
|
7
|
+
Input,
|
|
8
|
+
Textarea,
|
|
9
|
+
Select,
|
|
10
|
+
Badge,
|
|
11
|
+
Spinner,
|
|
12
|
+
} from '@alliance-droid/svelte-component-library';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
/** Explicit enable/disable — overrides auto-detection */
|
|
18
|
+
enabled?: boolean | undefined;
|
|
19
|
+
/** Max console errors to capture */
|
|
20
|
+
maxConsoleErrors?: number;
|
|
21
|
+
/** Show onboarding toast on first visit */
|
|
22
|
+
showOnboarding?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let {
|
|
26
|
+
apiUrl,
|
|
27
|
+
apiKey,
|
|
28
|
+
enabled = undefined,
|
|
29
|
+
maxConsoleErrors = 20,
|
|
30
|
+
showOnboarding = true,
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
let client = $derived(createStatusClient({ apiUrl, apiKey }));
|
|
34
|
+
|
|
35
|
+
// --- Environment gate ---
|
|
36
|
+
let isEnabled = $state(false);
|
|
37
|
+
|
|
38
|
+
function getEnvVar(key: string): string | undefined {
|
|
39
|
+
try {
|
|
40
|
+
// Works in Vite/SvelteKit apps
|
|
41
|
+
const env = (globalThis as Record<string, unknown>).__sveltekit_env as Record<string, string> | undefined;
|
|
42
|
+
if (env?.[key]) return env[key];
|
|
43
|
+
} catch { /* ignore */ }
|
|
44
|
+
try {
|
|
45
|
+
// Window-injected env (common pattern)
|
|
46
|
+
const win = window as Record<string, unknown>;
|
|
47
|
+
const env = win.__ENV as Record<string, string> | undefined;
|
|
48
|
+
if (env?.[key]) return env[key];
|
|
49
|
+
} catch { /* ignore */ }
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function detectEnvironment(): boolean {
|
|
54
|
+
if (enabled !== undefined) return enabled;
|
|
55
|
+
if (typeof window === 'undefined') return false;
|
|
56
|
+
|
|
57
|
+
// Check explicit env var
|
|
58
|
+
const feedbackEnabled = getEnvVar('PUBLIC_FEEDBACK_ENABLED');
|
|
59
|
+
if (feedbackEnabled === 'true') return true;
|
|
60
|
+
if (feedbackEnabled === 'false') return false;
|
|
61
|
+
|
|
62
|
+
// Vercel environment
|
|
63
|
+
const vercelEnv = getEnvVar('VERCEL_ENV');
|
|
64
|
+
if (vercelEnv === 'production') return false;
|
|
65
|
+
if (vercelEnv) return true;
|
|
66
|
+
|
|
67
|
+
// Localhost fallback
|
|
68
|
+
const host = window.location.hostname;
|
|
69
|
+
return host === 'localhost' || host === '127.0.0.1' || host.endsWith('.local');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --- State ---
|
|
73
|
+
let menuOpen = $state(false);
|
|
74
|
+
let menuX = $state(0);
|
|
75
|
+
let menuY = $state(0);
|
|
76
|
+
let clickedElement = $state<HTMLElement | null>(null);
|
|
77
|
+
|
|
78
|
+
let formOpen = $state(false);
|
|
79
|
+
let formType = $state<'bug' | 'feature'>('bug');
|
|
80
|
+
let formTitle = $state('');
|
|
81
|
+
let formMessage = $state('');
|
|
82
|
+
let formEmail = $state('');
|
|
83
|
+
let submitting = $state(false);
|
|
84
|
+
let submitResult = $state<'success' | 'error' | null>(null);
|
|
85
|
+
|
|
86
|
+
let screenshotData = $state<string | null>(null);
|
|
87
|
+
let capturingScreenshot = $state(false);
|
|
88
|
+
|
|
89
|
+
// Console error capture
|
|
90
|
+
let consoleErrors: string[] = [];
|
|
91
|
+
let originalConsoleError: typeof console.error;
|
|
92
|
+
|
|
93
|
+
// Onboarding
|
|
94
|
+
let showToast = $state(false);
|
|
95
|
+
const ONBOARDING_KEY = 'sf-devfeedback-onboarded';
|
|
96
|
+
|
|
97
|
+
onMount(() => {
|
|
98
|
+
isEnabled = detectEnvironment();
|
|
99
|
+
if (!isEnabled) return;
|
|
100
|
+
|
|
101
|
+
// Hook console.error
|
|
102
|
+
originalConsoleError = console.error;
|
|
103
|
+
console.error = (...args: unknown[]) => {
|
|
104
|
+
consoleErrors.push(args.map(String).join(' '));
|
|
105
|
+
if (consoleErrors.length > maxConsoleErrors) consoleErrors.shift();
|
|
106
|
+
originalConsoleError.apply(console, args);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Middle-click handler
|
|
110
|
+
document.addEventListener('auxclick', handleAuxClick);
|
|
111
|
+
// Close menu on any other click
|
|
112
|
+
document.addEventListener('click', closeMenu);
|
|
113
|
+
document.addEventListener('contextmenu', closeMenu);
|
|
114
|
+
// Escape closes everything
|
|
115
|
+
document.addEventListener('keydown', handleKeydown);
|
|
116
|
+
|
|
117
|
+
// Onboarding toast
|
|
118
|
+
if (showOnboarding && !localStorage.getItem(ONBOARDING_KEY)) {
|
|
119
|
+
showToast = true;
|
|
120
|
+
localStorage.setItem(ONBOARDING_KEY, 'true');
|
|
121
|
+
setTimeout(() => (showToast = false), 6000);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
onDestroy(() => {
|
|
126
|
+
if (!isEnabled) return;
|
|
127
|
+
if (originalConsoleError) console.error = originalConsoleError;
|
|
128
|
+
document.removeEventListener('auxclick', handleAuxClick);
|
|
129
|
+
document.removeEventListener('click', closeMenu);
|
|
130
|
+
document.removeEventListener('contextmenu', closeMenu);
|
|
131
|
+
document.removeEventListener('keydown', handleKeydown);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
function handleAuxClick(e: MouseEvent) {
|
|
135
|
+
// Button 1 = middle click
|
|
136
|
+
if (e.button !== 1) return;
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
e.stopPropagation();
|
|
139
|
+
|
|
140
|
+
clickedElement = e.target as HTMLElement;
|
|
141
|
+
menuX = e.clientX;
|
|
142
|
+
menuY = e.clientY;
|
|
143
|
+
menuOpen = true;
|
|
144
|
+
formOpen = false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function closeMenu() {
|
|
148
|
+
if (menuOpen) menuOpen = false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
152
|
+
if (e.key === 'Escape') {
|
|
153
|
+
menuOpen = false;
|
|
154
|
+
formOpen = false;
|
|
155
|
+
screenshotData = null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getElementSelector(el: HTMLElement | null): string {
|
|
160
|
+
if (!el) return '';
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
let current: HTMLElement | null = el;
|
|
163
|
+
while (current && current !== document.body) {
|
|
164
|
+
let selector = current.tagName.toLowerCase();
|
|
165
|
+
if (current.id) {
|
|
166
|
+
selector += `#${current.id}`;
|
|
167
|
+
parts.unshift(selector);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
if (current.className && typeof current.className === 'string') {
|
|
171
|
+
const classes = current.className.trim().split(/\s+/).slice(0, 2).join('.');
|
|
172
|
+
if (classes) selector += `.${classes}`;
|
|
173
|
+
}
|
|
174
|
+
parts.unshift(selector);
|
|
175
|
+
current = current.parentElement;
|
|
176
|
+
}
|
|
177
|
+
return parts.join(' > ');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getElementHtml(el: HTMLElement | null): string {
|
|
181
|
+
if (!el) return '';
|
|
182
|
+
const html = el.outerHTML;
|
|
183
|
+
return html.length > 500 ? html.slice(0, 500) + '...' : html;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function captureScreenshot() {
|
|
187
|
+
capturingScreenshot = true;
|
|
188
|
+
try {
|
|
189
|
+
const html2canvas = (await import('html2canvas')).default;
|
|
190
|
+
const canvas = await html2canvas(document.body, {
|
|
191
|
+
logging: false,
|
|
192
|
+
useCORS: true,
|
|
193
|
+
scale: 1,
|
|
194
|
+
width: window.innerWidth,
|
|
195
|
+
height: window.innerHeight,
|
|
196
|
+
windowWidth: window.innerWidth,
|
|
197
|
+
windowHeight: window.innerHeight,
|
|
198
|
+
});
|
|
199
|
+
screenshotData = canvas.toDataURL('image/png');
|
|
200
|
+
} catch (e) {
|
|
201
|
+
console.warn('Screenshot capture failed:', e);
|
|
202
|
+
screenshotData = null;
|
|
203
|
+
} finally {
|
|
204
|
+
capturingScreenshot = false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function openForm(type: 'bug' | 'feature') {
|
|
209
|
+
formType = type;
|
|
210
|
+
formTitle = '';
|
|
211
|
+
formMessage = '';
|
|
212
|
+
formEmail = '';
|
|
213
|
+
submitResult = null;
|
|
214
|
+
menuOpen = false;
|
|
215
|
+
formOpen = true;
|
|
216
|
+
|
|
217
|
+
// Auto-capture screenshot for bugs
|
|
218
|
+
if (type === 'bug') {
|
|
219
|
+
captureScreenshot();
|
|
220
|
+
} else {
|
|
221
|
+
screenshotData = null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function handleSubmit() {
|
|
226
|
+
if (!formMessage.trim()) return;
|
|
227
|
+
submitting = true;
|
|
228
|
+
submitResult = null;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const metadata: Record<string, unknown> = {
|
|
232
|
+
type: formType,
|
|
233
|
+
environment: getEnvironmentName(),
|
|
234
|
+
route: window.location.pathname,
|
|
235
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
236
|
+
userAgent: navigator.userAgent,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (clickedElement) {
|
|
240
|
+
metadata.elementSelector = getElementSelector(clickedElement);
|
|
241
|
+
metadata.elementHtml = getElementHtml(clickedElement);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (consoleErrors.length > 0) {
|
|
245
|
+
metadata.consoleErrors = [...consoleErrors];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (screenshotData) {
|
|
249
|
+
metadata.screenshot = screenshotData;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Deployment info
|
|
253
|
+
const gitCommit = getEnvVar('VERCEL_GIT_COMMIT_SHA');
|
|
254
|
+
const gitBranch = getEnvVar('VERCEL_GIT_COMMIT_REF');
|
|
255
|
+
const deployUrl = getEnvVar('VERCEL_URL');
|
|
256
|
+
if (gitCommit) metadata.gitCommit = gitCommit;
|
|
257
|
+
if (gitBranch) metadata.gitBranch = gitBranch;
|
|
258
|
+
if (deployUrl) metadata.deploymentUrl = deployUrl;
|
|
259
|
+
|
|
260
|
+
await client.submitFeedback({
|
|
261
|
+
title: formTitle.trim() || undefined,
|
|
262
|
+
message: formMessage.trim(),
|
|
263
|
+
category: formType === 'bug' ? 'Bug' : 'Feature Request',
|
|
264
|
+
email: formEmail.trim() || undefined,
|
|
265
|
+
pageUrl: window.location.href,
|
|
266
|
+
metadata,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
submitResult = 'success';
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
formOpen = false;
|
|
272
|
+
submitResult = null;
|
|
273
|
+
screenshotData = null;
|
|
274
|
+
}, 2000);
|
|
275
|
+
} catch {
|
|
276
|
+
submitResult = 'error';
|
|
277
|
+
} finally {
|
|
278
|
+
submitting = false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getEnvironmentName(): string {
|
|
283
|
+
const vercelEnv = getEnvVar('VERCEL_ENV');
|
|
284
|
+
if (vercelEnv) return vercelEnv;
|
|
285
|
+
if (window.location.hostname === 'localhost') return 'development';
|
|
286
|
+
return 'unknown';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function openBoard() {
|
|
290
|
+
menuOpen = false;
|
|
291
|
+
window.open('/feedback', '_blank');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Ensure menu stays in viewport
|
|
295
|
+
let adjustedX = $derived(Math.min(menuX, window.innerWidth - 200));
|
|
296
|
+
let adjustedY = $derived(Math.min(menuY, window.innerHeight - 160));
|
|
297
|
+
</script>
|
|
298
|
+
|
|
299
|
+
{#if isEnabled}
|
|
300
|
+
<!-- Context menu -->
|
|
301
|
+
{#if menuOpen}
|
|
302
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
303
|
+
<div
|
|
304
|
+
class="fixed z-[99999]"
|
|
305
|
+
style="left: {adjustedX}px; top: {adjustedY}px;"
|
|
306
|
+
onmousedown|stopPropagation={() => {}}
|
|
307
|
+
>
|
|
308
|
+
<div class="bg-surface-secondary border border-border rounded-lg shadow-xl overflow-hidden min-w-[180px]">
|
|
309
|
+
<button
|
|
310
|
+
class="flex items-center gap-2.5 w-full px-3 py-2.5 text-sm text-foreground hover:bg-surface-hover transition-colors text-left"
|
|
311
|
+
onclick={() => openForm('bug')}
|
|
312
|
+
>
|
|
313
|
+
<i class="fa-solid fa-bug text-red-400"></i>
|
|
314
|
+
Report Bug
|
|
315
|
+
</button>
|
|
316
|
+
<button
|
|
317
|
+
class="flex items-center gap-2.5 w-full px-3 py-2.5 text-sm text-foreground hover:bg-surface-hover transition-colors text-left"
|
|
318
|
+
onclick={() => openForm('feature')}
|
|
319
|
+
>
|
|
320
|
+
<i class="fa-solid fa-lightbulb text-amber-400"></i>
|
|
321
|
+
Request Feature
|
|
322
|
+
</button>
|
|
323
|
+
<div class="border-t border-border">
|
|
324
|
+
<button
|
|
325
|
+
class="flex items-center gap-2.5 w-full px-3 py-2.5 text-sm text-foreground-secondary hover:bg-surface-hover transition-colors text-left"
|
|
326
|
+
onclick={openBoard}
|
|
327
|
+
>
|
|
328
|
+
<i class="fa-solid fa-clipboard-list"></i>
|
|
329
|
+
View Open Issues
|
|
330
|
+
</button>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
{/if}
|
|
335
|
+
|
|
336
|
+
<!-- Form modal -->
|
|
337
|
+
{#if formOpen}
|
|
338
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
339
|
+
<div class="fixed inset-0 z-[99998] flex items-center justify-center bg-black/50 backdrop-blur-sm" onmousedown|self={() => (formOpen = false)}>
|
|
340
|
+
<div class="w-full max-w-lg mx-4" onmousedown|stopPropagation={() => {}}>
|
|
341
|
+
<Card variant="elevated" padding="none">
|
|
342
|
+
<!-- Header -->
|
|
343
|
+
<div class="flex items-center justify-between px-5 py-4 border-b border-border">
|
|
344
|
+
<div class="flex items-center gap-2">
|
|
345
|
+
{#if formType === 'bug'}
|
|
346
|
+
<i class="fa-solid fa-bug text-red-400"></i>
|
|
347
|
+
<h3 class="text-base font-semibold">Report Bug</h3>
|
|
348
|
+
{:else}
|
|
349
|
+
<i class="fa-solid fa-lightbulb text-amber-400"></i>
|
|
350
|
+
<h3 class="text-base font-semibold">Request Feature</h3>
|
|
351
|
+
{/if}
|
|
352
|
+
<Badge variant="neutral" size="sm">{getEnvironmentName()}</Badge>
|
|
353
|
+
</div>
|
|
354
|
+
<button
|
|
355
|
+
class="text-foreground-tertiary hover:text-foreground transition-colors bg-transparent border-none cursor-pointer text-lg"
|
|
356
|
+
onclick={() => (formOpen = false)}
|
|
357
|
+
>
|
|
358
|
+
<i class="fa-solid fa-xmark"></i>
|
|
359
|
+
</button>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
<!-- Body -->
|
|
363
|
+
<div class="p-5 space-y-4">
|
|
364
|
+
{#if submitResult === 'success'}
|
|
365
|
+
<div class="text-center py-6">
|
|
366
|
+
<i class="fa-solid fa-check-circle text-3xl text-green-400"></i>
|
|
367
|
+
<p class="mt-2 font-semibold">Submitted!</p>
|
|
368
|
+
<p class="text-sm text-foreground-secondary">
|
|
369
|
+
{formType === 'bug' ? "We'll look into this." : "Thanks for the suggestion!"}
|
|
370
|
+
</p>
|
|
371
|
+
</div>
|
|
372
|
+
{:else}
|
|
373
|
+
<Input
|
|
374
|
+
bind:value={formTitle}
|
|
375
|
+
placeholder={formType === 'bug' ? 'Brief summary of the bug' : 'Feature title'}
|
|
376
|
+
label="Title"
|
|
377
|
+
/>
|
|
378
|
+
|
|
379
|
+
<Textarea
|
|
380
|
+
bind:value={formMessage}
|
|
381
|
+
placeholder={formType === 'bug'
|
|
382
|
+
? 'What happened? What did you expect?'
|
|
383
|
+
: 'Describe the feature you\'d like...'}
|
|
384
|
+
label={formType === 'bug' ? 'Description' : 'Details'}
|
|
385
|
+
rows={4}
|
|
386
|
+
/>
|
|
387
|
+
|
|
388
|
+
<Input
|
|
389
|
+
type="email"
|
|
390
|
+
bind:value={formEmail}
|
|
391
|
+
placeholder="your@email.com (optional)"
|
|
392
|
+
label="Email"
|
|
393
|
+
/>
|
|
394
|
+
|
|
395
|
+
<!-- Auto-captured context (bugs only) -->
|
|
396
|
+
{#if formType === 'bug'}
|
|
397
|
+
<div class="space-y-2">
|
|
398
|
+
<p class="text-xs font-semibold text-foreground-tertiary uppercase tracking-wide">Auto-captured</p>
|
|
399
|
+
<div class="flex flex-wrap gap-1.5">
|
|
400
|
+
<Badge variant="info" size="sm">
|
|
401
|
+
<i class="fa-solid fa-route mr-1"></i>
|
|
402
|
+
{window.location.pathname}
|
|
403
|
+
</Badge>
|
|
404
|
+
<Badge variant="info" size="sm">
|
|
405
|
+
<i class="fa-solid fa-display mr-1"></i>
|
|
406
|
+
{window.innerWidth}×{window.innerHeight}
|
|
407
|
+
</Badge>
|
|
408
|
+
{#if consoleErrors.length > 0}
|
|
409
|
+
<Badge variant="danger" size="sm">
|
|
410
|
+
<i class="fa-solid fa-triangle-exclamation mr-1"></i>
|
|
411
|
+
{consoleErrors.length} console error{consoleErrors.length !== 1 ? 's' : ''}
|
|
412
|
+
</Badge>
|
|
413
|
+
{/if}
|
|
414
|
+
{#if clickedElement}
|
|
415
|
+
<Badge variant="neutral" size="sm">
|
|
416
|
+
<i class="fa-solid fa-crosshairs mr-1"></i>
|
|
417
|
+
{getElementSelector(clickedElement).slice(-40)}
|
|
418
|
+
</Badge>
|
|
419
|
+
{/if}
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<!-- Screenshot preview -->
|
|
423
|
+
{#if capturingScreenshot}
|
|
424
|
+
<div class="flex items-center gap-2 text-sm text-foreground-tertiary">
|
|
425
|
+
<Spinner /> Capturing screenshot...
|
|
426
|
+
</div>
|
|
427
|
+
{:else if screenshotData}
|
|
428
|
+
<div class="relative">
|
|
429
|
+
<img
|
|
430
|
+
src={screenshotData}
|
|
431
|
+
alt="Screenshot"
|
|
432
|
+
class="w-full rounded-md border border-border max-h-40 object-cover object-top"
|
|
433
|
+
/>
|
|
434
|
+
<button
|
|
435
|
+
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"
|
|
436
|
+
onclick={() => (screenshotData = null)}
|
|
437
|
+
>
|
|
438
|
+
<i class="fa-solid fa-xmark"></i>
|
|
439
|
+
</button>
|
|
440
|
+
</div>
|
|
441
|
+
{/if}
|
|
442
|
+
</div>
|
|
443
|
+
{/if}
|
|
444
|
+
|
|
445
|
+
{#if submitResult === 'error'}
|
|
446
|
+
<p class="text-sm text-red-400">
|
|
447
|
+
<i class="fa-solid fa-exclamation-circle mr-1"></i>
|
|
448
|
+
Failed to submit. Please try again.
|
|
449
|
+
</p>
|
|
450
|
+
{/if}
|
|
451
|
+
|
|
452
|
+
<div class="flex justify-end gap-2 pt-1">
|
|
453
|
+
<Button variant="neutral" style="outlined" onclick={() => (formOpen = false)}>
|
|
454
|
+
Cancel
|
|
455
|
+
</Button>
|
|
456
|
+
<Button
|
|
457
|
+
variant={formType === 'bug' ? 'danger' : 'primary'}
|
|
458
|
+
disabled={submitting || !formMessage.trim()}
|
|
459
|
+
loading={submitting}
|
|
460
|
+
onclick={handleSubmit}
|
|
461
|
+
>
|
|
462
|
+
{formType === 'bug' ? 'Report Bug' : 'Submit Request'}
|
|
463
|
+
</Button>
|
|
464
|
+
</div>
|
|
465
|
+
{/if}
|
|
466
|
+
</div>
|
|
467
|
+
</Card>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
{/if}
|
|
471
|
+
|
|
472
|
+
<!-- Onboarding toast -->
|
|
473
|
+
{#if showToast}
|
|
474
|
+
<div class="fixed bottom-4 right-4 z-[99997] animate-fade-in">
|
|
475
|
+
<div class="bg-surface-secondary border border-border rounded-lg shadow-lg px-4 py-3 flex items-center gap-3 max-w-sm">
|
|
476
|
+
<i class="fa-solid fa-computer-mouse text-primary text-lg"></i>
|
|
477
|
+
<div>
|
|
478
|
+
<p class="text-sm font-semibold">Middle-click anywhere</p>
|
|
479
|
+
<p class="text-xs text-foreground-secondary">to report bugs or request features</p>
|
|
480
|
+
</div>
|
|
481
|
+
<button
|
|
482
|
+
class="text-foreground-tertiary hover:text-foreground transition-colors bg-transparent border-none cursor-pointer ml-2"
|
|
483
|
+
onclick={() => (showToast = false)}
|
|
484
|
+
>
|
|
485
|
+
<i class="fa-solid fa-xmark"></i>
|
|
486
|
+
</button>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
{/if}
|
|
490
|
+
{/if}
|
|
491
|
+
|
|
492
|
+
<style>
|
|
493
|
+
@keyframes fade-in {
|
|
494
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
495
|
+
to { opacity: 1; transform: translateY(0); }
|
|
496
|
+
}
|
|
497
|
+
.animate-fade-in {
|
|
498
|
+
animation: fade-in 0.3s ease-out;
|
|
499
|
+
}
|
|
500
|
+
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
apiUrl: string;
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** Explicit enable/disable — overrides auto-detection */
|
|
5
|
+
enabled?: boolean | undefined;
|
|
6
|
+
/** Max console errors to capture */
|
|
7
|
+
maxConsoleErrors?: number;
|
|
8
|
+
/** Show onboarding toast on first visit */
|
|
9
|
+
showOnboarding?: boolean;
|
|
10
|
+
}
|
|
11
|
+
declare const DevFeedback: import("svelte").Component<Props, {}, "">;
|
|
12
|
+
type DevFeedback = ReturnType<typeof DevFeedback>;
|
|
13
|
+
export default DevFeedback;
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type { StatusClient, StatusClientConfig, StatusData, IncidentData, Uptime
|
|
|
13
13
|
export { default as StatusPage } from './components/StatusPage.svelte';
|
|
14
14
|
export { default as StatusBanner } from './components/StatusBanner.svelte';
|
|
15
15
|
export { default as FeedbackForm } from './components/FeedbackForm.svelte';
|
|
16
|
+
export { default as DevFeedback } from './components/DevFeedback.svelte';
|
|
16
17
|
export { default as FeedbackBoard } from './components/FeedbackBoard.svelte';
|
|
17
18
|
export { default as FeedbackButton } from './components/FeedbackButton.svelte';
|
|
18
19
|
export { default as StatusIndicator } from './components/shared/StatusIndicator.svelte';
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export { createStatusClient } from './api.js';
|
|
|
13
13
|
export { default as StatusPage } from './components/StatusPage.svelte';
|
|
14
14
|
export { default as StatusBanner } from './components/StatusBanner.svelte';
|
|
15
15
|
export { default as FeedbackForm } from './components/FeedbackForm.svelte';
|
|
16
|
+
export { default as DevFeedback } from './components/DevFeedback.svelte';
|
|
16
17
|
export { default as FeedbackBoard } from './components/FeedbackBoard.svelte';
|
|
17
18
|
export { default as FeedbackButton } from './components/FeedbackButton.svelte';
|
|
18
19
|
// ── Shared Components ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alliance-droid/status-feedback-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"access": "restricted"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@alliance-droid/svelte-component-library": ">=0.1.0"
|
|
60
|
+
"@alliance-droid/svelte-component-library": ">=0.1.0",
|
|
61
|
+
"html2canvas": "^1.4.1"
|
|
61
62
|
}
|
|
62
63
|
}
|