@heinrichb/console-toolkit 1.0.3 → 1.0.5

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 (41) hide show
  1. package/README.md +162 -2
  2. package/dist/components/progress.d.ts +75 -0
  3. package/dist/components/progress.js +43 -0
  4. package/dist/components/progress.test.d.ts +1 -0
  5. package/dist/components/progress.test.js +101 -0
  6. package/dist/components/spinner.d.ts +36 -0
  7. package/dist/components/spinner.js +33 -0
  8. package/dist/components/spinner.test.d.ts +1 -0
  9. package/dist/components/spinner.test.js +53 -0
  10. package/dist/core/layout.d.ts +25 -0
  11. package/dist/core/layout.js +57 -0
  12. package/dist/core/layout.test.d.ts +1 -0
  13. package/dist/core/layout.test.js +41 -0
  14. package/dist/core/printer.d.ts +20 -0
  15. package/dist/core/printer.js +41 -0
  16. package/dist/core/printer.test.d.ts +1 -0
  17. package/dist/core/printer.test.js +36 -0
  18. package/dist/core/style.d.ts +51 -0
  19. package/dist/core/style.js +127 -0
  20. package/dist/core/style.test.d.ts +1 -0
  21. package/dist/core/style.test.js +53 -0
  22. package/dist/core/types.d.ts +42 -0
  23. package/dist/core/types.js +4 -0
  24. package/dist/core/utils.d.ts +25 -0
  25. package/dist/core/utils.js +39 -0
  26. package/dist/core/utils.test.d.ts +1 -0
  27. package/dist/core/utils.test.js +25 -0
  28. package/dist/demo.d.ts +0 -4
  29. package/dist/demo.js +119 -15
  30. package/dist/index.d.ts +8 -99
  31. package/dist/index.js +14 -234
  32. package/dist/index.test.js +26 -6
  33. package/dist/presets/ascii.d.ts +5 -0
  34. package/dist/presets/ascii.js +33 -0
  35. package/dist/presets/ascii.test.d.ts +1 -0
  36. package/dist/presets/ascii.test.js +10 -0
  37. package/dist/spinner.d.ts +36 -0
  38. package/dist/spinner.js +33 -0
  39. package/dist/spinner.test.d.ts +1 -0
  40. package/dist/spinner.test.js +53 -0
  41. package/package.json +8 -6
package/dist/index.d.ts CHANGED
@@ -1,99 +1,8 @@
1
- /**
2
- * Core types for styled console output.
3
- */
4
- export type StandardColor = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "grey";
5
- export type StyleModifier = "bold" | "dim" | "italic" | "underline" | "reset" | "default" | "hidden" | "inverse" | "strikethrough";
6
- export type HexColor = `#${string}`;
7
- /**
8
- * A style can be a standard color name, a hex color string, or a style modifier.
9
- * It can also be a raw ANSI string (though discouraged) for backward compatibility or special cases.
10
- */
11
- export type Style = StandardColor | StyleModifier | HexColor | string;
12
- export interface StyledSegment {
13
- text: string;
14
- /** Style or array of styles to apply to the text */
15
- style: Style | Style[];
16
- }
17
- export interface StyledLine {
18
- segments: StyledSegment[];
19
- }
20
- /**
21
- * Configuration for the Printer engine.
22
- */
23
- export interface PrinterOptions {
24
- /** If true, the printer will overwrite previous lines instead of appending new ones. */
25
- interactive?: boolean;
26
- /** The default style to apply to padding or separators. */
27
- defaultStyle?: Style | Style[];
28
- }
29
- export declare const RESET = "\u001B[0m";
30
- /**
31
- * Converts a hex color string to an RGB object.
32
- * @param hex - Hex color in the form "#RRGGBB".
33
- */
34
- export declare function hexToRgb(hex: string): {
35
- r: number;
36
- g: number;
37
- b: number;
38
- };
39
- /**
40
- * Converts RGB to a 24-bit ANSI foreground color escape sequence.
41
- */
42
- export declare function rgbToAnsi(r: number, g: number, b: number): string;
43
- /**
44
- * Converts a hex color string directly to an ANSI escape sequence.
45
- */
46
- export declare function hexToAnsi(hex: string): string;
47
- /**
48
- * Interpolates between two hex colors based on a factor (0 to 1) and returns a Hex color string.
49
- */
50
- export declare function interpolateColor(color1: string, color2: string, factor: number): HexColor;
51
- /**
52
- * Resolves a single Style or array of Styles into an ANSI escape sequence.
53
- */
54
- export declare function resolveStyle(style: Style | Style[]): string;
55
- /**
56
- * Gets the plain text length of a StyledLine.
57
- */
58
- export declare function getLineLength(line: StyledLine): number;
59
- /**
60
- * Computes the maximum width among an array of StyledLines.
61
- */
62
- export declare function computeMaxWidth(lines: StyledLine[]): number;
63
- /**
64
- * Pads a StyledLine to a target width by adding an empty segment at the end.
65
- */
66
- export declare function padLine(line: StyledLine, targetWidth: number, padStyle: Style | Style[]): StyledLine;
67
- /**
68
- * Handles rendering StyledLines to the terminal with support for interactive overwriting.
69
- */
70
- export declare class Printer {
71
- private linesRendered;
72
- private isInteractive;
73
- constructor(options?: PrinterOptions);
74
- /**
75
- * Generates the clear sequence to move cursor and clear previously rendered lines.
76
- */
77
- private getClearSequence;
78
- /**
79
- * Renders an array of StyledLines to the standard output.
80
- */
81
- print(lines: StyledLine[]): void;
82
- }
83
- /**
84
- * Merges two columns of StyledLines into a single layout.
85
- */
86
- export declare function mergeColumns(leftColumn: StyledLine[], rightColumn: StyledLine[], leftWidth: number, separator: string, defaultStyle: Style | Style[]): StyledLine[];
87
- /**
88
- * Prints two columns of styled content to the console.
89
- */
90
- export declare function printDualColumn(left: StyledLine[], right: StyledLine[], options?: {
91
- leftWidth?: number;
92
- separator?: string;
93
- printer?: Printer;
94
- }): void;
95
- /**
96
- * Returns the classic Dragon ASCII art as StyledLines with a vertical color gradient.
97
- */
98
- export declare function getDragonLines(startColor?: string, endColor?: string): StyledLine[];
99
- export * from "./progress";
1
+ export * from "./core/types";
2
+ export * from "./core/style";
3
+ export * from "./core/utils";
4
+ export * from "./core/printer";
5
+ export * from "./core/layout";
6
+ export * from "./components/progress";
7
+ export * from "./components/spinner";
8
+ export * from "./presets/ascii";
package/dist/index.js CHANGED
@@ -1,234 +1,14 @@
1
- /**
2
- * Core types for styled console output.
3
- */
4
- // -----------------
5
- // Color Utilities
6
- // -----------------
7
- const ESC = "\x1b";
8
- export const RESET = `${ESC}[0m`;
9
- /**
10
- * Converts a hex color string to an RGB object.
11
- * @param hex - Hex color in the form "#RRGGBB".
12
- */
13
- export function hexToRgb(hex) {
14
- const h = hex.replace(/^#/, "");
15
- if (h.length !== 6)
16
- throw new Error("Invalid hex color.");
17
- return {
18
- r: parseInt(h.substring(0, 2), 16),
19
- g: parseInt(h.substring(2, 4), 16),
20
- b: parseInt(h.substring(4, 6), 16)
21
- };
22
- }
23
- /**
24
- * Converts RGB to a 24-bit ANSI foreground color escape sequence.
25
- */
26
- export function rgbToAnsi(r, g, b) {
27
- return `${ESC}[38;2;${r};${g};${b}m`;
28
- }
29
- /**
30
- * Converts a hex color string directly to an ANSI escape sequence.
31
- */
32
- export function hexToAnsi(hex) {
33
- const { r, g, b } = hexToRgb(hex);
34
- return rgbToAnsi(r, g, b);
35
- }
36
- /**
37
- * Converts a single RGB component to a 2-digit hex string.
38
- */
39
- function toHex(c) {
40
- const hex = Math.max(0, Math.min(255, Math.round(c))).toString(16);
41
- return hex.length === 1 ? "0" + hex : hex;
42
- }
43
- /**
44
- * Interpolates between two hex colors based on a factor (0 to 1) and returns a Hex color string.
45
- */
46
- export function interpolateColor(color1, color2, factor) {
47
- const f = Math.max(0, Math.min(1, factor));
48
- const c1 = hexToRgb(color1);
49
- const c2 = hexToRgb(color2);
50
- const r = c1.r + f * (c2.r - c1.r);
51
- const g = c1.g + f * (c2.g - c1.g);
52
- const b = c1.b + f * (c2.b - c1.b);
53
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
54
- }
55
- // -----------------
56
- // Style Resolution
57
- // -----------------
58
- const STYLE_CODES = {
59
- reset: "0",
60
- default: "0",
61
- bold: "1",
62
- dim: "2",
63
- italic: "3",
64
- underline: "4",
65
- inverse: "7",
66
- hidden: "8",
67
- strikethrough: "9",
68
- black: "30",
69
- red: "31",
70
- green: "32",
71
- yellow: "33",
72
- blue: "34",
73
- magenta: "35",
74
- cyan: "36",
75
- white: "37",
76
- gray: "90",
77
- grey: "90"
78
- };
79
- /**
80
- * Resolves a single Style or array of Styles into an ANSI escape sequence.
81
- */
82
- export function resolveStyle(style) {
83
- if (Array.isArray(style)) {
84
- return style.map(resolveStyle).join("");
85
- }
86
- if (typeof style !== "string") {
87
- return "";
88
- }
89
- // Check for hex color
90
- if (style.startsWith("#")) {
91
- try {
92
- return hexToAnsi(style);
93
- }
94
- catch {
95
- return ""; // Invalid hex, ignore
96
- }
97
- }
98
- // Check for standard styles/colors
99
- const code = STYLE_CODES[style.toLowerCase()];
100
- if (code) {
101
- return `${ESC}[${code}m`;
102
- }
103
- // Fallback: return as raw string if it looks like ANSI or just text
104
- // Ideally we would validate ANSI here, but for flexibility we return it.
105
- return style;
106
- }
107
- // -----------------
108
- // Line Manipulation Helpers
109
- // -----------------
110
- /**
111
- * Gets the plain text length of a StyledLine.
112
- */
113
- export function getLineLength(line) {
114
- return line.segments.reduce((acc, seg) => acc + seg.text.length, 0);
115
- }
116
- /**
117
- * Computes the maximum width among an array of StyledLines.
118
- */
119
- export function computeMaxWidth(lines) {
120
- return lines.length > 0 ? Math.max(...lines.map(getLineLength)) : 0;
121
- }
122
- /**
123
- * Pads a StyledLine to a target width by adding an empty segment at the end.
124
- */
125
- export function padLine(line, targetWidth, padStyle) {
126
- const currentLength = getLineLength(line);
127
- if (currentLength < targetWidth) {
128
- return {
129
- segments: [...line.segments, { text: " ".repeat(targetWidth - currentLength), style: padStyle }]
130
- };
131
- }
132
- return line;
133
- }
134
- // -----------------
135
- // Printer Class
136
- // -----------------
137
- /**
138
- * Handles rendering StyledLines to the terminal with support for interactive overwriting.
139
- */
140
- export class Printer {
141
- linesRendered = 0;
142
- isInteractive;
143
- constructor(options = {}) {
144
- this.isInteractive = options.interactive ?? false;
145
- }
146
- /**
147
- * Generates the clear sequence to move cursor and clear previously rendered lines.
148
- */
149
- getClearSequence() {
150
- if (!this.isInteractive || this.linesRendered === 0)
151
- return "";
152
- return `${ESC}[1A${ESC}[2K\r`.repeat(this.linesRendered);
153
- }
154
- /**
155
- * Renders an array of StyledLines to the standard output.
156
- */
157
- print(lines) {
158
- let output = this.getClearSequence();
159
- lines.forEach((line) => {
160
- line.segments.forEach((seg) => {
161
- const ansiStyle = resolveStyle(seg.style);
162
- output += `${ansiStyle}${seg.text}${RESET}`;
163
- });
164
- output += "\n";
165
- });
166
- process.stdout.write(output);
167
- this.linesRendered = lines.length;
168
- }
169
- }
170
- // -----------------
171
- // Core Layout & Printing
172
- // -----------------
173
- const defaultPrinter = new Printer();
174
- /**
175
- * Merges two columns of StyledLines into a single layout.
176
- */
177
- export function mergeColumns(leftColumn, rightColumn, leftWidth, separator, defaultStyle) {
178
- const maxLines = Math.max(leftColumn.length, rightColumn.length);
179
- const output = [];
180
- for (let i = 0; i < maxLines; i++) {
181
- const left = padLine(leftColumn[i] || { segments: [] }, leftWidth, defaultStyle);
182
- const right = rightColumn[i] || { segments: [] };
183
- output.push({
184
- segments: [...left.segments, { text: separator, style: defaultStyle }, ...right.segments]
185
- });
186
- }
187
- return output;
188
- }
189
- /**
190
- * Prints two columns of styled content to the console.
191
- */
192
- export function printDualColumn(left, right, options = {}) {
193
- const { leftWidth, separator = " ", printer = defaultPrinter } = options;
194
- const defaultStyle = RESET;
195
- const finalLeftWidth = leftWidth ?? computeMaxWidth(left);
196
- const mergedLines = mergeColumns(left, right, finalLeftWidth, separator, defaultStyle);
197
- printer.print(mergedLines);
198
- }
199
- // -----------------
200
- // Presets
201
- // -----------------
202
- /**
203
- * Returns the classic Dragon ASCII art as StyledLines with a vertical color gradient.
204
- */
205
- export function getDragonLines(startColor = "#EF4444", endColor = "#F59E0B") {
206
- const rawDragon = [
207
- " ^ ^",
208
- " / \\ //\\",
209
- " |\\___/| / \\// .\\",
210
- " /O O \\__ / // | \\ \\",
211
- "/ / \\_/_/ // | \\ \\",
212
- "@___@' \\_// // | \\ \\ ",
213
- " | \\_// // | \\ \\ ",
214
- " | \\/// | \\ \\ ",
215
- " _|_ / ) // | \\ _\\",
216
- " '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-.",
217
- " ,-{ _ `-.|.-~-. .~ `.",
218
- " '/\\ / ~-. _ .-~ .-~^-. \\",
219
- " `. { } / \\ \\",
220
- " .----~-\\. \\-' .~ \\ `. \\^-.",
221
- " ///.----..> c \\ _ -~ `. ^-` ^-_",
222
- " ///-._ _ _ _ _ _ _}^ - - - - ~ ~--, .-~",
223
- " /.-'"
224
- ];
225
- return rawDragon.map((text, i) => {
226
- const factor = rawDragon.length <= 1 ? 0 : i / (rawDragon.length - 1);
227
- const colorStyle = interpolateColor(startColor, endColor, factor);
228
- return { segments: [{ text, style: colorStyle }] };
229
- });
230
- }
231
- // -----------------
232
- // Progress Bar
233
- // -----------------
234
- export * from "./progress";
1
+ // Export Core Types
2
+ export * from "./core/types";
3
+ // Export Core Utilities
4
+ export * from "./core/style";
5
+ export * from "./core/utils";
6
+ // Export Printer Engine
7
+ export * from "./core/printer";
8
+ // Export Layout Helpers
9
+ export * from "./core/layout";
10
+ // Export Components
11
+ export * from "./components/progress";
12
+ export * from "./components/spinner";
13
+ // Export Presets
14
+ export * from "./presets/ascii";
@@ -1,5 +1,5 @@
1
1
  import { expect, test, describe, spyOn, afterEach } from "bun:test";
2
- import { hexToRgb, rgbToAnsi, hexToAnsi, interpolateColor, getLineLength, computeMaxWidth, padLine, Printer, printDualColumn, getDragonLines, mergeColumns, resolveStyle } from "./index";
2
+ import { hexToRgb, rgbToAnsi, hexToAnsi, interpolateColor, getLineLength, computeMaxWidth, padLine, Printer, printColumns, getDragonLines, mergeMultipleColumns, resolveStyle } from "./index";
3
3
  const lineA = { segments: [{ text: "Hello", style: [] }] };
4
4
  const lineB = { segments: [{ text: "World!!", style: [] }] };
5
5
  describe("Color Utilities", () => {
@@ -37,7 +37,6 @@ describe("Style Resolution", () => {
37
37
  });
38
38
  test("resolveStyle handles modifiers", () => {
39
39
  expect(resolveStyle("bold")).toBe("\x1b[1m");
40
- expect(resolveStyle("reset")).toBe("\x1b[0m");
41
40
  });
42
41
  test("resolveStyle handles hex colors", () => {
43
42
  expect(resolveStyle("#FF0000")).toBe("\x1b[38;2;255;0;0m");
@@ -45,6 +44,9 @@ describe("Style Resolution", () => {
45
44
  test("resolveStyle handles arrays of styles", () => {
46
45
  expect(resolveStyle(["bold", "red"])).toBe("\x1b[1m\x1b[31m");
47
46
  });
47
+ test("resolveStyle handles undefined style", () => {
48
+ expect(resolveStyle(undefined)).toBe("");
49
+ });
48
50
  test("resolveStyle passes through raw strings", () => {
49
51
  const raw = "\x1b[31m";
50
52
  expect(resolveStyle(raw)).toBe(raw);
@@ -71,8 +73,8 @@ describe("Line Utilities", () => {
71
73
  expect(getLineLength(ignored)).toBe(7);
72
74
  expect(ignored.segments.length).toBe(1);
73
75
  });
74
- test("mergeColumns handles asymmetric column lengths", () => {
75
- const merged = mergeColumns([lineA], [lineB, lineB], 10, " | ", "");
76
+ test("mergeMultipleColumns handles asymmetric column lengths", () => {
77
+ const merged = mergeMultipleColumns([[lineA], [lineB, lineB]], " | ", "", [10]);
76
78
  expect(merged.length).toBe(2);
77
79
  expect(getLineLength(merged[1])).toBe(10 + 3 + 7);
78
80
  });
@@ -110,8 +112,26 @@ describe("Printer & Layout", () => {
110
112
  const callArg = stdoutSpy.mock.calls[0][0];
111
113
  expect(callArg.startsWith(expectedClear)).toBe(true);
112
114
  });
113
- test("printDualColumn executes correctly", () => {
114
- printDualColumn([lineA], [lineB]);
115
+ test("printColumns executes correctly", () => {
116
+ printColumns([[lineA], [lineB]]);
117
+ expect(stdoutSpy).toHaveBeenCalled();
118
+ const output = stdoutSpy.mock.calls[0][0];
119
+ expect(output).toContain("Hello");
120
+ expect(output).toContain("World!!");
121
+ });
122
+ test("printColumns handles undefined style in segments", () => {
123
+ const lineNoStyle = { segments: [{ text: "NoStyle" }] };
124
+ printColumns([[lineNoStyle]]);
125
+ expect(stdoutSpy).toHaveBeenCalled();
126
+ const output = stdoutSpy.mock.calls[0][0];
127
+ expect(output).toContain("NoStyle");
128
+ });
129
+ test("printColumns handles empty columns", () => {
130
+ printColumns([]);
131
+ expect(stdoutSpy).toHaveBeenCalled(); // Should clear lines if interactive, or do nothing.
132
+ });
133
+ test("printColumns handles 3 columns", () => {
134
+ printColumns([[lineA], [lineA], [lineB]]);
115
135
  expect(stdoutSpy).toHaveBeenCalled();
116
136
  const output = stdoutSpy.mock.calls[0][0];
117
137
  expect(output).toContain("Hello");
@@ -0,0 +1,5 @@
1
+ import { StyledLine } from "../core/types";
2
+ /**
3
+ * Returns the classic Dragon ASCII art as StyledLines with a vertical color gradient.
4
+ */
5
+ export declare function getDragonLines(startColor?: string, endColor?: string): StyledLine[];
@@ -0,0 +1,33 @@
1
+ import { interpolateColor } from "../core/style";
2
+ // -----------------
3
+ // Presets
4
+ // -----------------
5
+ /**
6
+ * Returns the classic Dragon ASCII art as StyledLines with a vertical color gradient.
7
+ */
8
+ export function getDragonLines(startColor = "#EF4444", endColor = "#F59E0B") {
9
+ const rawDragon = [
10
+ " ^ ^",
11
+ " / \\ //\\",
12
+ " |\\___/| / \\// .\\",
13
+ " /O O \\__ / // | \\ \\",
14
+ "/ / \\_/_/ // | \\ \\",
15
+ "@___@' \\_// // | \\ \\ ",
16
+ " | \\_// // | \\ \\ ",
17
+ " | \\/// | \\ \\ ",
18
+ " _|_ / ) // | \\ _\\",
19
+ " '/,_ _ _/ ( ; -. | _ _\\.-~ .-~~~^-.",
20
+ " ,-{ _ `-.|.-~-. .~ `.",
21
+ " '/\\ / ~-. _ .-~ .-~^-. \\",
22
+ " `. { } / \\ \\",
23
+ " .----~-\\. \\-' .~ \\ `. \\^-.",
24
+ " ///.----..> c \\ _ -~ `. ^-` ^-_",
25
+ " ///-._ _ _ _ _ _ _}^ - - - - ~ ~--, .-~",
26
+ " /.-'"
27
+ ];
28
+ return rawDragon.map((text, i) => {
29
+ const factor = rawDragon.length <= 1 ? 0 : i / (rawDragon.length - 1);
30
+ const colorStyle = interpolateColor(startColor, endColor, factor);
31
+ return { segments: [{ text, style: colorStyle }] };
32
+ });
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { expect, test, describe } from "bun:test";
2
+ import { getDragonLines } from "./ascii";
3
+ describe("Presets", () => {
4
+ test("getDragonLines returns valid array", () => {
5
+ const lines = getDragonLines();
6
+ expect(lines.length).toBeGreaterThan(0);
7
+ const firstSegmentStyle = lines[0].segments[0].style;
8
+ expect(firstSegmentStyle.startsWith("#")).toBe(true);
9
+ });
10
+ });
@@ -0,0 +1,36 @@
1
+ export type SpinnerFrames = string[];
2
+ export interface SpinnerOptions {
3
+ /**
4
+ * The array of frames to cycle through.
5
+ */
6
+ frames: SpinnerFrames;
7
+ /**
8
+ * The interval in milliseconds between frames.
9
+ * Defaults to 80ms.
10
+ */
11
+ interval?: number;
12
+ }
13
+ /**
14
+ * Common spinner frame presets.
15
+ */
16
+ export declare const SPINNERS: {
17
+ dots: string[];
18
+ lines: string[];
19
+ arrows: string[];
20
+ circle: string[];
21
+ square: string[];
22
+ };
23
+ /**
24
+ * A stateful spinner that calculates the current frame based on elapsed time.
25
+ * Designed to be used within a render loop (e.g. Printer.print loop).
26
+ */
27
+ export declare class Spinner {
28
+ private frames;
29
+ private interval;
30
+ private startTime;
31
+ constructor(options: SpinnerOptions);
32
+ /**
33
+ * Returns the current frame based on the elapsed time.
34
+ */
35
+ getFrame(): string;
36
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Common spinner frame presets.
3
+ */
4
+ export const SPINNERS = {
5
+ dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
6
+ lines: ["-", "\\", "|", "/"],
7
+ arrows: ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
8
+ circle: ["◐", "◓", "◑", "◒"],
9
+ square: ["▖", "▘", "▝", "▗"]
10
+ };
11
+ /**
12
+ * A stateful spinner that calculates the current frame based on elapsed time.
13
+ * Designed to be used within a render loop (e.g. Printer.print loop).
14
+ */
15
+ export class Spinner {
16
+ frames;
17
+ interval;
18
+ startTime;
19
+ constructor(options) {
20
+ this.frames = options.frames;
21
+ this.interval = options.interval ?? 80;
22
+ this.startTime = Date.now();
23
+ }
24
+ /**
25
+ * Returns the current frame based on the elapsed time.
26
+ */
27
+ getFrame() {
28
+ const now = Date.now();
29
+ const elapsed = now - this.startTime;
30
+ const index = Math.floor(elapsed / this.interval) % this.frames.length;
31
+ return this.frames[index];
32
+ }
33
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { expect, test, describe, spyOn, afterEach, beforeEach } from "bun:test";
2
+ import { Spinner, SPINNERS } from "./spinner";
3
+ describe("Spinner Class", () => {
4
+ let now = 1000;
5
+ // Mock Date.now() to control time
6
+ const dateSpy = spyOn(Date, "now").mockImplementation(() => now);
7
+ beforeEach(() => {
8
+ now = 1000;
9
+ dateSpy.mockClear();
10
+ dateSpy.mockImplementation(() => now);
11
+ });
12
+ afterEach(() => {
13
+ dateSpy.mockClear();
14
+ });
15
+ test("initializes with correct defaults", () => {
16
+ const spinner = new Spinner({ frames: ["a", "b"] });
17
+ expect(spinner.getFrame()).toBe("a");
18
+ });
19
+ test("advances frames over time", () => {
20
+ const spinner = new Spinner({ frames: ["a", "b", "c"], interval: 100 });
21
+ // t=0 (1000)
22
+ expect(spinner.getFrame()).toBe("a");
23
+ // t=50 (1050) -> still frame 0
24
+ now = 1050;
25
+ expect(spinner.getFrame()).toBe("a");
26
+ // t=100 (1100) -> frame 1
27
+ now = 1100;
28
+ expect(spinner.getFrame()).toBe("b");
29
+ // t=200 (1200) -> frame 2
30
+ now = 1200;
31
+ expect(spinner.getFrame()).toBe("c");
32
+ // t=300 (1300) -> frame 0 (loop)
33
+ now = 1300;
34
+ expect(spinner.getFrame()).toBe("a");
35
+ });
36
+ test("uses custom interval", () => {
37
+ const spinner = new Spinner({ frames: ["a", "b"], interval: 50 });
38
+ // t=0
39
+ expect(spinner.getFrame()).toBe("a");
40
+ // t=50 -> frame 1
41
+ now = 1050;
42
+ expect(spinner.getFrame()).toBe("b");
43
+ });
44
+ });
45
+ describe("Spinner Presets", () => {
46
+ test("dots preset exists and has frames", () => {
47
+ expect(SPINNERS.dots).toBeDefined();
48
+ expect(SPINNERS.dots.length).toBeGreaterThan(0);
49
+ });
50
+ test("lines preset exists", () => {
51
+ expect(SPINNERS.lines).toBeDefined();
52
+ });
53
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heinrichb/console-toolkit",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "A versatile TypeScript utility library for enhanced console logging, formatting, and layout management.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,12 +33,14 @@
33
33
  "useTabs": true
34
34
  },
35
35
  "devDependencies": {
36
- "@eslint/js": "^9.0.0",
37
- "@types/bun": "latest",
38
- "eslint": "^9.0.0",
36
+ "@eslint/js": "^10.0.1",
37
+ "@types/bun": "^1.3.9",
38
+ "eslint": "^10.0.2",
39
39
  "eslint-config-prettier": "^10.1.8",
40
- "prettier": "^3.5.3",
41
- "typescript": "^5",
40
+ "globals": "^17.3.0",
41
+ "jiti": "^2.6.1",
42
+ "prettier": "^3.8.1",
43
+ "typescript": "^5.9.3",
42
44
  "typescript-eslint": "^8.56.1"
43
45
  }
44
46
  }