@alliance-droid/status-feedback-system 0.1.0 → 0.2.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.
Files changed (54) hide show
  1. package/dist/api.d.ts +94 -0
  2. package/dist/api.js +100 -0
  3. package/dist/components/FeedbackBoard.svelte +842 -0
  4. package/dist/components/FeedbackBoard.svelte.d.ts +19 -0
  5. package/dist/components/FeedbackButton.svelte +3 -3
  6. package/dist/components/FeedbackButton.svelte.d.ts +1 -1
  7. package/dist/components/FeedbackForm.svelte +3 -3
  8. package/dist/components/FeedbackForm.svelte.d.ts +1 -1
  9. package/dist/components/StatusBanner.svelte +36 -3
  10. package/dist/components/StatusBanner.svelte.d.ts +7 -1
  11. package/dist/components/StatusPage.svelte +150 -83
  12. package/dist/components/StatusPage.svelte.d.ts +10 -3
  13. package/dist/index.d.ts +7 -9
  14. package/dist/index.js +6 -8
  15. package/dist/types.d.ts +13 -6
  16. package/package.json +2 -13
  17. package/dist/components/admin/AdminDashboard.svelte +0 -274
  18. package/dist/components/admin/AdminDashboard.svelte.d.ts +0 -9
  19. package/dist/components/admin/ApiKeyManager.svelte +0 -68
  20. package/dist/components/admin/ApiKeyManager.svelte.d.ts +0 -7
  21. package/dist/components/admin/FeedbackList.svelte +0 -160
  22. package/dist/components/admin/FeedbackList.svelte.d.ts +0 -17
  23. package/dist/components/admin/IncidentForm.svelte +0 -135
  24. package/dist/components/admin/IncidentForm.svelte.d.ts +0 -14
  25. package/dist/components/admin/IncidentManager.svelte +0 -153
  26. package/dist/components/admin/IncidentManager.svelte.d.ts +0 -13
  27. package/dist/components/admin/ProjectForm.svelte +0 -106
  28. package/dist/components/admin/ProjectForm.svelte.d.ts +0 -13
  29. package/dist/components/admin/ProjectList.svelte +0 -57
  30. package/dist/components/admin/ProjectList.svelte.d.ts +0 -10
  31. package/dist/components/admin/StatusControl.svelte +0 -62
  32. package/dist/components/admin/StatusControl.svelte.d.ts +0 -8
  33. package/dist/components/admin/SystemManager.svelte +0 -119
  34. package/dist/components/admin/SystemManager.svelte.d.ts +0 -13
  35. package/dist/components/admin/index.d.ts +0 -9
  36. package/dist/components/admin/index.js +0 -9
  37. package/dist/server/db.d.ts +0 -17
  38. package/dist/server/db.js +0 -68
  39. package/dist/server/feedback.d.ts +0 -27
  40. package/dist/server/feedback.js +0 -124
  41. package/dist/server/handlers.d.ts +0 -41
  42. package/dist/server/handlers.js +0 -251
  43. package/dist/server/incidents.d.ts +0 -27
  44. package/dist/server/incidents.js +0 -180
  45. package/dist/server/index.d.ts +0 -101
  46. package/dist/server/index.js +0 -71
  47. package/dist/server/projects.d.ts +0 -45
  48. package/dist/server/projects.js +0 -156
  49. package/dist/server/schema.d.ts +0 -9
  50. package/dist/server/schema.js +0 -94
  51. package/dist/server/systems.d.ts +0 -20
  52. package/dist/server/systems.js +0 -146
  53. package/dist/server/types.d.ts +0 -4
  54. package/dist/server/types.js +0 -4
@@ -0,0 +1,842 @@
1
+ <script lang="ts">
2
+ import { createStatusClient, type BoardItem, type BoardCategory } from '../api.js';
3
+
4
+ interface Props {
5
+ apiUrl: string;
6
+ apiKey: string;
7
+ /** Category labels and icons — order determines sidebar order */
8
+ categoryConfig?: Record<string, { label?: string; icon?: string; description?: string }>;
9
+ /** Title shown at the top */
10
+ title?: string;
11
+ /** Show the submit form inline */
12
+ showSubmit?: boolean;
13
+ /** Available categories for the submit form */
14
+ submitCategories?: string[];
15
+ }
16
+
17
+ let {
18
+ apiUrl,
19
+ apiKey,
20
+ categoryConfig = {},
21
+ title = 'Feedback Board',
22
+ showSubmit = true,
23
+ submitCategories = [],
24
+ }: Props = $props();
25
+
26
+ let client = $derived(createStatusClient({ apiUrl, apiKey }));
27
+ let items = $state<BoardItem[]>([]);
28
+ let categories = $state<BoardCategory[]>([]);
29
+ let total = $state(0);
30
+ let loading = $state(true);
31
+ let error = $state('');
32
+
33
+ let activeCategory = $state<string | null>(null);
34
+ let statusFilter = $state<'open' | 'resolved' | 'all'>('open');
35
+ let sortBy = $state<'votes' | 'newest'>('votes');
36
+
37
+ // Expanded item for viewing responses
38
+ let expandedId = $state<string | null>(null);
39
+ let responses = $state<Record<string, { message: string; author: string | null; createdAt: number }[]>>({});
40
+ let loadingResponses = $state<Record<string, boolean>>({});
41
+
42
+ // Submit form
43
+ let showForm = $state(false);
44
+ let submitTitle = $state('');
45
+ let submitMessage = $state('');
46
+ let submitCategory = $state('');
47
+ let submitEmail = $state('');
48
+ let submitting = $state(false);
49
+ let submitSuccess = $state(false);
50
+
51
+ // Voting state (tracks which items the user voted on this session)
52
+ let votedItems = $state<Record<string, 'up' | 'down'>>({});
53
+
54
+ async function load() {
55
+ loading = true;
56
+ error = '';
57
+ try {
58
+ const data = await client.getBoard({
59
+ category: activeCategory || undefined,
60
+ status: statusFilter,
61
+ sort: sortBy,
62
+ });
63
+ items = data.items;
64
+ total = data.total;
65
+ categories = data.categories;
66
+ } catch (e) {
67
+ error = e instanceof Error ? e.message : 'Failed to load';
68
+ } finally {
69
+ loading = false;
70
+ }
71
+ }
72
+
73
+ $effect(() => {
74
+ // Re-fetch when filters change
75
+ void activeCategory;
76
+ void statusFilter;
77
+ void sortBy;
78
+ load();
79
+ });
80
+
81
+ async function handleVote(itemId: string, direction: 'up' | 'down') {
82
+ const prev = votedItems[itemId];
83
+ try {
84
+ const result = await client.vote(itemId, direction);
85
+ // Update local state
86
+ const item = items.find((i) => i.id === itemId);
87
+ if (item) item.votes = result.votes;
88
+
89
+ if (prev === direction) {
90
+ // Toggled off
91
+ delete votedItems[itemId];
92
+ votedItems = { ...votedItems };
93
+ } else {
94
+ votedItems = { ...votedItems, [itemId]: direction };
95
+ }
96
+ } catch {
97
+ // silently fail
98
+ }
99
+ }
100
+
101
+ async function toggleExpand(itemId: string) {
102
+ if (expandedId === itemId) {
103
+ expandedId = null;
104
+ return;
105
+ }
106
+ expandedId = itemId;
107
+ if (!responses[itemId]) {
108
+ loadingResponses = { ...loadingResponses, [itemId]: true };
109
+ try {
110
+ const data = await client.getResponses(itemId);
111
+ responses = { ...responses, [itemId]: data.responses };
112
+ } catch {
113
+ responses = { ...responses, [itemId]: [] };
114
+ } finally {
115
+ loadingResponses = { ...loadingResponses, [itemId]: false };
116
+ }
117
+ }
118
+ }
119
+
120
+ async function handleSubmit() {
121
+ if (!submitMessage.trim()) return;
122
+ submitting = true;
123
+ try {
124
+ await client.submitFeedback({
125
+ title: submitTitle.trim() || undefined,
126
+ message: submitMessage.trim(),
127
+ category: submitCategory || undefined,
128
+ email: submitEmail.trim() || undefined,
129
+ pageUrl: window.location.href,
130
+ });
131
+ submitSuccess = true;
132
+ submitTitle = '';
133
+ submitMessage = '';
134
+ submitCategory = '';
135
+ submitEmail = '';
136
+ setTimeout(() => {
137
+ submitSuccess = false;
138
+ showForm = false;
139
+ }, 2000);
140
+ // Reload board
141
+ await load();
142
+ } catch {
143
+ // ignore
144
+ } finally {
145
+ submitting = false;
146
+ }
147
+ }
148
+
149
+ function getCategoryIcon(name: string): string {
150
+ return categoryConfig[name]?.icon || 'fa-solid fa-circle';
151
+ }
152
+
153
+ function getCategoryLabel(name: string): string {
154
+ return categoryConfig[name]?.label || name;
155
+ }
156
+
157
+ function getCategoryDescription(name: string): string {
158
+ return categoryConfig[name]?.description || '';
159
+ }
160
+
161
+ function timeAgo(timestamp: number): string {
162
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
163
+ if (seconds < 60) return 'just now';
164
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
165
+ if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
166
+ const days = Math.floor(seconds / 86400);
167
+ if (days < 30) return `${days}d ago`;
168
+ return new Date(timestamp).toLocaleDateString();
169
+ }
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
+
186
+ {#each categories as cat}
187
+ <button
188
+ class="sf-sidebar-item"
189
+ class:sf-sidebar-active={activeCategory === cat.name}
190
+ onclick={() => (activeCategory = activeCategory === cat.name ? null : cat.name)}
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}
197
+
198
+ <hr class="sf-sidebar-divider" />
199
+
200
+ <div class="sf-sidebar-section">
201
+ <label class="sf-sidebar-label">Status</label>
202
+ <select bind:value={statusFilter} class="sf-select">
203
+ <option value="open">Open</option>
204
+ <option value="resolved">Resolved</option>
205
+ <option value="all">All</option>
206
+ </select>
207
+ </div>
208
+
209
+ <div class="sf-sidebar-section">
210
+ <label class="sf-sidebar-label">Sort By</label>
211
+ <select bind:value={sortBy} class="sf-select">
212
+ <option value="votes">Most Votes</option>
213
+ <option value="newest">Newest</option>
214
+ </select>
215
+ </div>
216
+ </aside>
217
+
218
+ <!-- Main content -->
219
+ <main class="sf-board-main">
220
+ <div class="sf-board-header">
221
+ <div>
222
+ <h2 class="sf-board-title">
223
+ {#if activeCategory}
224
+ <i class={getCategoryIcon(activeCategory)}></i>
225
+ {getCategoryLabel(activeCategory)}
226
+ {:else}
227
+ {title}
228
+ {/if}
229
+ </h2>
230
+ {#if activeCategory && getCategoryDescription(activeCategory)}
231
+ <p class="sf-board-subtitle">{getCategoryDescription(activeCategory)}</p>
232
+ {/if}
233
+ </div>
234
+ {#if showSubmit}
235
+ <button class="sf-submit-btn" onclick={() => (showForm = !showForm)}>
236
+ <i class="fa-solid fa-plus"></i> Submit
237
+ </button>
238
+ {/if}
239
+ </div>
240
+
241
+ <!-- Submit form -->
242
+ {#if showForm}
243
+ <div class="sf-submit-form">
244
+ {#if submitSuccess}
245
+ <div class="sf-success">
246
+ <i class="fa-solid fa-check-circle"></i> Submitted! It will appear once reviewed.
247
+ </div>
248
+ {:else}
249
+ <input
250
+ type="text"
251
+ bind:value={submitTitle}
252
+ placeholder="Title (optional)"
253
+ class="sf-input"
254
+ />
255
+ <textarea
256
+ bind:value={submitMessage}
257
+ placeholder="Describe your feedback..."
258
+ rows="3"
259
+ class="sf-textarea"
260
+ ></textarea>
261
+ <div class="sf-submit-row">
262
+ {#if submitCategories.length > 0}
263
+ <select bind:value={submitCategory} class="sf-select">
264
+ <option value="">Category...</option>
265
+ {#each submitCategories as cat}
266
+ <option value={cat}>{getCategoryLabel(cat)}</option>
267
+ {/each}
268
+ </select>
269
+ {/if}
270
+ <input
271
+ type="email"
272
+ bind:value={submitEmail}
273
+ placeholder="Email (optional)"
274
+ class="sf-input sf-input-sm"
275
+ />
276
+ <button
277
+ class="sf-submit-btn"
278
+ disabled={submitting || !submitMessage.trim()}
279
+ onclick={handleSubmit}
280
+ >
281
+ {submitting ? 'Sending...' : 'Submit'}
282
+ </button>
283
+ </div>
284
+ {/if}
285
+ </div>
286
+ {/if}
287
+
288
+ <!-- Items -->
289
+ {#if loading}
290
+ <div class="sf-empty">
291
+ <i class="fa-solid fa-spinner fa-spin"></i>
292
+ <p>Loading...</p>
293
+ </div>
294
+ {:else if error}
295
+ <div class="sf-empty">
296
+ <i class="fa-solid fa-exclamation-triangle"></i>
297
+ <p>{error}</p>
298
+ </div>
299
+ {:else if items.length === 0}
300
+ <div class="sf-empty">
301
+ <i class="fa-solid fa-inbox"></i>
302
+ <p>No feedback yet</p>
303
+ <p class="sf-empty-sub">Be the first to submit!</p>
304
+ </div>
305
+ {:else}
306
+ <div class="sf-items">
307
+ {#each items as item (item.id)}
308
+ <div class="sf-item">
309
+ <!-- Vote column -->
310
+ <div class="sf-vote-col">
311
+ <button
312
+ class="sf-vote-btn"
313
+ class:sf-voted={votedItems[item.id] === 'up'}
314
+ onclick={() => handleVote(item.id, 'up')}
315
+ aria-label="Upvote"
316
+ >
317
+ <i class="fa-solid fa-chevron-up"></i>
318
+ </button>
319
+ <span class="sf-vote-count">{item.votes}</span>
320
+ <button
321
+ class="sf-vote-btn"
322
+ class:sf-voted={votedItems[item.id] === 'down'}
323
+ onclick={() => handleVote(item.id, 'down')}
324
+ aria-label="Downvote"
325
+ >
326
+ <i class="fa-solid fa-chevron-down"></i>
327
+ </button>
328
+ </div>
329
+
330
+ <!-- Content -->
331
+ <div class="sf-item-content">
332
+ <div class="sf-item-header">
333
+ {#if item.category}
334
+ <span class="sf-item-category">
335
+ <i class={getCategoryIcon(item.category)}></i>
336
+ </span>
337
+ {/if}
338
+ <h3 class="sf-item-title">
339
+ {item.title || item.message.slice(0, 80)}
340
+ </h3>
341
+ <span class="sf-item-status" data-status={item.status}>
342
+ {item.status === 'new' ? 'Open' : item.status.charAt(0).toUpperCase() + item.status.slice(1)}
343
+ </span>
344
+ </div>
345
+
346
+ {#if item.title}
347
+ <p class="sf-item-message">{item.message}</p>
348
+ {/if}
349
+
350
+ <div class="sf-item-footer">
351
+ <span class="sf-item-time">{timeAgo(item.createdAt)}</span>
352
+ {#if item.responseCount > 0}
353
+ <button class="sf-item-responses" onclick={() => toggleExpand(item.id)}>
354
+ <i class="fa-solid fa-comment"></i>
355
+ {item.responseCount} {item.responseCount === 1 ? 'response' : 'responses'}
356
+ </button>
357
+ {/if}
358
+ {#if item.adminResponse}
359
+ <span class="sf-item-badge">
360
+ <i class="fa-solid fa-shield-check"></i> Official response
361
+ </span>
362
+ {/if}
363
+ </div>
364
+
365
+ <!-- Expanded responses -->
366
+ {#if expandedId === item.id}
367
+ <div class="sf-responses">
368
+ {#if loadingResponses[item.id]}
369
+ <p class="sf-responses-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</p>
370
+ {:else if responses[item.id]?.length}
371
+ {#each responses[item.id] as resp}
372
+ <div class="sf-response">
373
+ <div class="sf-response-header">
374
+ <span class="sf-response-author">
375
+ {#if resp.author}
376
+ <i class="fa-solid fa-shield"></i> {resp.author}
377
+ {:else}
378
+ Team
379
+ {/if}
380
+ </span>
381
+ <span class="sf-response-time">{timeAgo(resp.createdAt)}</span>
382
+ </div>
383
+ <p class="sf-response-message">{resp.message}</p>
384
+ </div>
385
+ {/each}
386
+ {:else}
387
+ <p class="sf-responses-empty">No responses yet.</p>
388
+ {/if}
389
+ </div>
390
+ {/if}
391
+ </div>
392
+ </div>
393
+ {/each}
394
+ </div>
395
+ {/if}
396
+ </main>
397
+ </div>
398
+ </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>