plum-e2e 1.1.0 → 1.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.
- package/.claude/settings.local.json +27 -25
- package/.husky/pre-commit +2 -2
- package/README.md +142 -70
- package/backend/Dockerfile +4 -2
- package/backend/app.js +4 -2
- package/backend/config/scripts/generate-report.js +38 -30
- package/backend/entrypoint.sh +22 -0
- package/backend/package-lock.json +453 -10
- package/backend/package.json +5 -2
- package/backend/prisma/migrations/20260614000000_init/migration.sql +35 -0
- package/backend/prisma/migrations/20260614000001_add_project/migration.sql +8 -0
- package/backend/prisma/migrations/migration_lock.toml +3 -0
- package/backend/prisma/schema.prisma +53 -0
- package/backend/routes/backup.routes.js +50 -0
- package/backend/routes/cron.routes.js +9 -60
- package/backend/routes/reports.routes.js +39 -6
- package/backend/routes/settings.routes.js +43 -0
- package/backend/server.js +52 -1
- package/backend/services/backupService.js +88 -0
- package/backend/services/cronService.js +68 -78
- package/backend/services/{scheduleService.js → prisma.js} +3 -15
- package/backend/services/reportService.js +44 -16
- package/backend/{routes/schedules.routes.js → services/settingsService.js} +17 -13
- package/bin/plum.js +213 -32
- package/docker-compose.yml +24 -0
- package/frontend/package-lock.json +2 -2
- package/frontend/package.json +1 -1
- package/frontend/src/lib/api/reports.js +38 -27
- package/frontend/src/lib/api/schedules.js +9 -25
- package/frontend/src/lib/api/settings.js +48 -0
- package/frontend/src/lib/components/layout/Nav.svelte +2 -1
- package/frontend/src/lib/components/layout/RunnerPanel.svelte +160 -21
- package/frontend/src/lib/components/ui/Terminal.svelte +2 -2
- package/frontend/src/lib/stores/runner.js +9 -0
- package/frontend/src/routes/+page.svelte +10 -3
- package/frontend/src/routes/reports/+page.svelte +342 -51
- package/frontend/src/routes/reports/[slug]/+page.svelte +2 -0
- package/frontend/src/routes/scheduled-tests/+page.svelte +247 -11
- package/frontend/src/routes/settings/+page.svelte +410 -0
- package/license-config.json +2 -2
- package/package.json +6 -2
- package/backend/config/scripts/create-settings.js +0 -53
|
@@ -17,21 +17,29 @@
|
|
|
17
17
|
|
|
18
18
|
<script>
|
|
19
19
|
import { onMount, tick } from 'svelte';
|
|
20
|
-
import { fetchReports } from '$lib/api/reports';
|
|
20
|
+
import { fetchReports, deleteReport, deleteReports } from '$lib/api/reports';
|
|
21
|
+
import { reportsVersion } from '$lib/stores/runner';
|
|
21
22
|
import Badge from '$lib/components/ui/Badge.svelte';
|
|
22
23
|
import Pagination from '$lib/components/ui/Pagination.svelte';
|
|
24
|
+
import Modal from '$lib/components/ui/Modal.svelte';
|
|
23
25
|
|
|
24
26
|
let reports = [];
|
|
25
27
|
let currentPage = 1;
|
|
26
28
|
const PER_PAGE = 15;
|
|
27
29
|
let animateBar = false;
|
|
28
30
|
|
|
31
|
+
let selected = new Set();
|
|
32
|
+
let deleteModal = { open: false, targets: [] };
|
|
33
|
+
let deleting = false;
|
|
34
|
+
|
|
29
35
|
$: totalPages = Math.ceil(reports.length / PER_PAGE);
|
|
30
36
|
$: paginated = reports.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE);
|
|
31
37
|
$: passCount = reports.filter((r) => r.status === 'PASS').length;
|
|
32
38
|
$: failCount = reports.length - passCount;
|
|
33
39
|
$: passRate = reports.length ? Math.round((passCount / reports.length) * 100) : 0;
|
|
34
40
|
$: trend = reports.slice(0, 12).reverse();
|
|
41
|
+
$: allOnPageSelected = paginated.length > 0 && paginated.every((r) => selected.has(r.fileName));
|
|
42
|
+
$: someSelected = selected.size > 0;
|
|
35
43
|
|
|
36
44
|
function triggerLabel(type) {
|
|
37
45
|
if (type === 'manual-trigger') return 'Manual';
|
|
@@ -45,17 +53,95 @@
|
|
|
45
53
|
return 'schedule';
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
async function loadReports() {
|
|
49
57
|
try {
|
|
50
58
|
reports = await fetchReports();
|
|
59
|
+
selected = new Set();
|
|
51
60
|
await tick();
|
|
52
61
|
animateBar = true;
|
|
53
62
|
} catch (e) {
|
|
54
63
|
console.error('Failed to fetch reports', e);
|
|
55
64
|
}
|
|
56
|
-
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onMount(loadReports);
|
|
68
|
+
$: if ($reportsVersion) loadReports();
|
|
69
|
+
|
|
70
|
+
function toggleSelect(fileName, e) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
const next = new Set(selected);
|
|
74
|
+
if (next.has(fileName)) next.delete(fileName);
|
|
75
|
+
else next.add(fileName);
|
|
76
|
+
selected = next;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function toggleAll(e) {
|
|
80
|
+
e.stopPropagation();
|
|
81
|
+
if (allOnPageSelected) {
|
|
82
|
+
const next = new Set(selected);
|
|
83
|
+
paginated.forEach((r) => next.delete(r.fileName));
|
|
84
|
+
selected = next;
|
|
85
|
+
} else {
|
|
86
|
+
const next = new Set(selected);
|
|
87
|
+
paginated.forEach((r) => next.add(r.fileName));
|
|
88
|
+
selected = next;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function openDeleteModal(targets) {
|
|
93
|
+
deleteModal = { open: true, targets };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function openSingleDelete(fileName, e) {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
e.stopPropagation();
|
|
99
|
+
openDeleteModal([fileName]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function confirmDelete() {
|
|
103
|
+
deleting = true;
|
|
104
|
+
try {
|
|
105
|
+
if (deleteModal.targets.length === 1) {
|
|
106
|
+
await deleteReport(deleteModal.targets[0]);
|
|
107
|
+
} else {
|
|
108
|
+
await deleteReports(deleteModal.targets);
|
|
109
|
+
}
|
|
110
|
+
deleteModal = { open: false, targets: [] };
|
|
111
|
+
await loadReports();
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.error('Delete failed', e);
|
|
114
|
+
} finally {
|
|
115
|
+
deleting = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
57
118
|
</script>
|
|
58
119
|
|
|
120
|
+
<svelte:head><title>Reports — Plum</title></svelte:head>
|
|
121
|
+
|
|
122
|
+
<Modal
|
|
123
|
+
bind:open={deleteModal.open}
|
|
124
|
+
title={deleteModal.targets.length === 1
|
|
125
|
+
? 'Delete report?'
|
|
126
|
+
: `Delete ${deleteModal.targets.length} reports?`}
|
|
127
|
+
>
|
|
128
|
+
<p class="modal-body">
|
|
129
|
+
{#if deleteModal.targets.length === 1}
|
|
130
|
+
This will permanently remove the report and its data file.
|
|
131
|
+
{:else}
|
|
132
|
+
This will permanently remove {deleteModal.targets.length} reports and their data files.
|
|
133
|
+
{/if}
|
|
134
|
+
</p>
|
|
135
|
+
<div class="modal-actions">
|
|
136
|
+
<button class="btn-cancel" on:click={() => (deleteModal.open = false)} disabled={deleting}>
|
|
137
|
+
Cancel
|
|
138
|
+
</button>
|
|
139
|
+
<button class="btn-danger" on:click={confirmDelete} disabled={deleting}>
|
|
140
|
+
{deleting ? 'Deleting…' : 'Delete'}
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
</Modal>
|
|
144
|
+
|
|
59
145
|
<div class="page-header">
|
|
60
146
|
<div class="header-top">
|
|
61
147
|
<div>
|
|
@@ -65,19 +151,27 @@
|
|
|
65
151
|
</p>
|
|
66
152
|
</div>
|
|
67
153
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
154
|
+
<div class="header-actions">
|
|
155
|
+
{#if someSelected}
|
|
156
|
+
<button class="btn-delete-selected" on:click={() => openDeleteModal([...selected])}>
|
|
157
|
+
Delete ({selected.size})
|
|
158
|
+
</button>
|
|
159
|
+
{/if}
|
|
160
|
+
|
|
161
|
+
{#if reports.length > 0}
|
|
162
|
+
<div class="rate-display">
|
|
163
|
+
<span
|
|
164
|
+
class="rate-number"
|
|
165
|
+
class:pass={passRate >= 80}
|
|
166
|
+
class:warn={passRate < 80 && passRate >= 50}
|
|
167
|
+
class:fail={passRate < 50}
|
|
168
|
+
>
|
|
169
|
+
{passRate}%
|
|
170
|
+
</span>
|
|
171
|
+
<span class="rate-label">passing</span>
|
|
172
|
+
</div>
|
|
173
|
+
{/if}
|
|
174
|
+
</div>
|
|
81
175
|
</div>
|
|
82
176
|
|
|
83
177
|
{#if reports.length > 0}
|
|
@@ -112,39 +206,84 @@
|
|
|
112
206
|
{#if reports.length === 0}
|
|
113
207
|
<p class="empty">No reports yet. Run a test to generate one.</p>
|
|
114
208
|
{:else}
|
|
209
|
+
<div class="list-header">
|
|
210
|
+
<label class="select-all-wrap" title="Select all on this page">
|
|
211
|
+
<input
|
|
212
|
+
type="checkbox"
|
|
213
|
+
class="checkbox"
|
|
214
|
+
checked={allOnPageSelected}
|
|
215
|
+
indeterminate={someSelected && !allOnPageSelected}
|
|
216
|
+
on:change={toggleAll}
|
|
217
|
+
/>
|
|
218
|
+
</label>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
115
221
|
<div class="report-list">
|
|
116
222
|
{#each paginated as report, i}
|
|
117
|
-
<
|
|
118
|
-
class="report-
|
|
119
|
-
class:is-
|
|
120
|
-
class:is-fail={report.status !== 'PASS'}
|
|
121
|
-
href="/reports/{encodeURIComponent(report.fileName)}"
|
|
223
|
+
<div
|
|
224
|
+
class="report-row"
|
|
225
|
+
class:is-selected={selected.has(report.fileName)}
|
|
122
226
|
style="animation-delay: {i * 45}ms"
|
|
123
227
|
>
|
|
124
|
-
<
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
class
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
228
|
+
<label class="row-check-wrap" title="Select">
|
|
229
|
+
<input
|
|
230
|
+
type="checkbox"
|
|
231
|
+
class="checkbox"
|
|
232
|
+
checked={selected.has(report.fileName)}
|
|
233
|
+
on:change={(e) => toggleSelect(report.fileName, e)}
|
|
234
|
+
/>
|
|
235
|
+
</label>
|
|
236
|
+
|
|
237
|
+
<a
|
|
238
|
+
class="report-item"
|
|
239
|
+
class:is-pass={report.status === 'PASS'}
|
|
240
|
+
class:is-fail={report.status !== 'PASS'}
|
|
241
|
+
href="/reports/{encodeURIComponent(report.fileName)}"
|
|
242
|
+
>
|
|
243
|
+
<div class="item-left">
|
|
244
|
+
<span
|
|
245
|
+
class="status-mark"
|
|
246
|
+
class:pass={report.status === 'PASS'}
|
|
247
|
+
class:fail={report.status !== 'PASS'}
|
|
248
|
+
>
|
|
249
|
+
{report.status === 'PASS' ? '✓' : '✗'}
|
|
250
|
+
</span>
|
|
251
|
+
<div class="item-meta">
|
|
252
|
+
<span class="item-tags">{report.tags}</span>
|
|
253
|
+
<div class="item-badges">
|
|
254
|
+
<Badge variant={triggerVariant(report.triggerType)}>
|
|
255
|
+
{triggerLabel(report.triggerType)}
|
|
256
|
+
</Badge>
|
|
257
|
+
<Badge variant="neutral">
|
|
258
|
+
{report.runners} runner{report.runners !== 1 ? 's' : ''}
|
|
259
|
+
</Badge>
|
|
260
|
+
</div>
|
|
141
261
|
</div>
|
|
142
262
|
</div>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
263
|
+
<div class="item-right">
|
|
264
|
+
<span class="item-date">{report.date}</span>
|
|
265
|
+
<svg
|
|
266
|
+
class="item-arrow"
|
|
267
|
+
width="14"
|
|
268
|
+
height="14"
|
|
269
|
+
viewBox="0 0 24 24"
|
|
270
|
+
fill="none"
|
|
271
|
+
stroke="currentColor"
|
|
272
|
+
stroke-width="2"
|
|
273
|
+
stroke-linecap="round"
|
|
274
|
+
>
|
|
275
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
276
|
+
<polyline points="12 5 19 12 12 19" />
|
|
277
|
+
</svg>
|
|
278
|
+
</div>
|
|
279
|
+
</a>
|
|
280
|
+
|
|
281
|
+
<button
|
|
282
|
+
class="row-delete-btn"
|
|
283
|
+
title="Delete report"
|
|
284
|
+
on:click={(e) => openSingleDelete(report.fileName, e)}
|
|
285
|
+
>
|
|
146
286
|
<svg
|
|
147
|
-
class="item-arrow"
|
|
148
287
|
width="14"
|
|
149
288
|
height="14"
|
|
150
289
|
viewBox="0 0 24 24"
|
|
@@ -152,12 +291,15 @@
|
|
|
152
291
|
stroke="currentColor"
|
|
153
292
|
stroke-width="2"
|
|
154
293
|
stroke-linecap="round"
|
|
294
|
+
stroke-linejoin="round"
|
|
155
295
|
>
|
|
156
|
-
<
|
|
157
|
-
<
|
|
296
|
+
<polyline points="3 6 5 6 21 6" />
|
|
297
|
+
<path d="M19 6l-1 14H6L5 6" />
|
|
298
|
+
<path d="M10 11v6M14 11v6" />
|
|
299
|
+
<path d="M9 6V4h6v2" />
|
|
158
300
|
</svg>
|
|
159
|
-
</
|
|
160
|
-
</
|
|
301
|
+
</button>
|
|
302
|
+
</div>
|
|
161
303
|
{/each}
|
|
162
304
|
</div>
|
|
163
305
|
|
|
@@ -197,6 +339,33 @@
|
|
|
197
339
|
font-size: 0.875rem;
|
|
198
340
|
}
|
|
199
341
|
|
|
342
|
+
.header-actions {
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
gap: 1rem;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.btn-delete-selected {
|
|
349
|
+
height: 32px;
|
|
350
|
+
padding: 0 0.875rem;
|
|
351
|
+
font-size: 0.8125rem;
|
|
352
|
+
font-family: inherit;
|
|
353
|
+
font-weight: 500;
|
|
354
|
+
color: var(--fail);
|
|
355
|
+
background: var(--fail-soft, rgba(239, 68, 68, 0.08));
|
|
356
|
+
border: 1px solid var(--fail);
|
|
357
|
+
border-radius: var(--radius-sm);
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
transition:
|
|
360
|
+
background var(--duration-fast),
|
|
361
|
+
opacity var(--duration-fast);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.btn-delete-selected:hover {
|
|
365
|
+
background: var(--fail);
|
|
366
|
+
color: #fff;
|
|
367
|
+
}
|
|
368
|
+
|
|
200
369
|
/* ── Pass rate ── */
|
|
201
370
|
.rate-display {
|
|
202
371
|
display: flex;
|
|
@@ -253,13 +422,11 @@
|
|
|
253
422
|
display: flex;
|
|
254
423
|
gap: 1rem;
|
|
255
424
|
}
|
|
256
|
-
|
|
257
425
|
.legend-pass {
|
|
258
426
|
font-size: 0.75rem;
|
|
259
427
|
color: var(--pass);
|
|
260
428
|
font-weight: 500;
|
|
261
429
|
}
|
|
262
|
-
|
|
263
430
|
.legend-fail {
|
|
264
431
|
font-size: 0.75rem;
|
|
265
432
|
color: var(--fail);
|
|
@@ -299,7 +466,6 @@
|
|
|
299
466
|
.trend-dot:hover {
|
|
300
467
|
transform: scale(1.4);
|
|
301
468
|
}
|
|
302
|
-
|
|
303
469
|
.trend-dot.pass {
|
|
304
470
|
background: var(--pass);
|
|
305
471
|
}
|
|
@@ -313,14 +479,51 @@
|
|
|
313
479
|
opacity: 0.6;
|
|
314
480
|
}
|
|
315
481
|
|
|
316
|
-
/* ──
|
|
482
|
+
/* ── Select all row ── */
|
|
483
|
+
.list-header {
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: center;
|
|
486
|
+
padding: 0 0.5rem 0.375rem;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.select-all-wrap {
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
cursor: pointer;
|
|
493
|
+
padding: 0.25rem;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* ── Report rows ── */
|
|
317
497
|
.report-list {
|
|
318
498
|
display: flex;
|
|
319
499
|
flex-direction: column;
|
|
320
500
|
gap: 0.5rem;
|
|
321
501
|
}
|
|
322
502
|
|
|
503
|
+
.report-row {
|
|
504
|
+
display: flex;
|
|
505
|
+
align-items: center;
|
|
506
|
+
gap: 0.5rem;
|
|
507
|
+
animation: fadeUp 0.32s var(--ease-out) both;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.row-check-wrap {
|
|
511
|
+
flex-shrink: 0;
|
|
512
|
+
display: flex;
|
|
513
|
+
align-items: center;
|
|
514
|
+
cursor: pointer;
|
|
515
|
+
padding: 0.25rem;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.checkbox {
|
|
519
|
+
width: 15px;
|
|
520
|
+
height: 15px;
|
|
521
|
+
accent-color: var(--accent, #7c3aed);
|
|
522
|
+
cursor: pointer;
|
|
523
|
+
}
|
|
524
|
+
|
|
323
525
|
.report-item {
|
|
526
|
+
flex: 1;
|
|
324
527
|
display: flex;
|
|
325
528
|
align-items: center;
|
|
326
529
|
justify-content: space-between;
|
|
@@ -336,7 +539,7 @@
|
|
|
336
539
|
background var(--duration-fast),
|
|
337
540
|
transform var(--duration-fast) var(--ease-out),
|
|
338
541
|
box-shadow var(--duration-fast);
|
|
339
|
-
|
|
542
|
+
min-width: 0;
|
|
340
543
|
}
|
|
341
544
|
|
|
342
545
|
.report-item:hover {
|
|
@@ -352,6 +555,11 @@
|
|
|
352
555
|
border-left-color: var(--fail);
|
|
353
556
|
}
|
|
354
557
|
|
|
558
|
+
.report-row.is-selected .report-item {
|
|
559
|
+
background: var(--bg-subtle);
|
|
560
|
+
border-color: var(--accent, #7c3aed);
|
|
561
|
+
}
|
|
562
|
+
|
|
355
563
|
.item-left {
|
|
356
564
|
display: flex;
|
|
357
565
|
align-items: center;
|
|
@@ -417,6 +625,89 @@
|
|
|
417
625
|
color: var(--text);
|
|
418
626
|
}
|
|
419
627
|
|
|
628
|
+
/* ── Per-row delete button ── */
|
|
629
|
+
.row-delete-btn {
|
|
630
|
+
flex-shrink: 0;
|
|
631
|
+
display: flex;
|
|
632
|
+
align-items: center;
|
|
633
|
+
justify-content: center;
|
|
634
|
+
width: 30px;
|
|
635
|
+
height: 30px;
|
|
636
|
+
border: none;
|
|
637
|
+
border-radius: var(--radius-sm);
|
|
638
|
+
background: transparent;
|
|
639
|
+
color: var(--text-muted);
|
|
640
|
+
cursor: pointer;
|
|
641
|
+
opacity: 0;
|
|
642
|
+
transition:
|
|
643
|
+
opacity var(--duration-fast),
|
|
644
|
+
color var(--duration-fast),
|
|
645
|
+
background var(--duration-fast);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.report-row:hover .row-delete-btn {
|
|
649
|
+
opacity: 1;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.row-delete-btn:hover {
|
|
653
|
+
color: var(--fail);
|
|
654
|
+
background: var(--fail-soft, rgba(239, 68, 68, 0.08));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* ── Modal ── */
|
|
658
|
+
.modal-actions {
|
|
659
|
+
display: flex;
|
|
660
|
+
justify-content: flex-end;
|
|
661
|
+
gap: 0.5rem;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.modal-body {
|
|
665
|
+
font-size: 0.9rem;
|
|
666
|
+
color: var(--text-muted);
|
|
667
|
+
line-height: 1.6;
|
|
668
|
+
margin: 0;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.btn-cancel {
|
|
672
|
+
height: 34px;
|
|
673
|
+
padding: 0 1rem;
|
|
674
|
+
font-size: 0.8125rem;
|
|
675
|
+
font-family: inherit;
|
|
676
|
+
background: var(--bg-elevated);
|
|
677
|
+
border: 1px solid var(--border);
|
|
678
|
+
border-radius: var(--radius-sm);
|
|
679
|
+
cursor: pointer;
|
|
680
|
+
color: var(--text);
|
|
681
|
+
transition: background var(--duration-fast);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.btn-cancel:hover {
|
|
685
|
+
background: var(--bg-subtle);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.btn-danger {
|
|
689
|
+
height: 34px;
|
|
690
|
+
padding: 0 1rem;
|
|
691
|
+
font-size: 0.8125rem;
|
|
692
|
+
font-family: inherit;
|
|
693
|
+
font-weight: 500;
|
|
694
|
+
background: var(--fail);
|
|
695
|
+
border: 1px solid var(--fail);
|
|
696
|
+
border-radius: var(--radius-sm);
|
|
697
|
+
cursor: pointer;
|
|
698
|
+
color: #fff;
|
|
699
|
+
transition: opacity var(--duration-fast);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.btn-danger:hover:not(:disabled) {
|
|
703
|
+
opacity: 0.85;
|
|
704
|
+
}
|
|
705
|
+
.btn-danger:disabled {
|
|
706
|
+
opacity: 0.5;
|
|
707
|
+
cursor: not-allowed;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/* ── Misc ── */
|
|
420
711
|
.pagination-wrap {
|
|
421
712
|
margin-top: 1.25rem;
|
|
422
713
|
}
|