plum-e2e 1.2.4 → 1.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.
Files changed (62) hide show
  1. package/CLAUDE.md +201 -0
  2. package/README.md +237 -90
  3. package/backend/_scaffold/utils/browser.ts +5 -2
  4. package/backend/app.js +9 -1
  5. package/backend/config/scripts/generate-report.js +34 -73
  6. package/backend/config/scripts/run-tests.js +7 -3
  7. package/backend/constants/triggers.js +67 -0
  8. package/backend/lib/reportFilename.js +37 -0
  9. package/backend/lib/testChunker.js +73 -0
  10. package/backend/middleware/auth.js +32 -0
  11. package/backend/package.json +4 -2
  12. package/backend/prisma/migrations/20260616000000_add_runners_and_browser/migration.sql +26 -0
  13. package/backend/prisma/migrations/20260616000001_cron_runner_ids/migration.sql +6 -0
  14. package/backend/prisma/migrations/20260617000000_cron_enabled/migration.sql +1 -0
  15. package/backend/prisma/migrations/20260617000001_report_content/migration.sql +8 -0
  16. package/backend/prisma/schema.prisma +21 -1
  17. package/backend/routes/cron.routes.js +28 -0
  18. package/backend/routes/node.routes.js +121 -0
  19. package/backend/routes/reports.routes.js +23 -20
  20. package/backend/routes/runners.routes.js +83 -0
  21. package/backend/scripts/add-local-runner.js +120 -0
  22. package/backend/scripts/create-test.js +148 -0
  23. package/backend/server.js +16 -7
  24. package/backend/services/backupService.js +3 -30
  25. package/backend/services/cronService.js +220 -36
  26. package/backend/services/reportService.js +227 -55
  27. package/backend/services/runnerService.js +179 -0
  28. package/backend/websockets/socketHandler.js +162 -21
  29. package/bin/plum.js +132 -19
  30. package/docker-compose.node.yml +59 -0
  31. package/docker-compose.yml +2 -0
  32. package/frontend/package.json +1 -4
  33. package/frontend/src/app.css +20 -254
  34. package/frontend/src/app.html +1 -1
  35. package/frontend/src/lib/api/reports.js +17 -36
  36. package/frontend/src/lib/api/runners.js +61 -0
  37. package/frontend/src/lib/api/schedules.js +34 -5
  38. package/frontend/src/lib/api/settings.js +5 -5
  39. package/frontend/src/lib/api/tests.js +2 -19
  40. package/frontend/src/lib/components/icons/BrowserIcon.svelte +75 -0
  41. package/frontend/src/lib/components/layout/Nav.svelte +42 -47
  42. package/frontend/src/lib/components/layout/RunnerPanel.svelte +913 -253
  43. package/frontend/src/lib/components/ui/Badge.svelte +6 -1
  44. package/frontend/src/lib/components/ui/ConfirmModal.svelte +98 -0
  45. package/frontend/{tailwind.config.js → src/lib/components/ui/EmptyState.svelte} +27 -8
  46. package/frontend/{postcss.config.js → src/lib/components/ui/Toast.svelte} +20 -7
  47. package/frontend/src/lib/constants.js +36 -0
  48. package/frontend/src/lib/stores/runner.js +23 -12
  49. package/frontend/src/lib/styles/global.css +176 -0
  50. package/frontend/src/lib/styles/reset.css +86 -0
  51. package/frontend/src/lib/styles/tokens.css +90 -0
  52. package/frontend/src/lib/utils/format.js +46 -0
  53. package/frontend/src/routes/+page.svelte +16 -35
  54. package/frontend/src/routes/reports/+page.svelte +84 -167
  55. package/frontend/src/routes/reports/{[slug] → [id]}/+page.svelte +304 -76
  56. package/frontend/src/routes/reports/live/+page.svelte +704 -0
  57. package/frontend/src/routes/scheduled-tests/+page.svelte +328 -88
  58. package/frontend/src/routes/settings/+page.svelte +774 -127
  59. package/frontend/static/favicon-32x32.png +0 -0
  60. package/frontend/static/favicon.ico +0 -0
  61. package/package.json +1 -1
  62. package/frontend/static/favicon.png +0 -0
@@ -16,7 +16,7 @@
16
16
  -->
17
17
 
18
18
  <script>
19
- /** @type {'pass' | 'fail' | 'tag' | 'schedule' | 'neutral'} */
19
+ /** @type {'pass' | 'fail' | 'tag' | 'schedule' | 'neutral' | 'node'} */
20
20
  export let variant = 'neutral';
21
21
  </script>
22
22
 
@@ -60,4 +60,9 @@
60
60
  color: var(--text-muted);
61
61
  border: 1px solid var(--border);
62
62
  }
63
+
64
+ .node {
65
+ background: var(--node-soft);
66
+ color: var(--node);
67
+ }
63
68
  </style>
@@ -0,0 +1,98 @@
1
+ <!--
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ -->
17
+
18
+ <script>
19
+ import { createEventDispatcher } from 'svelte';
20
+ import Modal from './Modal.svelte';
21
+
22
+ export let open = false;
23
+ export let title = 'Confirm';
24
+ export let confirmLabel = 'Delete';
25
+ export let loading = false;
26
+
27
+ const dispatch = createEventDispatcher();
28
+ </script>
29
+
30
+ <Modal bind:open {title}>
31
+ <div class="body">
32
+ <slot />
33
+ </div>
34
+ <div class="actions">
35
+ <button class="btn-danger" on:click={() => dispatch('confirm')} disabled={loading}>
36
+ {loading ? 'Working…' : confirmLabel}
37
+ </button>
38
+ <button class="btn-cancel" on:click={() => (open = false)} disabled={loading}>Cancel</button>
39
+ </div>
40
+ </Modal>
41
+
42
+ <style>
43
+ .body {
44
+ font-size: 0.9375rem;
45
+ color: var(--text-muted);
46
+ line-height: 1.6;
47
+ }
48
+
49
+ .body :global(strong) {
50
+ color: var(--text);
51
+ font-weight: 500;
52
+ }
53
+
54
+ .actions {
55
+ display: flex;
56
+ gap: 0.625rem;
57
+ padding-top: 0.25rem;
58
+ }
59
+
60
+ .btn-danger {
61
+ height: 34px;
62
+ padding: 0 1rem;
63
+ font-size: 0.8125rem;
64
+ font-family: inherit;
65
+ font-weight: 500;
66
+ background: var(--fail);
67
+ border: 1px solid var(--fail);
68
+ border-radius: var(--radius-sm);
69
+ cursor: pointer;
70
+ color: #fff;
71
+ transition: opacity var(--duration-fast);
72
+ }
73
+
74
+ .btn-danger:hover:not(:disabled) {
75
+ opacity: 0.85;
76
+ }
77
+ .btn-danger:disabled {
78
+ opacity: 0.5;
79
+ cursor: not-allowed;
80
+ }
81
+
82
+ .btn-cancel {
83
+ height: 34px;
84
+ padding: 0 1rem;
85
+ font-size: 0.8125rem;
86
+ font-family: inherit;
87
+ background: var(--bg-elevated);
88
+ border: 1px solid var(--border);
89
+ border-radius: var(--radius-sm);
90
+ cursor: pointer;
91
+ color: var(--text);
92
+ transition: background var(--duration-fast);
93
+ }
94
+
95
+ .btn-cancel:hover {
96
+ background: var(--bg-subtle);
97
+ }
98
+ </style>
@@ -1,4 +1,4 @@
1
- /*
1
+ <!--
2
2
  * This file is part of Plum.
3
3
  *
4
4
  * Plum is free software: you can redistribute it and/or modify
@@ -13,11 +13,30 @@
13
13
  *
14
14
  * You should have received a copy of the GNU General Public License
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
- */
16
+ -->
17
17
 
18
- /** @type {import('tailwindcss').Config} */
19
- export default {
20
- content: ['./src/**/*.{html,js,svelte,ts}'],
21
- theme: { extend: {} },
22
- plugins: []
23
- };
18
+ <script>
19
+ export let message = 'Nothing here yet.';
20
+ /** Optional extra padding class: 'sm' | 'md' (default) | 'lg' */
21
+ export let size = 'md';
22
+ </script>
23
+
24
+ <p class="empty {size}"><slot>{message}</slot></p>
25
+
26
+ <style>
27
+ .empty {
28
+ color: var(--text-muted);
29
+ font-size: 0.9375rem;
30
+ text-align: center;
31
+ }
32
+
33
+ .sm {
34
+ padding: 1.5rem 0;
35
+ }
36
+ .md {
37
+ padding: 3rem 0;
38
+ }
39
+ .lg {
40
+ padding: 5rem 0;
41
+ }
42
+ </style>
@@ -1,4 +1,4 @@
1
- /*
1
+ <!--
2
2
  * This file is part of Plum.
3
3
  *
4
4
  * Plum is free software: you can redistribute it and/or modify
@@ -13,11 +13,24 @@
13
13
  *
14
14
  * You should have received a copy of the GNU General Public License
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
- */
16
+ -->
17
17
 
18
- export default {
19
- plugins: {
20
- tailwindcss: {},
21
- autoprefixer: {}
18
+ <script>
19
+ import { fly } from 'svelte/transition';
20
+
21
+ /** @type {{ type: 'success' | 'error', message: string } | null} */
22
+ export let toast = null;
23
+ </script>
24
+
25
+ {#if toast}
26
+ <div class="toast alert alert-{toast.type}" transition:fly={{ y: -8, duration: 240 }}>
27
+ {toast.message}
28
+ </div>
29
+ {/if}
30
+
31
+ <style>
32
+ .toast {
33
+ margin-bottom: 1.25rem;
34
+ border-radius: var(--radius-md);
22
35
  }
23
- };
36
+ </style>
@@ -0,0 +1,36 @@
1
+ /*
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+
18
+ export const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:3001';
19
+
20
+ export const BROWSERS = [
21
+ { id: 'chromium', label: 'Chrome' },
22
+ { id: 'firefox', label: 'Firefox' },
23
+ { id: 'webkit', label: 'WebKit' }
24
+ ];
25
+
26
+ export const TRIGGER_TYPES = Object.freeze({
27
+ MANUAL: 'manual-trigger',
28
+ CLI: 'command-line-trigger'
29
+ });
30
+
31
+ export const WORKER_OPTIONS = [1, 2, 4, 8];
32
+
33
+ export const REPORTS_PER_PAGE = 15;
34
+
35
+ export const COPY_TIMEOUT_MS = 1400;
36
+ export const TOAST_TIMEOUT_MS = 4000;
@@ -20,25 +20,28 @@ import { writable, get } from 'svelte/store';
20
20
  export const socket = writable(null);
21
21
 
22
22
  export const runnerState = writable({
23
- output: 'Ready — select a test from the list.\n',
23
+ output: '',
24
24
  running: false,
25
25
  testCompleted: false,
26
- latestReport: null,
26
+ latestReportId: null, // number | null — set after test finishes
27
27
  status: 'idle', // 'idle' | 'running' | 'pass' | 'fail'
28
- lastRunId: ''
28
+ lastRunId: '',
29
+ lanes: [], // [{ id, name, testCount, status, logs }] multi-runner only
30
+ currentRun: null // { tag, workers, browser, runners } — set while running
29
31
  });
30
32
 
31
33
  export const runnerConfig = writable({
32
34
  workers: 1,
33
- testID: ''
35
+ testID: '',
36
+ browser: 'chromium',
37
+ selectedRunners: ['built-in']
34
38
  });
35
39
 
36
- export const panelExpanded = writable(true);
40
+ export const panelExpanded = writable(false);
37
41
 
38
- // Increments whenever the backend detects a change in tests/features/
39
- export const testsVersion = writable(0);
42
+ export const builtInEnabled = writable(true);
40
43
 
41
- // Increments whenever a new report file is detected
44
+ export const testsVersion = writable(0);
42
45
  export const reportsVersion = writable(0);
43
46
 
44
47
  // Map of taskName → true for every cron job currently executing
@@ -48,17 +51,25 @@ export function triggerRun(id) {
48
51
  const s = get(socket);
49
52
  if (!s) return;
50
53
 
51
- const { workers, testID } = get(runnerConfig);
54
+ const { workers, testID, browser, selectedRunners } = get(runnerConfig);
52
55
  const runId = (id !== undefined ? id : testID).trim().replace(/\sOR\s/gi, (m) => m.toLowerCase());
53
56
 
54
57
  runnerState.set({
55
58
  output: `Running: ${runId || '(all tests)'}\n`,
56
59
  running: true,
57
60
  testCompleted: false,
58
- latestReport: null,
61
+ latestReportId: null,
59
62
  status: 'running',
60
- lastRunId: runId
63
+ lastRunId: runId,
64
+ lanes: [],
65
+ currentRun: { tag: runId, workers, browser, runners: selectedRunners }
61
66
  });
62
67
  panelExpanded.set(true);
63
- s.emit('run-test', runId, workers);
68
+
69
+ s.emit('run-test', { tag: runId, workers, browser, runners: selectedRunners });
70
+ }
71
+
72
+ export function cancelRun() {
73
+ const s = get(socket);
74
+ if (s) s.emit('cancel-test');
64
75
  }
@@ -0,0 +1,176 @@
1
+ /*
2
+ This file is part of Plum.
3
+
4
+ Plum is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ Plum is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+ /*
18
+ * This file is part of Plum.
19
+ *
20
+ * Plum is free software: you can redistribute it and/or modify
21
+ * it under the terms of the GNU General Public License as published by
22
+ * the Free Software Foundation, either version 3 of the License, or
23
+ * (at your option) any later version.
24
+ *
25
+ * Plum is distributed in the hope that it will be useful,
26
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
+ * GNU General Public License for more details.
29
+ *
30
+ * You should have received a copy of the GNU General Public License
31
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
32
+ */
33
+
34
+ /* ── Form fields ────────────────────────────────────────────────────────── */
35
+
36
+ .field {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 0.375rem;
40
+ }
41
+
42
+ .field-label {
43
+ display: flex;
44
+ justify-content: space-between;
45
+ font-size: 0.8125rem;
46
+ font-weight: 400;
47
+ color: var(--text-muted);
48
+ }
49
+
50
+ .field-hint {
51
+ font-size: 0.75rem;
52
+ color: var(--text-muted);
53
+ }
54
+
55
+ .field-input {
56
+ width: 100%;
57
+ padding: 0.6rem 0.875rem;
58
+ border: 1px solid var(--border);
59
+ border-radius: var(--radius-md);
60
+ background: var(--bg-subtle);
61
+ color: var(--text);
62
+ font-family: var(--font-body);
63
+ font-size: 0.875rem;
64
+ font-weight: 300;
65
+ outline: none;
66
+ appearance: none;
67
+ transition:
68
+ border-color var(--duration-fast),
69
+ box-shadow var(--duration-fast);
70
+ }
71
+
72
+ .field-input:focus {
73
+ border-color: var(--accent);
74
+ box-shadow: 0 0 0 3px var(--accent-soft);
75
+ }
76
+
77
+ .field-input:disabled {
78
+ opacity: 0.5;
79
+ cursor: not-allowed;
80
+ }
81
+
82
+ .field-input::placeholder {
83
+ color: var(--text-muted);
84
+ }
85
+
86
+ /* ── Data table ─────────────────────────────────────────────────────────── */
87
+
88
+ .data-table {
89
+ width: 100%;
90
+ border-collapse: collapse;
91
+ font-size: 0.875rem;
92
+ }
93
+
94
+ .data-table th {
95
+ text-align: left;
96
+ padding: 0.5rem 1rem;
97
+ font-family: var(--font-body);
98
+ font-weight: 500;
99
+ font-size: 0.7rem;
100
+ letter-spacing: 0.07em;
101
+ text-transform: uppercase;
102
+ color: var(--text-muted);
103
+ border-bottom: 1px solid var(--border);
104
+ }
105
+
106
+ .data-table td {
107
+ padding: 0.875rem 1rem;
108
+ border-bottom: 1px solid var(--border);
109
+ color: var(--text);
110
+ vertical-align: middle;
111
+ }
112
+
113
+ .data-table tbody tr:last-child td {
114
+ border-bottom: none;
115
+ }
116
+
117
+ /* ── Card ───────────────────────────────────────────────────────────────── */
118
+
119
+ .card {
120
+ background: var(--bg-elevated);
121
+ border: 1px solid var(--border);
122
+ border-radius: var(--radius-lg);
123
+ padding: 1.5rem;
124
+ transition:
125
+ background var(--duration-base) var(--ease-out),
126
+ border-color var(--duration-base) var(--ease-out);
127
+ }
128
+
129
+ .card-title {
130
+ font-family: var(--font-display);
131
+ font-size: 1.25rem;
132
+ font-weight: 400;
133
+ color: var(--text);
134
+ margin-bottom: 0.25rem;
135
+ }
136
+
137
+ .card-subtitle {
138
+ font-size: 0.8125rem;
139
+ color: var(--text-muted);
140
+ margin-bottom: 1.25rem;
141
+ }
142
+
143
+ /* ── Alert ──────────────────────────────────────────────────────────────── */
144
+
145
+ .alert {
146
+ display: flex;
147
+ align-items: center;
148
+ gap: 0.625rem;
149
+ padding: 0.75rem 1rem;
150
+ border-radius: var(--radius-md);
151
+ font-size: 0.875rem;
152
+ font-weight: 400;
153
+ }
154
+
155
+ .alert-success {
156
+ background: var(--pass-soft);
157
+ color: var(--pass);
158
+ }
159
+
160
+ .alert-error {
161
+ background: var(--fail-soft);
162
+ color: var(--fail);
163
+ }
164
+
165
+ /* ── Animations ─────────────────────────────────────────────────────────── */
166
+
167
+ @keyframes fadeUp {
168
+ from {
169
+ opacity: 0;
170
+ transform: translateY(10px);
171
+ }
172
+ to {
173
+ opacity: 1;
174
+ transform: translateY(0);
175
+ }
176
+ }
@@ -0,0 +1,86 @@
1
+ /*
2
+ This file is part of Plum.
3
+
4
+ Plum is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ Plum is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+ /*
18
+ * This file is part of Plum.
19
+ *
20
+ * Plum is free software: you can redistribute it and/or modify
21
+ * it under the terms of the GNU General Public License as published by
22
+ * the Free Software Foundation, either version 3 of the License, or
23
+ * (at your option) any later version.
24
+ *
25
+ * Plum is distributed in the hope that it will be useful,
26
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
+ * GNU General Public License for more details.
29
+ *
30
+ * You should have received a copy of the GNU General Public License
31
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
32
+ */
33
+
34
+ /* ── Minimal modern reset ──────────────────────────────────────────────── */
35
+
36
+ *,
37
+ *::before,
38
+ *::after {
39
+ box-sizing: border-box;
40
+ margin: 0;
41
+ padding: 0;
42
+ }
43
+
44
+ html {
45
+ scroll-behavior: smooth;
46
+ }
47
+
48
+ body {
49
+ font-family: var(--font-body);
50
+ font-weight: 300;
51
+ font-size: 1rem;
52
+ line-height: 1.65;
53
+ background-color: var(--bg);
54
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)' opacity='0.07'/%3E%3C/svg%3E");
55
+ background-size: 200px 200px;
56
+ color: var(--text);
57
+ -webkit-font-smoothing: antialiased;
58
+ transition:
59
+ background-color var(--duration-base) var(--ease-out),
60
+ color var(--duration-base) var(--ease-out);
61
+ }
62
+
63
+ h1 {
64
+ font-family: var(--font-display);
65
+ font-weight: 400;
66
+ line-height: 1.1;
67
+ letter-spacing: -0.015em;
68
+ color: var(--text);
69
+ }
70
+
71
+ h2,
72
+ h3 {
73
+ font-family: var(--font-display);
74
+ font-weight: 400;
75
+ line-height: 1.2;
76
+ color: var(--text);
77
+ }
78
+
79
+ a {
80
+ color: inherit;
81
+ text-decoration: none;
82
+ }
83
+
84
+ button {
85
+ font-family: var(--font-body);
86
+ }
@@ -0,0 +1,90 @@
1
+ /*
2
+ This file is part of Plum.
3
+
4
+ Plum is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation, either version 3 of the License, or
7
+ (at your option) any later version.
8
+
9
+ Plum is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License
15
+ along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+ /*
18
+ * This file is part of Plum.
19
+ *
20
+ * Plum is free software: you can redistribute it and/or modify
21
+ * it under the terms of the GNU General Public License as published by
22
+ * the Free Software Foundation, either version 3 of the License, or
23
+ * (at your option) any later version.
24
+ *
25
+ * Plum is distributed in the hope that it will be useful,
26
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
+ * GNU General Public License for more details.
29
+ *
30
+ * You should have received a copy of the GNU General Public License
31
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
32
+ */
33
+
34
+ /* ── Design tokens (light) ─────────────────────────────────────────────── */
35
+
36
+ :root {
37
+ --bg: #fdfcfb;
38
+ --bg-subtle: #f7f6f4;
39
+ --bg-elevated: #ffffff;
40
+ --border: #e9e5df;
41
+ --text: #18160f;
42
+ --text-muted: #9c9185;
43
+ --accent: #5c1b87;
44
+ --accent-soft: #f3eaff;
45
+ --pass: #16a34a;
46
+ --pass-soft: #dcfce7;
47
+ --fail: #dc2626;
48
+ --fail-soft: #fee2e2;
49
+ --warn: #d97706;
50
+ --warn-soft: #fef3c7;
51
+ --node: #0284c7;
52
+ --node-soft: #e0f2fe;
53
+ --terminal-bg: #0d0c08;
54
+ --terminal-text: #d6d0c8;
55
+
56
+ --font-display: 'Playfair Display', Georgia, serif;
57
+ --font-body: 'DM Sans', system-ui, -apple-system, sans-serif;
58
+
59
+ --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
60
+ --duration-fast: 140ms;
61
+ --duration-base: 240ms;
62
+ --duration-slow: 400ms;
63
+
64
+ --radius-sm: 6px;
65
+ --radius-md: 10px;
66
+ --radius-lg: 16px;
67
+ }
68
+
69
+ /* ── Design tokens (dark) ──────────────────────────────────────────────── */
70
+
71
+ [data-theme='dark'] {
72
+ --bg: #101009;
73
+ --bg-subtle: #1a1910;
74
+ --bg-elevated: #201f14;
75
+ --border: #2e2c1e;
76
+ --text: #f0ede6;
77
+ --text-muted: #706a5e;
78
+ --accent: #d0a5f5;
79
+ --accent-soft: #1b0d30;
80
+ --pass: #22c55e;
81
+ --pass-soft: #0d2118;
82
+ --fail: #f87171;
83
+ --fail-soft: #2d1010;
84
+ --warn: #fbbf24;
85
+ --warn-soft: #2a1f06;
86
+ --node: #38bdf8;
87
+ --node-soft: #082032;
88
+ --terminal-bg: #080807;
89
+ --terminal-text: #cbc5bd;
90
+ }
@@ -0,0 +1,46 @@
1
+ /*
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+
18
+ import { TRIGGER_TYPES } from '$lib/constants';
19
+
20
+ const NON_SCHEDULED = new Set([TRIGGER_TYPES.MANUAL, TRIGGER_TYPES.CLI, 'undefined']);
21
+
22
+ export function isScheduled(type) {
23
+ return !!type && !NON_SCHEDULED.has(type);
24
+ }
25
+
26
+ export function triggerLabel(type) {
27
+ if (type === TRIGGER_TYPES.MANUAL) return 'Manual';
28
+ if (type === TRIGGER_TYPES.CLI || type === 'undefined') return 'CLI';
29
+ return 'Scheduled';
30
+ }
31
+
32
+ export function triggerVariant(type) {
33
+ if (type === TRIGGER_TYPES.MANUAL) return 'tag';
34
+ if (type === TRIGGER_TYPES.CLI || type === 'undefined') return 'neutral';
35
+ return 'schedule';
36
+ }
37
+
38
+ /** Returns an inline style string for staggered fadeUp animations. */
39
+ export function stagger(i, stepMs = 45) {
40
+ return `animation-delay: ${i * stepMs}ms`;
41
+ }
42
+
43
+ export function fmtDuration(ms) {
44
+ if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';
45
+ return ms + 'ms';
46
+ }