@tokentop/ttop 0.2.2 → 0.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.
- package/README.md +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +32 -32
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +29 -23
- package/src/plugins/notifications/visual-flash.ts +21 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +66 -57
- package/src/plugins/registry.ts +47 -43
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +295 -177
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +313 -166
- package/src/tui/components/SettingsModal.tsx +231 -214
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +193 -144
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +232 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +151 -117
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +3 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +127 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +466 -237
- package/src/tui/views/RealTimeDashboard.tsx +150 -111
- package/src/tui/views/SettingsView.tsx +222 -191
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
package/src/tui/views/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Dashboard } from
|
|
2
|
-
export { ProjectsView } from
|
|
1
|
+
export { Dashboard } from "./Dashboard.tsx";
|
|
2
|
+
export { ProjectsView } from "./ProjectsView.tsx";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
declare module
|
|
2
|
-
import type { TestRendererOptions } from
|
|
3
|
-
import type { ReactNode } from
|
|
1
|
+
declare module "@opentui/react/test-utils" {
|
|
2
|
+
import type { TestRendererOptions } from "@opentui/core/testing";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
export function testRender(
|
|
6
6
|
node: ReactNode,
|
|
7
|
-
testRendererOptions: TestRendererOptions
|
|
7
|
+
testRendererOptions: TestRendererOptions,
|
|
8
8
|
): Promise<{
|
|
9
|
-
renderer: import(
|
|
10
|
-
mockInput: import(
|
|
11
|
-
mockMouse: import(
|
|
9
|
+
renderer: import("@opentui/core/testing").TestRenderer;
|
|
10
|
+
mockInput: import("@opentui/core/testing").MockInput;
|
|
11
|
+
mockMouse: import("@opentui/core/testing").MockMouse;
|
|
12
12
|
renderOnce: () => Promise<void>;
|
|
13
13
|
captureCharFrame: () => string;
|
|
14
14
|
resize: (width: number, height: number) => void;
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $ } from
|
|
2
|
-
import { platform } from
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
import { platform } from "os";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Custom clipboard implementation with platform-specific fallbacks.
|
|
@@ -23,10 +23,10 @@ import { platform } from 'os';
|
|
|
23
23
|
*/
|
|
24
24
|
function writeOsc52(text: string): void {
|
|
25
25
|
if (!process.stdout.isTTY) return;
|
|
26
|
-
const base64 = Buffer.from(text).toString(
|
|
26
|
+
const base64 = Buffer.from(text).toString("base64");
|
|
27
27
|
const osc52 = `\x1b]52;c;${base64}\x07`;
|
|
28
28
|
// tmux and screen require DCS passthrough wrapping
|
|
29
|
-
const passthrough = process.env[
|
|
29
|
+
const passthrough = process.env["TMUX"] || process.env["STY"];
|
|
30
30
|
const sequence = passthrough ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52;
|
|
31
31
|
process.stdout.write(sequence);
|
|
32
32
|
}
|
|
@@ -38,30 +38,30 @@ function getCopyMethod(): (text: string) => Promise<void> {
|
|
|
38
38
|
|
|
39
39
|
const os = platform();
|
|
40
40
|
|
|
41
|
-
if (os ===
|
|
41
|
+
if (os === "darwin" && Bun.which("osascript")) {
|
|
42
42
|
copyMethod = async (text: string) => {
|
|
43
|
-
const escaped = text.replace(/\\/g,
|
|
43
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
44
44
|
await $`osascript -e 'set the clipboard to "${escaped}"'`.nothrow().quiet();
|
|
45
45
|
};
|
|
46
46
|
return copyMethod;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
if (os ===
|
|
50
|
-
if (process.env[
|
|
49
|
+
if (os === "linux") {
|
|
50
|
+
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
|
|
51
51
|
copyMethod = async (text: string) => {
|
|
52
|
-
const proc = Bun.spawn([
|
|
52
|
+
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" });
|
|
53
53
|
proc.stdin.write(text);
|
|
54
54
|
proc.stdin.end();
|
|
55
55
|
await proc.exited.catch(() => {});
|
|
56
56
|
};
|
|
57
57
|
return copyMethod;
|
|
58
58
|
}
|
|
59
|
-
if (Bun.which(
|
|
59
|
+
if (Bun.which("xclip")) {
|
|
60
60
|
copyMethod = async (text: string) => {
|
|
61
|
-
const proc = Bun.spawn([
|
|
62
|
-
stdin:
|
|
63
|
-
stdout:
|
|
64
|
-
stderr:
|
|
61
|
+
const proc = Bun.spawn(["xclip", "-selection", "clipboard"], {
|
|
62
|
+
stdin: "pipe",
|
|
63
|
+
stdout: "ignore",
|
|
64
|
+
stderr: "ignore",
|
|
65
65
|
});
|
|
66
66
|
proc.stdin.write(text);
|
|
67
67
|
proc.stdin.end();
|
|
@@ -69,12 +69,12 @@ function getCopyMethod(): (text: string) => Promise<void> {
|
|
|
69
69
|
};
|
|
70
70
|
return copyMethod;
|
|
71
71
|
}
|
|
72
|
-
if (Bun.which(
|
|
72
|
+
if (Bun.which("xsel")) {
|
|
73
73
|
copyMethod = async (text: string) => {
|
|
74
|
-
const proc = Bun.spawn([
|
|
75
|
-
stdin:
|
|
76
|
-
stdout:
|
|
77
|
-
stderr:
|
|
74
|
+
const proc = Bun.spawn(["xsel", "--clipboard", "--input"], {
|
|
75
|
+
stdin: "pipe",
|
|
76
|
+
stdout: "ignore",
|
|
77
|
+
stderr: "ignore",
|
|
78
78
|
});
|
|
79
79
|
proc.stdin.write(text);
|
|
80
80
|
proc.stdin.end();
|
|
@@ -84,21 +84,21 @@ function getCopyMethod(): (text: string) => Promise<void> {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
if (os ===
|
|
87
|
+
if (os === "win32") {
|
|
88
88
|
copyMethod = async (text: string) => {
|
|
89
89
|
const proc = Bun.spawn(
|
|
90
90
|
[
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
"powershell.exe",
|
|
92
|
+
"-NonInteractive",
|
|
93
|
+
"-NoProfile",
|
|
94
|
+
"-Command",
|
|
95
|
+
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
|
96
96
|
],
|
|
97
97
|
{
|
|
98
|
-
stdin:
|
|
99
|
-
stdout:
|
|
100
|
-
stderr:
|
|
101
|
-
}
|
|
98
|
+
stdin: "pipe",
|
|
99
|
+
stdout: "ignore",
|
|
100
|
+
stderr: "ignore",
|
|
101
|
+
},
|
|
102
102
|
);
|
|
103
103
|
proc.stdin.write(text);
|
|
104
104
|
proc.stdin.end();
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
import { describe, expect, test } from
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
formatBudgetDisplay,
|
|
4
4
|
formatCurrency,
|
|
5
5
|
isValidCurrencyInput,
|
|
6
6
|
parseCurrencyInput,
|
|
7
|
-
} from
|
|
7
|
+
} from "./currency.ts";
|
|
8
8
|
|
|
9
|
-
describe(
|
|
10
|
-
test(
|
|
11
|
-
expect(parseCurrencyInput(
|
|
12
|
-
expect(parseCurrencyInput(
|
|
9
|
+
describe("parseCurrencyInput", () => {
|
|
10
|
+
test("returns null for empty or whitespace input", () => {
|
|
11
|
+
expect(parseCurrencyInput("")).toBeNull();
|
|
12
|
+
expect(parseCurrencyInput(" ")).toBeNull();
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
test(
|
|
16
|
-
expect(parseCurrencyInput(
|
|
15
|
+
test("parses US format with thousands and decimal separators", () => {
|
|
16
|
+
expect(parseCurrencyInput("1,234.56")).toBe(1234.56);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test(
|
|
20
|
-
expect(parseCurrencyInput(
|
|
19
|
+
test("parses European format with period thousands and comma decimal", () => {
|
|
20
|
+
expect(parseCurrencyInput("1.234,56")).toBe(1234.56);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
test(
|
|
24
|
-
expect(parseCurrencyInput(
|
|
23
|
+
test("parses plain numbers", () => {
|
|
24
|
+
expect(parseCurrencyInput("42.50")).toBe(42.5);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
test(
|
|
28
|
-
expect(parseCurrencyInput(
|
|
29
|
-
expect(parseCurrencyInput(
|
|
30
|
-
expect(parseCurrencyInput(
|
|
27
|
+
test("strips supported currency symbols before parsing", () => {
|
|
28
|
+
expect(parseCurrencyInput("$100")).toBe(100);
|
|
29
|
+
expect(parseCurrencyInput("€100")).toBe(100);
|
|
30
|
+
expect(parseCurrencyInput("£100")).toBe(100);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
test(
|
|
34
|
-
expect(parseCurrencyInput(
|
|
33
|
+
test("treats comma as decimal separator when not thousands grouping", () => {
|
|
34
|
+
expect(parseCurrencyInput("10,50")).toBe(10.5);
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
test(
|
|
38
|
-
expect(parseCurrencyInput(
|
|
37
|
+
test("treats comma as thousands separator with 3 digits after comma", () => {
|
|
38
|
+
expect(parseCurrencyInput("1,234")).toBe(1234);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test(
|
|
42
|
-
expect(parseCurrencyInput(
|
|
41
|
+
test("returns null for invalid input", () => {
|
|
42
|
+
expect(parseCurrencyInput("abc")).toBeNull();
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
test(
|
|
46
|
-
const parsed = parseCurrencyInput(
|
|
45
|
+
test("rounds parsed values to 2 decimal places", () => {
|
|
46
|
+
const parsed = parseCurrencyInput("10.129");
|
|
47
47
|
expect(parsed).toBe(Math.round(10.129 * 100) / 100);
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
describe(
|
|
52
|
-
test(
|
|
53
|
-
expect(formatCurrency(null)).toBe(
|
|
51
|
+
describe("formatCurrency", () => {
|
|
52
|
+
test("returns None for null input", () => {
|
|
53
|
+
expect(formatCurrency(null)).toBe("None");
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
test(
|
|
57
|
-
expect(formatCurrency(1234, { compact: true })).toBe(
|
|
56
|
+
test("formats compact values >= 1000 as k notation", () => {
|
|
57
|
+
expect(formatCurrency(1234, { compact: true })).toBe("$1.2k");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
test(
|
|
61
|
-
expect(formatCurrency(234.4, { compact: true })).toBe(
|
|
60
|
+
test("formats compact values >= 100 as rounded integer", () => {
|
|
61
|
+
expect(formatCurrency(234.4, { compact: true })).toBe("$234");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
test(
|
|
65
|
-
expect(formatCurrency(42.34, { compact: true })).toBe(
|
|
64
|
+
test("formats compact values >= 10 with one decimal place", () => {
|
|
65
|
+
expect(formatCurrency(42.34, { compact: true })).toBe("$42.3");
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
test(
|
|
69
|
-
expect(formatCurrency(9.5, { compact: true })).toBe(
|
|
68
|
+
test("formats compact values < 10 with two decimal places", () => {
|
|
69
|
+
expect(formatCurrency(9.5, { compact: true })).toBe("$9.50");
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
test(
|
|
73
|
-
expect(formatCurrency(1234.5)).toBe(
|
|
72
|
+
test("formats non-compact values with locale separators and two decimals", () => {
|
|
73
|
+
expect(formatCurrency(1234.5)).toBe("$1,234.50");
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
test(
|
|
77
|
-
expect(formatCurrency(1234.5, { showSymbol: false })).toBe(
|
|
78
|
-
expect(formatCurrency(1234, { compact: true, showSymbol: false })).toBe(
|
|
76
|
+
test("omits currency symbol when showSymbol is false", () => {
|
|
77
|
+
expect(formatCurrency(1234.5, { showSymbol: false })).toBe("1,234.50");
|
|
78
|
+
expect(formatCurrency(1234, { compact: true, showSymbol: false })).toBe("1.2k");
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
describe(
|
|
83
|
-
test(
|
|
84
|
-
expect(formatBudgetDisplay(null)).toBe(
|
|
82
|
+
describe("formatBudgetDisplay", () => {
|
|
83
|
+
test("returns None for null input", () => {
|
|
84
|
+
expect(formatBudgetDisplay(null)).toBe("None");
|
|
85
85
|
});
|
|
86
86
|
|
|
87
|
-
test(
|
|
87
|
+
test("delegates formatting to formatCurrency for numeric values", () => {
|
|
88
88
|
expect(formatBudgetDisplay(1234.5)).toBe(formatCurrency(1234.5));
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
describe(
|
|
93
|
-
test(
|
|
94
|
-
expect(isValidCurrencyInput(
|
|
95
|
-
expect(isValidCurrencyInput(
|
|
92
|
+
describe("isValidCurrencyInput", () => {
|
|
93
|
+
test("returns true for empty input (represents no value)", () => {
|
|
94
|
+
expect(isValidCurrencyInput("")).toBe(true);
|
|
95
|
+
expect(isValidCurrencyInput(" ")).toBe(true);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
test(
|
|
99
|
-
expect(isValidCurrencyInput(
|
|
100
|
-
expect(isValidCurrencyInput(
|
|
98
|
+
test("returns true for valid numeric input", () => {
|
|
99
|
+
expect(isValidCurrencyInput("42.50")).toBe(true);
|
|
100
|
+
expect(isValidCurrencyInput("1.234,56")).toBe(true);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
test(
|
|
104
|
-
expect(isValidCurrencyInput(
|
|
103
|
+
test("returns false for invalid input", () => {
|
|
104
|
+
expect(isValidCurrencyInput("abc")).toBe(false);
|
|
105
105
|
});
|
|
106
106
|
});
|
package/src/utils/currency.ts
CHANGED
|
@@ -3,77 +3,80 @@
|
|
|
3
3
|
* Handles US format (1,234.56) and European format (1.234,56).
|
|
4
4
|
*/
|
|
5
5
|
export function parseCurrencyInput(input: string): number | null {
|
|
6
|
-
if (!input || input.trim() ===
|
|
6
|
+
if (!input || input.trim() === "") {
|
|
7
7
|
return null;
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
let normalized = input.trim().replace(/[$€£]/g,
|
|
11
|
-
|
|
12
|
-
const hasComma = normalized.includes(
|
|
13
|
-
const hasPeriod = normalized.includes(
|
|
14
|
-
|
|
9
|
+
|
|
10
|
+
let normalized = input.trim().replace(/[$€£]/g, "").replace(/\s/g, "");
|
|
11
|
+
|
|
12
|
+
const hasComma = normalized.includes(",");
|
|
13
|
+
const hasPeriod = normalized.includes(".");
|
|
14
|
+
|
|
15
15
|
if (hasComma && hasPeriod) {
|
|
16
|
-
const lastComma = normalized.lastIndexOf(
|
|
17
|
-
const lastPeriod = normalized.lastIndexOf(
|
|
18
|
-
|
|
16
|
+
const lastComma = normalized.lastIndexOf(",");
|
|
17
|
+
const lastPeriod = normalized.lastIndexOf(".");
|
|
18
|
+
|
|
19
19
|
if (lastComma > lastPeriod) {
|
|
20
20
|
// European: 1.234,56 → remove periods, comma becomes decimal
|
|
21
|
-
normalized = normalized.replace(/\./g,
|
|
21
|
+
normalized = normalized.replace(/\./g, "").replace(",", ".");
|
|
22
22
|
} else {
|
|
23
23
|
// US: 1,234.56 → remove commas
|
|
24
|
-
normalized = normalized.replace(/,/g,
|
|
24
|
+
normalized = normalized.replace(/,/g, "");
|
|
25
25
|
}
|
|
26
26
|
} else if (hasComma) {
|
|
27
|
-
const parts = normalized.split(
|
|
27
|
+
const parts = normalized.split(",");
|
|
28
28
|
// 3 digits after comma = thousands separator (1,234), otherwise decimal (10,50)
|
|
29
29
|
if (parts.length === 2 && parts[1]!.length === 3 && parts[0]!.length > 0) {
|
|
30
|
-
normalized = normalized.replace(
|
|
30
|
+
normalized = normalized.replace(",", "");
|
|
31
31
|
} else {
|
|
32
|
-
normalized = normalized.replace(
|
|
32
|
+
normalized = normalized.replace(",", ".");
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
const parsed = parseFloat(normalized);
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
if (isNaN(parsed) || !isFinite(parsed)) {
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
return Math.round(parsed * 100) / 100;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export function formatCurrency(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
export function formatCurrency(
|
|
46
|
+
value: number | null,
|
|
47
|
+
options?: {
|
|
48
|
+
compact?: boolean;
|
|
49
|
+
showSymbol?: boolean;
|
|
50
|
+
},
|
|
51
|
+
): string {
|
|
49
52
|
if (value === null) {
|
|
50
|
-
return
|
|
53
|
+
return "None";
|
|
51
54
|
}
|
|
52
|
-
|
|
55
|
+
|
|
53
56
|
const { compact = false, showSymbol = true } = options ?? {};
|
|
54
|
-
const symbol = showSymbol ?
|
|
55
|
-
|
|
57
|
+
const symbol = showSymbol ? "$" : "";
|
|
58
|
+
|
|
56
59
|
if (compact) {
|
|
57
60
|
if (value >= 1000) return `${symbol}${(value / 1000).toFixed(1)}k`;
|
|
58
61
|
if (value >= 100) return `${symbol}${Math.round(value)}`;
|
|
59
62
|
if (value >= 10) return `${symbol}${value.toFixed(1)}`;
|
|
60
63
|
return `${symbol}${value.toFixed(2)}`;
|
|
61
64
|
}
|
|
62
|
-
|
|
63
|
-
return `${symbol}${value.toLocaleString(
|
|
64
|
-
minimumFractionDigits: 2,
|
|
65
|
-
maximumFractionDigits: 2
|
|
65
|
+
|
|
66
|
+
return `${symbol}${value.toLocaleString("en-US", {
|
|
67
|
+
minimumFractionDigits: 2,
|
|
68
|
+
maximumFractionDigits: 2,
|
|
66
69
|
})}`;
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
export function formatBudgetDisplay(value: number | null): string {
|
|
70
73
|
if (value === null) {
|
|
71
|
-
return
|
|
74
|
+
return "None";
|
|
72
75
|
}
|
|
73
76
|
return formatCurrency(value);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
export function isValidCurrencyInput(input: string): boolean {
|
|
77
|
-
if (input.trim() ===
|
|
80
|
+
if (input.trim() === "") return true;
|
|
78
81
|
return parseCurrencyInput(input) !== null;
|
|
79
82
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION =
|
|
1
|
+
export const VERSION = "0.3.0"; // x-release-please-version
|