@alliance-droid/status-feedback-system 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -45
- package/LICENSE +11 -0
- package/README.md +7 -3
- package/dist/components/DevFeedback.svelte +162 -322
- package/dist/components/FeedbackBoard.svelte +32 -368
- package/dist/components/FeedbackBoard.svelte.d.ts +2 -5
- package/dist/components/FeedbackForm.svelte +14 -3
- package/dist/components/StatusBanner.svelte +48 -14
- package/dist/components/StatusBanner.svelte.d.ts +2 -0
- package/dist/components/StatusPage.svelte +43 -10
- package/dist/components/StatusPage.svelte.d.ts +2 -0
- package/dist/components/board/FeedbackDetail.svelte +7 -7
- package/dist/components/board/FeedbackItem.svelte +26 -12
- package/dist/components/board/FeedbackList.svelte +21 -1
- package/dist/components/board/FeedbackSidebar.svelte +3 -3
- package/dist/components/board/state.svelte.d.ts +6 -0
- package/dist/components/board/state.svelte.js +42 -2
- package/dist/components/dev/DevContextMenu.svelte +99 -0
- package/dist/components/dev/DevContextMenu.svelte.d.ts +11 -0
- package/dist/components/dev/DevOnboardingToast.svelte +73 -0
- package/dist/components/dev/DevOnboardingToast.svelte.d.ts +6 -0
- package/dist/components/dev/environment.d.ts +24 -0
- package/dist/components/dev/environment.js +93 -0
- package/dist/components/dev/screenshot.d.ts +6 -0
- package/dist/components/dev/screenshot.js +25 -0
- package/dist/components/shared/IncidentTimeline.svelte +3 -3
- package/dist/components/shared/StatusIndicator.svelte +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +18 -0
- package/package.json +11 -6
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount, onDestroy } from 'svelte';
|
|
3
|
-
import { createStatusClient } from '../api.js';
|
|
3
|
+
import { createStatusClient, type StatusClient } from '../api.js';
|
|
4
4
|
import {
|
|
5
5
|
Card,
|
|
6
6
|
Button,
|
|
7
7
|
Input,
|
|
8
8
|
Textarea,
|
|
9
|
-
Select,
|
|
10
9
|
Badge,
|
|
11
10
|
Spinner,
|
|
12
11
|
} from '@alliance-droid/svelte-component-library';
|
|
12
|
+
import DevContextMenu from './dev/DevContextMenu.svelte';
|
|
13
|
+
import DevOnboardingToast from './dev/DevOnboardingToast.svelte';
|
|
14
|
+
import { captureScreenshot } from './dev/screenshot.js';
|
|
15
|
+
import {
|
|
16
|
+
detectEnvironment,
|
|
17
|
+
getEnvironmentName,
|
|
18
|
+
getEnvVar,
|
|
19
|
+
getElementSelector,
|
|
20
|
+
getElementHtml,
|
|
21
|
+
} from './dev/environment.js';
|
|
13
22
|
|
|
14
23
|
interface Props {
|
|
15
24
|
apiUrl: string;
|
|
@@ -36,44 +45,22 @@
|
|
|
36
45
|
userEmail,
|
|
37
46
|
}: Props = $props();
|
|
38
47
|
|
|
39
|
-
|
|
48
|
+
// --- Client (lazy, stable) ---
|
|
49
|
+
let client: StatusClient | null = $state(null);
|
|
50
|
+
let lastApiUrl = '';
|
|
51
|
+
let lastApiKey = '';
|
|
40
52
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (env?.[key]) return env[key];
|
|
49
|
-
} catch { /* ignore */ }
|
|
50
|
-
try {
|
|
51
|
-
// Window-injected env (common pattern)
|
|
52
|
-
const win = window as unknown as Record<string, unknown>;
|
|
53
|
-
const env = win.__ENV as Record<string, string> | undefined;
|
|
54
|
-
if (env?.[key]) return env[key];
|
|
55
|
-
} catch { /* ignore */ }
|
|
56
|
-
return undefined;
|
|
53
|
+
function getClient(): StatusClient {
|
|
54
|
+
if (!client || lastApiUrl !== apiUrl || lastApiKey !== apiKey) {
|
|
55
|
+
client = createStatusClient({ apiUrl, apiKey });
|
|
56
|
+
lastApiUrl = apiUrl;
|
|
57
|
+
lastApiKey = apiKey;
|
|
58
|
+
}
|
|
59
|
+
return client;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (typeof window === 'undefined') return false;
|
|
62
|
-
|
|
63
|
-
// Check explicit env var
|
|
64
|
-
const feedbackEnabled = getEnvVar('PUBLIC_FEEDBACK_ENABLED');
|
|
65
|
-
if (feedbackEnabled === 'true') return true;
|
|
66
|
-
if (feedbackEnabled === 'false') return false;
|
|
67
|
-
|
|
68
|
-
// Vercel environment
|
|
69
|
-
const vercelEnv = getEnvVar('VERCEL_ENV');
|
|
70
|
-
if (vercelEnv === 'production') return false;
|
|
71
|
-
if (vercelEnv) return true;
|
|
72
|
-
|
|
73
|
-
// Localhost fallback
|
|
74
|
-
const host = window.location.hostname;
|
|
75
|
-
return host === 'localhost' || host === '127.0.0.1' || host.endsWith('.local');
|
|
76
|
-
}
|
|
62
|
+
// --- Environment gate ---
|
|
63
|
+
let isEnabled = $state(false);
|
|
77
64
|
|
|
78
65
|
// --- State ---
|
|
79
66
|
let menuOpen = $state(false);
|
|
@@ -90,7 +77,8 @@
|
|
|
90
77
|
let submitResult = $state<'success' | 'error' | null>(null);
|
|
91
78
|
|
|
92
79
|
let screenshotData = $state<string | null>(null);
|
|
93
|
-
let
|
|
80
|
+
let capturingScreenshot_ = $state(false);
|
|
81
|
+
let screenshotFailed = $state(false);
|
|
94
82
|
|
|
95
83
|
// Drag state
|
|
96
84
|
let isDragging = $state(false);
|
|
@@ -130,7 +118,7 @@
|
|
|
130
118
|
const ONBOARDING_KEY = 'sf-devfeedback-onboarded';
|
|
131
119
|
|
|
132
120
|
onMount(() => {
|
|
133
|
-
isEnabled = detectEnvironment();
|
|
121
|
+
isEnabled = detectEnvironment(enabled);
|
|
134
122
|
if (!isEnabled) return;
|
|
135
123
|
|
|
136
124
|
// Hook console.error
|
|
@@ -143,11 +131,8 @@
|
|
|
143
131
|
|
|
144
132
|
// Ctrl+Right-click handler
|
|
145
133
|
document.addEventListener('contextmenu', handleContextMenu);
|
|
146
|
-
// Close menu on any other click
|
|
147
134
|
document.addEventListener('click', closeMenu);
|
|
148
|
-
// Escape closes everything
|
|
149
135
|
document.addEventListener('keydown', handleKeydown);
|
|
150
|
-
// Drag listeners (on document so drag continues outside modal)
|
|
151
136
|
document.addEventListener('mousemove', handleDragMove);
|
|
152
137
|
document.addEventListener('mouseup', handleDragEnd);
|
|
153
138
|
|
|
@@ -170,7 +155,6 @@
|
|
|
170
155
|
});
|
|
171
156
|
|
|
172
157
|
function handleContextMenu(e: MouseEvent) {
|
|
173
|
-
// Only trigger on Ctrl+Right-click
|
|
174
158
|
if (!e.ctrlKey) return;
|
|
175
159
|
e.preventDefault();
|
|
176
160
|
e.stopPropagation();
|
|
@@ -194,54 +178,12 @@
|
|
|
194
178
|
}
|
|
195
179
|
}
|
|
196
180
|
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
let selector = current.tagName.toLowerCase();
|
|
204
|
-
if (current.id) {
|
|
205
|
-
selector += `#${current.id}`;
|
|
206
|
-
parts.unshift(selector);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
if (current.className && typeof current.className === 'string') {
|
|
210
|
-
const classes = current.className.trim().split(/\s+/).slice(0, 2).join('.');
|
|
211
|
-
if (classes) selector += `.${classes}`;
|
|
212
|
-
}
|
|
213
|
-
parts.unshift(selector);
|
|
214
|
-
current = current.parentElement;
|
|
215
|
-
}
|
|
216
|
-
return parts.join(' > ');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function getElementHtml(el: HTMLElement | null): string {
|
|
220
|
-
if (!el) return '';
|
|
221
|
-
const html = el.outerHTML;
|
|
222
|
-
return html.length > 500 ? html.slice(0, 500) + '...' : html;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
async function captureScreenshot() {
|
|
226
|
-
capturingScreenshot = true;
|
|
227
|
-
try {
|
|
228
|
-
const html2canvas = (await import('html2canvas')).default;
|
|
229
|
-
const canvas = await html2canvas(document.body, {
|
|
230
|
-
logging: false,
|
|
231
|
-
useCORS: true,
|
|
232
|
-
scale: 1,
|
|
233
|
-
width: window.innerWidth,
|
|
234
|
-
height: window.innerHeight,
|
|
235
|
-
windowWidth: window.innerWidth,
|
|
236
|
-
windowHeight: window.innerHeight,
|
|
237
|
-
});
|
|
238
|
-
screenshotData = canvas.toDataURL('image/png');
|
|
239
|
-
} catch {
|
|
240
|
-
// Screenshot capture can fail in cross-origin iframes or restricted contexts
|
|
241
|
-
screenshotData = null;
|
|
242
|
-
} finally {
|
|
243
|
-
capturingScreenshot = false;
|
|
244
|
-
}
|
|
181
|
+
async function doScreenshot() {
|
|
182
|
+
capturingScreenshot_ = true;
|
|
183
|
+
screenshotFailed = false;
|
|
184
|
+
screenshotData = await captureScreenshot();
|
|
185
|
+
if (!screenshotData) screenshotFailed = true;
|
|
186
|
+
capturingScreenshot_ = false;
|
|
245
187
|
}
|
|
246
188
|
|
|
247
189
|
function openForm(type: 'bug' | 'feature') {
|
|
@@ -252,14 +194,12 @@
|
|
|
252
194
|
submitResult = null;
|
|
253
195
|
menuOpen = false;
|
|
254
196
|
formOpen = true;
|
|
255
|
-
// Reset drag position (center on screen)
|
|
256
197
|
modalPositioned = false;
|
|
257
198
|
modalX = 0;
|
|
258
199
|
modalY = 0;
|
|
259
200
|
|
|
260
|
-
// Auto-capture screenshot for bugs
|
|
261
201
|
if (type === 'bug') {
|
|
262
|
-
|
|
202
|
+
doScreenshot();
|
|
263
203
|
} else {
|
|
264
204
|
screenshotData = null;
|
|
265
205
|
}
|
|
@@ -292,7 +232,6 @@
|
|
|
292
232
|
metadata.screenshot = screenshotData;
|
|
293
233
|
}
|
|
294
234
|
|
|
295
|
-
// Deployment info
|
|
296
235
|
const gitCommit = getEnvVar('VERCEL_GIT_COMMIT_SHA');
|
|
297
236
|
const gitBranch = getEnvVar('VERCEL_GIT_COMMIT_REF');
|
|
298
237
|
const deployUrl = getEnvVar('VERCEL_URL');
|
|
@@ -300,10 +239,10 @@
|
|
|
300
239
|
if (gitBranch) metadata.gitBranch = gitBranch;
|
|
301
240
|
if (deployUrl) metadata.deploymentUrl = deployUrl;
|
|
302
241
|
|
|
303
|
-
await
|
|
242
|
+
await getClient().submitFeedback({
|
|
304
243
|
title: formTitle.trim() || undefined,
|
|
305
244
|
message: formMessage.trim(),
|
|
306
|
-
category: formType === 'bug' ? '
|
|
245
|
+
category: formType === 'bug' ? 'bug' : 'feature',
|
|
307
246
|
email: formEmail.trim() || undefined,
|
|
308
247
|
pageUrl: window.location.href,
|
|
309
248
|
metadata,
|
|
@@ -322,57 +261,28 @@
|
|
|
322
261
|
}
|
|
323
262
|
}
|
|
324
263
|
|
|
325
|
-
function getEnvironmentName(): string {
|
|
326
|
-
const vercelEnv = getEnvVar('VERCEL_ENV');
|
|
327
|
-
if (vercelEnv) return vercelEnv;
|
|
328
|
-
if (window.location.hostname === 'localhost') return 'development';
|
|
329
|
-
return 'unknown';
|
|
330
|
-
}
|
|
331
|
-
|
|
332
264
|
function openBoard() {
|
|
333
265
|
if (!boardUrl) return;
|
|
334
266
|
menuOpen = false;
|
|
335
267
|
window.open(boardUrl, '_blank');
|
|
336
268
|
}
|
|
337
|
-
|
|
338
|
-
// Ensure menu stays in viewport
|
|
339
|
-
let adjustedX = $derived(Math.min(menuX, window.innerWidth - 200));
|
|
340
|
-
let adjustedY = $derived(Math.min(menuY, window.innerHeight - 160));
|
|
341
269
|
</script>
|
|
342
270
|
|
|
343
271
|
{#if isEnabled}
|
|
344
272
|
<!-- Context menu -->
|
|
345
273
|
{#if menuOpen}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
<i class="fa-solid fa-bug text-red-400"></i>
|
|
355
|
-
Report Bug
|
|
356
|
-
</button>
|
|
357
|
-
<button class="sf-ctx-item" onclick={() => openForm('feature')}>
|
|
358
|
-
<i class="fa-solid fa-lightbulb text-amber-400"></i>
|
|
359
|
-
Request Feature
|
|
360
|
-
</button>
|
|
361
|
-
{#if boardUrl}
|
|
362
|
-
<div class="sf-ctx-divider">
|
|
363
|
-
<button class="sf-ctx-item sf-ctx-item-secondary" onclick={openBoard}>
|
|
364
|
-
<i class="fa-solid fa-clipboard-list"></i>
|
|
365
|
-
View Open Issues
|
|
366
|
-
</button>
|
|
367
|
-
</div>
|
|
368
|
-
{/if}
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
274
|
+
<DevContextMenu
|
|
275
|
+
x={menuX}
|
|
276
|
+
y={menuY}
|
|
277
|
+
{boardUrl}
|
|
278
|
+
onReportBug={() => openForm('bug')}
|
|
279
|
+
onRequestFeature={() => openForm('feature')}
|
|
280
|
+
onViewBoard={boardUrl ? openBoard : undefined}
|
|
281
|
+
/>
|
|
371
282
|
{/if}
|
|
372
283
|
|
|
373
284
|
<!-- Form modal -->
|
|
374
285
|
{#if formOpen}
|
|
375
|
-
<!-- No backdrop — modal floats freely over the page -->
|
|
376
286
|
<div
|
|
377
287
|
class="sf-modal fixed z-[99998] w-full max-w-lg flex flex-col"
|
|
378
288
|
style={modalPositioned
|
|
@@ -381,7 +291,7 @@
|
|
|
381
291
|
}
|
|
382
292
|
>
|
|
383
293
|
<Card variant="elevated" padding="none" class="flex flex-col max-h-[calc(100vh-2rem)] overflow-hidden">
|
|
384
|
-
<!-- Header (
|
|
294
|
+
<!-- Header (draggable) -->
|
|
385
295
|
<!-- svelte-ignore a11y_no_static_element_interactions — drag handle needs mousedown -->
|
|
386
296
|
<div
|
|
387
297
|
class="flex items-center justify-between px-5 py-4 border-b border-border shrink-0 select-none"
|
|
@@ -409,208 +319,138 @@
|
|
|
409
319
|
|
|
410
320
|
<!-- Body (scrollable) -->
|
|
411
321
|
<div class="p-5 space-y-4 overflow-y-auto">
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
322
|
+
{#if submitResult === 'success'}
|
|
323
|
+
<div class="text-center py-6">
|
|
324
|
+
<i class="fa-solid fa-check-circle text-3xl text-green-400"></i>
|
|
325
|
+
<p class="mt-2 font-semibold">Submitted!</p>
|
|
326
|
+
<p class="text-sm text-foreground-secondary">
|
|
327
|
+
{formType === 'bug' ? "We'll look into this." : "Thanks for the suggestion!"}
|
|
328
|
+
</p>
|
|
329
|
+
</div>
|
|
330
|
+
{:else}
|
|
331
|
+
<Input
|
|
332
|
+
bind:value={formTitle}
|
|
333
|
+
placeholder={formType === 'bug' ? 'Brief summary of the bug' : 'Feature title'}
|
|
334
|
+
label="Title"
|
|
335
|
+
/>
|
|
336
|
+
|
|
337
|
+
<Textarea
|
|
338
|
+
value={formMessage} oninput={(e) => (formMessage = (e.currentTarget as HTMLTextAreaElement).value)}
|
|
339
|
+
placeholder={formType === 'bug'
|
|
340
|
+
? 'What happened? What did you expect?'
|
|
341
|
+
: 'Describe the feature you\'d like...'}
|
|
342
|
+
label={formType === 'bug' ? 'Description' : 'Details'}
|
|
343
|
+
rows={4}
|
|
344
|
+
/>
|
|
345
|
+
|
|
346
|
+
<Input
|
|
347
|
+
type="email"
|
|
348
|
+
bind:value={formEmail}
|
|
349
|
+
placeholder="your@email.com (optional)"
|
|
350
|
+
label="Email"
|
|
351
|
+
/>
|
|
352
|
+
|
|
353
|
+
<!-- Auto-captured context (bugs only) -->
|
|
354
|
+
{#if formType === 'bug'}
|
|
355
|
+
<div class="space-y-2">
|
|
356
|
+
<p class="text-xs font-semibold text-foreground-tertiary uppercase tracking-wide">Auto-captured</p>
|
|
357
|
+
<div class="flex flex-wrap gap-1.5">
|
|
358
|
+
<Badge variant="info" size="sm">
|
|
359
|
+
<i class="fa-solid fa-route mr-1"></i>
|
|
360
|
+
{window.location.pathname}
|
|
361
|
+
</Badge>
|
|
362
|
+
<Badge variant="info" size="sm">
|
|
363
|
+
<i class="fa-solid fa-display mr-1"></i>
|
|
364
|
+
{window.innerWidth}×{window.innerHeight}
|
|
365
|
+
</Badge>
|
|
366
|
+
{#if consoleErrors.length > 0}
|
|
367
|
+
<Badge variant="danger" size="sm">
|
|
368
|
+
<i class="fa-solid fa-triangle-exclamation mr-1"></i>
|
|
369
|
+
{consoleErrors.length} console error{consoleErrors.length !== 1 ? 's' : ''}
|
|
451
370
|
</Badge>
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
371
|
+
{/if}
|
|
372
|
+
{#if clickedElement}
|
|
373
|
+
<Badge variant="neutral" size="sm">
|
|
374
|
+
<i class="fa-solid fa-crosshairs mr-1"></i>
|
|
375
|
+
{getElementSelector(clickedElement).slice(-40)}
|
|
455
376
|
</Badge>
|
|
456
|
-
{#if consoleErrors.length > 0}
|
|
457
|
-
<Badge variant="danger" size="sm">
|
|
458
|
-
<i class="fa-solid fa-triangle-exclamation mr-1"></i>
|
|
459
|
-
{consoleErrors.length} console error{consoleErrors.length !== 1 ? 's' : ''}
|
|
460
|
-
</Badge>
|
|
461
|
-
{/if}
|
|
462
|
-
{#if clickedElement}
|
|
463
|
-
<Badge variant="neutral" size="sm">
|
|
464
|
-
<i class="fa-solid fa-crosshairs mr-1"></i>
|
|
465
|
-
{getElementSelector(clickedElement).slice(-40)}
|
|
466
|
-
</Badge>
|
|
467
|
-
{/if}
|
|
468
|
-
</div>
|
|
469
|
-
|
|
470
|
-
<!-- Screenshot preview -->
|
|
471
|
-
{#if capturingScreenshot}
|
|
472
|
-
<div class="flex items-center gap-2 text-sm text-foreground-tertiary">
|
|
473
|
-
<Spinner /> Capturing screenshot...
|
|
474
|
-
</div>
|
|
475
|
-
{:else if screenshotData}
|
|
476
|
-
<div class="relative">
|
|
477
|
-
<img
|
|
478
|
-
src={screenshotData}
|
|
479
|
-
alt="Screenshot"
|
|
480
|
-
class="w-full rounded-md border border-border max-h-40 object-cover object-top"
|
|
481
|
-
/>
|
|
482
|
-
<button
|
|
483
|
-
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"
|
|
484
|
-
onclick={() => (screenshotData = null)}
|
|
485
|
-
aria-label="Remove screenshot"
|
|
486
|
-
>
|
|
487
|
-
<i class="fa-solid fa-xmark"></i>
|
|
488
|
-
</button>
|
|
489
|
-
</div>
|
|
490
377
|
{/if}
|
|
491
378
|
</div>
|
|
492
|
-
{/if}
|
|
493
|
-
|
|
494
|
-
{#if submitResult === 'error'}
|
|
495
|
-
<p class="text-sm text-red-400">
|
|
496
|
-
<i class="fa-solid fa-exclamation-circle mr-1"></i>
|
|
497
|
-
Failed to submit. Please try again.
|
|
498
|
-
</p>
|
|
499
|
-
{/if}
|
|
500
379
|
|
|
501
|
-
|
|
502
|
-
|
|
380
|
+
<!-- Screenshot preview -->
|
|
381
|
+
{#if capturingScreenshot_}
|
|
382
|
+
<div class="flex items-center gap-2 text-sm text-foreground-tertiary">
|
|
383
|
+
<Spinner /> Capturing screenshot...
|
|
384
|
+
</div>
|
|
385
|
+
{:else if screenshotFailed}
|
|
386
|
+
<div class="rounded-md border border-border bg-surface p-3">
|
|
387
|
+
<div class="flex items-center gap-2 text-sm text-foreground-secondary mb-1">
|
|
388
|
+
<i class="fa-solid fa-image-slash" aria-hidden="true"></i>
|
|
389
|
+
<span>Screenshot capture failed</span>
|
|
390
|
+
</div>
|
|
391
|
+
<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>
|
|
392
|
+
<button
|
|
393
|
+
class="text-xs text-primary hover:underline bg-transparent border-none cursor-pointer p-0"
|
|
394
|
+
onclick={doScreenshot}
|
|
395
|
+
>
|
|
396
|
+
Retry
|
|
397
|
+
</button>
|
|
398
|
+
</div>
|
|
399
|
+
{:else if screenshotData}
|
|
400
|
+
<div class="relative">
|
|
401
|
+
<img
|
|
402
|
+
src={screenshotData}
|
|
403
|
+
alt="Screenshot"
|
|
404
|
+
class="w-full rounded-md border border-border max-h-40 object-cover object-top"
|
|
405
|
+
/>
|
|
406
|
+
<button
|
|
407
|
+
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"
|
|
408
|
+
onclick={() => (screenshotData = null)}
|
|
409
|
+
aria-label="Remove screenshot"
|
|
410
|
+
>
|
|
411
|
+
<i class="fa-solid fa-xmark"></i>
|
|
412
|
+
</button>
|
|
413
|
+
</div>
|
|
414
|
+
{/if}
|
|
415
|
+
</div>
|
|
416
|
+
{/if}
|
|
503
417
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
<Button
|
|
511
|
-
variant={formType === 'bug' ? 'danger' : 'primary'}
|
|
512
|
-
disabled={submitting || !formMessage.trim()}
|
|
513
|
-
loading={submitting}
|
|
514
|
-
onclick={handleSubmit}
|
|
515
|
-
>
|
|
516
|
-
{formType === 'bug' ? 'Report Bug' : 'Submit Request'}
|
|
517
|
-
</Button>
|
|
518
|
-
</div>
|
|
418
|
+
{#if submitResult === 'error'}
|
|
419
|
+
<p class="text-sm text-red-400">
|
|
420
|
+
<i class="fa-solid fa-exclamation-circle mr-1"></i>
|
|
421
|
+
Failed to submit. Please try again.
|
|
422
|
+
</p>
|
|
423
|
+
{/if}
|
|
519
424
|
{/if}
|
|
520
|
-
</
|
|
521
|
-
|
|
425
|
+
</div>
|
|
426
|
+
|
|
427
|
+
<!-- Footer -->
|
|
428
|
+
{#if submitResult !== 'success'}
|
|
429
|
+
<div class="flex justify-end gap-2 px-5 py-4 border-t border-border shrink-0">
|
|
430
|
+
<Button variant="neutral" style="outline" onclick={() => (formOpen = false)}>
|
|
431
|
+
Cancel
|
|
432
|
+
</Button>
|
|
433
|
+
<Button
|
|
434
|
+
variant={formType === 'bug' ? 'danger' : 'primary'}
|
|
435
|
+
disabled={submitting || !formMessage.trim()}
|
|
436
|
+
loading={submitting}
|
|
437
|
+
onclick={handleSubmit}
|
|
438
|
+
>
|
|
439
|
+
{formType === 'bug' ? 'Report Bug' : 'Submit Request'}
|
|
440
|
+
</Button>
|
|
441
|
+
</div>
|
|
442
|
+
{/if}
|
|
443
|
+
</Card>
|
|
444
|
+
</div>
|
|
522
445
|
{/if}
|
|
523
446
|
|
|
524
447
|
<!-- Onboarding toast -->
|
|
525
448
|
{#if showToast}
|
|
526
|
-
<
|
|
527
|
-
<div class="sf-toast">
|
|
528
|
-
<i class="fa-solid fa-computer-mouse" style="color: var(--color-primary, #3b82f6); font-size: 1.125rem;"></i>
|
|
529
|
-
<div>
|
|
530
|
-
<p style="font-size: 0.875rem; font-weight: 600; margin: 0;">Ctrl + Right-click anywhere</p>
|
|
531
|
-
<p style="font-size: 0.75rem; color: #94a3b8; margin: 0;">to report bugs or request features</p>
|
|
532
|
-
</div>
|
|
533
|
-
<button class="sf-toast-close" onclick={() => (showToast = false)} aria-label="Dismiss">
|
|
534
|
-
<i class="fa-solid fa-xmark"></i>
|
|
535
|
-
</button>
|
|
536
|
-
</div>
|
|
537
|
-
</div>
|
|
449
|
+
<DevOnboardingToast onDismiss={() => (showToast = false)} />
|
|
538
450
|
{/if}
|
|
539
451
|
{/if}
|
|
540
452
|
|
|
541
453
|
<style>
|
|
542
|
-
@keyframes fade-in {
|
|
543
|
-
from { opacity: 0; transform: translateY(8px); }
|
|
544
|
-
to { opacity: 1; transform: translateY(0); }
|
|
545
|
-
}
|
|
546
|
-
.animate-fade-in {
|
|
547
|
-
animation: fade-in 0.3s ease-out;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/* Context menu — hardcoded colors for guaranteed visibility */
|
|
551
|
-
.sf-ctx-menu {
|
|
552
|
-
background: #1e293b;
|
|
553
|
-
border: 1px solid #334155;
|
|
554
|
-
border-radius: 0.5rem;
|
|
555
|
-
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
|
|
556
|
-
overflow: hidden;
|
|
557
|
-
min-width: 180px;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
.sf-ctx-item {
|
|
561
|
-
display: flex;
|
|
562
|
-
align-items: center;
|
|
563
|
-
gap: 0.625rem;
|
|
564
|
-
width: 100%;
|
|
565
|
-
padding: 0.625rem 0.75rem;
|
|
566
|
-
font-size: 0.875rem;
|
|
567
|
-
color: #e2e8f0;
|
|
568
|
-
background: transparent;
|
|
569
|
-
border: none;
|
|
570
|
-
cursor: pointer;
|
|
571
|
-
text-align: left;
|
|
572
|
-
transition: background 0.15s;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
.sf-ctx-item:hover {
|
|
576
|
-
background: #334155;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
.sf-ctx-item-secondary {
|
|
580
|
-
color: #94a3b8;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
.sf-ctx-divider {
|
|
584
|
-
border-top: 1px solid #334155;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
/* Toast */
|
|
588
|
-
.sf-toast {
|
|
589
|
-
background: #1e293b;
|
|
590
|
-
border: 1px solid #334155;
|
|
591
|
-
border-radius: 0.5rem;
|
|
592
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
593
|
-
padding: 0.75rem 1rem;
|
|
594
|
-
display: flex;
|
|
595
|
-
align-items: center;
|
|
596
|
-
gap: 0.75rem;
|
|
597
|
-
max-width: 20rem;
|
|
598
|
-
color: #e2e8f0;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
.sf-toast-close {
|
|
602
|
-
color: #64748b;
|
|
603
|
-
background: transparent;
|
|
604
|
-
border: none;
|
|
605
|
-
cursor: pointer;
|
|
606
|
-
margin-left: 0.5rem;
|
|
607
|
-
transition: color 0.15s;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
.sf-toast-close:hover {
|
|
611
|
-
color: #e2e8f0;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
454
|
/* Floating modal shadow — no backdrop so needs strong depth cue */
|
|
615
455
|
.sf-modal :global(.card) {
|
|
616
456
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
|