@alliance-droid/status-feedback-system 1.2.1 → 1.3.1
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 +229 -337
- 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 +24 -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let
|
|
48
|
+
// --- Client (lazy, stable) ---
|
|
49
|
+
let client: StatusClient | null = $state(null);
|
|
50
|
+
let lastApiUrl = '';
|
|
51
|
+
let lastApiKey = '';
|
|
43
52
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
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,37 @@
|
|
|
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);
|
|
82
|
+
|
|
83
|
+
// Drag state
|
|
84
|
+
let isDragging = $state(false);
|
|
85
|
+
let dragOffsetX = 0;
|
|
86
|
+
let dragOffsetY = 0;
|
|
87
|
+
let modalX = $state(0);
|
|
88
|
+
let modalY = $state(0);
|
|
89
|
+
let modalPositioned = $state(false);
|
|
90
|
+
|
|
91
|
+
function handleDragStart(e: MouseEvent) {
|
|
92
|
+
isDragging = true;
|
|
93
|
+
const modalEl = (e.currentTarget as HTMLElement).closest('.sf-modal') as HTMLElement;
|
|
94
|
+
if (!modalEl) return;
|
|
95
|
+
const rect = modalEl.getBoundingClientRect();
|
|
96
|
+
dragOffsetX = e.clientX - rect.left;
|
|
97
|
+
dragOffsetY = e.clientY - rect.top;
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function handleDragMove(e: MouseEvent) {
|
|
102
|
+
if (!isDragging) return;
|
|
103
|
+
modalX = e.clientX - dragOffsetX;
|
|
104
|
+
modalY = e.clientY - dragOffsetY;
|
|
105
|
+
modalPositioned = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleDragEnd() {
|
|
109
|
+
isDragging = false;
|
|
110
|
+
}
|
|
94
111
|
|
|
95
112
|
// Console error capture
|
|
96
113
|
let consoleErrors: string[] = [];
|
|
@@ -101,7 +118,7 @@
|
|
|
101
118
|
const ONBOARDING_KEY = 'sf-devfeedback-onboarded';
|
|
102
119
|
|
|
103
120
|
onMount(() => {
|
|
104
|
-
isEnabled = detectEnvironment();
|
|
121
|
+
isEnabled = detectEnvironment(enabled);
|
|
105
122
|
if (!isEnabled) return;
|
|
106
123
|
|
|
107
124
|
// Hook console.error
|
|
@@ -114,10 +131,10 @@
|
|
|
114
131
|
|
|
115
132
|
// Ctrl+Right-click handler
|
|
116
133
|
document.addEventListener('contextmenu', handleContextMenu);
|
|
117
|
-
// Close menu on any other click
|
|
118
134
|
document.addEventListener('click', closeMenu);
|
|
119
|
-
// Escape closes everything
|
|
120
135
|
document.addEventListener('keydown', handleKeydown);
|
|
136
|
+
document.addEventListener('mousemove', handleDragMove);
|
|
137
|
+
document.addEventListener('mouseup', handleDragEnd);
|
|
121
138
|
|
|
122
139
|
// Onboarding toast
|
|
123
140
|
if (showOnboarding && !localStorage.getItem(ONBOARDING_KEY)) {
|
|
@@ -133,10 +150,11 @@
|
|
|
133
150
|
document.removeEventListener('contextmenu', handleContextMenu);
|
|
134
151
|
document.removeEventListener('click', closeMenu);
|
|
135
152
|
document.removeEventListener('keydown', handleKeydown);
|
|
153
|
+
document.removeEventListener('mousemove', handleDragMove);
|
|
154
|
+
document.removeEventListener('mouseup', handleDragEnd);
|
|
136
155
|
});
|
|
137
156
|
|
|
138
157
|
function handleContextMenu(e: MouseEvent) {
|
|
139
|
-
// Only trigger on Ctrl+Right-click
|
|
140
158
|
if (!e.ctrlKey) return;
|
|
141
159
|
e.preventDefault();
|
|
142
160
|
e.stopPropagation();
|
|
@@ -160,54 +178,12 @@
|
|
|
160
178
|
}
|
|
161
179
|
}
|
|
162
180
|
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let selector = current.tagName.toLowerCase();
|
|
170
|
-
if (current.id) {
|
|
171
|
-
selector += `#${current.id}`;
|
|
172
|
-
parts.unshift(selector);
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
if (current.className && typeof current.className === 'string') {
|
|
176
|
-
const classes = current.className.trim().split(/\s+/).slice(0, 2).join('.');
|
|
177
|
-
if (classes) selector += `.${classes}`;
|
|
178
|
-
}
|
|
179
|
-
parts.unshift(selector);
|
|
180
|
-
current = current.parentElement;
|
|
181
|
-
}
|
|
182
|
-
return parts.join(' > ');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function getElementHtml(el: HTMLElement | null): string {
|
|
186
|
-
if (!el) return '';
|
|
187
|
-
const html = el.outerHTML;
|
|
188
|
-
return html.length > 500 ? html.slice(0, 500) + '...' : html;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function captureScreenshot() {
|
|
192
|
-
capturingScreenshot = true;
|
|
193
|
-
try {
|
|
194
|
-
const html2canvas = (await import('html2canvas')).default;
|
|
195
|
-
const canvas = await html2canvas(document.body, {
|
|
196
|
-
logging: false,
|
|
197
|
-
useCORS: true,
|
|
198
|
-
scale: 1,
|
|
199
|
-
width: window.innerWidth,
|
|
200
|
-
height: window.innerHeight,
|
|
201
|
-
windowWidth: window.innerWidth,
|
|
202
|
-
windowHeight: window.innerHeight,
|
|
203
|
-
});
|
|
204
|
-
screenshotData = canvas.toDataURL('image/png');
|
|
205
|
-
} catch {
|
|
206
|
-
// Screenshot capture can fail in cross-origin iframes or restricted contexts
|
|
207
|
-
screenshotData = null;
|
|
208
|
-
} finally {
|
|
209
|
-
capturingScreenshot = false;
|
|
210
|
-
}
|
|
181
|
+
async function doScreenshot() {
|
|
182
|
+
capturingScreenshot_ = true;
|
|
183
|
+
screenshotFailed = false;
|
|
184
|
+
screenshotData = await captureScreenshot();
|
|
185
|
+
if (!screenshotData) screenshotFailed = true;
|
|
186
|
+
capturingScreenshot_ = false;
|
|
211
187
|
}
|
|
212
188
|
|
|
213
189
|
function openForm(type: 'bug' | 'feature') {
|
|
@@ -218,10 +194,12 @@
|
|
|
218
194
|
submitResult = null;
|
|
219
195
|
menuOpen = false;
|
|
220
196
|
formOpen = true;
|
|
197
|
+
modalPositioned = false;
|
|
198
|
+
modalX = 0;
|
|
199
|
+
modalY = 0;
|
|
221
200
|
|
|
222
|
-
// Auto-capture screenshot for bugs
|
|
223
201
|
if (type === 'bug') {
|
|
224
|
-
|
|
202
|
+
doScreenshot();
|
|
225
203
|
} else {
|
|
226
204
|
screenshotData = null;
|
|
227
205
|
}
|
|
@@ -254,7 +232,6 @@
|
|
|
254
232
|
metadata.screenshot = screenshotData;
|
|
255
233
|
}
|
|
256
234
|
|
|
257
|
-
// Deployment info
|
|
258
235
|
const gitCommit = getEnvVar('VERCEL_GIT_COMMIT_SHA');
|
|
259
236
|
const gitBranch = getEnvVar('VERCEL_GIT_COMMIT_REF');
|
|
260
237
|
const deployUrl = getEnvVar('VERCEL_URL');
|
|
@@ -262,10 +239,10 @@
|
|
|
262
239
|
if (gitBranch) metadata.gitBranch = gitBranch;
|
|
263
240
|
if (deployUrl) metadata.deploymentUrl = deployUrl;
|
|
264
241
|
|
|
265
|
-
await
|
|
242
|
+
await getClient().submitFeedback({
|
|
266
243
|
title: formTitle.trim() || undefined,
|
|
267
244
|
message: formMessage.trim(),
|
|
268
|
-
category: formType === 'bug' ? '
|
|
245
|
+
category: formType === 'bug' ? 'bug' : 'feature',
|
|
269
246
|
email: formEmail.trim() || undefined,
|
|
270
247
|
pageUrl: window.location.href,
|
|
271
248
|
metadata,
|
|
@@ -284,280 +261,195 @@
|
|
|
284
261
|
}
|
|
285
262
|
}
|
|
286
263
|
|
|
287
|
-
function getEnvironmentName(): string {
|
|
288
|
-
const vercelEnv = getEnvVar('VERCEL_ENV');
|
|
289
|
-
if (vercelEnv) return vercelEnv;
|
|
290
|
-
if (window.location.hostname === 'localhost') return 'development';
|
|
291
|
-
return 'unknown';
|
|
292
|
-
}
|
|
293
|
-
|
|
294
264
|
function openBoard() {
|
|
295
265
|
if (!boardUrl) return;
|
|
296
266
|
menuOpen = false;
|
|
297
267
|
window.open(boardUrl, '_blank');
|
|
298
268
|
}
|
|
299
|
-
|
|
300
|
-
// Ensure menu stays in viewport
|
|
301
|
-
let adjustedX = $derived(Math.min(menuX, window.innerWidth - 200));
|
|
302
|
-
let adjustedY = $derived(Math.min(menuY, window.innerHeight - 160));
|
|
303
269
|
</script>
|
|
304
270
|
|
|
305
271
|
{#if isEnabled}
|
|
306
272
|
<!-- Context menu -->
|
|
307
273
|
{#if menuOpen}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
<i class="fa-solid fa-bug text-red-400"></i>
|
|
317
|
-
Report Bug
|
|
318
|
-
</button>
|
|
319
|
-
<button class="sf-ctx-item" onclick={() => openForm('feature')}>
|
|
320
|
-
<i class="fa-solid fa-lightbulb text-amber-400"></i>
|
|
321
|
-
Request Feature
|
|
322
|
-
</button>
|
|
323
|
-
{#if boardUrl}
|
|
324
|
-
<div class="sf-ctx-divider">
|
|
325
|
-
<button class="sf-ctx-item sf-ctx-item-secondary" onclick={openBoard}>
|
|
326
|
-
<i class="fa-solid fa-clipboard-list"></i>
|
|
327
|
-
View Open Issues
|
|
328
|
-
</button>
|
|
329
|
-
</div>
|
|
330
|
-
{/if}
|
|
331
|
-
</div>
|
|
332
|
-
</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
|
+
/>
|
|
333
282
|
{/if}
|
|
334
283
|
|
|
335
284
|
<!-- Form modal -->
|
|
336
285
|
{#if formOpen}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
class="text-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
</
|
|
286
|
+
<div
|
|
287
|
+
class="sf-modal fixed z-[99998] w-full max-w-lg flex flex-col"
|
|
288
|
+
style={modalPositioned
|
|
289
|
+
? `left: ${modalX}px; top: ${modalY}px;`
|
|
290
|
+
: 'left: 50%; top: 50%; transform: translate(-50%, -50%);'
|
|
291
|
+
}
|
|
292
|
+
>
|
|
293
|
+
<Card variant="elevated" padding="none" class="flex flex-col max-h-[calc(100vh-2rem)] overflow-hidden">
|
|
294
|
+
<!-- Header (draggable) -->
|
|
295
|
+
<!-- svelte-ignore a11y_no_static_element_interactions — drag handle needs mousedown -->
|
|
296
|
+
<div
|
|
297
|
+
class="flex items-center justify-between px-5 py-4 border-b border-border shrink-0 select-none"
|
|
298
|
+
style="cursor: {isDragging ? 'grabbing' : 'grab'};"
|
|
299
|
+
onmousedown={handleDragStart}
|
|
300
|
+
>
|
|
301
|
+
<div class="flex items-center gap-2">
|
|
302
|
+
{#if formType === 'bug'}
|
|
303
|
+
<i class="fa-solid fa-bug text-red-400"></i>
|
|
304
|
+
<h3 class="text-base font-semibold">Report Bug</h3>
|
|
305
|
+
{:else}
|
|
306
|
+
<i class="fa-solid fa-lightbulb text-amber-400"></i>
|
|
307
|
+
<h3 class="text-base font-semibold">Request Feature</h3>
|
|
308
|
+
{/if}
|
|
309
|
+
<Badge variant="neutral" size="sm">{getEnvironmentName()}</Badge>
|
|
361
310
|
</div>
|
|
311
|
+
<button
|
|
312
|
+
class="text-foreground-tertiary hover:text-foreground transition-colors bg-transparent border-none cursor-pointer text-lg"
|
|
313
|
+
onclick={() => (formOpen = false)}
|
|
314
|
+
aria-label="Close feedback form"
|
|
315
|
+
>
|
|
316
|
+
<i class="fa-solid fa-xmark"></i>
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
362
319
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
320
|
+
<!-- Body (scrollable) -->
|
|
321
|
+
<div class="p-5 space-y-4 overflow-y-auto">
|
|
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' : ''}
|
|
404
370
|
</Badge>
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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)}
|
|
408
376
|
</Badge>
|
|
409
|
-
{#if consoleErrors.length > 0}
|
|
410
|
-
<Badge variant="danger" size="sm">
|
|
411
|
-
<i class="fa-solid fa-triangle-exclamation mr-1"></i>
|
|
412
|
-
{consoleErrors.length} console error{consoleErrors.length !== 1 ? 's' : ''}
|
|
413
|
-
</Badge>
|
|
414
|
-
{/if}
|
|
415
|
-
{#if clickedElement}
|
|
416
|
-
<Badge variant="neutral" size="sm">
|
|
417
|
-
<i class="fa-solid fa-crosshairs mr-1"></i>
|
|
418
|
-
{getElementSelector(clickedElement).slice(-40)}
|
|
419
|
-
</Badge>
|
|
420
|
-
{/if}
|
|
421
|
-
</div>
|
|
422
|
-
|
|
423
|
-
<!-- Screenshot preview -->
|
|
424
|
-
{#if capturingScreenshot}
|
|
425
|
-
<div class="flex items-center gap-2 text-sm text-foreground-tertiary">
|
|
426
|
-
<Spinner /> Capturing screenshot...
|
|
427
|
-
</div>
|
|
428
|
-
{:else if screenshotData}
|
|
429
|
-
<div class="relative">
|
|
430
|
-
<img
|
|
431
|
-
src={screenshotData}
|
|
432
|
-
alt="Screenshot"
|
|
433
|
-
class="w-full rounded-md border border-border max-h-40 object-cover object-top"
|
|
434
|
-
/>
|
|
435
|
-
<button
|
|
436
|
-
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"
|
|
437
|
-
onclick={() => (screenshotData = null)}
|
|
438
|
-
aria-label="Remove screenshot"
|
|
439
|
-
>
|
|
440
|
-
<i class="fa-solid fa-xmark"></i>
|
|
441
|
-
</button>
|
|
442
|
-
</div>
|
|
443
377
|
{/if}
|
|
444
378
|
</div>
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
<
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
<Button
|
|
459
|
-
variant={formType === 'bug' ? 'danger' : 'primary'}
|
|
460
|
-
disabled={submitting || !formMessage.trim()}
|
|
461
|
-
loading={submitting}
|
|
462
|
-
onclick={handleSubmit}
|
|
379
|
+
|
|
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="flex items-center gap-2 text-sm text-foreground-tertiary">
|
|
387
|
+
<i class="fa-solid fa-image-slash" aria-hidden="true"></i>
|
|
388
|
+
<span>Screenshot unavailable</span>
|
|
389
|
+
<button
|
|
390
|
+
class="text-xs text-primary hover:underline bg-transparent border-none cursor-pointer p-0"
|
|
391
|
+
onclick={doScreenshot}
|
|
463
392
|
>
|
|
464
|
-
|
|
465
|
-
</
|
|
393
|
+
Retry
|
|
394
|
+
</button>
|
|
395
|
+
</div>
|
|
396
|
+
{:else if screenshotData}
|
|
397
|
+
<div class="relative">
|
|
398
|
+
<img
|
|
399
|
+
src={screenshotData}
|
|
400
|
+
alt="Screenshot"
|
|
401
|
+
class="w-full rounded-md border border-border max-h-40 object-cover object-top"
|
|
402
|
+
/>
|
|
403
|
+
<button
|
|
404
|
+
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"
|
|
405
|
+
onclick={() => (screenshotData = null)}
|
|
406
|
+
aria-label="Remove screenshot"
|
|
407
|
+
>
|
|
408
|
+
<i class="fa-solid fa-xmark"></i>
|
|
409
|
+
</button>
|
|
410
|
+
</div>
|
|
411
|
+
{/if}
|
|
466
412
|
</div>
|
|
467
413
|
{/if}
|
|
414
|
+
|
|
415
|
+
{#if submitResult === 'error'}
|
|
416
|
+
<p class="text-sm text-red-400">
|
|
417
|
+
<i class="fa-solid fa-exclamation-circle mr-1"></i>
|
|
418
|
+
Failed to submit. Please try again.
|
|
419
|
+
</p>
|
|
420
|
+
{/if}
|
|
421
|
+
{/if}
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<!-- Footer -->
|
|
425
|
+
{#if submitResult !== 'success'}
|
|
426
|
+
<div class="flex justify-end gap-2 px-5 py-4 border-t border-border shrink-0">
|
|
427
|
+
<Button variant="neutral" style="outline" onclick={() => (formOpen = false)}>
|
|
428
|
+
Cancel
|
|
429
|
+
</Button>
|
|
430
|
+
<Button
|
|
431
|
+
variant={formType === 'bug' ? 'danger' : 'primary'}
|
|
432
|
+
disabled={submitting || !formMessage.trim()}
|
|
433
|
+
loading={submitting}
|
|
434
|
+
onclick={handleSubmit}
|
|
435
|
+
>
|
|
436
|
+
{formType === 'bug' ? 'Report Bug' : 'Submit Request'}
|
|
437
|
+
</Button>
|
|
468
438
|
</div>
|
|
469
|
-
|
|
470
|
-
</
|
|
439
|
+
{/if}
|
|
440
|
+
</Card>
|
|
471
441
|
</div>
|
|
472
442
|
{/if}
|
|
473
443
|
|
|
474
444
|
<!-- Onboarding toast -->
|
|
475
445
|
{#if showToast}
|
|
476
|
-
<
|
|
477
|
-
<div class="sf-toast">
|
|
478
|
-
<i class="fa-solid fa-computer-mouse" style="color: var(--color-primary, #3b82f6); font-size: 1.125rem;"></i>
|
|
479
|
-
<div>
|
|
480
|
-
<p style="font-size: 0.875rem; font-weight: 600; margin: 0;">Ctrl + Right-click anywhere</p>
|
|
481
|
-
<p style="font-size: 0.75rem; color: #94a3b8; margin: 0;">to report bugs or request features</p>
|
|
482
|
-
</div>
|
|
483
|
-
<button class="sf-toast-close" onclick={() => (showToast = false)} aria-label="Dismiss">
|
|
484
|
-
<i class="fa-solid fa-xmark"></i>
|
|
485
|
-
</button>
|
|
486
|
-
</div>
|
|
487
|
-
</div>
|
|
446
|
+
<DevOnboardingToast onDismiss={() => (showToast = false)} />
|
|
488
447
|
{/if}
|
|
489
448
|
{/if}
|
|
490
449
|
|
|
491
450
|
<style>
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
}
|
|
496
|
-
.animate-fade-in {
|
|
497
|
-
animation: fade-in 0.3s ease-out;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/* Context menu — hardcoded colors for guaranteed visibility */
|
|
501
|
-
.sf-ctx-menu {
|
|
502
|
-
background: #1e293b;
|
|
503
|
-
border: 1px solid #334155;
|
|
504
|
-
border-radius: 0.5rem;
|
|
505
|
-
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
|
|
506
|
-
overflow: hidden;
|
|
507
|
-
min-width: 180px;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
.sf-ctx-item {
|
|
511
|
-
display: flex;
|
|
512
|
-
align-items: center;
|
|
513
|
-
gap: 0.625rem;
|
|
514
|
-
width: 100%;
|
|
515
|
-
padding: 0.625rem 0.75rem;
|
|
516
|
-
font-size: 0.875rem;
|
|
517
|
-
color: #e2e8f0;
|
|
518
|
-
background: transparent;
|
|
519
|
-
border: none;
|
|
520
|
-
cursor: pointer;
|
|
521
|
-
text-align: left;
|
|
522
|
-
transition: background 0.15s;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
.sf-ctx-item:hover {
|
|
526
|
-
background: #334155;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
.sf-ctx-item-secondary {
|
|
530
|
-
color: #94a3b8;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
.sf-ctx-divider {
|
|
534
|
-
border-top: 1px solid #334155;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/* Toast */
|
|
538
|
-
.sf-toast {
|
|
539
|
-
background: #1e293b;
|
|
540
|
-
border: 1px solid #334155;
|
|
541
|
-
border-radius: 0.5rem;
|
|
542
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
543
|
-
padding: 0.75rem 1rem;
|
|
544
|
-
display: flex;
|
|
545
|
-
align-items: center;
|
|
546
|
-
gap: 0.75rem;
|
|
547
|
-
max-width: 20rem;
|
|
548
|
-
color: #e2e8f0;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
.sf-toast-close {
|
|
552
|
-
color: #64748b;
|
|
553
|
-
background: transparent;
|
|
554
|
-
border: none;
|
|
555
|
-
cursor: pointer;
|
|
556
|
-
margin-left: 0.5rem;
|
|
557
|
-
transition: color 0.15s;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
.sf-toast-close:hover {
|
|
561
|
-
color: #e2e8f0;
|
|
451
|
+
/* Floating modal shadow — no backdrop so needs strong depth cue */
|
|
452
|
+
.sf-modal :global(.card) {
|
|
453
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
|
|
562
454
|
}
|
|
563
455
|
</style>
|