@runwisp/ui 0.10.0 → 0.11.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/package.json +4 -4
- package/src/lib/assets/runwisp-logo.svg +2 -2
- package/src/lib/components/LogConsole.svelte +518 -68
- package/src/lib/components/Popover.svelte +64 -31
- package/src/lib/components/dashboard/RunDetailPanel.svelte +753 -212
- package/src/lib/components/dashboard/RunFilterPopover.svelte +299 -0
- package/src/lib/components/dashboard/RunsList.svelte +418 -234
- package/src/lib/components/dashboard/run-filters.test.ts +299 -0
- package/src/lib/components/dashboard/run-filters.ts +384 -0
- package/src/lib/components/dashboard/run-helpers.test.ts +20 -0
- package/src/lib/components/dashboard/run-helpers.ts +15 -0
- package/src/lib/index.ts +20 -4
- package/src/lib/log-console/LogCache.svelte.ts +50 -1
- package/src/lib/log-console/types.ts +17 -0
- package/src/lib/theme-tokens.css +74 -62
- package/src/lib/theme.css +1 -0
- package/src/lib/utils/format.test.ts +38 -0
- package/src/lib/utils/format.ts +36 -0
- package/src/stories/assets/runwisp_c.svg +2 -2
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
runStartDelay,
|
|
8
8
|
formatTriggeredByLabel,
|
|
9
9
|
runRetryLabel,
|
|
10
|
+
instanceSuffix,
|
|
10
11
|
} from "./run-helpers.js";
|
|
11
12
|
|
|
12
13
|
describe("runDuration", () => {
|
|
@@ -88,3 +89,22 @@ describe("runRetryLabel", () => {
|
|
|
88
89
|
expect(runRetryLabel({ retry_attempt: 1, retry_of_run_id: "01JABC" })).toBe("retry #1");
|
|
89
90
|
});
|
|
90
91
|
});
|
|
92
|
+
|
|
93
|
+
describe("instanceSuffix", () => {
|
|
94
|
+
it("returns no suffix for a single-instance task", () => {
|
|
95
|
+
expect(instanceSuffix(0, 1)).toBe("");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns no suffix for a non-service (count 0)", () => {
|
|
99
|
+
expect(instanceSuffix(0, 0)).toBe("");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("suffixes slot 0 of a multi-instance service as #1 (1-based)", () => {
|
|
103
|
+
expect(instanceSuffix(0, 3)).toBe("#1");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("maps the stored 0-based slot to a 1-based suffix", () => {
|
|
107
|
+
expect(instanceSuffix(1, 3)).toBe("#2");
|
|
108
|
+
expect(instanceSuffix(2, 3)).toBe("#3");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -26,6 +26,21 @@ export function runStartDelay(run: Pick<Run, "created_at" | "start_at">): string
|
|
|
26
26
|
return formatDuration(delay);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Display suffix for a run's instance slot. A service configured with more than
|
|
31
|
+
* one instance gets a 1-based suffix (`#1`, `#2`, …) on every one of its runs;
|
|
32
|
+
* a single-instance task (or any non-service, where `instanceCount` is 1)
|
|
33
|
+
* returns an empty string so the bare task name is shown. `instanceIndex` is
|
|
34
|
+
* the stored 0-based slot; `instanceCount` is the task's currently configured
|
|
35
|
+
* instance count.
|
|
36
|
+
*/
|
|
37
|
+
export function instanceSuffix(instanceIndex: number, instanceCount: number): string {
|
|
38
|
+
if (instanceCount > 1) {
|
|
39
|
+
return `#${String(instanceIndex + 1)}`;
|
|
40
|
+
}
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
/** Human label for why a run fired (the `triggered_by` source). */
|
|
30
45
|
export function formatTriggeredByLabel(triggeredBy: Run["triggered_by"]): string {
|
|
31
46
|
if (triggeredBy === "api") return "API";
|
package/src/lib/index.ts
CHANGED
|
@@ -80,10 +80,25 @@ export { LogFetcher } from "./log-console/LogFetcher.svelte.js";
|
|
|
80
80
|
|
|
81
81
|
export { default as RunDetailPanel } from "./components/dashboard/RunDetailPanel.svelte";
|
|
82
82
|
export { default as RunsList } from "./components/dashboard/RunsList.svelte";
|
|
83
|
-
export
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
export { default as RunFilterPopover } from "./components/dashboard/RunFilterPopover.svelte";
|
|
84
|
+
export type { RunOutputMatch } from "./components/dashboard/RunsList.svelte";
|
|
85
|
+
export {
|
|
86
|
+
emptyRunFilters,
|
|
87
|
+
NEEDS_ATTENTION_STATUSES,
|
|
88
|
+
isNeedsAttention,
|
|
89
|
+
activeFilterCount,
|
|
90
|
+
activeDimensions,
|
|
91
|
+
dimensionActive,
|
|
92
|
+
clearDimension,
|
|
93
|
+
clearPopoverFilters,
|
|
94
|
+
humanizeStatus,
|
|
95
|
+
statusChipLabel,
|
|
96
|
+
exitCodeRange,
|
|
97
|
+
type ExitCodeRange,
|
|
98
|
+
type RunsListFilters,
|
|
99
|
+
type RunsListSortDirection,
|
|
100
|
+
type FilterDimension,
|
|
101
|
+
} from "./components/dashboard/run-filters.js";
|
|
87
102
|
export {
|
|
88
103
|
getRunStatusConfig,
|
|
89
104
|
runDisplayStatus,
|
|
@@ -96,6 +111,7 @@ export {
|
|
|
96
111
|
runPhaseOrder,
|
|
97
112
|
formatTriggeredByLabel,
|
|
98
113
|
runRetryLabel,
|
|
114
|
+
instanceSuffix,
|
|
99
115
|
} from "./components/dashboard/run-helpers.js";
|
|
100
116
|
export { humanizeCron, type HumanizedCron } from "./components/dashboard/cron-format.js";
|
|
101
117
|
export type { DaemonState, DaemonStats } from "./components/dashboard/types.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: PoppyCake, s.r.o.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import type { LogSlice, LogEvent } from "./types.js";
|
|
4
|
+
import type { LogSlice, LogEvent, RegionUpdate } from "./types.js";
|
|
5
5
|
import { SvelteMap } from "svelte/reactivity";
|
|
6
6
|
import { visibleColumns } from "./ansi.js";
|
|
7
7
|
|
|
@@ -14,6 +14,13 @@ import { visibleColumns } from "./ansi.js";
|
|
|
14
14
|
*/
|
|
15
15
|
export class LogCache {
|
|
16
16
|
lines = $state(new SvelteMap<number, string>());
|
|
17
|
+
// Live-region overlay, keyed by stream (stdout/stderr). Each stream has at
|
|
18
|
+
// most one active region; the frame is replaced wholesale on each update and
|
|
19
|
+
// never persisted. Rendered below the committed lines as an in-place tail.
|
|
20
|
+
regions = $state(new SvelteMap<string, { epoch: number; rows: string[] }>());
|
|
21
|
+
// Per-line frame-history counts. A non-zero entry marks a settled progress
|
|
22
|
+
// bar / multi-line redraw whose prior frames can be fetched on click.
|
|
23
|
+
frameCounts = $state(new SvelteMap<number, number>());
|
|
17
24
|
totalLines = $state(0);
|
|
18
25
|
totalBytes = $state(0);
|
|
19
26
|
finished = $state(false);
|
|
@@ -50,13 +57,53 @@ export class LogCache {
|
|
|
50
57
|
return { touched, min, max };
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
// Flattened overlay rows for rendering, stdout before stderr (then any
|
|
61
|
+
// other streams, sorted) for stable ordering. Reactive: reads the regions
|
|
62
|
+
// map so it recomputes on update.
|
|
63
|
+
get overlayRows(): string[] {
|
|
64
|
+
const preferred = ["stdout", "stderr"];
|
|
65
|
+
const rest = [...this.regions.keys()]
|
|
66
|
+
.filter((s) => !preferred.includes(s))
|
|
67
|
+
.sort((a, b) => a.localeCompare(b));
|
|
68
|
+
const out: string[] = [];
|
|
69
|
+
for (const stream of [...preferred, ...rest]) {
|
|
70
|
+
const region = this.regions.get(stream);
|
|
71
|
+
if (region) out.push(...region.rows);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
applyRegion(update: RegionUpdate) {
|
|
77
|
+
if (update.rows.length === 0) {
|
|
78
|
+
this.regions.delete(update.stream);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.regions.set(update.stream, { epoch: update.epoch, rows: update.rows });
|
|
82
|
+
for (const row of update.rows) {
|
|
83
|
+
this.maxLineColumns = Math.max(this.maxLineColumns, visibleColumns(row));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
applyEvent(event: LogEvent) {
|
|
88
|
+
if (event.region) {
|
|
89
|
+
this.applyRegion(event.region);
|
|
90
|
+
return { touched: false, min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY };
|
|
91
|
+
}
|
|
54
92
|
this.totalLines = Math.max(this.totalLines, Math.floor(event.sizeLines));
|
|
55
93
|
this.totalBytes = Math.max(this.totalBytes, Math.floor(event.sizeBytes ?? 0));
|
|
56
94
|
this.finished = event.finished;
|
|
95
|
+
// A finished run has no live region; drop any lingering overlay so a
|
|
96
|
+
// dropped clear-frame can't leave a stale tail painted forever.
|
|
97
|
+
if (event.finished && this.regions.size > 0) this.regions.clear();
|
|
57
98
|
if (event.firstAvailableLine !== undefined && event.firstAvailableLine > 0) {
|
|
58
99
|
this.firstAvailableLine = Math.max(this.firstAvailableLine, event.firstAvailableLine);
|
|
59
100
|
}
|
|
101
|
+
if (event.frameCounts) {
|
|
102
|
+
for (const [k, v] of Object.entries(event.frameCounts)) {
|
|
103
|
+
const line = Number(k);
|
|
104
|
+
if (Number.isFinite(line) && line >= 0 && v > 0) this.frameCounts.set(line, v);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
60
107
|
|
|
61
108
|
return this.mergeSlice(event.lines);
|
|
62
109
|
}
|
|
@@ -78,6 +125,8 @@ export class LogCache {
|
|
|
78
125
|
|
|
79
126
|
reset() {
|
|
80
127
|
this.lines.clear();
|
|
128
|
+
this.regions.clear();
|
|
129
|
+
this.frameCounts.clear();
|
|
81
130
|
this.totalLines = 0;
|
|
82
131
|
this.totalBytes = 0;
|
|
83
132
|
this.finished = false;
|
|
@@ -3,12 +3,29 @@
|
|
|
3
3
|
|
|
4
4
|
export type LogSlice = Record<number, string>;
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* RegionUpdate is a live snapshot of a still-animating output region (a `\r`
|
|
8
|
+
* progress bar or a multi-line ANSI redraw). It is never persisted: the cache
|
|
9
|
+
* holds it in a transient overlay keyed by stream, replacing the whole frame on
|
|
10
|
+
* each update. Empty `rows` clears the overlay for that stream.
|
|
11
|
+
*/
|
|
12
|
+
export type RegionUpdate = {
|
|
13
|
+
stream: string;
|
|
14
|
+
epoch: number;
|
|
15
|
+
rows: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
6
18
|
export type LogEvent = {
|
|
7
19
|
lines: LogSlice;
|
|
8
20
|
sizeLines: number;
|
|
9
21
|
sizeBytes?: number;
|
|
10
22
|
finished: boolean;
|
|
11
23
|
firstAvailableLine?: number;
|
|
24
|
+
region?: RegionUpdate;
|
|
25
|
+
// Per-line count of recorded prior frames, keyed by line number. Present only
|
|
26
|
+
// for settled progress-bar / multi-line-redraw anchor lines, marking them as
|
|
27
|
+
// clickable to rewind through earlier states. Plain lines are absent.
|
|
28
|
+
frameCounts?: Record<number, number>;
|
|
12
29
|
};
|
|
13
30
|
|
|
14
31
|
export function isLogEvent(value: unknown): value is LogEvent {
|
package/src/lib/theme-tokens.css
CHANGED
|
@@ -20,73 +20,76 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
:root {
|
|
23
|
-
/* Primary — Wisp
|
|
24
|
-
|
|
25
|
-
--rw-color-wisp-
|
|
26
|
-
--rw-color-wisp-
|
|
27
|
-
--rw-color-wisp-
|
|
28
|
-
--rw-color-wisp-
|
|
29
|
-
--rw-color-wisp-
|
|
30
|
-
--rw-color-wisp-
|
|
31
|
-
--rw-color-wisp-
|
|
32
|
-
--rw-color-wisp-
|
|
33
|
-
--rw-color-wisp-
|
|
34
|
-
--rw-color-wisp-
|
|
23
|
+
/* Primary — Wisp Teal (brand = #0E7C86, the artifact's deep teal). Deeper
|
|
24
|
+
* and less cyan than Aurora (running/info) so the two stay distinct. */
|
|
25
|
+
--rw-color-wisp-50: oklch(0.97 0.02 205);
|
|
26
|
+
--rw-color-wisp-100: oklch(0.95 0.035 205);
|
|
27
|
+
--rw-color-wisp-200: oklch(0.89 0.055 204);
|
|
28
|
+
--rw-color-wisp-300: oklch(0.8 0.075 204);
|
|
29
|
+
--rw-color-wisp-400: oklch(0.68 0.088 203);
|
|
30
|
+
--rw-color-wisp-500: oklch(0.6 0.09 204);
|
|
31
|
+
--rw-color-wisp-600: oklch(0.536 0.089 205); /* ≈ #0E7C86 */
|
|
32
|
+
--rw-color-wisp-700: oklch(0.46 0.08 206);
|
|
33
|
+
--rw-color-wisp-800: oklch(0.38 0.066 207);
|
|
34
|
+
--rw-color-wisp-900: oklch(0.3 0.048 208);
|
|
35
|
+
--rw-color-wisp-950: oklch(0.2 0.034 210);
|
|
35
36
|
|
|
36
|
-
/* Accent — Aurora Teal */
|
|
37
|
-
--rw-color-aurora-50: oklch(0.97 0.02
|
|
38
|
-
--rw-color-aurora-100: oklch(0.94 0.
|
|
39
|
-
--rw-color-aurora-200: oklch(0.88 0.
|
|
40
|
-
--rw-color-aurora-300: oklch(0.78 0.
|
|
41
|
-
--rw-color-aurora-400: oklch(0.68 0.
|
|
42
|
-
--rw-color-aurora-500: oklch(0.58 0.
|
|
43
|
-
--rw-color-aurora-600: oklch(0.5 0.
|
|
44
|
-
--rw-color-aurora-700: oklch(0.42 0.
|
|
45
|
-
--rw-color-aurora-800: oklch(0.35 0.
|
|
46
|
-
--rw-color-aurora-900: oklch(0.28 0.
|
|
37
|
+
/* Accent — Aurora Teal (running status = #13A0A8, a cyan-leaning teal) */
|
|
38
|
+
--rw-color-aurora-50: oklch(0.97 0.02 192);
|
|
39
|
+
--rw-color-aurora-100: oklch(0.94 0.045 192);
|
|
40
|
+
--rw-color-aurora-200: oklch(0.88 0.075 190);
|
|
41
|
+
--rw-color-aurora-300: oklch(0.78 0.105 189);
|
|
42
|
+
--rw-color-aurora-400: oklch(0.68 0.115 190);
|
|
43
|
+
--rw-color-aurora-500: oklch(0.58 0.115 191);
|
|
44
|
+
--rw-color-aurora-600: oklch(0.5 0.105 192);
|
|
45
|
+
--rw-color-aurora-700: oklch(0.42 0.09 192);
|
|
46
|
+
--rw-color-aurora-800: oklch(0.35 0.07 192);
|
|
47
|
+
--rw-color-aurora-900: oklch(0.28 0.055 192);
|
|
47
48
|
|
|
48
|
-
/* Mist —
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
--rw-color-mist-
|
|
52
|
-
--rw-color-mist-
|
|
53
|
-
--rw-color-mist-
|
|
54
|
-
--rw-color-mist-
|
|
55
|
-
--rw-color-mist-
|
|
56
|
-
--rw-color-mist-
|
|
57
|
-
--rw-color-mist-
|
|
58
|
-
--rw-color-mist-
|
|
59
|
-
--rw-color-mist-
|
|
49
|
+
/* Mist — cool slate-blue neutrals (the artifact's #EDF1F4 / #E0E6EC /
|
|
50
|
+
* #18212E family). No purple tint; hue ~248, low chroma rising into the
|
|
51
|
+
* mids the way the design's slate does. */
|
|
52
|
+
--rw-color-mist-50: oklch(0.985 0.004 240);
|
|
53
|
+
--rw-color-mist-100: oklch(0.956 0.006 240); /* ≈ ground #EDF1F4 */
|
|
54
|
+
--rw-color-mist-200: oklch(0.922 0.01 248); /* ≈ line #E0E6EC */
|
|
55
|
+
--rw-color-mist-300: oklch(0.83 0.016 248);
|
|
56
|
+
--rw-color-mist-400: oklch(0.676 0.026 248); /* ≈ faint #8B99A7 */
|
|
57
|
+
--rw-color-mist-500: oklch(0.6 0.03 248);
|
|
58
|
+
--rw-color-mist-600: oklch(0.531 0.032 248); /* ≈ muted #5E6E7E */
|
|
59
|
+
--rw-color-mist-700: oklch(0.4 0.031 252);
|
|
60
|
+
--rw-color-mist-800: oklch(0.31 0.029 255);
|
|
61
|
+
--rw-color-mist-900: oklch(0.245 0.028 258); /* ≈ ink #18212E */
|
|
62
|
+
--rw-color-mist-950: oklch(0.17 0.022 258);
|
|
60
63
|
|
|
61
|
-
/* Success —
|
|
62
|
-
--rw-color-success-50: oklch(0.97 0.
|
|
63
|
-
--rw-color-success-100: oklch(0.94 0.
|
|
64
|
-
--rw-color-success-200: oklch(0.88 0.
|
|
65
|
-
--rw-color-success-300: oklch(0.78 0.
|
|
66
|
-
--rw-color-success-400: oklch(0.68 0.
|
|
67
|
-
--rw-color-success-500: oklch(0.58 0.
|
|
68
|
-
--rw-color-success-600: oklch(0.5 0.
|
|
69
|
-
--rw-color-success-700: oklch(0.42 0.
|
|
64
|
+
/* Success — Field Green (success status = #1F9D57, a true grass green) */
|
|
65
|
+
--rw-color-success-50: oklch(0.97 0.02 150);
|
|
66
|
+
--rw-color-success-100: oklch(0.94 0.04 150);
|
|
67
|
+
--rw-color-success-200: oklch(0.88 0.07 150);
|
|
68
|
+
--rw-color-success-300: oklch(0.78 0.11 150);
|
|
69
|
+
--rw-color-success-400: oklch(0.68 0.14 150);
|
|
70
|
+
--rw-color-success-500: oklch(0.58 0.145 150);
|
|
71
|
+
--rw-color-success-600: oklch(0.5 0.13 150);
|
|
72
|
+
--rw-color-success-700: oklch(0.42 0.11 150);
|
|
70
73
|
|
|
71
|
-
/* Danger —
|
|
72
|
-
--rw-color-danger-50: oklch(0.97 0.
|
|
73
|
-
--rw-color-danger-100: oklch(0.94 0.
|
|
74
|
-
--rw-color-danger-200: oklch(0.88 0.
|
|
75
|
-
--rw-color-danger-300: oklch(0.78 0.
|
|
76
|
-
--rw-color-danger-400: oklch(0.68 0.
|
|
77
|
-
--rw-color-danger-500: oklch(0.58 0.
|
|
78
|
-
--rw-color-danger-600: oklch(0.5 0.
|
|
79
|
-
--rw-color-danger-700: oklch(0.42 0.
|
|
74
|
+
/* Danger — Signal Red (failed status = #D6453D, a warm tomato red) */
|
|
75
|
+
--rw-color-danger-50: oklch(0.97 0.016 27);
|
|
76
|
+
--rw-color-danger-100: oklch(0.94 0.03 27);
|
|
77
|
+
--rw-color-danger-200: oklch(0.88 0.06 27);
|
|
78
|
+
--rw-color-danger-300: oklch(0.78 0.1 27);
|
|
79
|
+
--rw-color-danger-400: oklch(0.68 0.14 27);
|
|
80
|
+
--rw-color-danger-500: oklch(0.58 0.175 27);
|
|
81
|
+
--rw-color-danger-600: oklch(0.5 0.17 27);
|
|
82
|
+
--rw-color-danger-700: oklch(0.42 0.145 27);
|
|
80
83
|
|
|
81
|
-
/* Warning */
|
|
82
|
-
--rw-color-warning-50: oklch(0.
|
|
83
|
-
--rw-color-warning-100: oklch(0.
|
|
84
|
-
--rw-color-warning-200: oklch(0.9 0.
|
|
85
|
-
--rw-color-warning-300: oklch(0.
|
|
86
|
-
--rw-color-warning-400: oklch(0.
|
|
87
|
-
--rw-color-warning-500: oklch(0.
|
|
88
|
-
--rw-color-warning-600: oklch(0.
|
|
89
|
-
--rw-color-warning-700: oklch(0.
|
|
84
|
+
/* Warning — Ember Amber (stopped status = #D98324, a warm orange-amber) */
|
|
85
|
+
--rw-color-warning-50: oklch(0.97 0.03 78);
|
|
86
|
+
--rw-color-warning-100: oklch(0.94 0.07 74);
|
|
87
|
+
--rw-color-warning-200: oklch(0.9 0.11 72);
|
|
88
|
+
--rw-color-warning-300: oklch(0.84 0.14 70);
|
|
89
|
+
--rw-color-warning-400: oklch(0.77 0.155 67);
|
|
90
|
+
--rw-color-warning-500: oklch(0.69 0.155 64);
|
|
91
|
+
--rw-color-warning-600: oklch(0.6 0.14 62);
|
|
92
|
+
--rw-color-warning-700: oklch(0.52 0.12 60);
|
|
90
93
|
|
|
91
94
|
/* ANSI console palette — 16-color map for the log console, tuned for
|
|
92
95
|
* its always-dark surface (no .dark override needed). Hues follow the
|
|
@@ -109,6 +112,15 @@
|
|
|
109
112
|
--rw-ansi-bright-cyan: var(--rw-color-aurora-300);
|
|
110
113
|
--rw-ansi-bright-white: var(--rw-color-mist-50);
|
|
111
114
|
|
|
115
|
+
/* Console device surface — the log console is a "black box" instrument
|
|
116
|
+
* with its own fixed palette that never follows the light/dark UI theme.
|
|
117
|
+
* Exact values from the approved execution-detail design. */
|
|
118
|
+
--rw-con-bg: #0e141b; /* body / gutter backing */
|
|
119
|
+
--rw-con-panel: #11181f; /* toolbar + footer chrome */
|
|
120
|
+
--rw-con-text: #c3cfda; /* default log text */
|
|
121
|
+
--rw-con-dim: #6c7d8c; /* footer + muted readouts */
|
|
122
|
+
--rw-con-gutter: #3c4a58; /* line numbers + sentinel rule */
|
|
123
|
+
|
|
112
124
|
/* Light-mode semantic tokens (default) */
|
|
113
125
|
/* Surfaces */
|
|
114
126
|
--rw-surface: var(--rw-color-mist-50);
|
package/src/lib/theme.css
CHANGED
|
@@ -197,6 +197,7 @@
|
|
|
197
197
|
--color-warning-soft-text: var(--rw-warning-soft-text);
|
|
198
198
|
/* Info */
|
|
199
199
|
--color-info: var(--rw-info);
|
|
200
|
+
--color-info-surface: var(--rw-info);
|
|
200
201
|
--color-info-hover: var(--rw-info-hover);
|
|
201
202
|
--color-on-info: var(--rw-on-info);
|
|
202
203
|
--color-info-soft: var(--rw-info-soft);
|
|
@@ -4,11 +4,21 @@
|
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
5
|
import {
|
|
6
6
|
formatBytes,
|
|
7
|
+
formatCalendarDate,
|
|
8
|
+
formatClockTime,
|
|
9
|
+
formatDayMonth,
|
|
7
10
|
formatDuration,
|
|
8
11
|
formatRelativeTime,
|
|
9
12
|
formatRelativeTimeWithAbsolute,
|
|
13
|
+
formatTimeHM,
|
|
10
14
|
} from "./format.js";
|
|
11
15
|
|
|
16
|
+
// The clock/date helpers below delegate to toLocaleTimeString / toLocaleDateString,
|
|
17
|
+
// whose exact rendering is locale- and timezone-dependent. We assert the
|
|
18
|
+
// locale-invariant *shape* of each format (how many numeric groups, year present
|
|
19
|
+
// or not) rather than a literal string, mirroring formatRelativeTimeWithAbsolute.
|
|
20
|
+
const numericGroups = (s: string): number => (s.match(/\d+/g) ?? []).length;
|
|
21
|
+
|
|
12
22
|
describe("formatBytes", () => {
|
|
13
23
|
it("formats 0 bytes", () => {
|
|
14
24
|
expect(formatBytes(0)).toBe("0 B");
|
|
@@ -122,3 +132,31 @@ describe("formatRelativeTime", () => {
|
|
|
122
132
|
);
|
|
123
133
|
});
|
|
124
134
|
});
|
|
135
|
+
|
|
136
|
+
describe("formatClockTime", () => {
|
|
137
|
+
it("renders three numeric groups (hours, minutes, seconds)", () => {
|
|
138
|
+
// 24-hour h:m:s — the seconds component is what sets it apart from formatTimeHM.
|
|
139
|
+
expect(numericGroups(formatClockTime("2026-06-22T17:15:02Z"))).toBe(3);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("formatTimeHM", () => {
|
|
144
|
+
it("renders two numeric groups (hours, minutes) — no seconds", () => {
|
|
145
|
+
expect(numericGroups(formatTimeHM("2026-06-22T17:15:02Z"))).toBe(2);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("formatCalendarDate", () => {
|
|
150
|
+
it("includes the four-digit year", () => {
|
|
151
|
+
// Midday UTC keeps the calendar year stable across every timezone.
|
|
152
|
+
expect(formatCalendarDate("2026-06-22T12:00:00Z")).toContain("2026");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("formatDayMonth", () => {
|
|
157
|
+
it("includes a day digit but omits the year", () => {
|
|
158
|
+
const r = formatDayMonth("2026-06-22T12:00:00Z");
|
|
159
|
+
expect(r).toMatch(/\d/);
|
|
160
|
+
expect(r).not.toContain("2026");
|
|
161
|
+
});
|
|
162
|
+
});
|
package/src/lib/utils/format.ts
CHANGED
|
@@ -53,6 +53,42 @@ export function formatDateTime(dateStr: string): string {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** Wall-clock time of day, seconds included, 24-hour — e.g. "17:15:02". */
|
|
57
|
+
export function formatClockTime(dateStr: string): string {
|
|
58
|
+
return new Date(dateStr).toLocaleTimeString(undefined, {
|
|
59
|
+
hour: "2-digit",
|
|
60
|
+
minute: "2-digit",
|
|
61
|
+
second: "2-digit",
|
|
62
|
+
hour12: false,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Calendar date without the time — e.g. "22 Jun 2026". */
|
|
67
|
+
export function formatCalendarDate(dateStr: string): string {
|
|
68
|
+
return new Date(dateStr).toLocaleDateString(undefined, {
|
|
69
|
+
year: "numeric",
|
|
70
|
+
month: "short",
|
|
71
|
+
day: "numeric",
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Time of day, no seconds, 24-hour — e.g. "17:15". */
|
|
76
|
+
export function formatTimeHM(dateStr: string): string {
|
|
77
|
+
return new Date(dateStr).toLocaleTimeString(undefined, {
|
|
78
|
+
hour: "2-digit",
|
|
79
|
+
minute: "2-digit",
|
|
80
|
+
hour12: false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Day and month, no year — e.g. "22 Jun". */
|
|
85
|
+
export function formatDayMonth(dateStr: string): string {
|
|
86
|
+
return new Date(dateStr).toLocaleDateString(undefined, {
|
|
87
|
+
month: "short",
|
|
88
|
+
day: "numeric",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
56
92
|
export function formatDuration(ms: number): string {
|
|
57
93
|
if (ms < 1000) return String(ms) + "ms";
|
|
58
94
|
const s = Math.floor(ms / 1000);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<svg width="512" height="512" version="1.1" viewBox="0 0 5120 5120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
2
2
|
<defs>
|
|
3
3
|
<linearGradient id="b">
|
|
4
|
-
<stop stop-color="#
|
|
5
|
-
<stop stop-color="#
|
|
4
|
+
<stop stop-color="#15a0a8" offset="0"/>
|
|
5
|
+
<stop stop-color="#0e7c86" offset="1"/>
|
|
6
6
|
</linearGradient>
|
|
7
7
|
<radialGradient id="a" cx="2375.3" cy="3240.7" r="1333.9" gradientTransform="matrix(1.01 -.012494 .026846 2.1701 -110.74 -3341.2)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
|
|
8
8
|
</defs>
|