@alliance-droid/status-feedback-system 0.3.2 → 1.0.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 ADDED
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ First stable release. Client-only package with full component suite.
6
+
7
+ ### Components
8
+ - `StatusPage` — Full status page with systems, incidents, uptime bars (self-fetching or pass-through)
9
+ - `StatusBanner` — Compact inline status indicator
10
+ - `FeedbackButton` — Floating action button with embedded feedback form
11
+ - `FeedbackForm` — Standalone feedback submission form
12
+ - `FeedbackBoard` — Public feedback board with voting, categories, filtering
13
+ - `DevFeedback` — Developer feedback overlay (Ctrl+right-click) with screenshot capture
14
+ - Shared: `StatusIndicator`, `UptimeBar`, `IncidentTimeline`, `SystemCard`
15
+
16
+ ### API Client
17
+ - `createStatusClient()` with typed methods for all endpoints
18
+ - Custom `fetch` support for SSR
19
+
20
+ ### Quality
21
+ - 150 tests across 13 test files
22
+ - 0 svelte-check errors/warnings
23
+ - Full TypeScript types for all exports
24
+ - Comprehensive README with architecture docs and props tables
25
+
26
+ ## 0.4.0
27
+
28
+ ### Breaking Changes
29
+ - Package is now **client-only** — server/admin components moved to `status-feedback-admin` app
30
+ - Removed `./server` and `./admin` subpath exports
31
+ - All data is fetched from the admin backend via `createStatusClient` API
32
+
33
+ ### Added
34
+ - `DevFeedback` — Ctrl+right-click developer feedback with screenshot capture, console error collection, and environment auto-detection
35
+ - `FeedbackBoard` — Public feedback board with voting, categories, filtering, and inline responses
36
+ - `FeedbackButton` — Floating action button with embedded feedback form
37
+ - `StatusBanner` — Compact inline status indicator (self-fetching or pass-through)
38
+ - `StatusPage` — Full status page with systems, incidents, uptime bars (self-fetching or pass-through)
39
+ - `FeedbackForm` — Standalone feedback submission form
40
+ - Shared components: `StatusIndicator`, `UptimeBar`, `IncidentTimeline`, `SystemCard`
41
+ - Full API client (`createStatusClient`) with typed methods for all endpoints
42
+
43
+ ### Fixed
44
+ - Proper TypeScript types for all event handlers
45
+ - Accessibility: aria-labels on all icon-only buttons
46
+
47
+ ## 0.1.1
48
+
49
+ - Initial client package extraction from monolith
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alliance Technical Group
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,95 +1,271 @@
1
1
  # @alliance-droid/status-feedback-system
2
2
 
3
- Drop-in system status pages and user feedback forms for SvelteKit apps.
3
+ Drop-in system status pages and user feedback components for SvelteKit apps.
4
4
 
5
- ## Features
5
+ This is the **client package** — UI components that talk to a [status-feedback-admin](https://github.com/alliance-droid/status-feedback-admin) backend via API. The admin app handles data storage, API key management, and the admin dashboard.
6
6
 
7
- - **Status Page** — Public-facing system health display with uptime bars
8
- - **Feedback Form** — Users submit bugs, feature requests, general feedback
9
- - **Admin Dashboard** — Manage projects, systems, incidents, and feedback
10
- - **API Key Auth** — Multi-project support with scoped API keys
11
- - **MSSQL** — Auto-migration, all tables prefixed `sf_`
7
+ ## Architecture
12
8
 
13
- ## Quick Start
9
+ ```
10
+ ┌─────────────────────────┐ ┌──────────────────────────┐
11
+ │ Your SvelteKit App │ │ status-feedback-admin │
12
+ │ │ │ │
13
+ │ ┌───────────────────┐ │ API │ ┌────────────────────┐ │
14
+ │ │ StatusPage │──┼───────┼──│ /api/status │ │
15
+ │ │ FeedbackButton │ │ │ │ /api/feedback │ │
16
+ │ │ DevFeedback │ │ │ │ /api/incidents │ │
17
+ │ │ FeedbackBoard │ │ │ │ /api/board │ │
18
+ │ └───────────────────┘ │ │ └────────────────────┘ │
19
+ │ │ │ │ │
20
+ │ npm package │ │ MSSQL + Azure Blob │
21
+ └─────────────────────────┘ └──────────────────────────┘
22
+ ```
14
23
 
15
- ### Server Setup
24
+ ## Setup
16
25
 
17
- ```typescript
18
- // src/lib/server/status.ts
19
- import { createStatusServer } from '@alliance-droid/status-feedback-system/server';
26
+ ### 1. Deploy the admin backend
20
27
 
21
- export const statusServer = createStatusServer({
22
- database: { connectionString: env.MSSQL_CONNECTION_STRING }
23
- });
28
+ The admin app ([status-feedback-admin](https://github.com/alliance-droid/status-feedback-admin)) provides the API, database, and admin dashboard. Deploy it first.
29
+
30
+ Required env vars on the admin app:
31
+ ```bash
32
+ MSSQL_CONNECTION_STRING=Server=...;Initial Catalog=StatusFeedback;...
33
+ SESSION_SECRET=your-secret
24
34
 
25
- await statusServer.init();
35
+ # Optional — stores screenshots in Azure Blob Storage instead of base64 in DB
36
+ AZURE_BLOB_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=...
37
+ AZURE_BLOB_CONTAINER=screenshots
26
38
  ```
27
39
 
28
- ### API Routes
40
+ ### 2. Create a project and get an API key
29
41
 
30
- ```typescript
31
- // src/routes/api/status/[...path]/+server.ts
32
- import { statusServer } from '$lib/server/status';
33
-
34
- const handlers = statusServer.createHandlers();
35
- export const GET = handlers.GET;
36
- export const POST = handlers.POST;
37
- export const PATCH = handlers.PATCH;
38
- export const DELETE = handlers.DELETE;
42
+ Log into the admin dashboard, create a project, and copy its API key (prefixed `sf_`).
43
+
44
+ ### 3. Install the client package
45
+
46
+ ```bash
47
+ npm install @alliance-droid/status-feedback-system
39
48
  ```
40
49
 
41
- ### Status Page
50
+ **Peer dependencies:** `svelte ^5.0.0`, `@alliance-droid/svelte-component-library >=1.0.0`
51
+
52
+ ## Components
53
+
54
+ ### StatusPage
55
+
56
+ Full status page with systems, incidents, and uptime history bars. Supports **self-fetching** (provide `apiUrl` + `apiKey`) or **pass-through** (provide data directly).
42
57
 
43
58
  ```svelte
44
59
  <script>
45
60
  import { StatusPage } from '@alliance-droid/status-feedback-system';
46
- let { data } = $props();
47
61
  </script>
48
62
 
63
+ <!-- Self-fetching mode -->
64
+ <StatusPage
65
+ projectName="My App"
66
+ apiUrl="https://status-admin.example.com"
67
+ apiKey="sf_abc123..."
68
+ />
69
+
70
+ <!-- Pass-through mode -->
49
71
  <StatusPage
50
72
  projectName="My App"
51
73
  systems={data.systems}
52
74
  incidents={data.incidents}
53
75
  statusHistory={data.statusHistory}
76
+ showUptime={true}
54
77
  />
55
78
  ```
56
79
 
57
- ### Feedback Button
80
+ | Prop | Type | Default | Description |
81
+ |------|------|---------|-------------|
82
+ | `projectName` | `string` | required | Display name in the header |
83
+ | `apiUrl` | `string` | — | Admin backend URL (self-fetching mode) |
84
+ | `apiKey` | `string` | — | Project API key (self-fetching mode) |
85
+ | `systems` | `System[]` | — | Pass-through: system data |
86
+ | `incidents` | `Incident[]` | — | Pass-through: incident data |
87
+ | `statusHistory` | `Record<string, StatusHistoryEntry[]>` | — | Pass-through: uptime data |
88
+ | `showUptime` | `boolean` | `true` | Show 90-day uptime bars |
89
+
90
+ ### StatusBanner
91
+
92
+ Compact inline status indicator — good for footers or nav bars.
58
93
 
59
94
  ```svelte
60
- <script>
61
- import { FeedbackButton } from '@alliance-droid/status-feedback-system';
62
- </script>
95
+ <StatusBanner
96
+ apiUrl="https://status-admin.example.com"
97
+ apiKey="sf_abc123..."
98
+ statusUrl="/status"
99
+ />
100
+ ```
101
+
102
+ | Prop | Type | Default | Description |
103
+ |------|------|---------|-------------|
104
+ | `systems` | `System[]` | — | Pass-through mode |
105
+ | `apiUrl` | `string` | — | Self-fetching mode |
106
+ | `apiKey` | `string` | — | Self-fetching mode |
107
+ | `statusUrl` | `string` | — | Links to full status page (renders as `<a>`) |
108
+
109
+ ### FeedbackButton
110
+
111
+ Floating action button with an embedded feedback form. Fixed position, bottom corner.
63
112
 
113
+ ```svelte
64
114
  <FeedbackButton
65
115
  apiKey="sf_abc123..."
66
- endpoint="/api/status"
116
+ apiUrl="https://status-admin.example.com"
67
117
  position="bottom-right"
118
+ categories={['Bug', 'Feature Request', 'General']}
119
+ userEmail={currentUser?.email}
68
120
  />
69
121
  ```
70
122
 
71
- ### Admin Dashboard
123
+ | Prop | Type | Default | Description |
124
+ |------|------|---------|-------------|
125
+ | `apiUrl` | `string` | required | Admin backend URL |
126
+ | `apiKey` | `string` | required | Project API key |
127
+ | `categories` | `string[]` | `['Bug', 'Feature Request', 'General']` | Category options |
128
+ | `userEmail` | `string` | — | Pre-fill email field |
129
+ | `position` | `'bottom-right' \| 'bottom-left'` | `'bottom-right'` | Button position |
130
+
131
+ ### FeedbackForm
132
+
133
+ Standalone feedback form — embed it wherever you need it.
134
+
135
+ ```svelte
136
+ <FeedbackForm
137
+ apiKey="sf_abc123..."
138
+ apiUrl="https://status-admin.example.com"
139
+ categories={['Bug', 'Feature Request']}
140
+ userEmail={currentUser?.email}
141
+ onSuccess={() => toast('Thanks!')}
142
+ onError={(msg) => toast(msg, 'error')}
143
+ />
144
+ ```
145
+
146
+ | Prop | Type | Default | Description |
147
+ |------|------|---------|-------------|
148
+ | `apiUrl` | `string` | required | Admin backend URL |
149
+ | `apiKey` | `string` | required | Project API key |
150
+ | `categories` | `string[]` | `['Bug', 'Feature Request', 'General']` | Category options |
151
+ | `userEmail` | `string` | — | Pre-fill email field |
152
+ | `onSuccess` | `() => void` | — | Called after successful submission |
153
+ | `onError` | `(error: string) => void` | — | Called on submission error |
154
+
155
+ ### FeedbackBoard
156
+
157
+ Public feedback board with voting, category filtering, and admin responses.
158
+
159
+ ```svelte
160
+ <FeedbackBoard
161
+ apiUrl="https://status-admin.example.com"
162
+ apiKey="sf_abc123..."
163
+ title="Feature Requests"
164
+ showSubmit={true}
165
+ submitCategories={['Bug', 'Feature Request', 'Improvement']}
166
+ categoryConfig={{
167
+ Bug: { icon: 'fa-solid fa-bug', label: 'Bugs', description: 'Something broken' },
168
+ 'Feature Request': { icon: 'fa-solid fa-lightbulb', label: 'Ideas' },
169
+ }}
170
+ />
171
+ ```
172
+
173
+ | Prop | Type | Default | Description |
174
+ |------|------|---------|-------------|
175
+ | `apiUrl` | `string` | required | Admin backend URL |
176
+ | `apiKey` | `string` | required | Project API key |
177
+ | `title` | `string` | `'Feedback Board'` | Header title |
178
+ | `showSubmit` | `boolean` | `true` | Show inline submit form |
179
+ | `submitCategories` | `string[]` | `[]` | Category options for submit form |
180
+ | `categoryConfig` | `Record<string, { label?, icon?, description? }>` | `{}` | Customize sidebar categories |
181
+
182
+ ### DevFeedback
183
+
184
+ Developer feedback overlay for staging/preview environments. Activated via **Ctrl+right-click** anywhere on the page.
185
+
186
+ Auto-captures: screenshots (via html2canvas), console errors, viewport size, current route, clicked element selector, and deployment context (Vercel env vars).
72
187
 
73
188
  ```svelte
74
189
  <script>
75
- import { AdminDashboard } from '@alliance-droid/status-feedback-system/admin';
76
- let { data } = $props();
190
+ import { DevFeedback } from '@alliance-droid/status-feedback-system';
77
191
  </script>
78
192
 
79
- <AdminDashboard
80
- projects={data.projects}
81
- apiEndpoint="/api/status"
193
+ <!-- In your root +layout.svelte -->
194
+ <DevFeedback
195
+ apiUrl="https://status-admin.example.com"
196
+ apiKey="sf_abc123..."
197
+ boardUrl="/feedback"
82
198
  />
83
199
  ```
84
200
 
85
- ## Exports
201
+ | Prop | Type | Default | Description |
202
+ |------|------|---------|-------------|
203
+ | `apiUrl` | `string` | required | Admin backend URL |
204
+ | `apiKey` | `string` | required | Project API key |
205
+ | `enabled` | `boolean` | auto-detect | Override environment detection |
206
+ | `boardUrl` | `string` | — | URL for "View Open Issues" link. Hidden if omitted. |
207
+ | `maxConsoleErrors` | `number` | `20` | Max console errors to capture |
208
+ | `showOnboarding` | `boolean` | `true` | Show "Ctrl+right-click" toast on first visit |
209
+
210
+ **Environment auto-detection:** Enabled on `localhost`, `127.0.0.1`, `*.local`, and Vercel preview deployments. Disabled in Vercel production. Override with the `enabled` prop or `PUBLIC_FEEDBACK_ENABLED` env var.
211
+
212
+ **Screenshot storage:** If the admin backend has `AZURE_BLOB_CONNECTION_STRING` configured, screenshots are uploaded to Azure Blob Storage. Otherwise they're stored as base64 in the database.
213
+
214
+ ## API Client
215
+
216
+ All components use a shared API client. You can also use it directly:
86
217
 
87
- | Path | Description |
88
- |------|-------------|
89
- | `.` | Client components (StatusPage, FeedbackForm, etc.) |
90
- | `./server` | Server module (createStatusServer, types) |
91
- | `./admin` | Admin components (AdminDashboard, etc.) |
218
+ ```typescript
219
+ import { createStatusClient } from '@alliance-droid/status-feedback-system';
220
+
221
+ const client = createStatusClient({
222
+ apiUrl: 'https://status-admin.example.com',
223
+ apiKey: 'sf_abc123...',
224
+ fetch: event.fetch, // Optional: pass SvelteKit's fetch for SSR
225
+ });
226
+
227
+ const { systems, overallStatus } = await client.getStatus();
228
+ const { incidents } = await client.getIncidents({ limit: 5, includeResolved: true });
229
+ const { history } = await client.getUptime({ days: 90 });
230
+ const feedback = await client.submitFeedback({ message: 'Great product!' });
231
+ const { items, total, categories } = await client.getBoard({ sort: 'votes', status: 'open' });
232
+ ```
233
+
234
+ ## Shared Components
235
+
236
+ Lower-level building blocks used by StatusPage — exported for custom layouts:
237
+
238
+ ```typescript
239
+ import {
240
+ StatusIndicator, // Status dot + label (operational/degraded/outage/maintenance)
241
+ UptimeBar, // 90-day uptime bar chart
242
+ IncidentTimeline, // Chronological incident updates
243
+ SystemCard, // Single system status card with uptime bar
244
+ } from '@alliance-droid/status-feedback-system';
245
+ ```
246
+
247
+ ## All Exports
248
+
249
+ ```typescript
250
+ // Components
251
+ import {
252
+ StatusPage, StatusBanner, FeedbackForm, FeedbackButton,
253
+ FeedbackBoard, DevFeedback,
254
+ StatusIndicator, UptimeBar, IncidentTimeline, SystemCard,
255
+ } from '@alliance-droid/status-feedback-system';
256
+
257
+ // API Client
258
+ import { createStatusClient } from '@alliance-droid/status-feedback-system';
259
+ import type { StatusClient, StatusClientConfig } from '@alliance-droid/status-feedback-system';
260
+
261
+ // Types
262
+ import type {
263
+ System, Incident, IncidentUpdate, Feedback, FeedbackResponse,
264
+ SystemStatus, IncidentStatus, IncidentSeverity, FeedbackStatus,
265
+ StatusData, IncidentData, UptimeData, BoardData, BoardItem,
266
+ } from '@alliance-droid/status-feedback-system';
267
+ ```
92
268
 
93
269
  ## License
94
270
 
95
- UNLICENSED — Private package.
271
+ MIT © 2026 Alliance Technical Group
@@ -20,6 +20,8 @@
20
20
  maxConsoleErrors?: number;
21
21
  /** Show onboarding toast on first visit */
22
22
  showOnboarding?: boolean;
23
+ /** URL to the feedback board page. If omitted, "View Open Issues" is hidden from the context menu. */
24
+ boardUrl?: string;
23
25
  }
24
26
 
25
27
  let {
@@ -28,6 +30,7 @@
28
30
  enabled = undefined,
29
31
  maxConsoleErrors = 20,
30
32
  showOnboarding = true,
33
+ boardUrl,
31
34
  }: Props = $props();
32
35
 
33
36
  let client = $derived(createStatusClient({ apiUrl, apiKey }));
@@ -43,7 +46,7 @@
43
46
  } catch { /* ignore */ }
44
47
  try {
45
48
  // Window-injected env (common pattern)
46
- const win = window as Record<string, unknown>;
49
+ const win = window as unknown as Record<string, unknown>;
47
50
  const env = win.__ENV as Record<string, string> | undefined;
48
51
  if (env?.[key]) return env[key];
49
52
  } catch { /* ignore */ }
@@ -106,11 +109,10 @@
106
109
  originalConsoleError.apply(console, args);
107
110
  };
108
111
 
109
- // Middle-click handler
110
- document.addEventListener('auxclick', handleAuxClick);
112
+ // Ctrl+Right-click handler
113
+ document.addEventListener('contextmenu', handleContextMenu);
111
114
  // Close menu on any other click
112
115
  document.addEventListener('click', closeMenu);
113
- document.addEventListener('contextmenu', closeMenu);
114
116
  // Escape closes everything
115
117
  document.addEventListener('keydown', handleKeydown);
116
118
 
@@ -125,15 +127,14 @@
125
127
  onDestroy(() => {
126
128
  if (!isEnabled) return;
127
129
  if (originalConsoleError) console.error = originalConsoleError;
128
- document.removeEventListener('auxclick', handleAuxClick);
130
+ document.removeEventListener('contextmenu', handleContextMenu);
129
131
  document.removeEventListener('click', closeMenu);
130
- document.removeEventListener('contextmenu', closeMenu);
131
132
  document.removeEventListener('keydown', handleKeydown);
132
133
  });
133
134
 
134
- function handleAuxClick(e: MouseEvent) {
135
- // Button 1 = middle click
136
- if (e.button !== 1) return;
135
+ function handleContextMenu(e: MouseEvent) {
136
+ // Only trigger on Ctrl+Right-click
137
+ if (!e.ctrlKey) return;
137
138
  e.preventDefault();
138
139
  e.stopPropagation();
139
140
 
@@ -161,6 +162,7 @@
161
162
  const parts: string[] = [];
162
163
  let current: HTMLElement | null = el;
163
164
  while (current && current !== document.body) {
165
+ if (!current.tagName) { current = current.parentElement; continue; }
164
166
  let selector = current.tagName.toLowerCase();
165
167
  if (current.id) {
166
168
  selector += `#${current.id}`;
@@ -197,8 +199,8 @@
197
199
  windowHeight: window.innerHeight,
198
200
  });
199
201
  screenshotData = canvas.toDataURL('image/png');
200
- } catch (e) {
201
- console.warn('Screenshot capture failed:', e);
202
+ } catch {
203
+ // Screenshot capture can fail in cross-origin iframes or restricted contexts
202
204
  screenshotData = null;
203
205
  } finally {
204
206
  capturingScreenshot = false;
@@ -287,8 +289,9 @@
287
289
  }
288
290
 
289
291
  function openBoard() {
292
+ if (!boardUrl) return;
290
293
  menuOpen = false;
291
- window.open('/feedback', '_blank');
294
+ window.open(boardUrl, '_blank');
292
295
  }
293
296
 
294
297
  // Ensure menu stays in viewport
@@ -299,8 +302,7 @@
299
302
  {#if isEnabled}
300
303
  <!-- Context menu -->
301
304
  {#if menuOpen}
302
- <!-- svelte-ignore a11y_no_static_element_interactions -->
303
- <!-- svelte-ignore a11y_no_static_element_interactions -->
305
+ <!-- svelte-ignore a11y_no_static_element_interactions — context menu needs mousedown to prevent close-on-click -->
304
306
  <div
305
307
  class="fixed z-[99999]"
306
308
  style="left: {adjustedX}px; top: {adjustedY}px;"
@@ -315,22 +317,23 @@
315
317
  <i class="fa-solid fa-lightbulb text-amber-400"></i>
316
318
  Request Feature
317
319
  </button>
318
- <div class="sf-ctx-divider">
319
- <button class="sf-ctx-item sf-ctx-item-secondary" onclick={openBoard}>
320
- <i class="fa-solid fa-clipboard-list"></i>
321
- View Open Issues
322
- </button>
323
- </div>
320
+ {#if boardUrl}
321
+ <div class="sf-ctx-divider">
322
+ <button class="sf-ctx-item sf-ctx-item-secondary" onclick={openBoard}>
323
+ <i class="fa-solid fa-clipboard-list"></i>
324
+ View Open Issues
325
+ </button>
326
+ </div>
327
+ {/if}
324
328
  </div>
325
329
  </div>
326
330
  {/if}
327
331
 
328
332
  <!-- Form modal -->
329
333
  {#if formOpen}
330
- <!-- svelte-ignore a11y_no_static_element_interactions -->
331
- <!-- svelte-ignore a11y_no_static_element_interactions -->
334
+ <!-- svelte-ignore a11y_no_static_element_interactions — modal backdrop needs mousedown for click-outside-to-close -->
332
335
  <div class="fixed inset-0 z-[99998] flex items-center justify-center bg-black/50 backdrop-blur-sm" onmousedown={(e) => { if (e.target === e.currentTarget) formOpen = false; }}>
333
- <!-- svelte-ignore a11y_no_static_element_interactions -->
336
+ <!-- svelte-ignore a11y_no_static_element_interactions — prevents modal close when clicking inside form -->
334
337
  <div class="w-full max-w-lg mx-4" onmousedown={(e) => e.stopPropagation()}>
335
338
  <Card variant="elevated" padding="none">
336
339
  <!-- Header -->
@@ -348,6 +351,7 @@
348
351
  <button
349
352
  class="text-foreground-tertiary hover:text-foreground transition-colors bg-transparent border-none cursor-pointer text-lg"
350
353
  onclick={() => (formOpen = false)}
354
+ aria-label="Close feedback form"
351
355
  >
352
356
  <i class="fa-solid fa-xmark"></i>
353
357
  </button>
@@ -371,7 +375,7 @@
371
375
  />
372
376
 
373
377
  <Textarea
374
- bind:value={formMessage}
378
+ value={formMessage} oninput={(e) => (formMessage = (e.currentTarget as HTMLTextAreaElement).value)}
375
379
  placeholder={formType === 'bug'
376
380
  ? 'What happened? What did you expect?'
377
381
  : 'Describe the feature you\'d like...'}
@@ -428,6 +432,7 @@
428
432
  <button
429
433
  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"
430
434
  onclick={() => (screenshotData = null)}
435
+ aria-label="Remove screenshot"
431
436
  >
432
437
  <i class="fa-solid fa-xmark"></i>
433
438
  </button>
@@ -444,7 +449,7 @@
444
449
  {/if}
445
450
 
446
451
  <div class="flex justify-end gap-2 pt-1">
447
- <Button variant="neutral" style="outlined" onclick={() => (formOpen = false)}>
452
+ <Button variant="neutral" style="outline" onclick={() => (formOpen = false)}>
448
453
  Cancel
449
454
  </Button>
450
455
  <Button
@@ -469,10 +474,10 @@
469
474
  <div class="sf-toast">
470
475
  <i class="fa-solid fa-computer-mouse" style="color: var(--color-primary, #3b82f6); font-size: 1.125rem;"></i>
471
476
  <div>
472
- <p style="font-size: 0.875rem; font-weight: 600; margin: 0;">Middle-click anywhere</p>
477
+ <p style="font-size: 0.875rem; font-weight: 600; margin: 0;">Ctrl + Right-click anywhere</p>
473
478
  <p style="font-size: 0.75rem; color: #94a3b8; margin: 0;">to report bugs or request features</p>
474
479
  </div>
475
- <button class="sf-toast-close" onclick={() => (showToast = false)}>
480
+ <button class="sf-toast-close" onclick={() => (showToast = false)} aria-label="Dismiss">
476
481
  <i class="fa-solid fa-xmark"></i>
477
482
  </button>
478
483
  </div>
@@ -7,6 +7,8 @@ interface Props {
7
7
  maxConsoleErrors?: number;
8
8
  /** Show onboarding toast on first visit */
9
9
  showOnboarding?: boolean;
10
+ /** URL to the feedback board page. If omitted, "View Open Issues" is hidden from the context menu. */
11
+ boardUrl?: string;
10
12
  }
11
13
  declare const DevFeedback: import("svelte").Component<Props, {}, "">;
12
14
  type DevFeedback = ReturnType<typeof DevFeedback>;
@@ -56,6 +56,7 @@
56
56
  let submitEmail = $state('');
57
57
  let submitting = $state(false);
58
58
  let submitSuccess = $state(false);
59
+ let submitError = $state('');
59
60
 
60
61
  let votedItems = $state<Record<string, 'up' | 'down'>>({});
61
62
 
@@ -98,7 +99,7 @@
98
99
  votedItems = { ...votedItems, [itemId]: direction };
99
100
  }
100
101
  } catch {
101
- // silently fail
102
+ // Vote failures are silent — optimistic UI, vote just doesn't persist
102
103
  }
103
104
  }
104
105
 
@@ -114,6 +115,7 @@
114
115
  const data = await client.getResponses(itemId);
115
116
  responses = { ...responses, [itemId]: data.responses };
116
117
  } catch {
118
+ // Show empty responses on failure — user can re-expand to retry
117
119
  responses = { ...responses, [itemId]: [] };
118
120
  } finally {
119
121
  loadingResponses = { ...loadingResponses, [itemId]: false };
@@ -124,13 +126,14 @@
124
126
  async function handleSubmit() {
125
127
  if (!submitMessage.trim()) return;
126
128
  submitting = true;
129
+ submitError = '';
127
130
  try {
128
131
  await client.submitFeedback({
129
132
  title: submitTitle.trim() || undefined,
130
133
  message: submitMessage.trim(),
131
134
  category: submitCategory || undefined,
132
135
  email: submitEmail.trim() || undefined,
133
- pageUrl: window.location.href,
136
+ pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
134
137
  });
135
138
  submitSuccess = true;
136
139
  submitTitle = '';
@@ -142,8 +145,8 @@
142
145
  showForm = false;
143
146
  }, 2000);
144
147
  await load();
145
- } catch {
146
- // ignore
148
+ } catch (err) {
149
+ submitError = err instanceof Error ? err.message : 'Failed to submit feedback';
147
150
  } finally {
148
151
  submitting = false;
149
152
  }
@@ -271,10 +274,16 @@
271
274
  placeholder="Title (optional)"
272
275
  />
273
276
  <Textarea
274
- bind:value={submitMessage}
277
+ value={submitMessage} oninput={(e) => (submitMessage = (e.currentTarget as HTMLTextAreaElement).value)}
275
278
  placeholder="Describe your feedback..."
276
279
  rows={3}
277
280
  />
281
+ {#if submitError}
282
+ <p class="text-sm text-red-400">
283
+ <i class="fa-solid fa-exclamation-circle mr-1"></i>
284
+ {submitError}
285
+ </p>
286
+ {/if}
278
287
  <div class="flex gap-2 items-center flex-wrap">
279
288
  {#if submitCategoryOptions.length > 0}
280
289
  <div class="min-w-[140px]">
@@ -333,7 +342,7 @@
333
342
  size="sm"
334
343
  variant={votedItems[item.id] === 'up' ? 'primary' : 'neutral'}
335
344
  onclick={() => handleVote(item.id, 'up')}
336
- aria-label="Upvote"
345
+ label="Upvote"
337
346
  >
338
347
  <i class="fa-solid fa-chevron-up text-xs"></i>
339
348
  </IconButton>
@@ -342,7 +351,7 @@
342
351
  size="sm"
343
352
  variant={votedItems[item.id] === 'down' ? 'primary' : 'neutral'}
344
353
  onclick={() => handleVote(item.id, 'down')}
345
- aria-label="Downvote"
354
+ label="Downvote"
346
355
  >
347
356
  <i class="fa-solid fa-chevron-down text-xs"></i>
348
357
  </IconButton>
@@ -1,4 +1,7 @@
1
1
  <script lang="ts">
2
+ import { untrack } from 'svelte';
3
+ import { createStatusClient } from '../api.js';
4
+
2
5
  interface Props {
3
6
  apiKey: string;
4
7
  apiUrl: string;
@@ -17,9 +20,11 @@
17
20
  onError,
18
21
  }: Props = $props();
19
22
 
20
- let category = $state(categories[0] || '');
23
+ let client = $derived(createStatusClient({ apiUrl, apiKey }));
24
+
25
+ let category = $state(untrack(() => categories[0] || ''));
21
26
  let message = $state('');
22
- let email = $state(userEmail ?? '');
27
+ let email = $state(untrack(() => userEmail ?? ''));
23
28
  let submitting = $state(false);
24
29
  let submitted = $state(false);
25
30
  let errorMsg = $state('');
@@ -32,25 +37,13 @@
32
37
  errorMsg = '';
33
38
 
34
39
  try {
35
- const res = await fetch(`${apiUrl.replace(/\/$/, '')}/api/feedback`, {
36
- method: 'POST',
37
- headers: {
38
- 'Content-Type': 'application/json',
39
- 'x-api-key': apiKey,
40
- },
41
- body: JSON.stringify({
42
- category,
43
- message: message.trim(),
44
- email: email.trim() || undefined,
45
- pageUrl: window.location.href,
46
- }),
40
+ await client.submitFeedback({
41
+ category,
42
+ message: message.trim(),
43
+ email: email.trim() || undefined,
44
+ pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
47
45
  });
48
46
 
49
- if (!res.ok) {
50
- const data = await res.json();
51
- throw new Error(data.error || 'Failed to submit feedback');
52
- }
53
-
54
47
  submitted = true;
55
48
  onSuccess?.();
56
49
  } catch (err) {
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
2
+ import { onMount, untrack } from 'svelte';
3
3
  import type { System, SystemStatus } from '../types.js';
4
4
  import { createStatusClient } from '../api.js';
5
5
  import StatusIndicator from './shared/StatusIndicator.svelte';
@@ -17,8 +17,8 @@
17
17
 
18
18
  let { systems: systemsProp, apiUrl, apiKey, statusUrl }: Props = $props();
19
19
 
20
- let systems: System[] = $state(systemsProp ?? []);
21
- let loading = $state(!systemsProp && !!apiUrl && !!apiKey);
20
+ let systems: System[] = $state(untrack(() => systemsProp ?? []));
21
+ let loading = $state(untrack(() => !systemsProp && !!apiUrl && !!apiKey));
22
22
 
23
23
  onMount(async () => {
24
24
  if (systemsProp || !apiUrl || !apiKey) return;
@@ -26,8 +26,9 @@
26
26
  const client = createStatusClient({ apiUrl, apiKey });
27
27
  const data = await client.getStatus();
28
28
  systems = data.systems;
29
- } catch (err) {
30
- console.error('StatusBanner fetch error:', err);
29
+ } catch {
30
+ // Error is silent — banner simply stays in loading state
31
+ // Consumer can use StatusPage for full error display
31
32
  } finally {
32
33
  loading = false;
33
34
  }
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
2
+ import { onMount, untrack } from 'svelte';
3
3
  import type { System, Incident, StatusHistoryEntry, SystemStatus } from '../types.js';
4
4
  import { createStatusClient } from '../api.js';
5
5
  import StatusIndicator from './shared/StatusIndicator.svelte';
@@ -33,10 +33,10 @@
33
33
  }: Props = $props();
34
34
 
35
35
  // ── State ──
36
- let systems: System[] = $state(systemsProp ?? []);
37
- let incidents: Incident[] = $state(incidentsProp ?? []);
38
- let statusHistory: Record<string, StatusHistoryEntry[]> = $state(statusHistoryProp ?? {});
39
- let loading = $state(!systemsProp);
36
+ let systems: System[] = $state(untrack(() => systemsProp ?? []));
37
+ let incidents: Incident[] = $state(untrack(() => incidentsProp ?? []));
38
+ let statusHistory: Record<string, StatusHistoryEntry[]> = $state(untrack(() => statusHistoryProp ?? {}));
39
+ let loading = $state(untrack(() => !systemsProp));
40
40
  let error = $state('');
41
41
 
42
42
  // ── Self-fetching mode ──
@@ -59,7 +59,6 @@
59
59
  statusHistory = uptimeData.history;
60
60
  } catch (err) {
61
61
  error = err instanceof Error ? err.message : 'Failed to load status data';
62
- console.error('StatusPage fetch error:', err);
63
62
  } finally {
64
63
  loading = false;
65
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alliance-droid/status-feedback-system",
3
- "version": "0.3.2",
3
+ "version": "1.0.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",
@@ -13,37 +13,38 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
- "!dist/**/*.test.*"
16
+ "!dist/**/*.test.*",
17
+ "LICENSE",
18
+ "CHANGELOG.md"
17
19
  ],
18
20
  "scripts": {
19
- "dev": "vite dev",
20
- "build": "vite build && npm run package",
21
- "package": "svelte-kit sync && svelte-package -o dist",
22
- "preview": "vite preview",
23
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
21
+ "build": "svelte-package -o dist && publint",
22
+ "check": "svelte-check --tsconfig ./tsconfig.json",
24
23
  "test": "vitest run",
25
24
  "test:watch": "vitest",
26
- "prepublishOnly": "npm run package"
25
+ "prepublishOnly": "npm run build"
27
26
  },
28
27
  "peerDependencies": {
29
- "@alliance-droid/svelte-component-library": ">=0.1.0",
28
+ "@alliance-droid/svelte-component-library": ">=1.0.0",
30
29
  "svelte": "^5.0.0"
31
30
  },
32
31
  "devDependencies": {
33
- "@alliance-droid/svelte-component-library": "^0.1.88",
34
- "@sveltejs/adapter-auto": "^7.0.0",
35
- "@sveltejs/kit": "^2.50.0",
32
+ "@alliance-droid/svelte-component-library": "^1.0.5",
36
33
  "@sveltejs/package": "^2.3.0",
37
34
  "@sveltejs/vite-plugin-svelte": "^6.0.0",
38
- "@tailwindcss/vite": "^4.1.0",
35
+ "@testing-library/jest-dom": "^6.9.1",
36
+ "@testing-library/svelte": "^5.3.1",
37
+ "jsdom": "^29.0.0",
39
38
  "publint": "^0.3.0",
40
39
  "svelte": "^5.48.0",
41
40
  "svelte-check": "^4.3.0",
42
- "tailwindcss": "^4.1.0",
43
41
  "typescript": "^5.9.0",
44
42
  "vite": "^7.3.0",
45
43
  "vitest": "^4.0.0"
46
44
  },
45
+ "dependencies": {
46
+ "html2canvas": "^1.4.1"
47
+ },
47
48
  "keywords": [
48
49
  "svelte",
49
50
  "sveltekit",
@@ -52,12 +53,12 @@
52
53
  "uptime"
53
54
  ],
54
55
  "author": "Alliance Technical Group",
55
- "license": "UNLICENSED",
56
+ "license": "MIT",
56
57
  "publishConfig": {
57
58
  "access": "restricted"
58
59
  },
59
- "dependencies": {
60
- "@alliance-droid/svelte-component-library": ">=0.1.0",
61
- "html2canvas": "^1.4.1"
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/alliance-droid/status-feedback-system.git"
62
63
  }
63
64
  }