@alliance-droid/status-feedback-system 1.1.3 → 1.2.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.
@@ -241,16 +241,16 @@
241
241
  <main class="flex-1 min-w-0">
242
242
  <div class="flex items-start justify-between mb-4">
243
243
  <div>
244
- <h2 class="text-xl font-bold flex items-center gap-2">
245
- {#if activeCategory}
244
+ {#if activeCategory}
245
+ <h2 class="text-xl font-bold flex items-center gap-2">
246
246
  <i class={getCategoryIcon(activeCategory)}></i>
247
247
  {getCategoryLabel(activeCategory)}
248
- {:else}
249
- {title}
248
+ </h2>
249
+ {#if getCategoryDescription(activeCategory)}
250
+ <p class="text-sm text-foreground-secondary mt-1">{getCategoryDescription(activeCategory)}</p>
250
251
  {/if}
251
- </h2>
252
- {#if activeCategory && getCategoryDescription(activeCategory)}
253
- <p class="text-sm text-foreground-secondary mt-1">{getCategoryDescription(activeCategory)}</p>
252
+ {:else if title}
253
+ <h2 class="text-xl font-bold">{title}</h2>
254
254
  {/if}
255
255
  </div>
256
256
  {#if showSubmit}
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { Badge, Spinner } from '@alliance-droid/svelte-component-library';
4
+ import type { BoardItem } from '../../api.js';
5
+ import type { FeedbackBoardState } from './state.svelte.js';
6
+ import { timeAgo, getStatusBadgeVariant } from './utils.js';
7
+
8
+ interface Props {
9
+ board: FeedbackBoardState;
10
+ item: BoardItem;
11
+ }
12
+
13
+ let { board, item }: Props = $props();
14
+
15
+ let responses = $state<{ message: string; author: string | null; createdAt: number }[]>([]);
16
+ let loadingResponses = $state(true);
17
+
18
+ onMount(async () => {
19
+ responses = await board.getResponses(item.id);
20
+ loadingResponses = false;
21
+ });
22
+ </script>
23
+
24
+ <div class="space-y-4">
25
+ <!-- Header -->
26
+ <div class="flex items-center gap-2 flex-wrap">
27
+ {#if item.category}
28
+ <i class="{board.getCategoryIcon(item.category)} text-foreground-secondary"></i>
29
+ {/if}
30
+ <Badge variant={getStatusBadgeVariant(item.status)} size="sm">
31
+ {item.status === 'new' ? 'Open' : item.status.charAt(0).toUpperCase() + item.status.slice(1)}
32
+ </Badge>
33
+ <span class="text-xs text-foreground-tertiary">{timeAgo(item.createdAt)}</span>
34
+ {#if item.responseCount > 0}
35
+ <span class="text-xs text-foreground-tertiary flex items-center gap-1">
36
+ <i class="fa-solid fa-comment"></i> {item.responseCount}
37
+ </span>
38
+ {/if}
39
+ </div>
40
+
41
+ <!-- Title + Message -->
42
+ {#if item.title}
43
+ <h3 class="text-lg font-semibold text-foreground">{item.title}</h3>
44
+ {/if}
45
+ <p class="text-sm text-foreground-secondary leading-relaxed whitespace-pre-wrap">{item.message}</p>
46
+
47
+ <!-- Votes -->
48
+ <div class="flex items-center gap-2 text-sm text-foreground-tertiary">
49
+ <i class="fa-solid fa-arrow-up"></i>
50
+ <span class="font-semibold">{item.votes}</span>
51
+ <span>votes</span>
52
+ </div>
53
+
54
+ <!-- Admin response -->
55
+ {#if item.adminResponse}
56
+ <div class="rounded-lg bg-primary/5 border border-primary/20 p-3">
57
+ <p class="text-xs font-semibold text-primary mb-1">
58
+ <i class="fa-solid fa-shield mr-1"></i> Official Response
59
+ </p>
60
+ <p class="text-sm text-foreground-secondary">{item.adminResponse}</p>
61
+ </div>
62
+ {/if}
63
+
64
+ <!-- Responses -->
65
+ {#if loadingResponses}
66
+ <div class="text-foreground-tertiary text-sm">
67
+ <Spinner /> Loading responses...
68
+ </div>
69
+ {:else if responses.length > 0}
70
+ <div class="space-y-3">
71
+ <h4 class="text-sm font-semibold text-foreground">
72
+ <i class="fa-solid fa-comments mr-1"></i> Responses
73
+ </h4>
74
+ {#each responses as resp}
75
+ <div class="rounded-lg bg-surface-secondary p-3">
76
+ <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"></i>{resp.author || 'Team'}
79
+ </span>
80
+ <span class="text-[0.6875rem] text-foreground-tertiary">{timeAgo(resp.createdAt)}</span>
81
+ </div>
82
+ <p class="text-sm text-foreground-secondary leading-relaxed whitespace-pre-wrap">{resp.message}</p>
83
+ </div>
84
+ {/each}
85
+ </div>
86
+ {/if}
87
+ </div>
@@ -0,0 +1,9 @@
1
+ import type { BoardItem } from '../../api.js';
2
+ import type { FeedbackBoardState } from './state.svelte.js';
3
+ interface Props {
4
+ board: FeedbackBoardState;
5
+ item: BoardItem;
6
+ }
7
+ declare const FeedbackDetail: import("svelte").Component<Props, {}, "">;
8
+ type FeedbackDetail = ReturnType<typeof FeedbackDetail>;
9
+ export default FeedbackDetail;
@@ -0,0 +1,98 @@
1
+ <script lang="ts">
2
+ import { Badge, IconButton } from '@alliance-droid/svelte-component-library';
3
+ import type { BoardItem } from '../../api.js';
4
+ import type { FeedbackBoardState } from './state.svelte.js';
5
+ import { timeAgo, getStatusBadgeVariant } from './utils.js';
6
+
7
+ interface Props {
8
+ board: FeedbackBoardState;
9
+ item: BoardItem;
10
+ onclick?: (item: BoardItem) => void;
11
+ }
12
+
13
+ let { board, item, onclick }: Props = $props();
14
+
15
+ let isClickable = $derived(!!onclick);
16
+ </script>
17
+
18
+ <!-- svelte-ignore a11y_no_static_element_interactions — clickable variant uses onclick for item selection, keyboard handled manually -->
19
+ <div
20
+ class="rounded-xl border border-border bg-surface transition-colors {isClickable ? 'cursor-pointer hover:bg-surface-hover' : ''}"
21
+ onclick={() => onclick?.(item)}
22
+ onkeydown={(e) => { if (isClickable && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); onclick?.(item); } }}
23
+ >
24
+ <div class="flex gap-3 p-4">
25
+ <!-- Vote column -->
26
+ <div class="flex flex-col items-center gap-0.5 min-w-[2.5rem]">
27
+ <!-- svelte-ignore a11y_click_events_have_key_events — IconButton handles keyboard -->
28
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
29
+ <div onclick={(e) => e.stopPropagation()}>
30
+ <IconButton
31
+ size="sm"
32
+ variant={board.votedItems[item.id] === 'up' ? 'primary' : 'neutral'}
33
+ onclick={() => board.vote(item.id, 'up')}
34
+ label="Upvote"
35
+ >
36
+ <i class="fa-solid fa-chevron-up text-xs"></i>
37
+ </IconButton>
38
+ </div>
39
+ <span class="text-sm font-bold tabular-nums">{item.votes}</span>
40
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
41
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
42
+ <div onclick={(e) => e.stopPropagation()}>
43
+ <IconButton
44
+ size="sm"
45
+ variant={board.votedItems[item.id] === 'down' ? 'primary' : 'neutral'}
46
+ onclick={() => board.vote(item.id, 'down')}
47
+ label="Downvote"
48
+ >
49
+ <i class="fa-solid fa-chevron-down text-xs"></i>
50
+ </IconButton>
51
+ </div>
52
+ </div>
53
+
54
+ <!-- Content -->
55
+ <div class="flex-1 min-w-0">
56
+ <div class="flex items-center gap-2 flex-wrap">
57
+ {#if item.category}
58
+ <i class="{board.getCategoryIcon(item.category)} text-sm text-foreground-secondary"></i>
59
+ {/if}
60
+ <h3 class="text-[0.9375rem] font-semibold flex-1 leading-snug">
61
+ {item.title || item.message.slice(0, 80)}
62
+ </h3>
63
+ <Badge variant={getStatusBadgeVariant(item.status)} size="sm">
64
+ {item.status === 'new' ? 'Open' : item.status.charAt(0).toUpperCase() + item.status.slice(1)}
65
+ </Badge>
66
+ </div>
67
+
68
+ {#if item.title}
69
+ <p class="text-sm text-foreground-secondary mt-1.5 leading-relaxed line-clamp-2">{item.message}</p>
70
+ {/if}
71
+
72
+ <div class="flex items-center gap-3 mt-2 text-[0.8125rem] text-foreground-tertiary">
73
+ <span>{timeAgo(item.createdAt)}</span>
74
+ {#if item.responseCount > 0}
75
+ <span class="flex items-center gap-1">
76
+ <i class="fa-solid fa-comment"></i>
77
+ {item.responseCount}
78
+ </span>
79
+ {/if}
80
+ {#if item.adminResponse}
81
+ <span class="text-xs text-primary">
82
+ <i class="fa-solid fa-shield"></i> Official response
83
+ </span>
84
+ {/if}
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <style>
91
+ .line-clamp-2 {
92
+ display: -webkit-box;
93
+ -webkit-line-clamp: 2;
94
+ line-clamp: 2;
95
+ -webkit-box-orient: vertical;
96
+ overflow: hidden;
97
+ }
98
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { BoardItem } from '../../api.js';
2
+ import type { FeedbackBoardState } from './state.svelte.js';
3
+ interface Props {
4
+ board: FeedbackBoardState;
5
+ item: BoardItem;
6
+ onclick?: (item: BoardItem) => void;
7
+ }
8
+ declare const FeedbackItem: import("svelte").Component<Props, {}, "">;
9
+ type FeedbackItem = ReturnType<typeof FeedbackItem>;
10
+ export default FeedbackItem;
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { Spinner } from '@alliance-droid/svelte-component-library';
3
+ import type { BoardItem } from '../../api.js';
4
+ import type { FeedbackBoardState } from './state.svelte.js';
5
+ import FeedbackItem from './FeedbackItem.svelte';
6
+
7
+ interface Props {
8
+ board: FeedbackBoardState;
9
+ /** Called when an item is clicked. If not provided, items are not clickable. */
10
+ onSelect?: (item: BoardItem) => void;
11
+ }
12
+
13
+ let { board, onSelect }: Props = $props();
14
+ </script>
15
+
16
+ {#if board.loading}
17
+ <div class="text-center py-12 text-foreground-tertiary">
18
+ <Spinner />
19
+ <p class="mt-2">Loading...</p>
20
+ </div>
21
+ {:else if board.error}
22
+ <div class="text-center py-12 text-foreground-tertiary">
23
+ <i class="fa-solid fa-exclamation-triangle text-2xl"></i>
24
+ <p class="mt-2">{board.error}</p>
25
+ </div>
26
+ {:else if board.items.length === 0}
27
+ <div class="text-center py-12 text-foreground-tertiary">
28
+ <i class="fa-solid fa-inbox text-3xl"></i>
29
+ <p class="mt-2">No feedback yet</p>
30
+ <p class="text-sm">Be the first to submit!</p>
31
+ </div>
32
+ {:else}
33
+ <div class="flex flex-col gap-2">
34
+ {#each board.items as item (item.id)}
35
+ <FeedbackItem {board} {item} onclick={onSelect ? () => onSelect(item) : undefined} />
36
+ {/each}
37
+ </div>
38
+ {/if}
@@ -0,0 +1,10 @@
1
+ import type { BoardItem } from '../../api.js';
2
+ import type { FeedbackBoardState } from './state.svelte.js';
3
+ interface Props {
4
+ board: FeedbackBoardState;
5
+ /** Called when an item is clicked. If not provided, items are not clickable. */
6
+ onSelect?: (item: BoardItem) => void;
7
+ }
8
+ declare const FeedbackList: import("svelte").Component<Props, {}, "">;
9
+ type FeedbackList = ReturnType<typeof FeedbackList>;
10
+ export default FeedbackList;
@@ -0,0 +1,60 @@
1
+ <script lang="ts">
2
+ import { Divider, Select } from '@alliance-droid/svelte-component-library';
3
+ import type { FeedbackBoardState } from './state.svelte.js';
4
+
5
+ interface Props {
6
+ board: FeedbackBoardState;
7
+ }
8
+
9
+ let { board }: Props = $props();
10
+
11
+ const statusOptions = [
12
+ { value: 'open', label: 'Open' },
13
+ { value: 'resolved', label: 'Resolved' },
14
+ { value: 'all', label: 'All' },
15
+ ];
16
+
17
+ const sortOptions = [
18
+ { value: 'votes', label: 'Most Votes' },
19
+ { value: 'newest', label: 'Newest' },
20
+ ];
21
+ </script>
22
+
23
+ <aside class="flex flex-col gap-1">
24
+ <button
25
+ class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left w-full transition-colors
26
+ {board.activeCategory === null ? 'bg-surface-secondary text-foreground font-semibold' : 'text-foreground-secondary hover:bg-surface-hover hover:text-foreground'}"
27
+ onclick={() => (board.activeCategory = null)}
28
+ >
29
+ <i class="fa-solid fa-layer-group"></i>
30
+ <span>All</span>
31
+ <span class="ml-auto bg-surface-hover px-2 py-0.5 rounded-full text-xs">{board.total}</span>
32
+ </button>
33
+
34
+ {#each board.categories as cat}
35
+ <button
36
+ class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left w-full transition-colors
37
+ {board.activeCategory === cat.name ? 'bg-surface-secondary text-foreground font-semibold' : 'text-foreground-secondary hover:bg-surface-hover hover:text-foreground'}"
38
+ onclick={() => (board.activeCategory = board.activeCategory === cat.name ? null : cat.name)}
39
+ >
40
+ <i class={board.getCategoryIcon(cat.name)}></i>
41
+ <span>{board.getCategoryLabel(cat.name)}</span>
42
+ <span class="ml-auto bg-surface-hover px-2 py-0.5 rounded-full text-xs">{cat.count}</span>
43
+ </button>
44
+ {/each}
45
+
46
+ <Divider class="my-2" />
47
+
48
+ <div class="px-3 space-y-3">
49
+ <Select
50
+ label="Status"
51
+ options={statusOptions}
52
+ bind:value={board.statusFilter}
53
+ />
54
+ <Select
55
+ label="Sort By"
56
+ options={sortOptions}
57
+ bind:value={board.sortBy}
58
+ />
59
+ </div>
60
+ </aside>
@@ -0,0 +1,7 @@
1
+ import type { FeedbackBoardState } from './state.svelte.js';
2
+ interface Props {
3
+ board: FeedbackBoardState;
4
+ }
5
+ declare const FeedbackSidebar: import("svelte").Component<Props, {}, "">;
6
+ type FeedbackSidebar = ReturnType<typeof FeedbackSidebar>;
7
+ export default FeedbackSidebar;
@@ -0,0 +1,100 @@
1
+ <script lang="ts">
2
+ import { Card, Button, Input, Textarea, Select } from '@alliance-droid/svelte-component-library';
3
+ import type { FeedbackBoardState } from './state.svelte.js';
4
+
5
+ interface Props {
6
+ board: FeedbackBoardState;
7
+ /** Available category options for the dropdown */
8
+ categories?: string[];
9
+ }
10
+
11
+ let { board, categories = [] }: Props = $props();
12
+
13
+ let title = $state('');
14
+ let message = $state('');
15
+ let category = $state('');
16
+ let email = $state('');
17
+ let submitting = $state(false);
18
+ let success = $state(false);
19
+ let error = $state('');
20
+
21
+ let categoryOptions = $derived(
22
+ categories.map((c) => ({ value: c, label: board.getCategoryLabel(c) }))
23
+ );
24
+
25
+ async function handleSubmit() {
26
+ if (!message.trim()) return;
27
+ submitting = true;
28
+ error = '';
29
+ try {
30
+ await board.submitFeedback({
31
+ title: title.trim() || undefined,
32
+ message: message.trim(),
33
+ category: category || undefined,
34
+ email: email.trim() || undefined,
35
+ });
36
+ success = true;
37
+ title = '';
38
+ message = '';
39
+ category = '';
40
+ email = '';
41
+ setTimeout(() => { success = false; }, 2000);
42
+ } catch (err) {
43
+ error = err instanceof Error ? err.message : 'Failed to submit';
44
+ } finally {
45
+ submitting = false;
46
+ }
47
+ }
48
+ </script>
49
+
50
+ <Card variant="outlined" padding="md">
51
+ {#if success}
52
+ <div class="text-center py-2 text-green-400 font-semibold">
53
+ <i class="fa-solid fa-check-circle mr-1"></i> Submitted!
54
+ </div>
55
+ {:else}
56
+ <div class="space-y-3">
57
+ <Input
58
+ bind:value={title}
59
+ placeholder="Title (optional)"
60
+ />
61
+ <Textarea
62
+ value={message}
63
+ oninput={(e) => (message = (e.currentTarget as HTMLTextAreaElement).value)}
64
+ placeholder="Describe your feedback..."
65
+ rows={3}
66
+ />
67
+ {#if error}
68
+ <p class="text-sm text-red-400">
69
+ <i class="fa-solid fa-exclamation-circle mr-1"></i>
70
+ {error}
71
+ </p>
72
+ {/if}
73
+ <div class="flex gap-2 items-center flex-wrap">
74
+ {#if categoryOptions.length > 0}
75
+ <div class="min-w-[140px]">
76
+ <Select
77
+ options={[{ value: '', label: 'Category...' }, ...categoryOptions]}
78
+ bind:value={category}
79
+ />
80
+ </div>
81
+ {/if}
82
+ <div class="flex-1">
83
+ <Input
84
+ type="email"
85
+ bind:value={email}
86
+ placeholder="Email (optional)"
87
+ />
88
+ </div>
89
+ <Button
90
+ variant="primary"
91
+ disabled={submitting || !message.trim()}
92
+ loading={submitting}
93
+ onclick={handleSubmit}
94
+ >
95
+ Submit
96
+ </Button>
97
+ </div>
98
+ </div>
99
+ {/if}
100
+ </Card>
@@ -0,0 +1,9 @@
1
+ import type { FeedbackBoardState } from './state.svelte.js';
2
+ interface Props {
3
+ board: FeedbackBoardState;
4
+ /** Available category options for the dropdown */
5
+ categories?: string[];
6
+ }
7
+ declare const FeedbackSubmitForm: import("svelte").Component<Props, {}, "">;
8
+ type FeedbackSubmitForm = ReturnType<typeof FeedbackSubmitForm>;
9
+ export default FeedbackSubmitForm;
@@ -0,0 +1,8 @@
1
+ export { createFeedbackBoard } from './state.svelte.js';
2
+ export type { FeedbackBoardState, FeedbackBoardOptions, CategoryConfig } from './state.svelte.js';
3
+ export { default as FeedbackSidebar } from './FeedbackSidebar.svelte';
4
+ export { default as FeedbackList } from './FeedbackList.svelte';
5
+ export { default as FeedbackItem } from './FeedbackItem.svelte';
6
+ export { default as FeedbackDetail } from './FeedbackDetail.svelte';
7
+ export { default as FeedbackSubmitForm } from './FeedbackSubmitForm.svelte';
8
+ export { timeAgo, getStatusBadgeVariant } from './utils.js';
@@ -0,0 +1,7 @@
1
+ export { createFeedbackBoard } from './state.svelte.js';
2
+ export { default as FeedbackSidebar } from './FeedbackSidebar.svelte';
3
+ export { default as FeedbackList } from './FeedbackList.svelte';
4
+ export { default as FeedbackItem } from './FeedbackItem.svelte';
5
+ export { default as FeedbackDetail } from './FeedbackDetail.svelte';
6
+ export { default as FeedbackSubmitForm } from './FeedbackSubmitForm.svelte';
7
+ export { timeAgo, getStatusBadgeVariant } from './utils.js';
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Reactive state manager for FeedbackBoard composable components.
3
+ *
4
+ * Usage:
5
+ * const board = createFeedbackBoard({ apiUrl, apiKey });
6
+ * <FeedbackSidebar {board} />
7
+ * <FeedbackList {board} />
8
+ */
9
+ import { type BoardItem, type BoardCategory, type StatusClient } from '../../api.js';
10
+ export interface CategoryConfig {
11
+ label?: string;
12
+ icon?: string;
13
+ description?: string;
14
+ }
15
+ export interface FeedbackBoardOptions {
16
+ apiUrl: string;
17
+ apiKey: string;
18
+ categoryConfig?: Record<string, CategoryConfig>;
19
+ }
20
+ export interface FeedbackBoardState {
21
+ readonly client: StatusClient;
22
+ readonly categoryConfig: Record<string, CategoryConfig>;
23
+ readonly items: BoardItem[];
24
+ readonly categories: BoardCategory[];
25
+ readonly total: number;
26
+ readonly loading: boolean;
27
+ readonly error: string;
28
+ activeCategory: string | null;
29
+ statusFilter: string;
30
+ sortBy: string;
31
+ readonly votedItems: Record<string, 'up' | 'down'>;
32
+ reload(): Promise<void>;
33
+ vote(itemId: string, direction: 'up' | 'down'): Promise<void>;
34
+ getResponses(itemId: string): Promise<{
35
+ message: string;
36
+ author: string | null;
37
+ createdAt: number;
38
+ }[]>;
39
+ submitFeedback(data: {
40
+ title?: string;
41
+ message: string;
42
+ category?: string;
43
+ email?: string;
44
+ }): Promise<void>;
45
+ getCategoryIcon(name: string): string;
46
+ getCategoryLabel(name: string): string;
47
+ getCategoryDescription(name: string): string;
48
+ }
49
+ export declare function createFeedbackBoard(options: FeedbackBoardOptions): FeedbackBoardState;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Reactive state manager for FeedbackBoard composable components.
3
+ *
4
+ * Usage:
5
+ * const board = createFeedbackBoard({ apiUrl, apiKey });
6
+ * <FeedbackSidebar {board} />
7
+ * <FeedbackList {board} />
8
+ */
9
+ import { createStatusClient } from '../../api.js';
10
+ export function createFeedbackBoard(options) {
11
+ const { apiUrl, apiKey, categoryConfig = {} } = options;
12
+ const client = createStatusClient({ apiUrl, apiKey });
13
+ // Reactive state
14
+ let items = $state([]);
15
+ let categories = $state([]);
16
+ let total = $state(0);
17
+ let loading = $state(true);
18
+ let error = $state('');
19
+ let activeCategory = $state(null);
20
+ let statusFilter = $state('open');
21
+ let sortBy = $state('votes');
22
+ let votedItems = $state({});
23
+ // Response cache
24
+ const responseCache = {};
25
+ async function load() {
26
+ loading = true;
27
+ error = '';
28
+ try {
29
+ const data = await client.getBoard({
30
+ category: activeCategory || undefined,
31
+ status: statusFilter,
32
+ sort: sortBy,
33
+ });
34
+ items = data.items;
35
+ total = data.total;
36
+ categories = data.categories;
37
+ }
38
+ catch (e) {
39
+ error = e instanceof Error ? e.message : 'Failed to load';
40
+ }
41
+ finally {
42
+ loading = false;
43
+ }
44
+ }
45
+ // Auto-reload when filters change
46
+ $effect(() => {
47
+ void activeCategory;
48
+ void statusFilter;
49
+ void sortBy;
50
+ load();
51
+ });
52
+ async function vote(itemId, direction) {
53
+ const prev = votedItems[itemId];
54
+ try {
55
+ const result = await client.vote(itemId, direction);
56
+ const item = items.find((i) => i.id === itemId);
57
+ if (item)
58
+ item.votes = result.votes;
59
+ if (prev === direction) {
60
+ delete votedItems[itemId];
61
+ votedItems = { ...votedItems };
62
+ }
63
+ else {
64
+ votedItems = { ...votedItems, [itemId]: direction };
65
+ }
66
+ }
67
+ catch {
68
+ // Vote failures are silent — optimistic UI
69
+ }
70
+ }
71
+ async function getResponses(itemId) {
72
+ if (responseCache[itemId])
73
+ return responseCache[itemId];
74
+ try {
75
+ const data = await client.getResponses(itemId);
76
+ responseCache[itemId] = data.responses;
77
+ return data.responses;
78
+ }
79
+ catch {
80
+ return [];
81
+ }
82
+ }
83
+ async function submitFeedback(data) {
84
+ await client.submitFeedback({
85
+ ...data,
86
+ pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
87
+ });
88
+ await load();
89
+ }
90
+ return {
91
+ get client() { return client; },
92
+ get categoryConfig() { return categoryConfig; },
93
+ get items() { return items; },
94
+ get categories() { return categories; },
95
+ get total() { return total; },
96
+ get loading() { return loading; },
97
+ get error() { return error; },
98
+ get activeCategory() { return activeCategory; },
99
+ set activeCategory(v) { activeCategory = v; },
100
+ get statusFilter() { return statusFilter; },
101
+ set statusFilter(v) { statusFilter = v; },
102
+ get sortBy() { return sortBy; },
103
+ set sortBy(v) { sortBy = v; },
104
+ get votedItems() { return votedItems; },
105
+ reload: load,
106
+ vote,
107
+ getResponses,
108
+ submitFeedback,
109
+ getCategoryIcon(name) {
110
+ return categoryConfig[name]?.icon || 'fa-solid fa-circle';
111
+ },
112
+ getCategoryLabel(name) {
113
+ return categoryConfig[name]?.label || name;
114
+ },
115
+ getCategoryDescription(name) {
116
+ return categoryConfig[name]?.description || '';
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,4 @@
1
+ /** Relative time formatting for feedback timestamps */
2
+ export declare function timeAgo(timestamp: number): string;
3
+ /** Map feedback status to badge variant */
4
+ export declare function getStatusBadgeVariant(status: string): 'success' | 'neutral' | 'info';
@@ -0,0 +1,22 @@
1
+ /** Relative time formatting for feedback timestamps */
2
+ export function timeAgo(timestamp) {
3
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
4
+ if (seconds < 60)
5
+ return 'just now';
6
+ if (seconds < 3600)
7
+ return `${Math.floor(seconds / 60)}m ago`;
8
+ if (seconds < 86400)
9
+ return `${Math.floor(seconds / 3600)}h ago`;
10
+ const days = Math.floor(seconds / 86400);
11
+ if (days < 30)
12
+ return `${days}d ago`;
13
+ return new Date(timestamp).toLocaleDateString();
14
+ }
15
+ /** Map feedback status to badge variant */
16
+ export function getStatusBadgeVariant(status) {
17
+ if (status === 'new' || status === 'reviewed')
18
+ return 'success';
19
+ if (status === 'resolved')
20
+ return 'neutral';
21
+ return 'info';
22
+ }
package/dist/index.d.ts CHANGED
@@ -16,6 +16,8 @@ export { default as FeedbackForm } from './components/FeedbackForm.svelte';
16
16
  export { default as DevFeedback } from './components/DevFeedback.svelte';
17
17
  export { default as FeedbackBoard } from './components/FeedbackBoard.svelte';
18
18
  export { default as FeedbackButton } from './components/FeedbackButton.svelte';
19
+ export { createFeedbackBoard, FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm, } from './components/board/index.js';
20
+ export type { FeedbackBoardState, FeedbackBoardOptions, CategoryConfig } from './components/board/index.js';
19
21
  export { default as StatusIndicator } from './components/shared/StatusIndicator.svelte';
20
22
  export { default as UptimeBar } from './components/shared/UptimeBar.svelte';
21
23
  export { default as IncidentTimeline } from './components/shared/IncidentTimeline.svelte';
package/dist/index.js CHANGED
@@ -16,6 +16,8 @@ export { default as FeedbackForm } from './components/FeedbackForm.svelte';
16
16
  export { default as DevFeedback } from './components/DevFeedback.svelte';
17
17
  export { default as FeedbackBoard } from './components/FeedbackBoard.svelte';
18
18
  export { default as FeedbackButton } from './components/FeedbackButton.svelte';
19
+ // ── Composable Board Components ──
20
+ export { createFeedbackBoard, FeedbackSidebar, FeedbackList, FeedbackItem, FeedbackDetail, FeedbackSubmitForm, } from './components/board/index.js';
19
21
  // ── Shared Components ──
20
22
  export { default as StatusIndicator } from './components/shared/StatusIndicator.svelte';
21
23
  export { default as UptimeBar } from './components/shared/UptimeBar.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alliance-droid/status-feedback-system",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
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",