@alliance-droid/status-feedback-system 1.3.7 → 1.3.9

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/dist/api.d.ts CHANGED
@@ -39,6 +39,7 @@ export interface BoardItem {
39
39
  title: string | null;
40
40
  category: string | null;
41
41
  message: string;
42
+ email: string | null;
42
43
  status: string;
43
44
  votes: number;
44
45
  adminResponse: string | null;
@@ -87,6 +88,11 @@ export interface StatusClient {
87
88
  getResponses(feedbackId: string): Promise<{
88
89
  responses: FeedbackResponse[];
89
90
  }>;
91
+ /** Submit a public response to a feedback item */
92
+ submitResponse(feedbackId: string, data: {
93
+ message: string;
94
+ author?: string;
95
+ }): Promise<FeedbackResponse>;
90
96
  }
91
97
  /**
92
98
  * Create a status API client instance
package/dist/api.js CHANGED
@@ -96,5 +96,11 @@ export function createStatusClient(config) {
96
96
  async getResponses(feedbackId) {
97
97
  return request(`/api/board/${feedbackId}/responses`);
98
98
  },
99
+ async submitResponse(feedbackId, data) {
100
+ return request(`/api/board/${feedbackId}/responses`, {
101
+ method: 'POST',
102
+ body: JSON.stringify(data),
103
+ });
104
+ },
99
105
  };
100
106
  }
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
- import { Badge, Spinner } from '@alliance-droid/svelte-component-library';
3
+ import { Badge, Spinner, Button, Textarea, Input } from '@alliance-droid/svelte-component-library';
4
4
  import type { BoardItem } from '../../api.js';
5
+ import type { FeedbackResponse } from '../../types.js';
5
6
  import type { FeedbackBoardState } from './state.svelte.js';
6
7
  import { timeAgo, getStatusBadgeVariant } from './utils.js';
7
8
 
@@ -12,13 +13,42 @@
12
13
 
13
14
  let { board, item }: Props = $props();
14
15
 
15
- let responses = $state<{ message: string; author: string | null; createdAt: number }[]>([]);
16
+ let responses = $state<FeedbackResponse[]>([]);
16
17
  let loadingResponses = $state(true);
17
18
 
19
+ // Response form state
20
+ let replyMessage = $state('');
21
+ let replyAuthor = $state('');
22
+ let submittingReply = $state(false);
23
+ let replyError = $state('');
24
+ let replySuccess = $state(false);
25
+
18
26
  onMount(async () => {
19
27
  responses = await board.getResponses(item.id);
20
28
  loadingResponses = false;
21
29
  });
30
+
31
+ async function handleSubmitReply() {
32
+ if (!replyMessage.trim()) return;
33
+ submittingReply = true;
34
+ replyError = '';
35
+ try {
36
+ await board.submitResponse(item.id, {
37
+ message: replyMessage.trim(),
38
+ author: replyAuthor.trim() || undefined,
39
+ });
40
+ // Refresh responses
41
+ responses = await board.getResponses(item.id);
42
+ replyMessage = '';
43
+ replyAuthor = '';
44
+ replySuccess = true;
45
+ setTimeout(() => { replySuccess = false; }, 2000);
46
+ } catch (err) {
47
+ replyError = err instanceof Error ? err.message : 'Failed to submit';
48
+ } finally {
49
+ submittingReply = false;
50
+ }
51
+ }
22
52
  </script>
23
53
 
24
54
  <div class="space-y-4">
@@ -38,6 +68,14 @@
38
68
  {/if}
39
69
  </div>
40
70
 
71
+ <!-- Submitter email -->
72
+ {#if item.email}
73
+ <p class="text-sm text-foreground-tertiary flex items-center gap-1">
74
+ <i class="fa-solid fa-envelope" aria-hidden="true"></i>
75
+ {item.email}
76
+ </p>
77
+ {/if}
78
+
41
79
  <!-- Title + Message -->
42
80
  {#if item.title}
43
81
  <h3 class="text-lg font-semibold text-foreground">{item.title}</h3>
@@ -71,12 +109,18 @@
71
109
  <h4 class="text-sm font-semibold text-foreground">
72
110
  <i class="fa-solid fa-comments mr-1" aria-hidden="true"></i> Responses
73
111
  </h4>
74
- {#each responses as resp}
112
+ {#each responses as resp (resp.id)}
75
113
  <div class="rounded-lg bg-surface-secondary p-3">
76
114
  <div class="flex items-center justify-between mb-1">
77
- <span class="text-xs font-semibold text-primary">
78
- <i class="fa-solid fa-shield mr-1" aria-hidden="true"></i>{resp.author || 'Team'}
79
- </span>
115
+ {#if !resp.author}
116
+ <span class="text-xs font-semibold text-primary">
117
+ <i class="fa-solid fa-shield mr-1" aria-hidden="true"></i>Team
118
+ </span>
119
+ {:else}
120
+ <span class="text-xs font-semibold text-foreground-secondary">
121
+ <i class="fa-solid fa-user mr-1" aria-hidden="true"></i>{resp.author}
122
+ </span>
123
+ {/if}
80
124
  <span class="text-[0.6875rem] text-foreground-tertiary">{timeAgo(resp.createdAt)}</span>
81
125
  </div>
82
126
  <p class="text-sm text-foreground-secondary leading-relaxed whitespace-pre-wrap">{resp.message}</p>
@@ -84,4 +128,46 @@
84
128
  {/each}
85
129
  </div>
86
130
  {/if}
131
+
132
+ <!-- Reply form -->
133
+ <div class="border-t border-border pt-4 space-y-2">
134
+ <h4 class="text-sm font-semibold text-foreground">
135
+ <i class="fa-solid fa-reply mr-1" aria-hidden="true"></i> Add a Response
136
+ </h4>
137
+ {#if replySuccess}
138
+ <p class="text-sm text-green-400 font-semibold">
139
+ <i class="fa-solid fa-check-circle mr-1" aria-hidden="true"></i> Response submitted!
140
+ </p>
141
+ {:else}
142
+ <Textarea
143
+ value={replyMessage}
144
+ oninput={(e) => (replyMessage = (e.currentTarget as HTMLTextAreaElement).value)}
145
+ placeholder="Share your thoughts..."
146
+ rows={3}
147
+ />
148
+ <div class="flex gap-2 items-center">
149
+ <div class="flex-1">
150
+ <Input
151
+ bind:value={replyAuthor}
152
+ placeholder="Your name (optional)"
153
+ />
154
+ </div>
155
+ <Button
156
+ variant="primary"
157
+ size="sm"
158
+ disabled={submittingReply || !replyMessage.trim()}
159
+ loading={submittingReply}
160
+ onclick={handleSubmitReply}
161
+ >
162
+ Submit
163
+ </Button>
164
+ </div>
165
+ {#if replyError}
166
+ <p class="text-sm text-red-400">
167
+ <i class="fa-solid fa-exclamation-circle mr-1" aria-hidden="true"></i>
168
+ {replyError}
169
+ </p>
170
+ {/if}
171
+ {/if}
172
+ </div>
87
173
  </div>
@@ -33,9 +33,7 @@
33
33
  <div class="flex gap-3 p-4">
34
34
  <!-- Vote column -->
35
35
  <div class="flex flex-col items-center gap-0.5 min-w-[2.5rem]" role="group" aria-label="Vote on this feedback">
36
- <!-- svelte-ignore a11y_click_events_have_key_events — IconButton handles keyboard -->
37
- <!-- svelte-ignore a11y_no_static_element_interactions -->
38
- <div onclick={(e) => e.stopPropagation()}>
36
+ <div role="presentation" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
39
37
  <IconButton
40
38
  size="sm"
41
39
  variant={board.votedItems[item.id] === 'up' ? 'primary' : 'neutral'}
@@ -51,9 +49,7 @@
51
49
  <i class="fa-solid fa-exclamation-circle" aria-hidden="true"></i>
52
50
  </span>
53
51
  {/if}
54
- <!-- svelte-ignore a11y_click_events_have_key_events -->
55
- <!-- svelte-ignore a11y_no_static_element_interactions -->
56
- <div onclick={(e) => e.stopPropagation()}>
52
+ <div role="presentation" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
57
53
  <IconButton
58
54
  size="sm"
59
55
  variant={board.votedItems[item.id] === 'down' ? 'primary' : 'neutral'}
@@ -83,17 +79,26 @@
83
79
  <p class="text-sm text-foreground-secondary mt-1.5 leading-relaxed line-clamp-2">{item.message}</p>
84
80
  {/if}
85
81
 
86
- <div class="flex items-center gap-3 mt-2 text-[0.8125rem] text-foreground-tertiary">
87
- <span>{timeAgo(item.createdAt)}</span>
82
+ {#if item.email}
83
+ <p class="text-[0.8125rem] text-foreground-tertiary mt-1.5 flex items-center gap-1">
84
+ <i class="fa-solid fa-envelope" aria-hidden="true"></i>
85
+ {item.email}
86
+ </p>
87
+ {/if}
88
+
89
+ <div class="flex items-center justify-between mt-2 text-[0.8125rem] text-foreground-tertiary">
90
+ <div class="flex items-center gap-3">
91
+ <span>{timeAgo(item.createdAt)}</span>
92
+ {#if item.adminResponse}
93
+ <span class="text-xs text-primary">
94
+ <i class="fa-solid fa-shield" aria-hidden="true"></i> Official response
95
+ </span>
96
+ {/if}
97
+ </div>
88
98
  {#if item.responseCount > 0}
89
99
  <span class="flex items-center gap-1" aria-label="{item.responseCount} {item.responseCount === 1 ? 'response' : 'responses'}">
90
100
  <i class="fa-solid fa-comment" aria-hidden="true"></i>
91
- {item.responseCount} {item.responseCount === 1 ? 'response' : 'responses'}
92
- </span>
93
- {/if}
94
- {#if item.adminResponse}
95
- <span class="text-xs text-primary">
96
- <i class="fa-solid fa-shield" aria-hidden="true"></i> Official response
101
+ {item.responseCount}
97
102
  </span>
98
103
  {/if}
99
104
  </div>
@@ -6,14 +6,16 @@
6
6
  board: FeedbackBoardState;
7
7
  /** Available category options for the dropdown */
8
8
  categories?: string[];
9
+ /** Pre-fill the email field (e.g. from the logged-in user's session) */
10
+ userEmail?: string;
9
11
  }
10
12
 
11
- let { board, categories = [] }: Props = $props();
13
+ let { board, categories = [], userEmail }: Props = $props();
12
14
 
13
15
  let title = $state('');
14
16
  let message = $state('');
15
17
  let category = $state('');
16
- let email = $state('');
18
+ let email = $derived(userEmail ?? '');
17
19
  let submitting = $state(false);
18
20
  let success = $state(false);
19
21
  let error = $state('');
@@ -3,6 +3,8 @@ interface Props {
3
3
  board: FeedbackBoardState;
4
4
  /** Available category options for the dropdown */
5
5
  categories?: string[];
6
+ /** Pre-fill the email field (e.g. from the logged-in user's session) */
7
+ userEmail?: string;
6
8
  }
7
9
  declare const FeedbackSubmitForm: import("svelte").Component<Props, {}, "">;
8
10
  type FeedbackSubmitForm = ReturnType<typeof FeedbackSubmitForm>;
@@ -6,6 +6,7 @@
6
6
  * <FeedbackSidebar {board} />
7
7
  * <FeedbackList {board} />
8
8
  */
9
+ import type { FeedbackResponse } from '../../types.js';
9
10
  import { type BoardItem, type BoardCategory, type StatusClient } from '../../api.js';
10
11
  export interface CategoryConfig {
11
12
  label?: string;
@@ -37,17 +38,17 @@ export interface FeedbackBoardState {
37
38
  reload(): Promise<void>;
38
39
  loadMore(): Promise<void>;
39
40
  vote(itemId: string, direction: 'up' | 'down'): Promise<void>;
40
- getResponses(itemId: string): Promise<{
41
- message: string;
42
- author: string | null;
43
- createdAt: number;
44
- }[]>;
41
+ getResponses(itemId: string): Promise<FeedbackResponse[]>;
45
42
  submitFeedback(data: {
46
43
  title?: string;
47
44
  message: string;
48
45
  category?: string;
49
46
  email?: string;
50
47
  }): Promise<void>;
48
+ submitResponse(itemId: string, data: {
49
+ message: string;
50
+ author?: string;
51
+ }): Promise<void>;
51
52
  getCategoryIcon(name: string): string;
52
53
  getCategoryLabel(name: string): string;
53
54
  getCategoryDescription(name: string): string;
@@ -123,6 +123,15 @@ export function createFeedbackBoard(options) {
123
123
  });
124
124
  await load();
125
125
  }
126
+ async function submitResponse(itemId, data) {
127
+ await client.submitResponse(itemId, data);
128
+ // Invalidate cache so next getResponses fetches fresh data
129
+ delete responseCache[itemId];
130
+ // Update response count on the item
131
+ const item = items.find((i) => i.id === itemId);
132
+ if (item)
133
+ item.responseCount += 1;
134
+ }
126
135
  return {
127
136
  get client() { return client; },
128
137
  get categoryConfig() { return categoryConfig; },
@@ -146,6 +155,7 @@ export function createFeedbackBoard(options) {
146
155
  vote,
147
156
  getResponses,
148
157
  submitFeedback,
158
+ submitResponse,
149
159
  getCategoryIcon(name) {
150
160
  return categoryConfig[name]?.icon || 'fa-solid fa-circle';
151
161
  },
@@ -94,18 +94,17 @@
94
94
  }
95
95
  </script>
96
96
 
97
- <!-- svelte-ignore a11y_no_static_element_interactions — context menu positioned at cursor -->
98
97
  <!-- popover="manual" promotes to browser top layer (above <dialog> modals) -->
99
98
  <div
100
99
  bind:this={popoverEl}
100
+ role="presentation"
101
101
  popover="manual"
102
102
  class="sf-ctx-popover"
103
103
  style="left: {adjustedX}px; top: {adjustedY}px;"
104
104
  onmousedown={(e) => e.stopPropagation()}
105
105
  onclick={(e) => e.stopPropagation()}
106
+ onkeydown={(e) => e.stopPropagation()}
106
107
  >
107
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
108
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
109
108
  <div class="sf-ctx-menu" role="menu" tabindex="-1" aria-label="Feedback actions" onkeydown={handleKeydown}>
110
109
  <button class="sf-ctx-item" role="menuitem" onclick={onReportBug}>
111
110
  <i class="fa-solid fa-bug text-red-400" aria-hidden="true"></i>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alliance-droid/status-feedback-system",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
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",