voonex 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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -0
  3. package/dist/components/Box.d.ts +33 -0
  4. package/dist/components/Box.js +242 -0
  5. package/dist/components/Button.d.ts +23 -0
  6. package/dist/components/Button.js +76 -0
  7. package/dist/components/Checkbox.d.ts +20 -0
  8. package/dist/components/Checkbox.js +40 -0
  9. package/dist/components/Input.d.ts +23 -0
  10. package/dist/components/Input.js +79 -0
  11. package/dist/components/Menu.d.ts +27 -0
  12. package/dist/components/Menu.js +93 -0
  13. package/dist/components/Popup.d.ts +7 -0
  14. package/dist/components/Popup.js +50 -0
  15. package/dist/components/ProgressBar.d.ts +28 -0
  16. package/dist/components/ProgressBar.js +47 -0
  17. package/dist/components/Radio.d.ts +22 -0
  18. package/dist/components/Radio.js +64 -0
  19. package/dist/components/Select.d.ts +24 -0
  20. package/dist/components/Select.js +126 -0
  21. package/dist/components/Tab.d.ts +24 -0
  22. package/dist/components/Tab.js +72 -0
  23. package/dist/components/Table.d.ts +25 -0
  24. package/dist/components/Table.js +116 -0
  25. package/dist/components/Textarea.d.ts +26 -0
  26. package/dist/components/Textarea.js +176 -0
  27. package/dist/components/Tree.d.ts +33 -0
  28. package/dist/components/Tree.js +166 -0
  29. package/dist/core/Component.d.ts +6 -0
  30. package/dist/core/Component.js +5 -0
  31. package/dist/core/Cursor.d.ts +7 -0
  32. package/dist/core/Cursor.js +59 -0
  33. package/dist/core/Events.d.ts +11 -0
  34. package/dist/core/Events.js +37 -0
  35. package/dist/core/Focus.d.ts +16 -0
  36. package/dist/core/Focus.js +69 -0
  37. package/dist/core/Input.d.ts +13 -0
  38. package/dist/core/Input.js +88 -0
  39. package/dist/core/Layout.d.ts +18 -0
  40. package/dist/core/Layout.js +65 -0
  41. package/dist/core/Screen.d.ts +78 -0
  42. package/dist/core/Screen.js +373 -0
  43. package/dist/core/Styler.d.ts +71 -0
  44. package/dist/core/Styler.js +157 -0
  45. package/dist/index.d.ts +25 -0
  46. package/dist/index.js +41 -0
  47. package/package.json +29 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CodeTease
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,223 @@
1
+ # Voonex
2
+
3
+ **Voonex** is a modern, zero-dependency Terminal UI (TUI) library for Node.js, built with TypeScript. It provides a robust virtual buffer, reactive rendering system, and a rich set of widgets to build complex command-line interfaces with ease.
4
+
5
+ ## Features
6
+
7
+ - **Zero Dependencies**: Lightweight and easy to audit.
8
+ - **Double Buffering & Diffing**: Efficient rendering that eliminates flickering and minimizes I/O.
9
+ - **Component System**: Built-in widgets like `Box`, `Menu`, `ProgressBar`, `Input`, `Table`, and more.
10
+ - **Reactive Rendering**: Automated screen updates via `Screen.mount()` and `Screen.scheduleRender()`.
11
+ - **Focus Management**: Built-in keyboard navigation and focus delegation.
12
+ - **Styling Engine**: Simple yet powerful API for ANSI colors and text modifiers.
13
+ - **TypeScript Support**: Written in TypeScript with full type definitions included.
14
+
15
+ ## Installation
16
+
17
+ Install via npm:
18
+
19
+ ```bash
20
+ npm install voonex
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ Here is a minimal example showing how to initialize the screen and display a simple box.
26
+
27
+ ```typescript
28
+ import { Screen, Box, Styler, Input } from 'voonex';
29
+
30
+ // 1. Enter the alternate screen buffer
31
+ Screen.enter();
32
+
33
+ // 2. Define your render function
34
+ function render() {
35
+ // Render a Box at (5, 5) with a title
36
+ Box.render([
37
+ "Welcome to Voonex!",
38
+ "Press 'q' to exit."
39
+ ], {
40
+ title: "Hello World",
41
+ x: 5,
42
+ y: 5,
43
+ padding: 1,
44
+ style: 'double',
45
+ borderColor: 'cyan'
46
+ });
47
+ }
48
+
49
+ // 3. Mount the render function to the screen loop
50
+ Screen.mount(render);
51
+
52
+ // 4. Handle Input
53
+ Input.onKey((key) => {
54
+ if (key.name === 'q') {
55
+ // Leave the screen buffer properly before exiting
56
+ Screen.leave();
57
+ process.exit(0);
58
+ }
59
+ });
60
+ ```
61
+
62
+ Run it with:
63
+ ```bash
64
+ npx ts-node my-app.ts
65
+ ```
66
+
67
+ ## Core Concepts
68
+
69
+ ### The Screen
70
+ The `Screen` class is the heart of Voonex. It manages the terminal buffer, handles resizing, and optimizes rendering using a diffing algorithm.
71
+
72
+ - `Screen.enter()`: Switches to the alternate buffer (like `vim` or `nano`).
73
+ - `Screen.leave()`: Restores the original terminal state. **Always call this before exiting.**
74
+ - `Screen.mount(renderFn)`: Registers a function to be called during the render cycle. Voonex uses a "Painter's Algorithm", so functions mounted later are drawn on top.
75
+ - `Screen.scheduleRender()`: Triggers a screen update. This is automatically called by most interactive components, but you can call it manually if you update state asynchronously (e.g., inside a `setInterval`).
76
+
77
+ ### Input Handling
78
+ Voonex provides a global input listener wrapper around Node's `process.stdin`.
79
+
80
+ ```typescript
81
+ import { Input } from 'voonex';
82
+
83
+ Input.onKey((key) => {
84
+ console.log(key.name); // 'up', 'down', 'enter', 'a', 'b', etc.
85
+ console.log(key.ctrl); // true if Ctrl is pressed
86
+ });
87
+ ```
88
+
89
+ ### Styling
90
+ The `Styler` class provides utilities for coloring and formatting text.
91
+
92
+ ```typescript
93
+ import { Styler } from 'voonex';
94
+
95
+ const text = Styler.style("Success!", 'green', 'bold', 'underline');
96
+ ```
97
+
98
+ ## Components
99
+
100
+ Voonex comes with several built-in components. Components can be used in two ways:
101
+ 1. **Static Rendering**: Using static methods like `Box.render()`.
102
+ 2. **Stateful Instances**: Creating an instance (e.g., `new Menu()`) and calling its methods.
103
+
104
+ ### Box
105
+ A container for text with optional borders, padding, and titles.
106
+
107
+ ```typescript
108
+ Box.render([
109
+ "Line 1",
110
+ "Line 2"
111
+ ], {
112
+ x: 2, y: 2,
113
+ width: 30,
114
+ borderColor: 'green',
115
+ style: 'round' // 'single', 'double', or 'round'
116
+ });
117
+ ```
118
+
119
+ ### Menu
120
+ A vertical list of selectable items.
121
+
122
+ ```typescript
123
+ const menu = new Menu({
124
+ title: "Main Menu",
125
+ x: 4, y: 4,
126
+ items: ["Start", "Options", "Exit"],
127
+ onSelect: (index, item) => {
128
+ console.log(`Selected: ${item}`);
129
+ }
130
+ });
131
+
132
+ // In your input handler:
133
+ Input.onKey((key) => {
134
+ menu.handleKey(key); // Passes key events to the menu
135
+ });
136
+
137
+ // In your render function:
138
+ menu.render();
139
+ ```
140
+
141
+ ### ProgressBar
142
+ Displays a progress bar.
143
+
144
+ ```typescript
145
+ const bar = new ProgressBar({
146
+ width: 20,
147
+ total: 100,
148
+ x: 4, y: 10,
149
+ completeChar: '█',
150
+ incompleteChar: '░'
151
+ });
152
+
153
+ // Update progress
154
+ bar.update(50); // 50%
155
+ ```
156
+
157
+ ### Popup
158
+ A modal dialog that overlays other content.
159
+
160
+ ```typescript
161
+ // Shows a message and waits for user to press Enter/Esc
162
+ await Popup.alert("This is an important message!", { title: "Alert" });
163
+
164
+ // Asks for confirmation (returns boolean)
165
+ const confirmed = await Popup.confirm("Are you sure?", { title: "Confirm" });
166
+ ```
167
+
168
+ ### Input Field
169
+ A text input field for capturing user input.
170
+
171
+ ```typescript
172
+ const nameInput = new InputField({
173
+ x: 2, y: 2,
174
+ width: 20,
175
+ placeholder: "Enter name..."
176
+ });
177
+
178
+ Input.onKey(key => {
179
+ if (key.name === 'tab') {
180
+ nameInput.focus();
181
+ }
182
+ nameInput.handleKey(key);
183
+ });
184
+
185
+ // In render loop
186
+ nameInput.render();
187
+ ```
188
+
189
+ ## Advanced Usage
190
+
191
+ ### Manual Layout
192
+ Voonex relies on absolute positioning `(x, y)`. For complex layouts, you can calculate coordinates dynamically based on `Screen.size`.
193
+
194
+ ```typescript
195
+ const { width, height } = Screen.size;
196
+ const centerX = Math.floor(width / 2);
197
+ const centerY = Math.floor(height / 2);
198
+ ```
199
+
200
+ ### Creating Custom Components
201
+ Any class can be a component. To integrate with the Voonex ecosystem, it's recommended (but not required) to implement the `Focusable` interface if the component handles input.
202
+
203
+ ```typescript
204
+ import { Screen, Styler, Focusable } from 'voonex';
205
+
206
+ class MyWidget implements Focusable {
207
+ focus() { /* handle focus */ }
208
+ blur() { /* handle blur */ }
209
+
210
+ handleKey(key) {
211
+ // return true if key was consumed
212
+ return false;
213
+ }
214
+
215
+ render() {
216
+ Screen.write(10, 10, "My Custom Widget");
217
+ }
218
+ }
219
+ ```
220
+
221
+ ## License
222
+
223
+ This project is under the **MIT License**.
@@ -0,0 +1,33 @@
1
+ import { ColorName } from '../core/Styler';
2
+ import { Focusable } from '../core/Focus';
3
+ import * as readline from 'readline';
4
+ export interface BoxOptions {
5
+ id?: string;
6
+ x?: number;
7
+ y?: number;
8
+ width?: number;
9
+ height?: number;
10
+ padding?: number;
11
+ borderColor?: ColorName;
12
+ title?: string;
13
+ style?: 'single' | 'double' | 'round';
14
+ scrollable?: boolean;
15
+ wrap?: boolean;
16
+ }
17
+ export declare class Box implements Focusable {
18
+ id: string;
19
+ private options;
20
+ private content;
21
+ private processedLines;
22
+ private scrollTop;
23
+ private isFocused;
24
+ constructor(content: string | string[], options?: BoxOptions);
25
+ private calculateDimensions;
26
+ private processContent;
27
+ private wrapLine;
28
+ focus(): void;
29
+ blur(): void;
30
+ handleKey(key: readline.Key): boolean;
31
+ render(): void;
32
+ static render(content: string | string[], options?: BoxOptions): void;
33
+ }
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Box = void 0;
4
+ const Styler_1 = require("../core/Styler");
5
+ const Screen_1 = require("../core/Screen");
6
+ const BORDERS = {
7
+ single: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' },
8
+ double: { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' },
9
+ round: { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' }
10
+ };
11
+ class Box {
12
+ constructor(content, options = {}) {
13
+ this.processedLines = [];
14
+ this.scrollTop = 0;
15
+ this.isFocused = false;
16
+ this.id = options.id || `box-${Math.random().toString(36).substr(2, 9)}`;
17
+ this.options = {
18
+ padding: 1,
19
+ style: 'round',
20
+ borderColor: 'white',
21
+ scrollable: false,
22
+ wrap: false,
23
+ x: 1,
24
+ y: 1,
25
+ ...options
26
+ };
27
+ this.content = Array.isArray(content) ? content : content.split('\n');
28
+ this.calculateDimensions();
29
+ this.processContent();
30
+ }
31
+ calculateDimensions() {
32
+ const { padding } = this.options;
33
+ const pad = padding || 1;
34
+ // If width/height not provided, calculate from content
35
+ if (!this.options.width) {
36
+ const contentMaxWidth = Math.max(...this.content.map(l => Styler_1.Styler.len(l)));
37
+ this.options.width = contentMaxWidth + (pad * 2) + 2;
38
+ }
39
+ if (!this.options.height) {
40
+ this.options.height = this.content.length + (pad * 2) + 2;
41
+ }
42
+ }
43
+ processContent() {
44
+ const { width, padding, wrap } = this.options;
45
+ const pad = padding || 1;
46
+ // Ensure innerWidth is at least 1 to avoid infinite loops
47
+ const calculatedInnerWidth = (width || 0) - 2 - (pad * 2);
48
+ const innerWidth = Math.max(1, calculatedInnerWidth);
49
+ if (wrap) {
50
+ this.processedLines = [];
51
+ for (const line of this.content) {
52
+ if (Styler_1.Styler.len(line) <= innerWidth) {
53
+ this.processedLines.push(line);
54
+ }
55
+ else {
56
+ // Simple wrap logic
57
+ let currentLine = line;
58
+ let safetyCounter = 0;
59
+ const MAX_LOOPS = 1000; // Prevent infinite loops
60
+ while (Styler_1.Styler.len(currentLine) > innerWidth) {
61
+ if (safetyCounter++ > MAX_LOOPS) {
62
+ console.error(`Box processContent infinite loop detected. line="${currentLine.substring(0, 20)}..."`);
63
+ break;
64
+ }
65
+ const wrapped = this.wrapLine(currentLine, innerWidth);
66
+ // If head is empty, we force a split to avoid infinite loop
67
+ if (!wrapped.head && wrapped.tail === currentLine) {
68
+ this.processedLines.push(currentLine.substring(0, innerWidth));
69
+ currentLine = currentLine.substring(innerWidth);
70
+ }
71
+ else {
72
+ this.processedLines.push(wrapped.head);
73
+ currentLine = wrapped.tail;
74
+ }
75
+ }
76
+ if (currentLine)
77
+ this.processedLines.push(currentLine);
78
+ }
79
+ }
80
+ }
81
+ else {
82
+ this.processedLines = this.content;
83
+ }
84
+ }
85
+ wrapLine(line, width) {
86
+ // Updated implementation to use Styler.len (visual width) and Styler.truncateWithState
87
+ const raw = line;
88
+ const visualLen = Styler_1.Styler.len(raw);
89
+ if (visualLen <= width)
90
+ return { head: raw, tail: '' };
91
+ // Use the new truncateWithState to correctly handle ANSI split
92
+ const { result, remaining } = Styler_1.Styler.truncateWithState(raw, width);
93
+ // Word wrapping optimization (optional but nice)
94
+ // If the cut was in the middle of a word, try to backtrack to last space.
95
+ // But backtracking with visual width and ANSI is hard.
96
+ // For now, stick to char wrap which is robust.
97
+ return {
98
+ head: result,
99
+ tail: remaining
100
+ };
101
+ }
102
+ focus() {
103
+ this.isFocused = true;
104
+ this.render(); // Re-render to show focus indicator (border color change?)
105
+ }
106
+ blur() {
107
+ this.isFocused = false;
108
+ this.render();
109
+ }
110
+ handleKey(key) {
111
+ if (!this.options.scrollable)
112
+ return false;
113
+ const { height, padding } = this.options;
114
+ const pad = padding || 1;
115
+ const innerHeight = (height || 0) - 2 - (pad * 2);
116
+ const maxScroll = Math.max(0, this.processedLines.length - innerHeight);
117
+ let consumed = false;
118
+ if (key.name === 'up') {
119
+ if (this.scrollTop > 0) {
120
+ this.scrollTop--;
121
+ consumed = true;
122
+ }
123
+ }
124
+ else if (key.name === 'down') {
125
+ if (this.scrollTop < maxScroll) {
126
+ this.scrollTop++;
127
+ consumed = true;
128
+ }
129
+ }
130
+ else if (key.name === 'pageup') {
131
+ this.scrollTop = Math.max(0, this.scrollTop - innerHeight);
132
+ consumed = true;
133
+ }
134
+ else if (key.name === 'pagedown') {
135
+ this.scrollTop = Math.min(maxScroll, this.scrollTop + innerHeight);
136
+ consumed = true;
137
+ }
138
+ if (consumed) {
139
+ this.render();
140
+ return true;
141
+ }
142
+ return false;
143
+ }
144
+ render() {
145
+ const { x, y, width, height, padding, title, style: borderStyle, borderColor, scrollable } = this.options;
146
+ const style = BORDERS[borderStyle || 'round'];
147
+ // Use bright color if focused
148
+ const color = this.isFocused ? 'brightCyan' : (borderColor || 'white');
149
+ const startX = x || 1;
150
+ const startY = y || 1;
151
+ const pad = padding || 1;
152
+ const innerWidth = (width || 0) - 2;
153
+ const innerHeight = (height || 0) - 2;
154
+ const contentHeight = innerHeight - (pad * 2);
155
+ // 1. Draw Top Border
156
+ let topBorder = style.tl + style.h.repeat(innerWidth) + style.tr;
157
+ if (title) {
158
+ const titleStr = ` ${title} `;
159
+ const leftLen = Math.floor((innerWidth - titleStr.length) / 2);
160
+ const rightLen = innerWidth - leftLen - titleStr.length;
161
+ if (leftLen >= 0) {
162
+ topBorder = style.tl + style.h.repeat(leftLen) + Styler_1.Styler.style(titleStr, 'bold') + style.h.repeat(rightLen) + style.tr;
163
+ }
164
+ }
165
+ Screen_1.Screen.write(startX, startY, Styler_1.Styler.style(topBorder, color));
166
+ // 2. Draw Content Body
167
+ for (let i = 0; i < innerHeight; i++) {
168
+ // Check if we are in padding zone or content zone
169
+ // Padding is applied at top and bottom inside the box?
170
+ // Usually padding is around content.
171
+ // Let's assume uniform padding for simplicity in calculation,
172
+ // but here we iterate lines.
173
+ let lineContent = "";
174
+ if (i >= pad && i < innerHeight - pad) {
175
+ const contentIndex = (i - pad) + this.scrollTop;
176
+ if (contentIndex < this.processedLines.length) {
177
+ lineContent = this.processedLines[contentIndex];
178
+ }
179
+ }
180
+ const rawLen = Styler_1.Styler.len(lineContent);
181
+ const contentWidth = innerWidth - (pad * 2);
182
+ // Ensure we don't overflow horizontally even if wrap failed or wasn't on
183
+ if (rawLen > contentWidth) {
184
+ lineContent = Styler_1.Styler.truncate(lineContent, contentWidth);
185
+ }
186
+ const remainingSpace = contentWidth - Styler_1.Styler.len(lineContent);
187
+ const rowString = ' '.repeat(pad) + lineContent + ' '.repeat(Math.max(0, remainingSpace)) + ' '.repeat(pad);
188
+ // Trim/Pad to exact innerWidth using visual logic if possible,
189
+ // but padEnd/substring use string length.
190
+ // Since we ensured lineContent visual length is correct and we added spaces (width 1),
191
+ // we should be careful.
192
+ // Re-calculate visual length of the constructed row
193
+ // We want 'rowString' to have visual width exactly 'innerWidth'
194
+ // If we constructed it correctly:
195
+ // pad (len) + lineContent (len) + remaining (len) + pad (len)
196
+ // = pad + (contentWidth - remaining) + remaining + pad = contentWidth + 2*pad = innerWidth.
197
+ // So visual width should be correct.
198
+ // However, padEnd/substring operate on string length.
199
+ // If lineContent has wide chars, string length < visual width.
200
+ // 'safeRowString' logic below was:
201
+ // const safeRowString = rowString.padEnd(innerWidth).substring(0, innerWidth);
202
+ // This is DANGEROUS because it truncates by string index!
203
+ // We should trust our construction above.
204
+ const safeRowString = rowString;
205
+ let rightBorder = style.v;
206
+ // Scrollbar logic
207
+ if (scrollable) {
208
+ const maxScroll = Math.max(0, this.processedLines.length - contentHeight);
209
+ if (maxScroll > 0) {
210
+ // Calculate scrollbar position
211
+ const scrollbarHeight = Math.max(1, Math.floor((contentHeight / this.processedLines.length) * contentHeight));
212
+ const scrollPercent = this.scrollTop / maxScroll;
213
+ const scrollPos = Math.floor(scrollPercent * (contentHeight - scrollbarHeight));
214
+ // Are we in the scrollbar zone? (i is relative to inner box, 0 to innerHeight-1)
215
+ // But content area is from pad to innerHeight-pad
216
+ if (i >= pad && i < innerHeight - pad) {
217
+ const contentRow = i - pad;
218
+ if (contentRow >= scrollPos && contentRow < scrollPos + scrollbarHeight) {
219
+ rightBorder = '│'; // Active scrollbar part, maybe color it differently
220
+ // Actually user suggested using unicode or color change
221
+ // Let's make it a colored block or pipe
222
+ rightBorder = Styler_1.Styler.style('│', 'brightWhite');
223
+ }
224
+ else {
225
+ rightBorder = Styler_1.Styler.style('│', 'dim'); // Track
226
+ }
227
+ }
228
+ }
229
+ }
230
+ const row = `${style.v}${safeRowString}${rightBorder}`;
231
+ Screen_1.Screen.write(startX, startY + 1 + i, Styler_1.Styler.style(row, color));
232
+ }
233
+ // 3. Draw Bottom Border
234
+ Screen_1.Screen.write(startX, startY + (height || 0) - 1, Styler_1.Styler.style(style.bl + style.h.repeat(innerWidth) + style.br, color));
235
+ }
236
+ // Static compatibility method
237
+ static render(content, options = {}) {
238
+ const box = new Box(content, options);
239
+ box.render();
240
+ }
241
+ }
242
+ exports.Box = Box;
@@ -0,0 +1,23 @@
1
+ import { Focusable } from '../core/Focus';
2
+ import * as readline from 'readline';
3
+ interface ButtonOptions {
4
+ id: string;
5
+ text: string;
6
+ x: number;
7
+ y: number;
8
+ width?: number;
9
+ style?: 'simple' | 'brackets';
10
+ onPress: () => void;
11
+ }
12
+ export declare class Button implements Focusable {
13
+ id: string;
14
+ private options;
15
+ private isFocused;
16
+ private isPressed;
17
+ constructor(options: ButtonOptions);
18
+ focus(): void;
19
+ blur(): void;
20
+ handleKey(key: readline.Key): boolean;
21
+ render(): void;
22
+ }
23
+ export {};
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Button = void 0;
4
+ const Styler_1 = require("../core/Styler");
5
+ const Screen_1 = require("../core/Screen");
6
+ class Button {
7
+ constructor(options) {
8
+ this.isFocused = false;
9
+ this.isPressed = false; // For visual feedback
10
+ this.id = options.id;
11
+ this.options = { style: 'brackets', ...options };
12
+ }
13
+ focus() {
14
+ this.isFocused = true;
15
+ this.render(); // Ensure render called on focus
16
+ }
17
+ blur() {
18
+ this.isFocused = false;
19
+ this.isPressed = false;
20
+ this.render();
21
+ }
22
+ handleKey(key) {
23
+ if (!this.isFocused)
24
+ return false;
25
+ if (key.name === 'return' || key.name === 'enter' || key.name === 'space') {
26
+ this.isPressed = true;
27
+ this.render(); // Show pressed state visually
28
+ // Trigger action slightly delayed to show visual feedback
29
+ setTimeout(() => {
30
+ this.isPressed = false;
31
+ this.render();
32
+ this.options.onPress();
33
+ }, 150);
34
+ return true;
35
+ }
36
+ return false;
37
+ }
38
+ render() {
39
+ const { x, y, text, width } = this.options;
40
+ let label = text;
41
+ // Simple visual centering if width is provided
42
+ if (width) {
43
+ const pad = Math.max(0, width - text.length);
44
+ const padLeft = Math.floor(pad / 2);
45
+ const padRight = pad - padLeft;
46
+ label = ' '.repeat(padLeft) + text + ' '.repeat(padRight);
47
+ }
48
+ let renderedText = "";
49
+ let color = 'white';
50
+ let bgStyle = '';
51
+ if (this.isFocused) {
52
+ color = this.isPressed ? 'black' : 'white';
53
+ bgStyle = this.isPressed ? 'bgGreen' : 'bgBlue'; // Green when clicked, Blue when focused
54
+ if (this.options.style === 'brackets') {
55
+ renderedText = `[ ${label} ]`;
56
+ }
57
+ else {
58
+ renderedText = ` ${label} `;
59
+ }
60
+ // Apply styles
61
+ // Note: Styler.style takes varargs, we need to handle types carefully or cast
62
+ Screen_1.Screen.write(x, y, Styler_1.Styler.style(renderedText, color, 'bold', bgStyle));
63
+ }
64
+ else {
65
+ // Normal State
66
+ if (this.options.style === 'brackets') {
67
+ renderedText = `[ ${label} ]`;
68
+ }
69
+ else {
70
+ renderedText = ` ${label} `;
71
+ }
72
+ Screen_1.Screen.write(x, y, Styler_1.Styler.style(renderedText, 'dim'));
73
+ }
74
+ }
75
+ }
76
+ exports.Button = Button;
@@ -0,0 +1,20 @@
1
+ import { Focusable } from '../core/Focus';
2
+ import * as readline from 'readline';
3
+ export interface CheckboxOptions {
4
+ id: string;
5
+ label: string;
6
+ checked?: boolean;
7
+ x: number;
8
+ y: number;
9
+ }
10
+ export declare class Checkbox implements Focusable {
11
+ id: string;
12
+ checked: boolean;
13
+ private options;
14
+ private isFocused;
15
+ constructor(options: CheckboxOptions);
16
+ focus(): void;
17
+ blur(): void;
18
+ handleKey(key: readline.Key): boolean;
19
+ render(): void;
20
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Checkbox = void 0;
4
+ const Styler_1 = require("../core/Styler");
5
+ const Screen_1 = require("../core/Screen");
6
+ class Checkbox {
7
+ constructor(options) {
8
+ this.isFocused = false;
9
+ this.id = options.id;
10
+ this.options = options;
11
+ this.checked = options.checked || false;
12
+ }
13
+ focus() {
14
+ this.isFocused = true;
15
+ this.render();
16
+ }
17
+ blur() {
18
+ this.isFocused = false;
19
+ this.render();
20
+ }
21
+ handleKey(key) {
22
+ if (!this.isFocused)
23
+ return false;
24
+ if (key.name === 'space' || key.name === 'return' || key.name === 'enter') {
25
+ this.checked = !this.checked;
26
+ this.render();
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ render() {
32
+ const { x, y, label } = this.options;
33
+ const box = this.checked ? '[x]' : '[ ]';
34
+ const style = this.isFocused ? 'brightCyan' : 'white';
35
+ const boxStyle = this.checked ? 'green' : 'dim';
36
+ const content = Styler_1.Styler.style(box, boxStyle) + ' ' + Styler_1.Styler.style(label, style);
37
+ Screen_1.Screen.write(x, y, content);
38
+ }
39
+ }
40
+ exports.Checkbox = Checkbox;