@alliance-droid/status-feedback-system 0.2.0 → 0.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.
@@ -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<'open' | 'resolved' | 'all'>('open');
35
- let sortBy = $state<'votes' | 'newest'>('votes');
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
- {#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}
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
- <hr class="sf-sidebar-divider" />
180
+ const statusOptions = [
181
+ { value: 'open', label: 'Open' },
182
+ { value: 'resolved', label: 'Resolved' },
183
+ { value: 'all', label: 'All' },
184
+ ];
199
185
 
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>
186
+ const sortOptions = [
187
+ { value: 'votes', label: 'Most Votes' },
188
+ { value: 'newest', label: 'Newest' },
189
+ ];
208
190
 
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>
191
+ let submitCategoryOptions = $derived(
192
+ submitCategories.map((c) => ({ value: c, label: getCategoryLabel(c) }))
193
+ );
194
+ </script>
217
195
 
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>
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
- </div>
234
- {#if showSubmit}
235
- <button class="sf-submit-btn" onclick={() => (showForm = !showForm)}>
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
- <!-- 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"
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
- <textarea
273
+ <Textarea
256
274
  bind:value={submitMessage}
257
275
  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>
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
- <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"
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
- {submitting ? 'Sending...' : 'Submit'}
282
- </button>
300
+ Submit
301
+ </Button>
283
302
  </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">
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="sf-vote-col">
311
- <button
312
- class="sf-vote-btn"
313
- class:sf-voted={votedItems[item.id] === 'up'}
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
- </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'}
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
- </button>
347
+ <i class="fa-solid fa-chevron-down text-xs"></i>
348
+ </IconButton>
328
349
  </div>
329
350
 
330
351
  <!-- Content -->
331
- <div class="sf-item-content">
332
- <div class="sf-item-header">
352
+ <div class="flex-1 min-w-0">
353
+ <div class="flex items-center gap-2 flex-wrap">
333
354
  {#if item.category}
334
- <span class="sf-item-category">
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="sf-item-title">
357
+ <h3 class="text-[0.9375rem] font-semibold flex-1">
339
358
  {item.title || item.message.slice(0, 80)}
340
359
  </h3>
341
- <span class="sf-item-status" data-status={item.status}>
360
+ <Badge variant={getStatusBadgeVariant(item.status)} size="sm">
342
361
  {item.status === 'new' ? 'Open' : item.status.charAt(0).toUpperCase() + item.status.slice(1)}
343
- </span>
362
+ </Badge>
344
363
  </div>
345
364
 
346
365
  {#if item.title}
347
- <p class="sf-item-message">{item.message}</p>
366
+ <p class="text-sm text-foreground-secondary mt-1.5 leading-relaxed">{item.message}</p>
348
367
  {/if}
349
368
 
350
- <div class="sf-item-footer">
351
- <span class="sf-item-time">{timeAgo(item.createdAt)}</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 class="sf-item-responses" onclick={() => toggleExpand(item.id)}>
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="sf-item-badge">
360
- <i class="fa-solid fa-shield-check"></i> Official response
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="sf-responses">
389
+ <div class="mt-3 pt-3 border-t border-border space-y-2">
368
390
  {#if loadingResponses[item.id]}
369
- <p class="sf-responses-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading...</p>
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="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}
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="sf-response-time">{timeAgo(resp.createdAt)}</span>
400
+ <span class="text-xs text-foreground-tertiary">{timeAgo(resp.createdAt)}</span>
382
401
  </div>
383
- <p class="sf-response-message">{resp.message}</p>
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="sf-responses-empty">No responses yet.</p>
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
- {/each}
394
- </div>
395
- {/if}
396
- </main>
397
- </div>
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>
package/package.json CHANGED
@@ -1,60 +1,62 @@
1
1
  {
2
- "name": "@alliance-droid/status-feedback-system",
3
- "version": "0.2.0",
4
- "type": "module",
5
- "description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
6
- "svelte": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "svelte": "./dist/index.js"
12
- }
13
- },
14
- "files": [
15
- "dist",
16
- "!dist/**/*.test.*"
17
- ],
18
- "scripts": {
19
- "dev": "vite dev",
20
- "build": "vite build && npm run package",
21
- "package": "svelte-kit sync && svelte-package -o dist",
22
- "preview": "vite preview",
23
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
24
- "test": "vitest run",
25
- "test:watch": "vitest",
26
- "prepublishOnly": "npm run package"
27
- },
28
- "peerDependencies": {
29
- "svelte": "^5.0.0"
30
- },
31
- "dependencies": {
32
- "@alliance-droid/svelte-component-library": "^0.1.27"
33
- },
34
- "devDependencies": {
35
- "@sveltejs/adapter-auto": "^7.0.0",
36
- "@sveltejs/kit": "^2.50.0",
37
- "@sveltejs/package": "^2.3.0",
38
- "@sveltejs/vite-plugin-svelte": "^6.0.0",
39
- "@tailwindcss/vite": "^4.1.0",
40
- "publint": "^0.3.0",
41
- "svelte": "^5.48.0",
42
- "svelte-check": "^4.3.0",
43
- "tailwindcss": "^4.1.0",
44
- "typescript": "^5.9.0",
45
- "vite": "^7.3.0",
46
- "vitest": "^4.0.0"
47
- },
48
- "keywords": [
49
- "svelte",
50
- "sveltekit",
51
- "status-page",
52
- "feedback",
53
- "uptime"
54
- ],
55
- "author": "Alliance Technical Group",
56
- "license": "UNLICENSED",
57
- "publishConfig": {
58
- "access": "restricted"
59
- }
2
+ "name": "@alliance-droid/status-feedback-system",
3
+ "version": "0.2.1",
4
+ "type": "module",
5
+ "description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
6
+ "svelte": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "svelte": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "!dist/**/*.test.*"
17
+ ],
18
+ "scripts": {
19
+ "dev": "vite dev",
20
+ "build": "vite build && npm run package",
21
+ "package": "svelte-kit sync && svelte-package -o dist",
22
+ "preview": "vite preview",
23
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "prepublishOnly": "npm run package"
27
+ },
28
+ "peerDependencies": {
29
+ "@alliance-droid/svelte-component-library": ">=0.1.0",
30
+ "svelte": "^5.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@alliance-droid/svelte-component-library": "^0.1.88",
34
+ "@sveltejs/adapter-auto": "^7.0.0",
35
+ "@sveltejs/kit": "^2.50.0",
36
+ "@sveltejs/package": "^2.3.0",
37
+ "@sveltejs/vite-plugin-svelte": "^6.0.0",
38
+ "@tailwindcss/vite": "^4.1.0",
39
+ "publint": "^0.3.0",
40
+ "svelte": "^5.48.0",
41
+ "svelte-check": "^4.3.0",
42
+ "tailwindcss": "^4.1.0",
43
+ "typescript": "^5.9.0",
44
+ "vite": "^7.3.0",
45
+ "vitest": "^4.0.0"
46
+ },
47
+ "keywords": [
48
+ "svelte",
49
+ "sveltekit",
50
+ "status-page",
51
+ "feedback",
52
+ "uptime"
53
+ ],
54
+ "author": "Alliance Technical Group",
55
+ "license": "UNLICENSED",
56
+ "publishConfig": {
57
+ "access": "restricted"
58
+ },
59
+ "dependencies": {
60
+ "@alliance-droid/svelte-component-library": ">=0.1.0"
61
+ }
60
62
  }