@alliance-droid/status-feedback-system 0.2.0 → 0.3.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/dist/components/DevFeedback.svelte +500 -0
- package/dist/components/DevFeedback.svelte.d.ts +13 -0
- package/dist/components/FeedbackBoard.svelte +196 -621
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +61 -58
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { createStatusClient, type BoardItem, type BoardCategory } from '../api.js';
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
Button,
|
|
6
|
+
Badge,
|
|
7
|
+
Select,
|
|
8
|
+
Input,
|
|
9
|
+
Textarea,
|
|
10
|
+
Spinner,
|
|
11
|
+
Divider,
|
|
12
|
+
IconButton,
|
|
13
|
+
} from '@alliance-droid/svelte-component-library';
|
|
3
14
|
|
|
4
15
|
interface Props {
|
|
5
16
|
apiUrl: string;
|
|
@@ -31,15 +42,13 @@
|
|
|
31
42
|
let error = $state('');
|
|
32
43
|
|
|
33
44
|
let activeCategory = $state<string | null>(null);
|
|
34
|
-
let statusFilter = $state<
|
|
35
|
-
let sortBy = $state<
|
|
45
|
+
let statusFilter = $state<string>('open');
|
|
46
|
+
let sortBy = $state<string>('votes');
|
|
36
47
|
|
|
37
|
-
// Expanded item for viewing responses
|
|
38
48
|
let expandedId = $state<string | null>(null);
|
|
39
49
|
let responses = $state<Record<string, { message: string; author: string | null; createdAt: number }[]>>({});
|
|
40
50
|
let loadingResponses = $state<Record<string, boolean>>({});
|
|
41
51
|
|
|
42
|
-
// Submit form
|
|
43
52
|
let showForm = $state(false);
|
|
44
53
|
let submitTitle = $state('');
|
|
45
54
|
let submitMessage = $state('');
|
|
@@ -48,7 +57,6 @@
|
|
|
48
57
|
let submitting = $state(false);
|
|
49
58
|
let submitSuccess = $state(false);
|
|
50
59
|
|
|
51
|
-
// Voting state (tracks which items the user voted on this session)
|
|
52
60
|
let votedItems = $state<Record<string, 'up' | 'down'>>({});
|
|
53
61
|
|
|
54
62
|
async function load() {
|
|
@@ -57,8 +65,8 @@
|
|
|
57
65
|
try {
|
|
58
66
|
const data = await client.getBoard({
|
|
59
67
|
category: activeCategory || undefined,
|
|
60
|
-
status: statusFilter,
|
|
61
|
-
sort: sortBy,
|
|
68
|
+
status: statusFilter as 'open' | 'resolved' | 'all',
|
|
69
|
+
sort: sortBy as 'votes' | 'newest',
|
|
62
70
|
});
|
|
63
71
|
items = data.items;
|
|
64
72
|
total = data.total;
|
|
@@ -71,7 +79,6 @@
|
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
$effect(() => {
|
|
74
|
-
// Re-fetch when filters change
|
|
75
82
|
void activeCategory;
|
|
76
83
|
void statusFilter;
|
|
77
84
|
void sortBy;
|
|
@@ -82,12 +89,9 @@
|
|
|
82
89
|
const prev = votedItems[itemId];
|
|
83
90
|
try {
|
|
84
91
|
const result = await client.vote(itemId, direction);
|
|
85
|
-
// Update local state
|
|
86
92
|
const item = items.find((i) => i.id === itemId);
|
|
87
93
|
if (item) item.votes = result.votes;
|
|
88
|
-
|
|
89
94
|
if (prev === direction) {
|
|
90
|
-
// Toggled off
|
|
91
95
|
delete votedItems[itemId];
|
|
92
96
|
votedItems = { ...votedItems };
|
|
93
97
|
} else {
|
|
@@ -137,7 +141,6 @@
|
|
|
137
141
|
submitSuccess = false;
|
|
138
142
|
showForm = false;
|
|
139
143
|
}, 2000);
|
|
140
|
-
// Reload board
|
|
141
144
|
await load();
|
|
142
145
|
} catch {
|
|
143
146
|
// ignore
|
|
@@ -167,676 +170,248 @@
|
|
|
167
170
|
if (days < 30) return `${days}d ago`;
|
|
168
171
|
return new Date(timestamp).toLocaleDateString();
|
|
169
172
|
}
|
|
170
|
-
</script>
|
|
171
|
-
|
|
172
|
-
<div class="sf-board">
|
|
173
|
-
<div class="sf-board-layout">
|
|
174
|
-
<!-- Sidebar -->
|
|
175
|
-
<aside class="sf-board-sidebar">
|
|
176
|
-
<button
|
|
177
|
-
class="sf-sidebar-item"
|
|
178
|
-
class:sf-sidebar-active={activeCategory === null}
|
|
179
|
-
onclick={() => (activeCategory = null)}
|
|
180
|
-
>
|
|
181
|
-
<i class="fa-solid fa-layer-group"></i>
|
|
182
|
-
<span>All</span>
|
|
183
|
-
<span class="sf-sidebar-count">{total}</span>
|
|
184
|
-
</button>
|
|
185
173
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
>
|
|
192
|
-
<i class={getCategoryIcon(cat.name)}></i>
|
|
193
|
-
<span>{getCategoryLabel(cat.name)}</span>
|
|
194
|
-
<span class="sf-sidebar-count">{cat.count}</span>
|
|
195
|
-
</button>
|
|
196
|
-
{/each}
|
|
174
|
+
function getStatusBadgeVariant(status: string): 'success' | 'neutral' | 'info' {
|
|
175
|
+
if (status === 'new' || status === 'reviewed') return 'success';
|
|
176
|
+
if (status === 'resolved') return 'neutral';
|
|
177
|
+
return 'info';
|
|
178
|
+
}
|
|
197
179
|
|
|
198
|
-
|
|
180
|
+
const statusOptions = [
|
|
181
|
+
{ value: 'open', label: 'Open' },
|
|
182
|
+
{ value: 'resolved', label: 'Resolved' },
|
|
183
|
+
{ value: 'all', label: 'All' },
|
|
184
|
+
];
|
|
199
185
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<option value="resolved">Resolved</option>
|
|
205
|
-
<option value="all">All</option>
|
|
206
|
-
</select>
|
|
207
|
-
</div>
|
|
186
|
+
const sortOptions = [
|
|
187
|
+
{ value: 'votes', label: 'Most Votes' },
|
|
188
|
+
{ value: 'newest', label: 'Newest' },
|
|
189
|
+
];
|
|
208
190
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
<option value="newest">Newest</option>
|
|
214
|
-
</select>
|
|
215
|
-
</div>
|
|
216
|
-
</aside>
|
|
191
|
+
let submitCategoryOptions = $derived(
|
|
192
|
+
submitCategories.map((c) => ({ value: c, label: getCategoryLabel(c) }))
|
|
193
|
+
);
|
|
194
|
+
</script>
|
|
217
195
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
196
|
+
<div class="flex gap-6 min-h-[400px]">
|
|
197
|
+
<!-- Sidebar -->
|
|
198
|
+
<aside class="w-56 flex-shrink-0 flex flex-col gap-1">
|
|
199
|
+
<button
|
|
200
|
+
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left w-full transition-colors
|
|
201
|
+
{activeCategory === null ? 'bg-surface-secondary text-foreground font-semibold' : 'text-foreground-secondary hover:bg-surface-hover hover:text-foreground'}"
|
|
202
|
+
onclick={() => (activeCategory = null)}
|
|
203
|
+
>
|
|
204
|
+
<i class="fa-solid fa-layer-group"></i>
|
|
205
|
+
<span>All</span>
|
|
206
|
+
<span class="ml-auto bg-surface-hover px-2 py-0.5 rounded-full text-xs">{total}</span>
|
|
207
|
+
</button>
|
|
208
|
+
|
|
209
|
+
{#each categories as cat}
|
|
210
|
+
<button
|
|
211
|
+
class="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-left w-full transition-colors
|
|
212
|
+
{activeCategory === cat.name ? 'bg-surface-secondary text-foreground font-semibold' : 'text-foreground-secondary hover:bg-surface-hover hover:text-foreground'}"
|
|
213
|
+
onclick={() => (activeCategory = activeCategory === cat.name ? null : cat.name)}
|
|
214
|
+
>
|
|
215
|
+
<i class={getCategoryIcon(cat.name)}></i>
|
|
216
|
+
<span>{getCategoryLabel(cat.name)}</span>
|
|
217
|
+
<span class="ml-auto bg-surface-hover px-2 py-0.5 rounded-full text-xs">{cat.count}</span>
|
|
218
|
+
</button>
|
|
219
|
+
{/each}
|
|
220
|
+
|
|
221
|
+
<Divider class="my-2" />
|
|
222
|
+
|
|
223
|
+
<div class="px-3 space-y-3">
|
|
224
|
+
<Select
|
|
225
|
+
label="Status"
|
|
226
|
+
options={statusOptions}
|
|
227
|
+
bind:value={statusFilter}
|
|
228
|
+
/>
|
|
229
|
+
<Select
|
|
230
|
+
label="Sort By"
|
|
231
|
+
options={sortOptions}
|
|
232
|
+
bind:value={sortBy}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
</aside>
|
|
236
|
+
|
|
237
|
+
<!-- Main -->
|
|
238
|
+
<main class="flex-1 min-w-0">
|
|
239
|
+
<div class="flex items-start justify-between mb-4">
|
|
240
|
+
<div>
|
|
241
|
+
<h2 class="text-xl font-bold flex items-center gap-2">
|
|
242
|
+
{#if activeCategory}
|
|
243
|
+
<i class={getCategoryIcon(activeCategory)}></i>
|
|
244
|
+
{getCategoryLabel(activeCategory)}
|
|
245
|
+
{:else}
|
|
246
|
+
{title}
|
|
232
247
|
{/if}
|
|
233
|
-
</
|
|
234
|
-
{#if
|
|
235
|
-
<
|
|
236
|
-
<i class="fa-solid fa-plus"></i> Submit
|
|
237
|
-
</button>
|
|
248
|
+
</h2>
|
|
249
|
+
{#if activeCategory && getCategoryDescription(activeCategory)}
|
|
250
|
+
<p class="text-sm text-foreground-secondary mt-1">{getCategoryDescription(activeCategory)}</p>
|
|
238
251
|
{/if}
|
|
239
252
|
</div>
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
{#if showSubmit}
|
|
254
|
+
<Button variant="primary" onclick={() => (showForm = !showForm)}>
|
|
255
|
+
<i class="fa-solid fa-plus mr-1.5"></i> Submit
|
|
256
|
+
</Button>
|
|
257
|
+
{/if}
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<!-- Submit form -->
|
|
261
|
+
{#if showForm}
|
|
262
|
+
<Card variant="outlined" padding="md" class="mb-4">
|
|
263
|
+
{#if submitSuccess}
|
|
264
|
+
<div class="text-center py-2 text-green-400 font-semibold">
|
|
265
|
+
<i class="fa-solid fa-check-circle mr-1"></i> Submitted! It will appear once reviewed.
|
|
266
|
+
</div>
|
|
267
|
+
{:else}
|
|
268
|
+
<div class="space-y-3">
|
|
269
|
+
<Input
|
|
251
270
|
bind:value={submitTitle}
|
|
252
271
|
placeholder="Title (optional)"
|
|
253
|
-
class="sf-input"
|
|
254
272
|
/>
|
|
255
|
-
<
|
|
273
|
+
<Textarea
|
|
256
274
|
bind:value={submitMessage}
|
|
257
275
|
placeholder="Describe your feedback..."
|
|
258
|
-
rows=
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
</select>
|
|
276
|
+
rows={3}
|
|
277
|
+
/>
|
|
278
|
+
<div class="flex gap-2 items-center flex-wrap">
|
|
279
|
+
{#if submitCategoryOptions.length > 0}
|
|
280
|
+
<div class="min-w-[140px]">
|
|
281
|
+
<Select
|
|
282
|
+
options={[{ value: '', label: 'Category...' }, ...submitCategoryOptions]}
|
|
283
|
+
bind:value={submitCategory}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
269
286
|
{/if}
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
287
|
+
<div class="flex-1">
|
|
288
|
+
<Input
|
|
289
|
+
type="email"
|
|
290
|
+
bind:value={submitEmail}
|
|
291
|
+
placeholder="Email (optional)"
|
|
292
|
+
/>
|
|
293
|
+
</div>
|
|
294
|
+
<Button
|
|
295
|
+
variant="primary"
|
|
278
296
|
disabled={submitting || !submitMessage.trim()}
|
|
297
|
+
loading={submitting}
|
|
279
298
|
onclick={handleSubmit}
|
|
280
299
|
>
|
|
281
|
-
|
|
282
|
-
</
|
|
300
|
+
Submit
|
|
301
|
+
</Button>
|
|
283
302
|
</div>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
</
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
303
|
+
</div>
|
|
304
|
+
{/if}
|
|
305
|
+
</Card>
|
|
306
|
+
{/if}
|
|
307
|
+
|
|
308
|
+
<!-- Items -->
|
|
309
|
+
{#if loading}
|
|
310
|
+
<div class="text-center py-12 text-foreground-tertiary">
|
|
311
|
+
<Spinner />
|
|
312
|
+
<p class="mt-2">Loading...</p>
|
|
313
|
+
</div>
|
|
314
|
+
{:else if error}
|
|
315
|
+
<div class="text-center py-12 text-foreground-tertiary">
|
|
316
|
+
<i class="fa-solid fa-exclamation-triangle text-2xl"></i>
|
|
317
|
+
<p class="mt-2">{error}</p>
|
|
318
|
+
</div>
|
|
319
|
+
{:else if items.length === 0}
|
|
320
|
+
<div class="text-center py-12 text-foreground-tertiary">
|
|
321
|
+
<i class="fa-solid fa-inbox text-3xl"></i>
|
|
322
|
+
<p class="mt-2">No feedback yet</p>
|
|
323
|
+
<p class="text-sm">Be the first to submit!</p>
|
|
324
|
+
</div>
|
|
325
|
+
{:else}
|
|
326
|
+
<div class="flex flex-col gap-2">
|
|
327
|
+
{#each items as item (item.id)}
|
|
328
|
+
<Card variant="outlined" padding="none" hover>
|
|
329
|
+
<div class="flex gap-3 p-4">
|
|
309
330
|
<!-- Vote column -->
|
|
310
|
-
<div class="
|
|
311
|
-
<
|
|
312
|
-
|
|
313
|
-
|
|
331
|
+
<div class="flex flex-col items-center gap-0.5 min-w-[2.5rem]">
|
|
332
|
+
<IconButton
|
|
333
|
+
size="sm"
|
|
334
|
+
variant={votedItems[item.id] === 'up' ? 'primary' : 'neutral'}
|
|
314
335
|
onclick={() => handleVote(item.id, 'up')}
|
|
315
336
|
aria-label="Upvote"
|
|
316
337
|
>
|
|
317
|
-
<i class="fa-solid fa-chevron-up"></i>
|
|
318
|
-
</
|
|
319
|
-
<span class="
|
|
320
|
-
<
|
|
321
|
-
|
|
322
|
-
|
|
338
|
+
<i class="fa-solid fa-chevron-up text-xs"></i>
|
|
339
|
+
</IconButton>
|
|
340
|
+
<span class="text-sm font-bold">{item.votes}</span>
|
|
341
|
+
<IconButton
|
|
342
|
+
size="sm"
|
|
343
|
+
variant={votedItems[item.id] === 'down' ? 'primary' : 'neutral'}
|
|
323
344
|
onclick={() => handleVote(item.id, 'down')}
|
|
324
345
|
aria-label="Downvote"
|
|
325
346
|
>
|
|
326
|
-
<i class="fa-solid fa-chevron-down"></i>
|
|
327
|
-
</
|
|
347
|
+
<i class="fa-solid fa-chevron-down text-xs"></i>
|
|
348
|
+
</IconButton>
|
|
328
349
|
</div>
|
|
329
350
|
|
|
330
351
|
<!-- Content -->
|
|
331
|
-
<div class="
|
|
332
|
-
<div class="
|
|
352
|
+
<div class="flex-1 min-w-0">
|
|
353
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
333
354
|
{#if item.category}
|
|
334
|
-
<
|
|
335
|
-
<i class={getCategoryIcon(item.category)}></i>
|
|
336
|
-
</span>
|
|
355
|
+
<i class="{getCategoryIcon(item.category)} text-sm text-foreground-secondary"></i>
|
|
337
356
|
{/if}
|
|
338
|
-
<h3 class="
|
|
357
|
+
<h3 class="text-[0.9375rem] font-semibold flex-1">
|
|
339
358
|
{item.title || item.message.slice(0, 80)}
|
|
340
359
|
</h3>
|
|
341
|
-
<
|
|
360
|
+
<Badge variant={getStatusBadgeVariant(item.status)} size="sm">
|
|
342
361
|
{item.status === 'new' ? 'Open' : item.status.charAt(0).toUpperCase() + item.status.slice(1)}
|
|
343
|
-
</
|
|
362
|
+
</Badge>
|
|
344
363
|
</div>
|
|
345
364
|
|
|
346
365
|
{#if item.title}
|
|
347
|
-
<p class="
|
|
366
|
+
<p class="text-sm text-foreground-secondary mt-1.5 leading-relaxed">{item.message}</p>
|
|
348
367
|
{/if}
|
|
349
368
|
|
|
350
|
-
<div class="
|
|
351
|
-
<span
|
|
369
|
+
<div class="flex items-center gap-3 mt-2 text-[0.8125rem] text-foreground-tertiary">
|
|
370
|
+
<span>{timeAgo(item.createdAt)}</span>
|
|
352
371
|
{#if item.responseCount > 0}
|
|
353
|
-
<button
|
|
372
|
+
<button
|
|
373
|
+
class="flex items-center gap-1 hover:text-foreground transition-colors bg-transparent border-none cursor-pointer text-[0.8125rem] text-foreground-tertiary p-0"
|
|
374
|
+
onclick={() => toggleExpand(item.id)}
|
|
375
|
+
>
|
|
354
376
|
<i class="fa-solid fa-comment"></i>
|
|
355
377
|
{item.responseCount} {item.responseCount === 1 ? 'response' : 'responses'}
|
|
356
378
|
</button>
|
|
357
379
|
{/if}
|
|
358
380
|
{#if item.adminResponse}
|
|
359
|
-
<span class="
|
|
360
|
-
<i class="fa-solid fa-shield
|
|
381
|
+
<span class="text-xs text-primary">
|
|
382
|
+
<i class="fa-solid fa-shield"></i> Official response
|
|
361
383
|
</span>
|
|
362
384
|
{/if}
|
|
363
385
|
</div>
|
|
364
386
|
|
|
365
387
|
<!-- Expanded responses -->
|
|
366
388
|
{#if expandedId === item.id}
|
|
367
|
-
<div class="
|
|
389
|
+
<div class="mt-3 pt-3 border-t border-border space-y-2">
|
|
368
390
|
{#if loadingResponses[item.id]}
|
|
369
|
-
<p class="
|
|
391
|
+
<p class="text-sm text-foreground-tertiary"><Spinner /> Loading...</p>
|
|
370
392
|
{:else if responses[item.id]?.length}
|
|
371
393
|
{#each responses[item.id] as resp}
|
|
372
|
-
<div class="
|
|
373
|
-
<div class="
|
|
374
|
-
<span class="
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
{:else}
|
|
378
|
-
Team
|
|
379
|
-
{/if}
|
|
394
|
+
<div class="p-3 rounded-lg bg-surface">
|
|
395
|
+
<div class="flex items-center justify-between mb-1">
|
|
396
|
+
<span class="text-[0.8125rem] font-semibold text-primary">
|
|
397
|
+
<i class="fa-solid fa-shield mr-1"></i>
|
|
398
|
+
{resp.author || 'Team'}
|
|
380
399
|
</span>
|
|
381
|
-
<span class="
|
|
400
|
+
<span class="text-xs text-foreground-tertiary">{timeAgo(resp.createdAt)}</span>
|
|
382
401
|
</div>
|
|
383
|
-
<p class="
|
|
402
|
+
<p class="text-sm text-foreground-secondary leading-relaxed m-0">{resp.message}</p>
|
|
384
403
|
</div>
|
|
385
404
|
{/each}
|
|
386
405
|
{:else}
|
|
387
|
-
<p class="
|
|
406
|
+
<p class="text-sm text-foreground-tertiary">No responses yet.</p>
|
|
388
407
|
{/if}
|
|
389
408
|
</div>
|
|
390
409
|
{/if}
|
|
391
410
|
</div>
|
|
392
411
|
</div>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
</
|
|
412
|
+
</Card>
|
|
413
|
+
{/each}
|
|
414
|
+
</div>
|
|
415
|
+
{/if}
|
|
416
|
+
</main>
|
|
398
417
|
</div>
|
|
399
|
-
|
|
400
|
-
<style>
|
|
401
|
-
.sf-board {
|
|
402
|
-
--sf-bg: var(--color-surface, #0f172a);
|
|
403
|
-
--sf-bg-secondary: var(--color-surface-secondary, #1e293b);
|
|
404
|
-
--sf-bg-hover: var(--color-surface-hover, #334155);
|
|
405
|
-
--sf-text: var(--color-foreground, #e2e8f0);
|
|
406
|
-
--sf-text-secondary: var(--color-foreground-secondary, #94a3b8);
|
|
407
|
-
--sf-text-tertiary: var(--color-foreground-tertiary, #64748b);
|
|
408
|
-
--sf-border: var(--color-border, #334155);
|
|
409
|
-
--sf-primary: var(--color-primary, #3b82f6);
|
|
410
|
-
--sf-primary-text: var(--color-primary-foreground, #ffffff);
|
|
411
|
-
--sf-success: #22c55e;
|
|
412
|
-
--sf-radius: 0.5rem;
|
|
413
|
-
font-family: inherit;
|
|
414
|
-
color: var(--sf-text);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
.sf-board-layout {
|
|
418
|
-
display: flex;
|
|
419
|
-
gap: 1.5rem;
|
|
420
|
-
min-height: 400px;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/* Sidebar */
|
|
424
|
-
.sf-board-sidebar {
|
|
425
|
-
width: 220px;
|
|
426
|
-
flex-shrink: 0;
|
|
427
|
-
display: flex;
|
|
428
|
-
flex-direction: column;
|
|
429
|
-
gap: 0.25rem;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
.sf-sidebar-item {
|
|
433
|
-
display: flex;
|
|
434
|
-
align-items: center;
|
|
435
|
-
gap: 0.5rem;
|
|
436
|
-
padding: 0.5rem 0.75rem;
|
|
437
|
-
border-radius: var(--sf-radius);
|
|
438
|
-
border: none;
|
|
439
|
-
background: transparent;
|
|
440
|
-
color: var(--sf-text-secondary);
|
|
441
|
-
cursor: pointer;
|
|
442
|
-
font-size: 0.875rem;
|
|
443
|
-
text-align: left;
|
|
444
|
-
width: 100%;
|
|
445
|
-
transition: all 0.15s;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
.sf-sidebar-item:hover {
|
|
449
|
-
background: var(--sf-bg-hover);
|
|
450
|
-
color: var(--sf-text);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
.sf-sidebar-active {
|
|
454
|
-
background: var(--sf-bg-secondary);
|
|
455
|
-
color: var(--sf-text);
|
|
456
|
-
font-weight: 600;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
.sf-sidebar-count {
|
|
460
|
-
margin-left: auto;
|
|
461
|
-
background: var(--sf-bg-hover);
|
|
462
|
-
padding: 0.125rem 0.5rem;
|
|
463
|
-
border-radius: 999px;
|
|
464
|
-
font-size: 0.75rem;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
.sf-sidebar-divider {
|
|
468
|
-
border: none;
|
|
469
|
-
border-top: 1px solid var(--sf-border);
|
|
470
|
-
margin: 0.5rem 0;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
.sf-sidebar-section {
|
|
474
|
-
padding: 0.25rem 0.75rem;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
.sf-sidebar-label {
|
|
478
|
-
display: block;
|
|
479
|
-
font-size: 0.75rem;
|
|
480
|
-
color: var(--sf-text-tertiary);
|
|
481
|
-
margin-bottom: 0.25rem;
|
|
482
|
-
text-transform: uppercase;
|
|
483
|
-
letter-spacing: 0.05em;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/* Main */
|
|
487
|
-
.sf-board-main {
|
|
488
|
-
flex: 1;
|
|
489
|
-
min-width: 0;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
.sf-board-header {
|
|
493
|
-
display: flex;
|
|
494
|
-
align-items: flex-start;
|
|
495
|
-
justify-content: space-between;
|
|
496
|
-
margin-bottom: 1rem;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
.sf-board-title {
|
|
500
|
-
font-size: 1.25rem;
|
|
501
|
-
font-weight: 700;
|
|
502
|
-
margin: 0;
|
|
503
|
-
display: flex;
|
|
504
|
-
align-items: center;
|
|
505
|
-
gap: 0.5rem;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
.sf-board-subtitle {
|
|
509
|
-
font-size: 0.875rem;
|
|
510
|
-
color: var(--sf-text-secondary);
|
|
511
|
-
margin: 0.25rem 0 0;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/* Items */
|
|
515
|
-
.sf-items {
|
|
516
|
-
display: flex;
|
|
517
|
-
flex-direction: column;
|
|
518
|
-
gap: 0.5rem;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
.sf-item {
|
|
522
|
-
display: flex;
|
|
523
|
-
gap: 0.75rem;
|
|
524
|
-
padding: 1rem;
|
|
525
|
-
background: var(--sf-bg-secondary);
|
|
526
|
-
border: 1px solid var(--sf-border);
|
|
527
|
-
border-radius: var(--sf-radius);
|
|
528
|
-
transition: border-color 0.15s;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
.sf-item:hover {
|
|
532
|
-
border-color: var(--sf-text-tertiary);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/* Vote column */
|
|
536
|
-
.sf-vote-col {
|
|
537
|
-
display: flex;
|
|
538
|
-
flex-direction: column;
|
|
539
|
-
align-items: center;
|
|
540
|
-
gap: 0.125rem;
|
|
541
|
-
min-width: 2.5rem;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
.sf-vote-btn {
|
|
545
|
-
background: none;
|
|
546
|
-
border: 1px solid transparent;
|
|
547
|
-
color: var(--sf-text-tertiary);
|
|
548
|
-
cursor: pointer;
|
|
549
|
-
padding: 0.25rem;
|
|
550
|
-
border-radius: 0.25rem;
|
|
551
|
-
font-size: 0.75rem;
|
|
552
|
-
transition: all 0.15s;
|
|
553
|
-
line-height: 1;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
.sf-vote-btn:hover {
|
|
557
|
-
color: var(--sf-primary);
|
|
558
|
-
border-color: var(--sf-primary);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
.sf-voted {
|
|
562
|
-
color: var(--sf-primary) !important;
|
|
563
|
-
border-color: var(--sf-primary) !important;
|
|
564
|
-
background: color-mix(in srgb, var(--sf-primary) 10%, transparent);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
.sf-vote-count {
|
|
568
|
-
font-size: 0.875rem;
|
|
569
|
-
font-weight: 700;
|
|
570
|
-
color: var(--sf-text);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/* Item content */
|
|
574
|
-
.sf-item-content {
|
|
575
|
-
flex: 1;
|
|
576
|
-
min-width: 0;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
.sf-item-header {
|
|
580
|
-
display: flex;
|
|
581
|
-
align-items: center;
|
|
582
|
-
gap: 0.5rem;
|
|
583
|
-
flex-wrap: wrap;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
.sf-item-category {
|
|
587
|
-
font-size: 0.875rem;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
.sf-item-title {
|
|
591
|
-
font-size: 0.9375rem;
|
|
592
|
-
font-weight: 600;
|
|
593
|
-
margin: 0;
|
|
594
|
-
flex: 1;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
.sf-item-status {
|
|
598
|
-
font-size: 0.75rem;
|
|
599
|
-
padding: 0.125rem 0.5rem;
|
|
600
|
-
border-radius: 999px;
|
|
601
|
-
font-weight: 500;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
.sf-item-status[data-status='new'],
|
|
605
|
-
.sf-item-status[data-status='reviewed'] {
|
|
606
|
-
background: color-mix(in srgb, var(--sf-success) 15%, transparent);
|
|
607
|
-
color: var(--sf-success);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
.sf-item-status[data-status='resolved'] {
|
|
611
|
-
background: color-mix(in srgb, var(--sf-text-tertiary) 15%, transparent);
|
|
612
|
-
color: var(--sf-text-tertiary);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
.sf-item-message {
|
|
616
|
-
font-size: 0.875rem;
|
|
617
|
-
color: var(--sf-text-secondary);
|
|
618
|
-
margin: 0.5rem 0 0;
|
|
619
|
-
line-height: 1.5;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
.sf-item-footer {
|
|
623
|
-
display: flex;
|
|
624
|
-
align-items: center;
|
|
625
|
-
gap: 0.75rem;
|
|
626
|
-
margin-top: 0.5rem;
|
|
627
|
-
font-size: 0.8125rem;
|
|
628
|
-
color: var(--sf-text-tertiary);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
.sf-item-responses {
|
|
632
|
-
background: none;
|
|
633
|
-
border: none;
|
|
634
|
-
color: var(--sf-text-tertiary);
|
|
635
|
-
cursor: pointer;
|
|
636
|
-
font-size: 0.8125rem;
|
|
637
|
-
display: flex;
|
|
638
|
-
align-items: center;
|
|
639
|
-
gap: 0.25rem;
|
|
640
|
-
padding: 0;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
.sf-item-responses:hover {
|
|
644
|
-
color: var(--sf-text);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
.sf-item-badge {
|
|
648
|
-
font-size: 0.75rem;
|
|
649
|
-
color: var(--sf-primary);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/* Responses */
|
|
653
|
-
.sf-responses {
|
|
654
|
-
margin-top: 0.75rem;
|
|
655
|
-
padding-top: 0.75rem;
|
|
656
|
-
border-top: 1px solid var(--sf-border);
|
|
657
|
-
display: flex;
|
|
658
|
-
flex-direction: column;
|
|
659
|
-
gap: 0.5rem;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
.sf-response {
|
|
663
|
-
padding: 0.5rem 0.75rem;
|
|
664
|
-
background: var(--sf-bg);
|
|
665
|
-
border-radius: var(--sf-radius);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
.sf-response-header {
|
|
669
|
-
display: flex;
|
|
670
|
-
align-items: center;
|
|
671
|
-
justify-content: space-between;
|
|
672
|
-
margin-bottom: 0.25rem;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
.sf-response-author {
|
|
676
|
-
font-size: 0.8125rem;
|
|
677
|
-
font-weight: 600;
|
|
678
|
-
color: var(--sf-primary);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
.sf-response-time {
|
|
682
|
-
font-size: 0.75rem;
|
|
683
|
-
color: var(--sf-text-tertiary);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
.sf-response-message {
|
|
687
|
-
font-size: 0.875rem;
|
|
688
|
-
color: var(--sf-text-secondary);
|
|
689
|
-
margin: 0;
|
|
690
|
-
line-height: 1.5;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
.sf-responses-loading,
|
|
694
|
-
.sf-responses-empty {
|
|
695
|
-
font-size: 0.8125rem;
|
|
696
|
-
color: var(--sf-text-tertiary);
|
|
697
|
-
margin: 0;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/* Empty state */
|
|
701
|
-
.sf-empty {
|
|
702
|
-
text-align: center;
|
|
703
|
-
padding: 3rem 1rem;
|
|
704
|
-
color: var(--sf-text-tertiary);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
.sf-empty i {
|
|
708
|
-
font-size: 2rem;
|
|
709
|
-
margin-bottom: 0.5rem;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
.sf-empty p {
|
|
713
|
-
margin: 0.25rem 0;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
.sf-empty-sub {
|
|
717
|
-
font-size: 0.875rem;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
/* Form elements */
|
|
721
|
-
.sf-select {
|
|
722
|
-
width: 100%;
|
|
723
|
-
padding: 0.375rem 0.5rem;
|
|
724
|
-
background: var(--sf-bg-secondary);
|
|
725
|
-
border: 1px solid var(--sf-border);
|
|
726
|
-
border-radius: 0.375rem;
|
|
727
|
-
color: var(--sf-text);
|
|
728
|
-
font-size: 0.8125rem;
|
|
729
|
-
cursor: pointer;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
.sf-submit-btn {
|
|
733
|
-
display: inline-flex;
|
|
734
|
-
align-items: center;
|
|
735
|
-
gap: 0.375rem;
|
|
736
|
-
padding: 0.5rem 1rem;
|
|
737
|
-
background: var(--sf-primary);
|
|
738
|
-
color: var(--sf-primary-text);
|
|
739
|
-
border: none;
|
|
740
|
-
border-radius: var(--sf-radius);
|
|
741
|
-
font-size: 0.875rem;
|
|
742
|
-
font-weight: 600;
|
|
743
|
-
cursor: pointer;
|
|
744
|
-
transition: opacity 0.15s;
|
|
745
|
-
white-space: nowrap;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
.sf-submit-btn:hover {
|
|
749
|
-
opacity: 0.9;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
.sf-submit-btn:disabled {
|
|
753
|
-
opacity: 0.5;
|
|
754
|
-
cursor: not-allowed;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
.sf-submit-form {
|
|
758
|
-
padding: 1rem;
|
|
759
|
-
margin-bottom: 1rem;
|
|
760
|
-
background: var(--sf-bg-secondary);
|
|
761
|
-
border: 1px solid var(--sf-border);
|
|
762
|
-
border-radius: var(--sf-radius);
|
|
763
|
-
display: flex;
|
|
764
|
-
flex-direction: column;
|
|
765
|
-
gap: 0.5rem;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
.sf-input,
|
|
769
|
-
.sf-textarea {
|
|
770
|
-
width: 100%;
|
|
771
|
-
padding: 0.5rem 0.75rem;
|
|
772
|
-
background: var(--sf-bg);
|
|
773
|
-
border: 1px solid var(--sf-border);
|
|
774
|
-
border-radius: 0.375rem;
|
|
775
|
-
color: var(--sf-text);
|
|
776
|
-
font-size: 0.875rem;
|
|
777
|
-
font-family: inherit;
|
|
778
|
-
resize: vertical;
|
|
779
|
-
box-sizing: border-box;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
.sf-input:focus,
|
|
783
|
-
.sf-textarea:focus {
|
|
784
|
-
outline: none;
|
|
785
|
-
border-color: var(--sf-primary);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
.sf-input-sm {
|
|
789
|
-
flex: 1;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
.sf-submit-row {
|
|
793
|
-
display: flex;
|
|
794
|
-
gap: 0.5rem;
|
|
795
|
-
align-items: center;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
.sf-submit-row .sf-select {
|
|
799
|
-
width: auto;
|
|
800
|
-
min-width: 120px;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
.sf-success {
|
|
804
|
-
color: var(--sf-success);
|
|
805
|
-
text-align: center;
|
|
806
|
-
padding: 0.5rem;
|
|
807
|
-
font-weight: 600;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
/* Responsive */
|
|
811
|
-
@media (max-width: 768px) {
|
|
812
|
-
.sf-board-layout {
|
|
813
|
-
flex-direction: column;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
.sf-board-sidebar {
|
|
817
|
-
width: 100%;
|
|
818
|
-
flex-direction: row;
|
|
819
|
-
flex-wrap: wrap;
|
|
820
|
-
gap: 0.25rem;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
.sf-sidebar-divider {
|
|
824
|
-
display: none;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
.sf-sidebar-section {
|
|
828
|
-
display: flex;
|
|
829
|
-
align-items: center;
|
|
830
|
-
gap: 0.5rem;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
.sf-sidebar-label {
|
|
834
|
-
margin: 0;
|
|
835
|
-
white-space: nowrap;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
.sf-submit-row {
|
|
839
|
-
flex-wrap: wrap;
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
</style>
|