@alliance-droid/status-feedback-system 2.3.2 → 3.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 +78 -67
- package/LICENSE +11 -11
- package/README.md +275 -273
- package/dist/components/DevFeedback.svelte +589 -585
- package/dist/components/DevFeedback.svelte.d.ts +7 -2
- package/dist/components/FeedbackBoard.svelte +110 -110
- package/dist/components/FeedbackButton.svelte +72 -72
- package/dist/components/FeedbackForm.svelte +155 -155
- package/dist/components/StatusBanner.svelte +105 -105
- package/dist/components/StatusPage.svelte +244 -244
- package/dist/components/board/FeedbackDetail.svelte +299 -299
- package/dist/components/board/FeedbackItem.svelte +117 -117
- package/dist/components/board/FeedbackList.svelte +58 -58
- package/dist/components/board/FeedbackSidebar.svelte +60 -60
- package/dist/components/board/FeedbackSubmitForm.svelte +123 -123
- package/dist/components/dev/DevContextMenu.svelte +178 -178
- package/dist/components/dev/DevOnboardingToast.svelte +73 -73
- package/dist/components/dev/environment.d.ts +2 -6
- package/dist/components/dev/environment.js +2 -23
- package/dist/components/shared/IncidentTimeline.svelte +55 -55
- package/dist/components/shared/PasteImageInput.svelte +169 -169
- package/dist/components/shared/StatusIndicator.svelte +50 -50
- package/dist/components/shared/SystemCard.svelte +30 -30
- package/dist/components/shared/UptimeBar.svelte +85 -85
- package/package.json +66 -66
|
@@ -1,117 +1,117 @@
|
|
|
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
|
-
let voteLabel = $derived.by(() => {
|
|
17
|
-
const current = board.votedItems[item.id];
|
|
18
|
-
if (current === 'up') return 'Remove upvote';
|
|
19
|
-
if (current === 'down') return 'Remove downvote';
|
|
20
|
-
return undefined;
|
|
21
|
-
});
|
|
22
|
-
</script>
|
|
23
|
-
|
|
24
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
25
|
-
<article
|
|
26
|
-
class="rounded-xl border border-border bg-surface transition-colors {isClickable ? 'cursor-pointer hover:bg-surface-hover' : ''}"
|
|
27
|
-
role={isClickable ? 'button' : undefined}
|
|
28
|
-
tabindex={isClickable ? 0 : undefined}
|
|
29
|
-
aria-label={isClickable ? (item.title || item.message.slice(0, 80)) : undefined}
|
|
30
|
-
onclick={() => onclick?.(item)}
|
|
31
|
-
onkeydown={(e) => { if (isClickable && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); onclick?.(item); } }}
|
|
32
|
-
>
|
|
33
|
-
<div class="flex gap-3 p-4">
|
|
34
|
-
<!-- Vote column -->
|
|
35
|
-
<div class="flex flex-col items-center gap-0.5 min-w-[2.5rem]" role="group" aria-label="Vote on this feedback">
|
|
36
|
-
<div role="presentation" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
|
|
37
|
-
<IconButton
|
|
38
|
-
size="sm"
|
|
39
|
-
variant={board.votedItems[item.id] === 'up' ? 'primary' : 'neutral'}
|
|
40
|
-
onclick={() => board.vote(item.id, 'up')}
|
|
41
|
-
label={board.votedItems[item.id] === 'up' ? 'Remove upvote' : 'Upvote'}
|
|
42
|
-
>
|
|
43
|
-
<i class="fa-solid fa-chevron-up text-xs"></i>
|
|
44
|
-
</IconButton>
|
|
45
|
-
</div>
|
|
46
|
-
<span class="text-sm font-bold tabular-nums" aria-label="{item.votes} votes">{item.votes}</span>
|
|
47
|
-
{#if board.voteErrors[item.id]}
|
|
48
|
-
<span class="text-[0.625rem] text-red-400" role="alert">
|
|
49
|
-
<i class="fa-solid fa-exclamation-circle" aria-hidden="true"></i>
|
|
50
|
-
</span>
|
|
51
|
-
{/if}
|
|
52
|
-
<div role="presentation" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
|
|
53
|
-
<IconButton
|
|
54
|
-
size="sm"
|
|
55
|
-
variant={board.votedItems[item.id] === 'down' ? 'primary' : 'neutral'}
|
|
56
|
-
onclick={() => board.vote(item.id, 'down')}
|
|
57
|
-
label={board.votedItems[item.id] === 'down' ? 'Remove downvote' : 'Downvote'}
|
|
58
|
-
>
|
|
59
|
-
<i class="fa-solid fa-chevron-down text-xs"></i>
|
|
60
|
-
</IconButton>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<!-- Content -->
|
|
65
|
-
<div class="flex-1 min-w-0">
|
|
66
|
-
<div class="flex items-center gap-2 flex-wrap">
|
|
67
|
-
{#if item.category}
|
|
68
|
-
<i class="{board.getCategoryIcon(item.category)} text-sm text-foreground-secondary" aria-hidden="true"></i>
|
|
69
|
-
{/if}
|
|
70
|
-
<h3 class="text-[0.9375rem] font-semibold flex-1 leading-snug">
|
|
71
|
-
{item.title || item.message.slice(0, 80)}
|
|
72
|
-
</h3>
|
|
73
|
-
<Badge variant={getStatusBadgeVariant(item.status)} size="sm">
|
|
74
|
-
{item.status === 'new' ? 'Open' : item.status.charAt(0).toUpperCase() + item.status.slice(1)}
|
|
75
|
-
</Badge>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
{#if item.title}
|
|
79
|
-
<p class="text-sm text-foreground-secondary mt-1.5 leading-relaxed line-clamp-2">{item.message}</p>
|
|
80
|
-
{/if}
|
|
81
|
-
|
|
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>
|
|
98
|
-
{#if item.responseCount > 0}
|
|
99
|
-
<span class="flex items-center gap-1" aria-label="{item.responseCount} {item.responseCount === 1 ? 'response' : 'responses'}">
|
|
100
|
-
<i class="fa-solid fa-comment" aria-hidden="true"></i>
|
|
101
|
-
{item.responseCount}
|
|
102
|
-
</span>
|
|
103
|
-
{/if}
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</article>
|
|
108
|
-
|
|
109
|
-
<style>
|
|
110
|
-
.line-clamp-2 {
|
|
111
|
-
display: -webkit-box;
|
|
112
|
-
-webkit-line-clamp: 2;
|
|
113
|
-
line-clamp: 2;
|
|
114
|
-
-webkit-box-orient: vertical;
|
|
115
|
-
overflow: hidden;
|
|
116
|
-
}
|
|
117
|
-
</style>
|
|
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
|
+
let voteLabel = $derived.by(() => {
|
|
17
|
+
const current = board.votedItems[item.id];
|
|
18
|
+
if (current === 'up') return 'Remove upvote';
|
|
19
|
+
if (current === 'down') return 'Remove downvote';
|
|
20
|
+
return undefined;
|
|
21
|
+
});
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
25
|
+
<article
|
|
26
|
+
class="rounded-xl border border-border bg-surface transition-colors {isClickable ? 'cursor-pointer hover:bg-surface-hover' : ''}"
|
|
27
|
+
role={isClickable ? 'button' : undefined}
|
|
28
|
+
tabindex={isClickable ? 0 : undefined}
|
|
29
|
+
aria-label={isClickable ? (item.title || item.message.slice(0, 80)) : undefined}
|
|
30
|
+
onclick={() => onclick?.(item)}
|
|
31
|
+
onkeydown={(e) => { if (isClickable && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); onclick?.(item); } }}
|
|
32
|
+
>
|
|
33
|
+
<div class="flex gap-3 p-4">
|
|
34
|
+
<!-- Vote column -->
|
|
35
|
+
<div class="flex flex-col items-center gap-0.5 min-w-[2.5rem]" role="group" aria-label="Vote on this feedback">
|
|
36
|
+
<div role="presentation" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
|
|
37
|
+
<IconButton
|
|
38
|
+
size="sm"
|
|
39
|
+
variant={board.votedItems[item.id] === 'up' ? 'primary' : 'neutral'}
|
|
40
|
+
onclick={() => board.vote(item.id, 'up')}
|
|
41
|
+
label={board.votedItems[item.id] === 'up' ? 'Remove upvote' : 'Upvote'}
|
|
42
|
+
>
|
|
43
|
+
<i class="fa-solid fa-chevron-up text-xs"></i>
|
|
44
|
+
</IconButton>
|
|
45
|
+
</div>
|
|
46
|
+
<span class="text-sm font-bold tabular-nums" aria-label="{item.votes} votes">{item.votes}</span>
|
|
47
|
+
{#if board.voteErrors[item.id]}
|
|
48
|
+
<span class="text-[0.625rem] text-red-400" role="alert">
|
|
49
|
+
<i class="fa-solid fa-exclamation-circle" aria-hidden="true"></i>
|
|
50
|
+
</span>
|
|
51
|
+
{/if}
|
|
52
|
+
<div role="presentation" onclick={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}>
|
|
53
|
+
<IconButton
|
|
54
|
+
size="sm"
|
|
55
|
+
variant={board.votedItems[item.id] === 'down' ? 'primary' : 'neutral'}
|
|
56
|
+
onclick={() => board.vote(item.id, 'down')}
|
|
57
|
+
label={board.votedItems[item.id] === 'down' ? 'Remove downvote' : 'Downvote'}
|
|
58
|
+
>
|
|
59
|
+
<i class="fa-solid fa-chevron-down text-xs"></i>
|
|
60
|
+
</IconButton>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Content -->
|
|
65
|
+
<div class="flex-1 min-w-0">
|
|
66
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
67
|
+
{#if item.category}
|
|
68
|
+
<i class="{board.getCategoryIcon(item.category)} text-sm text-foreground-secondary" aria-hidden="true"></i>
|
|
69
|
+
{/if}
|
|
70
|
+
<h3 class="text-[0.9375rem] font-semibold flex-1 leading-snug">
|
|
71
|
+
{item.title || item.message.slice(0, 80)}
|
|
72
|
+
</h3>
|
|
73
|
+
<Badge variant={getStatusBadgeVariant(item.status)} size="sm">
|
|
74
|
+
{item.status === 'new' ? 'Open' : item.status.charAt(0).toUpperCase() + item.status.slice(1)}
|
|
75
|
+
</Badge>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{#if item.title}
|
|
79
|
+
<p class="text-sm text-foreground-secondary mt-1.5 leading-relaxed line-clamp-2">{item.message}</p>
|
|
80
|
+
{/if}
|
|
81
|
+
|
|
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>
|
|
98
|
+
{#if item.responseCount > 0}
|
|
99
|
+
<span class="flex items-center gap-1" aria-label="{item.responseCount} {item.responseCount === 1 ? 'response' : 'responses'}">
|
|
100
|
+
<i class="fa-solid fa-comment" aria-hidden="true"></i>
|
|
101
|
+
{item.responseCount}
|
|
102
|
+
</span>
|
|
103
|
+
{/if}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</article>
|
|
108
|
+
|
|
109
|
+
<style>
|
|
110
|
+
.line-clamp-2 {
|
|
111
|
+
display: -webkit-box;
|
|
112
|
+
-webkit-line-clamp: 2;
|
|
113
|
+
line-clamp: 2;
|
|
114
|
+
-webkit-box-orient: vertical;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Button, 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
|
-
<button
|
|
26
|
-
onclick={() => board.reload()}
|
|
27
|
-
class="mt-3 rounded-md bg-surface-hover px-4 py-2 text-sm text-foreground-secondary transition-colors hover:bg-border"
|
|
28
|
-
>
|
|
29
|
-
<i class="fa-solid fa-rotate-right mr-1"></i> Retry
|
|
30
|
-
</button>
|
|
31
|
-
</div>
|
|
32
|
-
{:else if board.items.length === 0}
|
|
33
|
-
<div class="text-center py-12 text-foreground-tertiary">
|
|
34
|
-
<i class="fa-solid fa-inbox text-3xl"></i>
|
|
35
|
-
<p class="mt-2">No feedback yet</p>
|
|
36
|
-
<p class="text-sm">Be the first to submit!</p>
|
|
37
|
-
</div>
|
|
38
|
-
{:else}
|
|
39
|
-
<div class="flex flex-col gap-2">
|
|
40
|
-
{#each board.items as item (item.id)}
|
|
41
|
-
<FeedbackItem {board} {item} onclick={onSelect ? () => onSelect(item) : undefined} />
|
|
42
|
-
{/each}
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
{#if board.hasMore}
|
|
46
|
-
<div class="mt-4 text-center">
|
|
47
|
-
<Button
|
|
48
|
-
variant="neutral"
|
|
49
|
-
style="outline"
|
|
50
|
-
disabled={board.loadingMore}
|
|
51
|
-
loading={board.loadingMore}
|
|
52
|
-
onclick={() => board.loadMore()}
|
|
53
|
-
>
|
|
54
|
-
Load More ({board.items.length} of {board.total})
|
|
55
|
-
</Button>
|
|
56
|
-
</div>
|
|
57
|
-
{/if}
|
|
58
|
-
{/if}
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button, 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
|
+
<button
|
|
26
|
+
onclick={() => board.reload()}
|
|
27
|
+
class="mt-3 rounded-md bg-surface-hover px-4 py-2 text-sm text-foreground-secondary transition-colors hover:bg-border"
|
|
28
|
+
>
|
|
29
|
+
<i class="fa-solid fa-rotate-right mr-1"></i> Retry
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
{:else if board.items.length === 0}
|
|
33
|
+
<div class="text-center py-12 text-foreground-tertiary">
|
|
34
|
+
<i class="fa-solid fa-inbox text-3xl"></i>
|
|
35
|
+
<p class="mt-2">No feedback yet</p>
|
|
36
|
+
<p class="text-sm">Be the first to submit!</p>
|
|
37
|
+
</div>
|
|
38
|
+
{:else}
|
|
39
|
+
<div class="flex flex-col gap-2">
|
|
40
|
+
{#each board.items as item (item.id)}
|
|
41
|
+
<FeedbackItem {board} {item} onclick={onSelect ? () => onSelect(item) : undefined} />
|
|
42
|
+
{/each}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{#if board.hasMore}
|
|
46
|
+
<div class="mt-4 text-center">
|
|
47
|
+
<Button
|
|
48
|
+
variant="neutral"
|
|
49
|
+
style="outline"
|
|
50
|
+
disabled={board.loadingMore}
|
|
51
|
+
loading={board.loadingMore}
|
|
52
|
+
onclick={() => board.loadMore()}
|
|
53
|
+
>
|
|
54
|
+
Load More ({board.items.length} of {board.total})
|
|
55
|
+
</Button>
|
|
56
|
+
</div>
|
|
57
|
+
{/if}
|
|
58
|
+
{/if}
|
|
@@ -1,60 +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" aria-label="Filter feedback">
|
|
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" aria-hidden="true"></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 (cat.name)}
|
|
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)} aria-hidden="true"></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>
|
|
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" aria-label="Filter feedback">
|
|
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" aria-hidden="true"></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 (cat.name)}
|
|
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)} aria-hidden="true"></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>
|