@abelfubu/dv 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/dist/ansi-html.d.ts +42 -0
  2. package/dist/ansi-html.d.ts.map +1 -0
  3. package/dist/ansi-html.js +327 -0
  4. package/dist/ansi-output.d.ts +22 -0
  5. package/dist/ansi-output.d.ts.map +1 -0
  6. package/dist/ansi-output.js +154 -0
  7. package/dist/balance-delimiters.d.ts +25 -0
  8. package/dist/balance-delimiters.d.ts.map +1 -0
  9. package/dist/balance-delimiters.js +539 -0
  10. package/dist/balance-delimiters.test.d.ts +2 -0
  11. package/dist/balance-delimiters.test.d.ts.map +1 -0
  12. package/dist/balance-delimiters.test.js +1029 -0
  13. package/dist/cli-copy-notification.test.d.ts +2 -0
  14. package/dist/cli-copy-notification.test.d.ts.map +1 -0
  15. package/dist/cli-copy-notification.test.js +80 -0
  16. package/dist/cli-scroll.test.d.ts +2 -0
  17. package/dist/cli-scroll.test.d.ts.map +1 -0
  18. package/dist/cli-scroll.test.js +283 -0
  19. package/dist/cli.d.ts +9 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +976 -0
  22. package/dist/clipboard.d.ts +16 -0
  23. package/dist/clipboard.d.ts.map +1 -0
  24. package/dist/clipboard.js +128 -0
  25. package/dist/components/diff-view.d.ts +32 -0
  26. package/dist/components/diff-view.d.ts.map +1 -0
  27. package/dist/components/diff-view.js +123 -0
  28. package/dist/components/diff-view.test.d.ts +5 -0
  29. package/dist/components/diff-view.test.d.ts.map +1 -0
  30. package/dist/components/diff-view.test.js +312 -0
  31. package/dist/components/directory-tree-view.d.ts +33 -0
  32. package/dist/components/directory-tree-view.d.ts.map +1 -0
  33. package/dist/components/directory-tree-view.js +262 -0
  34. package/dist/components/index.d.ts +4 -0
  35. package/dist/components/index.d.ts.map +1 -0
  36. package/dist/components/index.js +5 -0
  37. package/dist/components/toast.d.ts +21 -0
  38. package/dist/components/toast.d.ts.map +1 -0
  39. package/dist/components/toast.js +47 -0
  40. package/dist/diff-cursor-utils.d.ts +20 -0
  41. package/dist/diff-cursor-utils.d.ts.map +1 -0
  42. package/dist/diff-cursor-utils.js +105 -0
  43. package/dist/diff-cursor-utils.test.d.ts +2 -0
  44. package/dist/diff-cursor-utils.test.d.ts.map +1 -0
  45. package/dist/diff-cursor-utils.test.js +40 -0
  46. package/dist/diff-surface-copy.d.ts +23 -0
  47. package/dist/diff-surface-copy.d.ts.map +1 -0
  48. package/dist/diff-surface-copy.js +64 -0
  49. package/dist/diff-surface-copy.test.d.ts +5 -0
  50. package/dist/diff-surface-copy.test.d.ts.map +1 -0
  51. package/dist/diff-surface-copy.test.js +142 -0
  52. package/dist/diff-utils.d.ts +196 -0
  53. package/dist/diff-utils.d.ts.map +1 -0
  54. package/dist/diff-utils.js +682 -0
  55. package/dist/diff-utils.test.d.ts +2 -0
  56. package/dist/diff-utils.test.d.ts.map +1 -0
  57. package/dist/diff-utils.test.js +727 -0
  58. package/dist/directory-tree.d.ts +72 -0
  59. package/dist/directory-tree.d.ts.map +1 -0
  60. package/dist/directory-tree.js +161 -0
  61. package/dist/directory-tree.test.d.ts +2 -0
  62. package/dist/directory-tree.test.d.ts.map +1 -0
  63. package/dist/directory-tree.test.js +383 -0
  64. package/dist/dropdown.d.ts +26 -0
  65. package/dist/dropdown.d.ts.map +1 -0
  66. package/dist/dropdown.js +172 -0
  67. package/dist/dropdown.test.d.ts +2 -0
  68. package/dist/dropdown.test.d.ts.map +1 -0
  69. package/dist/dropdown.test.js +106 -0
  70. package/dist/filter-submodule.e2e.test.d.ts +2 -0
  71. package/dist/filter-submodule.e2e.test.d.ts.map +1 -0
  72. package/dist/filter-submodule.e2e.test.js +109 -0
  73. package/dist/hooks/use-copy-selection.d.ts +29 -0
  74. package/dist/hooks/use-copy-selection.d.ts.map +1 -0
  75. package/dist/hooks/use-copy-selection.js +46 -0
  76. package/dist/kv-codec.d.ts +16 -0
  77. package/dist/kv-codec.d.ts.map +1 -0
  78. package/dist/kv-codec.js +36 -0
  79. package/dist/license.d.ts +14 -0
  80. package/dist/license.d.ts.map +1 -0
  81. package/dist/license.js +63 -0
  82. package/dist/logger.d.ts +9 -0
  83. package/dist/logger.d.ts.map +1 -0
  84. package/dist/logger.js +78 -0
  85. package/dist/monochrome.d.ts +34 -0
  86. package/dist/monochrome.d.ts.map +1 -0
  87. package/dist/monochrome.js +613 -0
  88. package/dist/monotone.d.ts +22 -0
  89. package/dist/monotone.d.ts.map +1 -0
  90. package/dist/monotone.js +185 -0
  91. package/dist/parsers-config.d.ts +19 -0
  92. package/dist/parsers-config.d.ts.map +1 -0
  93. package/dist/parsers-config.js +271 -0
  94. package/dist/patch-terminal-dimensions.d.ts +2 -0
  95. package/dist/patch-terminal-dimensions.d.ts.map +1 -0
  96. package/dist/patch-terminal-dimensions.js +45 -0
  97. package/dist/stdin-pager.test.d.ts +2 -0
  98. package/dist/stdin-pager.test.d.ts.map +1 -0
  99. package/dist/stdin-pager.test.js +497 -0
  100. package/dist/store.d.ts +16 -0
  101. package/dist/store.d.ts.map +1 -0
  102. package/dist/store.js +48 -0
  103. package/dist/themes/github.json +247 -0
  104. package/dist/themes.d.ts +59 -0
  105. package/dist/themes.d.ts.map +1 -0
  106. package/dist/themes.js +248 -0
  107. package/dist/tree-icons.d.ts +4 -0
  108. package/dist/tree-icons.d.ts.map +1 -0
  109. package/dist/tree-icons.js +18 -0
  110. package/dist/utils.d.ts +2 -0
  111. package/dist/utils.d.ts.map +1 -0
  112. package/dist/utils.js +13 -0
  113. package/dist/web-utils.d.ts +56 -0
  114. package/dist/web-utils.d.ts.map +1 -0
  115. package/dist/web-utils.js +363 -0
  116. package/package.json +37 -0
  117. package/public/jetbrains-mono-nerd.ttf +0 -0
  118. package/public/jetbrains-mono-nerd.woff2 +0 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * File status based on git diff
3
+ */
4
+ export type FileStatus = "added" | "modified" | "deleted" | "renamed";
5
+ /**
6
+ * Input file info for tree building
7
+ */
8
+ export interface TreeFileInfo {
9
+ path: string;
10
+ status: FileStatus;
11
+ additions: number;
12
+ deletions: number;
13
+ /** Optional index for scroll-to functionality */
14
+ fileIndex?: number;
15
+ }
16
+ /**
17
+ * A single node in the rendered tree output
18
+ */
19
+ export interface TreeNode {
20
+ /** Display path (may be collapsed, e.g., "src/components") */
21
+ displayPath: string;
22
+ /** Full path from repo root (e.g., "src/components") */
23
+ fullPath: string;
24
+ /** Whether this is a file (true) or directory (false) */
25
+ isFile: boolean;
26
+ /** File index for scroll-to (only for files) */
27
+ fileIndex?: number;
28
+ /** File status (only for files) */
29
+ status?: FileStatus;
30
+ /** Number of added lines (only for files) */
31
+ additions?: number;
32
+ /** Number of deleted lines (only for files) */
33
+ deletions?: number;
34
+ /** Tree connector: spaces for indentation (no ASCII art) */
35
+ connector: string;
36
+ /** Prefix string for tree indentation, e.g., " " */
37
+ prefix: string;
38
+ }
39
+ /**
40
+ * A hierarchical tree node with children for sidebar collapse/expand
41
+ */
42
+ export interface HierarchicalTreeNode {
43
+ /** Display path (may be collapsed, e.g., "src/components") */
44
+ displayPath: string;
45
+ /** Full path from repo root (e.g., "src/components") */
46
+ fullPath: string;
47
+ /** Whether this is a file (true) or directory (false) */
48
+ isFile: boolean;
49
+ /** File index for scroll-to (only for files) */
50
+ fileIndex?: number;
51
+ /** File status (only for files) */
52
+ status?: FileStatus;
53
+ /** Number of added lines (only for files) */
54
+ additions?: number;
55
+ /** Number of deleted lines (only for files) */
56
+ deletions?: number;
57
+ /** Child nodes */
58
+ children: HierarchicalTreeNode[];
59
+ }
60
+ /**
61
+ * Build a hierarchical tree from file paths
62
+ * @param files Array of file info objects
63
+ * @returns Array of HierarchicalTreeNode objects with children
64
+ */
65
+ export declare function buildHierarchicalTree(files: TreeFileInfo[]): HierarchicalTreeNode[];
66
+ /**
67
+ * Build a directory tree from file paths
68
+ * @param files Array of file info objects
69
+ * @returns Array of TreeNode objects ready for rendering
70
+ */
71
+ export declare function buildDirectoryTree(files: TreeFileInfo[]): TreeNode[];
72
+ //# sourceMappingURL=directory-tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-tree.d.ts","sourceRoot":"","sources":["../src/directory-tree.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAA;AAErE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,UAAU,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAA;IACnB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,MAAM,EAAE,OAAO,CAAA;IACf,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAA;IACnB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAA;IAChB,yDAAyD;IACzD,MAAM,EAAE,OAAO,CAAA;IACf,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kBAAkB;IAClB,QAAQ,EAAE,oBAAoB,EAAE,CAAA;CACjC;AAYD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,oBAAoB,EAAE,CAQnF;AAoBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,QAAQ,EAAE,CAQpE"}
@@ -0,0 +1,161 @@
1
+ // Directory tree builder for displaying file changes in a tree structure.
2
+ // Builds a collapsible tree from file paths with status colors and change counts.
3
+ // Returns structured nodes that can be rendered by DirectoryTreeView component.
4
+ /**
5
+ * Build a hierarchical tree from file paths
6
+ * @param files Array of file info objects
7
+ * @returns Array of HierarchicalTreeNode objects with children
8
+ */
9
+ export function buildHierarchicalTree(files) {
10
+ if (files.length === 0) {
11
+ return [];
12
+ }
13
+ const tree = buildInternalTree(files);
14
+ sortInternalTree(tree);
15
+ return toHierarchical(tree);
16
+ }
17
+ function toHierarchical(nodes) {
18
+ return nodes.map((node) => {
19
+ const collapsed = collapseNode(node);
20
+ const isFile = collapsed.originalNode.status !== undefined;
21
+ return {
22
+ displayPath: collapsed.path,
23
+ fullPath: collapsed.originalNode.path,
24
+ isFile,
25
+ fileIndex: collapsed.originalNode.fileIndex,
26
+ status: collapsed.originalNode.status,
27
+ additions: collapsed.originalNode.additions,
28
+ deletions: collapsed.originalNode.deletions,
29
+ children: toHierarchical(collapsed.children),
30
+ };
31
+ });
32
+ }
33
+ /**
34
+ * Build a directory tree from file paths
35
+ * @param files Array of file info objects
36
+ * @returns Array of TreeNode objects ready for rendering
37
+ */
38
+ export function buildDirectoryTree(files) {
39
+ if (files.length === 0) {
40
+ return [];
41
+ }
42
+ const tree = buildInternalTree(files);
43
+ sortInternalTree(tree);
44
+ return flattenTree(tree);
45
+ }
46
+ /**
47
+ * Build internal tree structure from file paths
48
+ */
49
+ function buildInternalTree(files) {
50
+ const root = [];
51
+ const nodeMap = new Map();
52
+ for (const file of files) {
53
+ const parts = file.path.split("/").filter((part) => part !== "");
54
+ let currentPath = "";
55
+ let currentLevel = root;
56
+ for (let i = 0; i < parts.length; i++) {
57
+ const part = parts[i];
58
+ currentPath = currentPath ? `${currentPath}/${part}` : part;
59
+ let node = nodeMap.get(currentPath);
60
+ if (!node) {
61
+ node = {
62
+ path: currentPath,
63
+ children: [],
64
+ };
65
+ nodeMap.set(currentPath, node);
66
+ currentLevel.push(node);
67
+ }
68
+ // On the final part, assign file info
69
+ if (i === parts.length - 1) {
70
+ node.title = part;
71
+ node.fileIndex = file.fileIndex;
72
+ node.status = file.status;
73
+ node.additions = file.additions;
74
+ node.deletions = file.deletions;
75
+ }
76
+ currentLevel = node.children;
77
+ }
78
+ }
79
+ return root;
80
+ }
81
+ /**
82
+ * Get the base name from a path
83
+ */
84
+ function getName(node) {
85
+ const parts = node.path.split("/");
86
+ return parts[parts.length - 1] || node.path;
87
+ }
88
+ /**
89
+ * Sort tree nodes by name at every level so file ordering is deterministic
90
+ * and independent from incoming git diff section order.
91
+ */
92
+ function sortInternalTree(nodes) {
93
+ nodes.sort((a, b) => getName(a).toLowerCase().localeCompare(getName(b).toLowerCase()));
94
+ for (const node of nodes) {
95
+ if (node.children.length > 0) {
96
+ sortInternalTree(node.children);
97
+ }
98
+ }
99
+ }
100
+ /**
101
+ * Collapse directories that only contain a single subdirectory (no files)
102
+ */
103
+ function collapseNode(node) {
104
+ let currentNode = node;
105
+ let collapsedPath = getName(currentNode);
106
+ // Keep collapsing while:
107
+ // - Current node has exactly one child
108
+ // - Current node is not a file (no status/title means it's a directory)
109
+ // - The single child is also a directory (no status)
110
+ while (currentNode.children.length === 1 &&
111
+ currentNode.status === undefined &&
112
+ currentNode.children[0].status === undefined &&
113
+ currentNode.children[0].children.length > 0) {
114
+ currentNode = currentNode.children[0];
115
+ collapsedPath = collapsedPath + "/" + getName(currentNode);
116
+ }
117
+ return {
118
+ path: collapsedPath,
119
+ collapsed: collapsedPath !== getName(node),
120
+ children: currentNode.children,
121
+ originalNode: currentNode,
122
+ };
123
+ }
124
+ /**
125
+ * Flatten the tree into a linear array of TreeNode objects
126
+ */
127
+ function flattenTree(tree) {
128
+ const result = [];
129
+ function processNode(node, prefix, isLast, isRoot) {
130
+ const collapsed = collapseNode(node);
131
+ const displayPath = collapsed.path;
132
+ const connector = " ";
133
+ // Determine if this is a file (has status) or directory
134
+ const isFile = collapsed.originalNode.status !== undefined;
135
+ result.push({
136
+ displayPath,
137
+ fullPath: collapsed.originalNode.path,
138
+ isFile,
139
+ fileIndex: collapsed.originalNode.fileIndex,
140
+ status: collapsed.originalNode.status,
141
+ additions: collapsed.originalNode.additions,
142
+ deletions: collapsed.originalNode.deletions,
143
+ connector,
144
+ prefix,
145
+ });
146
+ // Process children
147
+ if (collapsed.children.length > 0) {
148
+ const childPrefix = prefix + " ";
149
+ collapsed.children.forEach((child, idx) => {
150
+ const childIsLast = idx === collapsed.children.length - 1;
151
+ processNode(child, childPrefix, childIsLast, false);
152
+ });
153
+ }
154
+ }
155
+ // Process root level nodes
156
+ tree.forEach((node, idx) => {
157
+ const isLast = idx === tree.length - 1;
158
+ processNode(node, "", isLast, true);
159
+ });
160
+ return result;
161
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=directory-tree.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-tree.test.d.ts","sourceRoot":"","sources":["../src/directory-tree.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,383 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentuah/react/jsx-runtime";
2
+ // Tests for directory tree building and rendering
3
+ // Uses opentui test renderer with captureCharFrame() for visual testing
4
+ import { testRender } from "@opentuah/react/test-utils";
5
+ import { afterEach, describe, expect, it } from "bun:test";
6
+ import { DirectoryTreeView } from "./components/directory-tree-view.js";
7
+ import { buildDirectoryTree, buildHierarchicalTree } from "./directory-tree.js";
8
+ /**
9
+ * Simple component to render tree nodes as text for testing
10
+ */
11
+ function TreeRenderer({ nodes }) {
12
+ return (_jsx("box", { style: { flexDirection: "column" }, children: nodes.map((node, idx) => {
13
+ // Build the line: prefix + connector + path + optional stats
14
+ const statsStr = node.isFile
15
+ ? ` (+${node.additions},-${node.deletions})`
16
+ : "";
17
+ return (_jsxs("text", { children: [node.prefix, node.connector, node.displayPath, statsStr] }, idx));
18
+ }) }));
19
+ }
20
+ describe("buildDirectoryTree", () => {
21
+ it("should return empty array for no files", () => {
22
+ const result = buildDirectoryTree([]);
23
+ expect(result).toEqual([]);
24
+ });
25
+ it("should handle single file at root", () => {
26
+ const files = [
27
+ { path: "README.md", status: "modified", additions: 5, deletions: 2 },
28
+ ];
29
+ const result = buildDirectoryTree(files);
30
+ expect(result).toHaveLength(1);
31
+ expect(result[0].displayPath).toBe("README.md");
32
+ expect(result[0].isFile).toBe(true);
33
+ expect(result[0].status).toBe("modified");
34
+ });
35
+ it("should collapse single-child directories", () => {
36
+ const files = [
37
+ { path: "src/components/Button.tsx", status: "added", additions: 50, deletions: 0 },
38
+ ];
39
+ const result = buildDirectoryTree(files);
40
+ // Should collapse src/components into one directory node
41
+ expect(result).toHaveLength(2);
42
+ expect(result[0].displayPath).toBe("src/components");
43
+ expect(result[0].isFile).toBe(false);
44
+ expect(result[1].displayPath).toBe("Button.tsx");
45
+ expect(result[1].isFile).toBe(true);
46
+ });
47
+ it("should sort nodes alphabetically regardless of input order", () => {
48
+ const files = [
49
+ { path: "website/src/index.ts", status: "modified", additions: 1, deletions: 0 },
50
+ { path: "db/schema.prisma", status: "modified", additions: 1, deletions: 0 },
51
+ { path: "discord/src/utils.ts", status: "modified", additions: 1, deletions: 0 },
52
+ { path: "discord/src/cli.ts", status: "modified", additions: 1, deletions: 0 },
53
+ { path: "gateway-proxy/src/main.rs", status: "modified", additions: 1, deletions: 0 },
54
+ ];
55
+ const result = buildDirectoryTree(files);
56
+ const rendered = result.map((node) => `${node.prefix}${node.connector}${node.displayPath}`);
57
+ expect(rendered).toEqual([
58
+ " db",
59
+ " schema.prisma",
60
+ " discord/src",
61
+ " cli.ts",
62
+ " utils.ts",
63
+ " gateway-proxy/src",
64
+ " main.rs",
65
+ " website/src",
66
+ " index.ts",
67
+ ]);
68
+ });
69
+ });
70
+ describe("TreeRenderer visual tests", () => {
71
+ let testSetup;
72
+ afterEach(() => {
73
+ if (testSetup) {
74
+ testSetup.renderer.destroy();
75
+ }
76
+ });
77
+ it("should render single file", async () => {
78
+ const files = [
79
+ { path: "package.json", status: "modified", additions: 1, deletions: 1 },
80
+ ];
81
+ const nodes = buildDirectoryTree(files);
82
+ testSetup = await testRender(_jsx(TreeRenderer, { nodes: nodes }), {
83
+ width: 50,
84
+ height: 5,
85
+ });
86
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
87
+ await testSetup.renderOnce();
88
+ const frame = testSetup.captureCharFrame();
89
+ expect(frame).toMatchSnapshot();
90
+ });
91
+ it("should render multiple root files", async () => {
92
+ const files = [
93
+ { path: "package.json", status: "modified", additions: 1, deletions: 1 },
94
+ { path: "README.md", status: "added", additions: 20, deletions: 0 },
95
+ { path: "tsconfig.json", status: "deleted", additions: 0, deletions: 15 },
96
+ ];
97
+ const nodes = buildDirectoryTree(files);
98
+ testSetup = await testRender(_jsx(TreeRenderer, { nodes: nodes }), {
99
+ width: 50,
100
+ height: 7,
101
+ });
102
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
103
+ await testSetup.renderOnce();
104
+ const frame = testSetup.captureCharFrame();
105
+ expect(frame).toMatchSnapshot();
106
+ });
107
+ it("should render nested directories with indentation", async () => {
108
+ const files = [
109
+ { path: "src/index.ts", status: "modified", additions: 5, deletions: 2 },
110
+ { path: "src/utils.ts", status: "added", additions: 30, deletions: 0 },
111
+ ];
112
+ const nodes = buildDirectoryTree(files);
113
+ testSetup = await testRender(_jsx(TreeRenderer, { nodes: nodes }), {
114
+ width: 50,
115
+ height: 7,
116
+ });
117
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
118
+ await testSetup.renderOnce();
119
+ const frame = testSetup.captureCharFrame();
120
+ expect(frame).toMatchSnapshot();
121
+ });
122
+ it("should collapse single-child directories", async () => {
123
+ const files = [
124
+ { path: "src/components/Button.tsx", status: "added", additions: 50, deletions: 0 },
125
+ { path: "src/components/Input.tsx", status: "added", additions: 40, deletions: 0 },
126
+ ];
127
+ const nodes = buildDirectoryTree(files);
128
+ testSetup = await testRender(_jsx(TreeRenderer, { nodes: nodes }), {
129
+ width: 50,
130
+ height: 7,
131
+ });
132
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
133
+ await testSetup.renderOnce();
134
+ const frame = testSetup.captureCharFrame();
135
+ expect(frame).toMatchSnapshot();
136
+ });
137
+ it("should render complex nested structure", async () => {
138
+ const files = [
139
+ { path: "package.json", status: "modified", additions: 2, deletions: 1, fileIndex: 0 },
140
+ { path: "src/index.ts", status: "modified", additions: 10, deletions: 5, fileIndex: 1 },
141
+ { path: "src/components/Button.tsx", status: "added", additions: 50, deletions: 0, fileIndex: 2 },
142
+ { path: "src/components/Input.tsx", status: "modified", additions: 15, deletions: 8, fileIndex: 3 },
143
+ { path: "src/utils/helpers.ts", status: "deleted", additions: 0, deletions: 30, fileIndex: 4 },
144
+ { path: "tests/index.test.ts", status: "added", additions: 25, deletions: 0, fileIndex: 5 },
145
+ ];
146
+ const nodes = buildDirectoryTree(files);
147
+ testSetup = await testRender(_jsx(TreeRenderer, { nodes: nodes }), {
148
+ width: 60,
149
+ height: 15,
150
+ });
151
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
152
+ await testSetup.renderOnce();
153
+ const frame = testSetup.captureCharFrame();
154
+ expect(frame).toMatchSnapshot();
155
+ });
156
+ it("should handle deeply nested paths with collapse", async () => {
157
+ const files = [
158
+ { path: "packages/core/src/lib/utils/helpers.ts", status: "modified", additions: 5, deletions: 3 },
159
+ { path: "packages/core/src/lib/utils/format.ts", status: "added", additions: 20, deletions: 0 },
160
+ ];
161
+ const nodes = buildDirectoryTree(files);
162
+ testSetup = await testRender(_jsx(TreeRenderer, { nodes: nodes }), {
163
+ width: 60,
164
+ height: 7,
165
+ });
166
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
167
+ await testSetup.renderOnce();
168
+ const frame = testSetup.captureCharFrame();
169
+ expect(frame).toMatchSnapshot();
170
+ });
171
+ it("should handle sibling directories at different levels", async () => {
172
+ const files = [
173
+ { path: "src/api/routes.ts", status: "modified", additions: 10, deletions: 5 },
174
+ { path: "src/api/handlers.ts", status: "added", additions: 30, deletions: 0 },
175
+ { path: "src/db/models.ts", status: "modified", additions: 8, deletions: 2 },
176
+ { path: "lib/utils.ts", status: "added", additions: 15, deletions: 0 },
177
+ ];
178
+ const nodes = buildDirectoryTree(files);
179
+ testSetup = await testRender(_jsx(TreeRenderer, { nodes: nodes }), {
180
+ width: 60,
181
+ height: 12,
182
+ });
183
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
184
+ await testSetup.renderOnce();
185
+ const frame = testSetup.captureCharFrame();
186
+ expect(frame).toMatchSnapshot();
187
+ });
188
+ });
189
+ describe("buildHierarchicalTree", () => {
190
+ it("should return empty array for no files", () => {
191
+ const result = buildHierarchicalTree([]);
192
+ expect(result).toEqual([]);
193
+ });
194
+ it("should handle single file at root", () => {
195
+ const files = [
196
+ { path: "README.md", status: "modified", additions: 5, deletions: 2 },
197
+ ];
198
+ const result = buildHierarchicalTree(files);
199
+ expect(result).toHaveLength(1);
200
+ expect(result[0].displayPath).toBe("README.md");
201
+ expect(result[0].isFile).toBe(true);
202
+ expect(result[0].status).toBe("modified");
203
+ expect(result[0].children).toEqual([]);
204
+ });
205
+ it("should build nested directory hierarchy", () => {
206
+ const files = [
207
+ { path: "src/index.ts", status: "modified", additions: 5, deletions: 2 },
208
+ { path: "src/utils.ts", status: "added", additions: 30, deletions: 0 },
209
+ ];
210
+ const result = buildHierarchicalTree(files);
211
+ expect(result).toHaveLength(1);
212
+ expect(result[0].displayPath).toBe("src");
213
+ expect(result[0].isFile).toBe(false);
214
+ expect(result[0].children).toHaveLength(2);
215
+ expect(result[0].children[0].displayPath).toBe("index.ts");
216
+ expect(result[0].children[0].isFile).toBe(true);
217
+ expect(result[0].children[1].displayPath).toBe("utils.ts");
218
+ expect(result[0].children[1].isFile).toBe(true);
219
+ });
220
+ it("should collapse single-child directories in hierarchy", () => {
221
+ const files = [
222
+ { path: "src/components/Button.tsx", status: "added", additions: 50, deletions: 0 },
223
+ ];
224
+ const result = buildHierarchicalTree(files);
225
+ expect(result).toHaveLength(1);
226
+ expect(result[0].displayPath).toBe("src/components");
227
+ expect(result[0].isFile).toBe(false);
228
+ expect(result[0].children).toHaveLength(1);
229
+ expect(result[0].children[0].displayPath).toBe("Button.tsx");
230
+ expect(result[0].children[0].isFile).toBe(true);
231
+ });
232
+ it("should sort nodes alphabetically at every level", () => {
233
+ const files = [
234
+ { path: "b/file.ts", status: "modified", additions: 1, deletions: 0 },
235
+ { path: "a/file.ts", status: "modified", additions: 1, deletions: 0 },
236
+ ];
237
+ const result = buildHierarchicalTree(files);
238
+ expect(result).toHaveLength(2);
239
+ expect(result[0].displayPath).toBe("a");
240
+ expect(result[1].displayPath).toBe("b");
241
+ });
242
+ });
243
+ describe("DirectoryTreeView component", () => {
244
+ let testSetup;
245
+ afterEach(() => {
246
+ if (testSetup) {
247
+ testSetup.renderer.destroy();
248
+ }
249
+ });
250
+ it("renders folder icon on directory rows", async () => {
251
+ const files = [
252
+ { path: "src/index.ts", status: "modified", additions: 5, deletions: 2, fileIndex: 0 },
253
+ ];
254
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github" }), { width: 60, height: 5 });
255
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
256
+ await testSetup.renderOnce();
257
+ const frame = testSetup.captureCharFrame();
258
+ expect(frame).toContain("󰝰");
259
+ });
260
+ it("renders file-type icon on file rows", async () => {
261
+ const files = [
262
+ { path: "README.md", status: "modified", additions: 5, deletions: 2, fileIndex: 0 },
263
+ { path: "src/index.ts", status: "added", additions: 30, deletions: 0, fileIndex: 1 },
264
+ ];
265
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github" }), { width: 60, height: 5 });
266
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
267
+ await testSetup.renderOnce();
268
+ const frame = testSetup.captureCharFrame();
269
+ expect(frame).toContain("󰍔");
270
+ expect(frame).toContain("󰛦");
271
+ });
272
+ it("renders generic file icon for unknown extensions", async () => {
273
+ const files = [
274
+ { path: "config.yaml", status: "modified", additions: 1, deletions: 0, fileIndex: 0 },
275
+ ];
276
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github" }), { width: 60, height: 3 });
277
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
278
+ await testSetup.renderOnce();
279
+ const frame = testSetup.captureCharFrame();
280
+ expect(frame).toContain("󰈙");
281
+ });
282
+ it("should render tree without border", async () => {
283
+ const files = [
284
+ { path: "src/index.ts", status: "modified", additions: 5, deletions: 2, fileIndex: 0 },
285
+ { path: "src/utils.ts", status: "added", additions: 30, deletions: 0, fileIndex: 1 },
286
+ { path: "README.md", status: "deleted", additions: 0, deletions: 15, fileIndex: 2 },
287
+ ];
288
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github" }), { width: 60, height: 12 });
289
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
290
+ await testSetup.renderOnce();
291
+ const frame = testSetup.captureCharFrame();
292
+ expect(frame).toMatchSnapshot();
293
+ });
294
+ it("truncates long file paths while preserving the filename", async () => {
295
+ const files = [
296
+ {
297
+ path: "packages/core/src/lib/really-long-file-name.ts",
298
+ status: "modified",
299
+ additions: 12,
300
+ deletions: 3,
301
+ },
302
+ ];
303
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github", width: 28 }), { width: 28, height: 4 });
304
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
305
+ await testSetup.renderOnce();
306
+ const frame = testSetup.captureCharFrame();
307
+ expect(frame).toContain("…");
308
+ expect(frame).toContain("name.ts");
309
+ expect(frame).toContain("(+12,-3)");
310
+ });
311
+ it("should render empty when no files", async () => {
312
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: [], themeName: "github" }), { width: 40, height: 5 });
313
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
314
+ await testSetup.renderOnce();
315
+ const frame = testSetup.captureCharFrame();
316
+ // Should render nothing (DirectoryTreeView returns null for empty)
317
+ expect(frame).toMatchSnapshot();
318
+ });
319
+ it("should highlight the active file row", async () => {
320
+ const files = [
321
+ { path: "README.md", status: "modified", additions: 5, deletions: 2, fileIndex: 0 },
322
+ { path: "src/index.ts", status: "added", additions: 30, deletions: 0, fileIndex: 1 },
323
+ { path: "src/utils.ts", status: "deleted", additions: 0, deletions: 15, fileIndex: 2 },
324
+ ];
325
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github", activeFileIndex: 1 }), { width: 60, height: 12 });
326
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
327
+ await testSetup.renderOnce();
328
+ const frame = testSetup.captureCharFrame();
329
+ // Should show the tree with the active file highlighted
330
+ expect(frame).toContain("README.md");
331
+ expect(frame).toContain("index.ts");
332
+ expect(frame).toContain("utils.ts");
333
+ });
334
+ it("filters out children of collapsed folders", async () => {
335
+ const files = [
336
+ { path: "src/index.ts", status: "modified", additions: 5, deletions: 2, fileIndex: 0 },
337
+ { path: "src/utils.ts", status: "added", additions: 30, deletions: 0, fileIndex: 1 },
338
+ ];
339
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github", initialCollapsedPaths: ["src"] }), { width: 60, height: 5 });
340
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
341
+ await testSetup.renderOnce();
342
+ const frame = testSetup.captureCharFrame();
343
+ expect(frame).toContain("src");
344
+ expect(frame).not.toContain("index.ts");
345
+ expect(frame).not.toContain("utils.ts");
346
+ });
347
+ it("shows closed folder icon for collapsed folders", async () => {
348
+ const files = [
349
+ { path: "src/index.ts", status: "modified", additions: 5, deletions: 2, fileIndex: 0 },
350
+ ];
351
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github", initialCollapsedPaths: ["src"] }), { width: 60, height: 3 });
352
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
353
+ await testSetup.renderOnce();
354
+ const frame = testSetup.captureCharFrame();
355
+ expect(frame).toContain("󰉋");
356
+ expect(frame).not.toContain("󰝰");
357
+ });
358
+ it("collapsing compressed tree path hides entire chain", async () => {
359
+ const files = [
360
+ { path: "src/components/Button.tsx", status: "added", additions: 50, deletions: 0, fileIndex: 0 },
361
+ { path: "src/components/Input.tsx", status: "modified", additions: 10, deletions: 5, fileIndex: 1 },
362
+ ];
363
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github", initialCollapsedPaths: ["src/components"] }), { width: 60, height: 5 });
364
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
365
+ await testSetup.renderOnce();
366
+ const frame = testSetup.captureCharFrame();
367
+ expect(frame).toContain("src/components");
368
+ expect(frame).not.toContain("Button.tsx");
369
+ expect(frame).not.toContain("Input.tsx");
370
+ });
371
+ it("renders with active file inside collapsed folder", async () => {
372
+ const files = [
373
+ { path: "src/index.ts", status: "modified", additions: 5, deletions: 2, fileIndex: 0 },
374
+ ];
375
+ testSetup = await testRender(_jsx(DirectoryTreeView, { files: files, themeName: "github", initialCollapsedPaths: ["src"], activeFileIndex: 0 }), { width: 60, height: 3 });
376
+ globalThis.IS_REACT_ACT_ENVIRONMENT = false;
377
+ await testSetup.renderOnce();
378
+ const frame = testSetup.captureCharFrame();
379
+ // Folder is visible, file is hidden
380
+ expect(frame).toContain("src");
381
+ expect(frame).not.toContain("index.ts");
382
+ });
383
+ });
@@ -0,0 +1,26 @@
1
+ import React, { type ReactNode } from "react";
2
+ import { type ResolvedTheme } from "./themes";
3
+ export interface DropdownOption {
4
+ title: string;
5
+ value: string;
6
+ icon?: ReactNode;
7
+ keywords?: string[];
8
+ label?: string;
9
+ }
10
+ export interface DropdownProps {
11
+ id?: string;
12
+ tooltip?: string;
13
+ placeholder?: string;
14
+ selectedValues?: string[];
15
+ itemsPerPage?: number;
16
+ options: DropdownOption[];
17
+ onChange?: (newValue: string) => void;
18
+ onFocus?: (value: string) => void;
19
+ onEscape?: () => void;
20
+ theme: ResolvedTheme;
21
+ }
22
+ export declare function filterDropdownOptions(options: DropdownOption[], searchText: string): DropdownOption[];
23
+ declare const Dropdown: (props: DropdownProps) => React.ReactNode;
24
+ export default Dropdown;
25
+ export { Dropdown };
26
+ //# sourceMappingURL=dropdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dropdown.d.ts","sourceRoot":"","sources":["../src/dropdown.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,EAA4C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGxF,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,UAAU,CAAC;AAEzD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,EAAE,CAerG;AAED,QAAA,MAAM,QAAQ,GAAI,OAAO,aAAa,KAAG,KAAK,CAAC,SA+M9C,CAAC;AAqFF,eAAe,QAAQ,CAAC;AACxB,OAAO,EAAE,QAAQ,EAAE,CAAC"}