saterminal 0.2.0 → 0.3.1

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/flake.lock DELETED
@@ -1,59 +0,0 @@
1
- {
2
- "nodes": {
3
- "flake-utils": {
4
- "inputs": {
5
- "systems": "systems"
6
- },
7
- "locked": {
8
- "lastModified": 1731533236,
9
- "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10
- "owner": "numtide",
11
- "repo": "flake-utils",
12
- "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13
- "type": "github"
14
- },
15
- "original": {
16
- "owner": "numtide",
17
- "repo": "flake-utils",
18
- "type": "github"
19
- }
20
- },
21
- "nixpkgs": {
22
- "locked": {
23
- "lastModified": 1781577229,
24
- "narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
25
- "rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
26
- "revCount": 1017464,
27
- "type": "tarball",
28
- "url": "https://api.flakehub.com/f/pinned/DeterminateSystems/nixpkgs-weekly/0.1.1017464%2Brev-567a49d1913ce81ac6e9582e3553dd90a955875f/019f1749-32d6-7283-9e9a-861bd8478e6a/source.tar.gz"
29
- },
30
- "original": {
31
- "type": "tarball",
32
- "url": "https://flakehub.com/f/DeterminateSystems/nixpkgs-weekly/0.tar.gz"
33
- }
34
- },
35
- "root": {
36
- "inputs": {
37
- "flake-utils": "flake-utils",
38
- "nixpkgs": "nixpkgs"
39
- }
40
- },
41
- "systems": {
42
- "locked": {
43
- "lastModified": 1681028828,
44
- "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
45
- "owner": "nix-systems",
46
- "repo": "default",
47
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
48
- "type": "github"
49
- },
50
- "original": {
51
- "owner": "nix-systems",
52
- "repo": "default",
53
- "type": "github"
54
- }
55
- }
56
- },
57
- "root": "root",
58
- "version": 7
59
- }
package/flake.nix DELETED
@@ -1,21 +0,0 @@
1
- {
2
- inputs = {
3
- nixpkgs.url = "https://flakehub.com/f/DeterminateSystems/nixpkgs-weekly/0.tar.gz";
4
- flake-utils.url = "github:numtide/flake-utils";
5
- };
6
-
7
- outputs = { nixpkgs, flake-utils, ... }:
8
- flake-utils.lib.eachDefaultSystem (
9
- system:
10
- let
11
- pkgs = import nixpkgs { inherit system; };
12
- in
13
- {
14
- devShells.default = pkgs.mkShell {
15
- packages = with pkgs; [
16
- bun
17
- ];
18
- };
19
- }
20
- );
21
- }
@@ -1,110 +0,0 @@
1
- import { wrapText } from "../text.ts";
2
- import type { TextSegment } from "../text.ts";
3
- import { term, gutter, terminalSize } from "./kit.ts";
4
-
5
- export { term, gutter, terminalSize, contentBounds } from "./kit.ts";
6
-
7
- export function line(y: number, value: string): void {
8
- term.moveTo(1, y)(value.slice(0, term.width - 1));
9
- }
10
-
11
- export function lineAt(x: number, y: number, width: number, value: string): void {
12
- term.moveTo(x, y)(value.slice(0, width));
13
- }
14
-
15
- export function lineAtColor(
16
- x: number,
17
- y: number,
18
- width: number,
19
- value: string,
20
- color: "cyan" | "green" | "red" | "yellow",
21
- bold = false,
22
- ): void {
23
- const output = value.slice(0, width);
24
- if (bold) {
25
- term.moveTo(x, y).bold[color](output);
26
- } else {
27
- term.moveTo(x, y)[color](output);
28
- }
29
- }
30
-
31
- export function paneLayout(): {
32
- leftX: number;
33
- leftWidth: number;
34
- rightX: number;
35
- rightWidth: number;
36
- } {
37
- const { width } = terminalSize();
38
- const usable = Math.max(40, width - gutter);
39
- const leftWidth = Math.max(20, Math.floor(usable / 2));
40
- const rightX = leftWidth + gutter + 1;
41
- const rightWidth = Math.max(20, width - rightX);
42
-
43
- return {
44
- leftX: 1,
45
- leftWidth,
46
- rightX,
47
- rightWidth,
48
- };
49
- }
50
-
51
- export function printWrapped(value: string | undefined, y: number, maxY: number, bold = false): number {
52
- return printWrappedAt(value, 1, y, terminalSize().width - 4, maxY, bold);
53
- }
54
-
55
- export function printWrappedAt(
56
- value: string | undefined,
57
- x: number,
58
- y: number,
59
- width: number,
60
- maxY: number,
61
- bold = false,
62
- ): number {
63
- for (const row of wrapText(value ?? "", width)) {
64
- if (y > maxY) {
65
- lineAt(x, y, width, "...");
66
- return y + 1;
67
- }
68
- if (bold) {
69
- term.moveTo(x, y++).bold(row.slice(0, width));
70
- } else {
71
- lineAt(x, y++, width, row);
72
- }
73
- }
74
-
75
- return y;
76
- }
77
-
78
- export function renderStyledLine(
79
- x: number,
80
- y: number,
81
- width: number,
82
- segments: TextSegment[],
83
- forceBold = false,
84
- ): void {
85
- let col = 0;
86
-
87
- for (const segment of segments) {
88
- if (col >= width) {
89
- break;
90
- }
91
-
92
- const text = segment.text.slice(0, width - col);
93
- if (!text) {
94
- continue;
95
- }
96
-
97
- const bold = forceBold || segment.style.bold;
98
- const underline = segment.style.underline;
99
- let output = term.moveTo(x + col, y);
100
- if (bold && underline) {
101
- output = output.bold.underline;
102
- } else if (bold) {
103
- output = output.bold;
104
- } else if (underline) {
105
- output = output.underline;
106
- }
107
- output(text);
108
- col += text.length;
109
- }
110
- }
package/test/api.test.ts DELETED
@@ -1,80 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { fetchQuestionBank } from "../src/api.ts";
3
-
4
- describe("api", () => {
5
- test("passes excluded short question ids to the question bank endpoint", async () => {
6
- const originalFetch = globalThis.fetch;
7
- let requested = "";
8
- globalThis.fetch = ((input: RequestInfo | URL) => {
9
- requested = String(input);
10
- return Promise.resolve(
11
- new Response(JSON.stringify({ success: true, data: [] }), {
12
- status: 200,
13
- headers: { "content-type": "application/json" },
14
- }),
15
- );
16
- }) as typeof fetch;
17
-
18
- try {
19
- await fetchQuestionBank(["a", "b"]);
20
- expect(requested).toContain("assessment=SAT");
21
- expect(requested).toContain("excludeIds=a%2Cb");
22
- expect(requested).toContain("difficulties=M%2CH");
23
- } finally {
24
- globalThis.fetch = originalFetch;
25
- }
26
- });
27
-
28
- test("passes focus filters to the question bank endpoint", async () => {
29
- const originalFetch = globalThis.fetch;
30
- let requested = "";
31
- globalThis.fetch = ((input: RequestInfo | URL) => {
32
- requested = String(input);
33
- return Promise.resolve(
34
- new Response(JSON.stringify({ success: true, data: [] }), {
35
- status: 200,
36
- headers: { "content-type": "application/json" },
37
- }),
38
- );
39
- }) as typeof fetch;
40
-
41
- try {
42
- await fetchQuestionBank([], {
43
- difficulties: ["H"],
44
- domains: ["SEC"],
45
- skills: ["BOU", "FSS"],
46
- });
47
- expect(requested).toContain("difficulties=H");
48
- expect(requested).toContain("domains=SEC");
49
- expect(requested).toContain("skills=BOU%2CFSS");
50
- } finally {
51
- globalThis.fetch = originalFetch;
52
- }
53
- });
54
-
55
- test("derives domain filters from selected skill filters", async () => {
56
- const originalFetch = globalThis.fetch;
57
- let requested = "";
58
- globalThis.fetch = ((input: RequestInfo | URL) => {
59
- requested = String(input);
60
- return Promise.resolve(
61
- new Response(JSON.stringify({ success: true, data: [] }), {
62
- status: 200,
63
- headers: { "content-type": "application/json" },
64
- }),
65
- );
66
- }) as typeof fetch;
67
-
68
- try {
69
- await fetchQuestionBank([], {
70
- difficulties: ["H"],
71
- domains: ["INI"],
72
- skills: ["WIC"],
73
- });
74
- expect(requested).toContain("domains=CAS");
75
- expect(requested).toContain("skills=WIC");
76
- } finally {
77
- globalThis.fetch = originalFetch;
78
- }
79
- });
80
- });
@@ -1,55 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { focusRows, toggleFocusRow } from "../src/focus.ts";
3
- import { focusGrid, moveFocusGridPosition, toggleFocusGridRow } from "../src/tui/focus-grid.ts";
4
- import type { Focus } from "../src/types.ts";
5
-
6
- describe("focus", () => {
7
- test("renders skills as nested domain children", () => {
8
- const rows = focusRows({ difficulties: ["H"], domains: ["CAS"], skills: ["WIC"] });
9
- const cas = rows.find((row) => row.kind === "option" && row.value === "CAS");
10
- const wic = rows.find((row) => row.kind === "option" && row.value === "WIC");
11
-
12
- expect(cas).toMatchObject({ kind: "option", checked: true, partial: true, depth: 0 });
13
- expect(wic).toMatchObject({ kind: "option", checked: true, depth: 1 });
14
- });
15
-
16
- test("domain toggles operate on their child skills", () => {
17
- const focus: Focus = { difficulties: ["H"], domains: ["CAS"], skills: ["WIC"] };
18
- const cas = focusRows(focus).find((row) => row.kind === "option" && row.value === "CAS");
19
-
20
- expect(toggleFocusRow(focus, cas)).toEqual({
21
- difficulties: ["H"],
22
- domains: ["CAS"],
23
- skills: ["WIC", "TSP", "CTC"],
24
- });
25
- });
26
-
27
- test("grid navigation moves predictably across columns and rows", () => {
28
- const focus: Focus = { difficulties: ["H"], domains: ["CAS"], skills: ["WIC"] };
29
- const columns = focusGrid(focus);
30
-
31
- expect(moveFocusGridPosition(columns, { column: 0, row: 1 }, "down")).toEqual({ column: 0, row: 2 });
32
- expect(moveFocusGridPosition(columns, { column: 0, row: 2 }, "down")).toEqual({ column: 0, row: 2 });
33
- expect(moveFocusGridPosition(columns, { column: 0, row: 2 }, "next")).toEqual({ column: 1, row: 2 });
34
- expect(moveFocusGridPosition(columns, { column: 0, row: 2 }, "previous")).toEqual({ column: 4, row: 2 });
35
- });
36
-
37
- test("grid navigation clamps row when moving into shorter columns", () => {
38
- const focus: Focus = { difficulties: ["H"], domains: ["INI"], skills: ["CID", "INF", "COE"] };
39
- const columns = focusGrid(focus);
40
-
41
- expect(moveFocusGridPosition(columns, { column: 1, row: 3 }, "next")).toEqual({ column: 2, row: 3 });
42
- expect(moveFocusGridPosition(columns, { column: 2, row: 3 }, "next")).toEqual({ column: 3, row: 2 });
43
- });
44
-
45
- test("grid toggles use focus constraints", () => {
46
- const focus: Focus = { difficulties: ["H"], domains: ["CAS"], skills: ["WIC"] };
47
-
48
- expect(toggleFocusGridRow(focus, { column: 0, row: 2 })).toBe(focus);
49
- expect(toggleFocusGridRow(focus, { column: 2, row: 0 })).toEqual({
50
- difficulties: ["H"],
51
- domains: ["CAS"],
52
- skills: ["WIC", "TSP", "CTC"],
53
- });
54
- });
55
- });
@@ -1,167 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { defaultFocus, normalizeFocus } from "../src/focus.ts";
6
- import {
7
- buildSummaryRows,
8
- displayStateDir,
9
- loadAttempts,
10
- loadFocus,
11
- nextOutcome,
12
- recordAttempt,
13
- resolveStateDir,
14
- saveAttempts,
15
- saveFocus,
16
- stateDirExists,
17
- } from "../src/state.ts";
18
-
19
- describe("state", () => {
20
- test("resolves state dir under the user home directory", () => {
21
- expect(resolveStateDir("/home/user")).toBe("/home/user/.saterminal/userlocal");
22
- });
23
-
24
- test("displays state dir with a tilde prefix", () => {
25
- expect(displayStateDir("/home/user/.saterminal/userlocal", "/home/user")).toBe("~/.saterminal/userlocal");
26
- expect(displayStateDir("/home/user", "/home/user")).toBe("~");
27
- });
28
-
29
- test("detects whether the state directory exists", async () => {
30
- const dir = await mkdtemp(join(tmpdir(), "saterminal-"));
31
-
32
- try {
33
- expect(await stateDirExists(dir)).toBe(true);
34
- expect(await stateDirExists(join(dir, "missing"))).toBe(false);
35
- } finally {
36
- await rm(dir, { recursive: true, force: true });
37
- }
38
- });
39
-
40
- test("creates and reads a compact attempts csv", async () => {
41
- const dir = await mkdtemp(join(tmpdir(), "saterminal-"));
42
- const path = join(dir, "attempts.csv");
43
-
44
- try {
45
- const attempts = await loadAttempts(path);
46
- recordAttempt(attempts, "abc12345", false, 42, new Date("2026-01-01T00:00:00.000Z"));
47
- await saveAttempts(attempts, path);
48
-
49
- const raw = await readFile(path, "utf8");
50
- expect(raw).toBe(
51
- "question_id,outcome,updated_at,elapsed_seconds\nabc12345,incorrect,2026-01-01T00:00:00.000Z,42\n",
52
- );
53
- expect(await loadAttempts(path)).toEqual(attempts);
54
- } finally {
55
- await rm(dir, { recursive: true, force: true });
56
- }
57
- });
58
-
59
- test("uses corrected for a later right answer after a miss", () => {
60
- const attempts = new Map();
61
- recordAttempt(attempts, "abc12345", false, 12, new Date("2026-01-01T00:00:00.000Z"));
62
- recordAttempt(attempts, "abc12345", true, 10, new Date("2026-01-02T00:00:00.000Z"));
63
-
64
- expect(attempts.get("abc12345")?.outcome).toBe("corrected");
65
- });
66
-
67
- test("loads escaped csv fields", async () => {
68
- const dir = await mkdtemp(join(tmpdir(), "saterminal-"));
69
- const path = join(dir, "attempts.csv");
70
-
71
- try {
72
- await writeFile(
73
- path,
74
- "question_id,outcome,updated_at,elapsed_seconds\n\"abc,123\",correct,\"2026-01-01T00:00:00.000Z\",12\n",
75
- "utf8",
76
- );
77
-
78
- expect(await loadAttempts(path)).toEqual(new Map([
79
- ["abc,123", {
80
- question_id: "abc,123",
81
- outcome: "correct",
82
- updated_at: "2026-01-01T00:00:00.000Z",
83
- elapsed_seconds: 12,
84
- }],
85
- ]));
86
- } finally {
87
- await rm(dir, { recursive: true, force: true });
88
- }
89
- });
90
-
91
- test("does not downgrade mastered outcomes", () => {
92
- expect(nextOutcome("correct", false)).toBe("correct");
93
- expect(nextOutcome("corrected", false)).toBe("corrected");
94
- });
95
-
96
- test("builds summary rows from attempts", () => {
97
- const attempts = new Map();
98
- recordAttempt(attempts, "a", true, 20, new Date("2026-01-01T00:00:00.000Z"));
99
- recordAttempt(attempts, "b", false, 10, new Date("2026-01-01T00:00:00.000Z"));
100
- recordAttempt(attempts, "b", true, 40, new Date("2026-01-02T00:00:00.000Z"));
101
-
102
- const rows = buildSummaryRows(attempts, new Date("2026-01-03T00:00:00.000Z"));
103
- expect(Object.fromEntries(rows.map((row) => [row.metric, row.value]))).toEqual({
104
- answered: "2",
105
- correct: "1",
106
- incorrect: "0",
107
- corrected: "1",
108
- accuracy: "1.00",
109
- avg_seconds: "30.0",
110
- });
111
- });
112
-
113
- test("normalizes invalid focus selections to valid defaults", () => {
114
- expect(normalizeFocus({ difficulties: [], domains: ["NOPE"], skills: ["CID"] })).toEqual({
115
- difficulties: defaultFocus.difficulties,
116
- domains: ["INI"],
117
- skills: ["CID"],
118
- });
119
- });
120
-
121
- test("derives focus domains from selected skills", () => {
122
- expect(normalizeFocus({ difficulties: ["H"], domains: ["INI"], skills: ["WIC"] })).toEqual({
123
- difficulties: ["H"],
124
- domains: ["CAS"],
125
- skills: ["WIC"],
126
- });
127
- });
128
-
129
- test("creates default focus when file is missing", async () => {
130
- const dir = await mkdtemp(join(tmpdir(), "saterminal-"));
131
- const path = join(dir, "focus.json");
132
-
133
- try {
134
- expect(await loadFocus(path)).toEqual(defaultFocus);
135
- expect(await readFile(path, "utf8")).toContain("\"difficulties\"");
136
- } finally {
137
- await rm(dir, { recursive: true, force: true });
138
- }
139
- });
140
-
141
- test("saves and loads focus json", async () => {
142
- const dir = await mkdtemp(join(tmpdir(), "saterminal-"));
143
- const path = join(dir, "focus.json");
144
-
145
- try {
146
- await saveFocus({ difficulties: ["H"], domains: ["SEC"], skills: ["BOU", "FSS"] }, path);
147
- expect(await loadFocus(path)).toEqual({ difficulties: ["H"], domains: ["SEC"], skills: ["BOU", "FSS"] });
148
-
149
- await writeFile(path, "{\"difficulties\":[],\"domains\":[],\"skills\":[]}", "utf8");
150
- expect(await loadFocus(path)).toEqual(defaultFocus);
151
- } finally {
152
- await rm(dir, { recursive: true, force: true });
153
- }
154
- });
155
-
156
- test("throws when focus json is invalid", async () => {
157
- const dir = await mkdtemp(join(tmpdir(), "saterminal-"));
158
- const path = join(dir, "focus.json");
159
-
160
- try {
161
- await writeFile(path, "{not json", "utf8");
162
- await expect(loadFocus(path)).rejects.toThrow(/Invalid focus file/);
163
- } finally {
164
- await rm(dir, { recursive: true, force: true });
165
- }
166
- });
167
- });
package/test/text.test.ts DELETED
@@ -1,72 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { hasHtmlTable, htmlToText, parseHtmlSegments, wrapSegments, wrapText } from "../src/text.ts";
3
-
4
- describe("text", () => {
5
- test("converts the API html subset into terminal text", () => {
6
- expect(htmlToText("<p><strong>Text 1</strong></p><p>Alice&rsquo;s claim&mdash;briefly.</p>")).toBe(
7
- "Text 1\nAlice's claim-briefly.",
8
- );
9
- });
10
-
11
- test("uses media labels instead of leaking svg internals", () => {
12
- expect(
13
- htmlToText(
14
- '<figure><svg aria-label="Line graph titled Exports"><text>90807060</text></svg></figure><p>Read the graph.</p>',
15
- ),
16
- ).toBe("[Graph: Line graph titled Exports]\nRead the graph.");
17
- });
18
-
19
- test("keeps image alt text visible", () => {
20
- expect(htmlToText('<p><img alt="Triangle ABC" src="/triangle.png"></p>')).toBe("[Image: Triangle ABC]");
21
- });
22
-
23
- test("detects html tables", () => {
24
- expect(hasHtmlTable("<p>Text</p>", "<table><tr><td>1</td></tr></table>")).toBe(true);
25
- expect(hasHtmlTable("<p>Text</p>")).toBe(false);
26
- });
27
-
28
- test("normalizes SAT blank markers", () => {
29
- expect(htmlToText("<p>The answer is ______blank because of the data.</p>")).toBe(
30
- "The answer is _______ because of the data.",
31
- );
32
- });
33
-
34
- test("wraps text without dropping words", () => {
35
- expect(wrapText("one two three four", 8)).toEqual(["one two", "three", "four"]);
36
- });
37
-
38
- test("preserves underline segments from u tags", () => {
39
- const segments = parseHtmlSegments("<p>Before <u>underlined claim</u> after.</p>");
40
- expect(segments.some((segment) => segment.style.underline && segment.text.includes("underlined claim"))).toBe(true);
41
- expect(htmlToText("<p>Before <u>underlined claim</u> after.</p>")).toBe("Before underlined claim after.");
42
- });
43
-
44
- test("normalizes API blank spans in parsed segments", () => {
45
- const html =
46
- '<p>The answer is <span aria-hidden="true">______</span><span class="sr-only">blank</span> because of the data.</p>';
47
- const text = parseHtmlSegments(html)
48
- .map((segment) => segment.text)
49
- .join("")
50
- .trim();
51
- expect(text).toBe("The answer is _______ because of the data.");
52
- });
53
-
54
- test("wraps styled segments without losing underline spans", () => {
55
- const segments = parseHtmlSegments("<u>This insightful depiction of a preteen girl</u>");
56
- const lines = wrapSegments(segments, 20);
57
- expect(lines.flat().some((segment) => segment.style.underline)).toBe(true);
58
- });
59
-
60
- test("collapses spacer paragraphs and decodes accented entities in dual-text stimuli", () => {
61
- const html = `<p><strong><span role="heading">Text 1</span></strong></p>
62
- <p>the object that struck the Yucat&aacute;n Peninsula</p>
63
- <p>&nbsp;</p>
64
- <p><strong><span role="heading">Text 2</span></strong></p>
65
- <p>Artemieva argues that an asteroid is plausible.</p>`;
66
- const lines = wrapSegments(parseHtmlSegments(html), 40).map((line) => line.map((segment) => segment.text).join(""));
67
- const blankLines = lines.filter((line) => !line.trim()).length;
68
- expect(blankLines).toBeLessThanOrEqual(2);
69
- expect(lines.join("\n")).toContain("Yucatán");
70
- expect(lines.join("\n")).not.toContain("&aacute;");
71
- });
72
- });
@@ -1,39 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- clampScroll,
4
- ensureRangeVisible,
5
- ensureRowVisible,
6
- maxScroll,
7
- scrollBy,
8
- scrollPage,
9
- scrollToEdge,
10
- } from "../src/tui/viewport.ts";
11
-
12
- describe("viewport", () => {
13
- test("clamps scroll to the available content", () => {
14
- expect(maxScroll({ scroll: 0, height: 5, contentRows: 12 })).toBe(7);
15
- expect(clampScroll({ scroll: -3, height: 5, contentRows: 12 })).toBe(0);
16
- expect(clampScroll({ scroll: 20, height: 5, contentRows: 12 })).toBe(7);
17
- expect(clampScroll({ scroll: 20, height: 5, contentRows: 3 })).toBe(0);
18
- });
19
-
20
- test("scrolls by lines and pages", () => {
21
- const viewport = { scroll: 4, height: 5, contentRows: 20 };
22
-
23
- expect(scrollBy(viewport, 2)).toBe(6);
24
- expect(scrollBy(viewport, -10)).toBe(0);
25
- expect(scrollPage(viewport, 1)).toBe(8);
26
- expect(scrollPage(viewport, -1)).toBe(0);
27
- expect(scrollToEdge(viewport, "bottom")).toBe(15);
28
- });
29
-
30
- test("keeps a selected row or wrapped range visible", () => {
31
- const viewport = { scroll: 4, height: 5, contentRows: 20 };
32
-
33
- expect(ensureRowVisible(viewport, 3)).toBe(3);
34
- expect(ensureRowVisible(viewport, 8)).toBe(4);
35
- expect(ensureRowVisible(viewport, 12)).toBe(8);
36
- expect(ensureRangeVisible(viewport, 11, 13)).toBe(9);
37
- expect(ensureRangeVisible(viewport, 5, 7)).toBe(4);
38
- });
39
- });
package/tsconfig.json DELETED
@@ -1,13 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "strict": true,
7
- "skipLibCheck": true,
8
- "types": ["bun"],
9
- "allowImportingTsExtensions": true,
10
- "noEmit": true
11
- },
12
- "include": ["src/**/*.ts", "src/**/*.d.ts", "test/**/*.ts"]
13
- }