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
@@ -0,0 +1,23 @@
1
+ import { Focusable } from '../core/Focus';
2
+ import * as readline from 'readline';
3
+ export interface InputOptions {
4
+ id: string;
5
+ label?: string;
6
+ placeholder?: string;
7
+ x: number;
8
+ y: number;
9
+ width?: number;
10
+ type?: 'text' | 'password';
11
+ }
12
+ export declare class InputField implements Focusable {
13
+ id: string;
14
+ value: string;
15
+ private isFocused;
16
+ private options;
17
+ constructor(options: InputOptions);
18
+ focus(): void;
19
+ blur(): void;
20
+ setValue(val: string): void;
21
+ handleKey(key: readline.Key): boolean;
22
+ render(): void;
23
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InputField = void 0;
4
+ const Styler_1 = require("../core/Styler");
5
+ const Screen_1 = require("../core/Screen");
6
+ class InputField {
7
+ constructor(options) {
8
+ this.value = "";
9
+ this.isFocused = false;
10
+ this.id = options.id;
11
+ this.options = { width: 30, type: 'text', ...options };
12
+ }
13
+ focus() {
14
+ this.isFocused = true;
15
+ }
16
+ blur() {
17
+ this.isFocused = false;
18
+ }
19
+ setValue(val) {
20
+ this.value = val;
21
+ }
22
+ handleKey(key) {
23
+ if (!this.isFocused)
24
+ return false;
25
+ if (key.name === 'backspace') {
26
+ this.value = this.value.slice(0, -1);
27
+ this.render();
28
+ return true;
29
+ }
30
+ else if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
31
+ // Basic text input filtering
32
+ // Only accept printable characters (rough check)
33
+ if (/^[\x20-\x7E]$/.test(key.sequence)) {
34
+ this.value += key.sequence;
35
+ this.render();
36
+ return true;
37
+ }
38
+ }
39
+ // Did not consume (e.g., arrow keys, tabs)
40
+ return false;
41
+ }
42
+ render() {
43
+ const { x, y, label, width, type } = this.options;
44
+ const labelText = label ? `${label}: ` : '';
45
+ const labelLen = labelText.length;
46
+ // Draw Label
47
+ if (label) {
48
+ const labelStyle = this.isFocused ? 'brightCyan' : 'dim';
49
+ Screen_1.Screen.write(x, y, Styler_1.Styler.style(labelText, labelStyle, 'bold'));
50
+ }
51
+ // Draw Input Box Background
52
+ const inputX = x + labelLen;
53
+ const maxWidth = (width || 30) - labelLen;
54
+ let displayValue = this.value;
55
+ if (type === 'password') {
56
+ displayValue = '*'.repeat(this.value.length);
57
+ }
58
+ // Cursor logic
59
+ const showCursor = this.isFocused;
60
+ const cursorChar = '█';
61
+ // Display content calculation
62
+ // Ensure we don't overflow width
63
+ if (displayValue.length >= maxWidth - 1) {
64
+ const start = displayValue.length - (maxWidth - 2);
65
+ displayValue = displayValue.substring(start);
66
+ }
67
+ const fieldContent = displayValue + (showCursor ? Styler_1.Styler.style(cursorChar, 'green') : ' ');
68
+ // Fill remaining space to clear old chars
69
+ const paddingLen = Math.max(0, maxWidth - Styler_1.Styler.len(fieldContent));
70
+ const padding = ' '.repeat(paddingLen);
71
+ const style = this.isFocused ? 'white' : 'gray';
72
+ Screen_1.Screen.write(inputX, y, Styler_1.Styler.style(fieldContent + padding, style));
73
+ // Underline
74
+ const underline = '─'.repeat(maxWidth);
75
+ const underlineColor = this.isFocused ? 'brightGreen' : 'dim';
76
+ Screen_1.Screen.write(inputX, y + 1, Styler_1.Styler.style(underline, underlineColor));
77
+ }
78
+ }
79
+ exports.InputField = InputField;
@@ -0,0 +1,27 @@
1
+ import { Focusable } from '../core/Focus';
2
+ import * as readline from 'readline';
3
+ interface MenuOptions {
4
+ id?: string;
5
+ title?: string;
6
+ items: string[];
7
+ onSelect: (index: number, item: string) => void;
8
+ x?: number;
9
+ y?: number;
10
+ }
11
+ export declare class Menu implements Focusable {
12
+ id: string;
13
+ private selectedIndex;
14
+ private options;
15
+ private active;
16
+ private isFocused;
17
+ constructor(options: MenuOptions);
18
+ focus(): void;
19
+ blur(): void;
20
+ handleKey(key: readline.Key): boolean;
21
+ render(): void;
22
+ /**
23
+ * Legacy Standalone Start
24
+ */
25
+ start(): void;
26
+ }
27
+ export {};
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Menu = void 0;
4
+ const Input_1 = require("../core/Input");
5
+ const Screen_1 = require("../core/Screen");
6
+ const Styler_1 = require("../core/Styler");
7
+ const Cursor_1 = require("../core/Cursor");
8
+ class Menu {
9
+ constructor(options) {
10
+ this.selectedIndex = 0;
11
+ this.active = false; // "Active" in standalone mode, or used for "Focused" logic?
12
+ this.isFocused = false;
13
+ this.id = options.id || `menu-${Math.random().toString(36).substr(2, 9)}`;
14
+ this.options = options;
15
+ this.options.x = options.x || 2;
16
+ this.options.y = options.y || 2;
17
+ }
18
+ focus() {
19
+ this.isFocused = true;
20
+ this.render();
21
+ }
22
+ blur() {
23
+ this.isFocused = false;
24
+ this.render();
25
+ }
26
+ handleKey(key) {
27
+ if (!this.isFocused && !this.active)
28
+ return false;
29
+ let consumed = false;
30
+ switch (key.name) {
31
+ case 'up':
32
+ this.selectedIndex = (this.selectedIndex - 1 + this.options.items.length) % this.options.items.length;
33
+ this.render();
34
+ consumed = true;
35
+ break;
36
+ case 'down':
37
+ this.selectedIndex = (this.selectedIndex + 1) % this.options.items.length;
38
+ this.render();
39
+ consumed = true;
40
+ break;
41
+ case 'return': // Enter key
42
+ case 'enter':
43
+ //this.active = false; // Don't deactivate if managed by FocusManager?
44
+ // Provide visual feedback
45
+ // We should probably just call select
46
+ this.options.onSelect(this.selectedIndex, this.options.items[this.selectedIndex]);
47
+ consumed = true;
48
+ break;
49
+ case 'escape':
50
+ // Maybe defocus or exit?
51
+ break;
52
+ }
53
+ return consumed;
54
+ }
55
+ render() {
56
+ const { x, y, items, title } = this.options;
57
+ if (title) {
58
+ Screen_1.Screen.write(x, y, Styler_1.Styler.style(title, 'bold', 'underline'));
59
+ }
60
+ items.forEach((item, index) => {
61
+ const isSelected = index === this.selectedIndex;
62
+ // Show different cursor if focused
63
+ const prefix = isSelected ? (this.isFocused || this.active ? '❯ ' : '> ') : ' ';
64
+ let style = ['dim'];
65
+ if (isSelected) {
66
+ style = this.isFocused || this.active ? ['cyan', 'bold'] : ['white'];
67
+ }
68
+ const label = Styler_1.Styler.style(item, ...style);
69
+ // +1 (or +2) for title offset
70
+ Screen_1.Screen.write(x, y + index + (title ? 2 : 0), `${prefix}${label}`);
71
+ });
72
+ }
73
+ /**
74
+ * Legacy Standalone Start
75
+ */
76
+ start() {
77
+ this.active = true;
78
+ Cursor_1.Cursor.hide(); // Hide cursor for cleaner UI
79
+ this.render();
80
+ // This effectively takes over input globally, effectively "Mocking" FocusManager for this single component
81
+ Input_1.Input.onKey((key) => {
82
+ if (!this.active)
83
+ return;
84
+ // Re-use logic
85
+ const consumed = this.handleKey(key);
86
+ if (key.name === 'q' || key.name === 'escape') {
87
+ this.active = false;
88
+ process.exit(0);
89
+ }
90
+ });
91
+ }
92
+ }
93
+ exports.Menu = Menu;
@@ -0,0 +1,7 @@
1
+ import { ColorName } from '../core/Styler';
2
+ export declare class Popup {
3
+ static alert(message: string, options?: {
4
+ title?: string;
5
+ color?: ColorName;
6
+ }): void;
7
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Popup = void 0;
4
+ const Box_1 = require("./Box");
5
+ const Screen_1 = require("../core/Screen");
6
+ const Styler_1 = require("../core/Styler");
7
+ const Input_1 = require("../core/Input");
8
+ class Popup {
9
+ static alert(message, options = {}) {
10
+ // Mount a high-priority render function
11
+ const renderPopup = () => {
12
+ const { width, height } = Screen_1.Screen.size;
13
+ const lines = message.split('\n');
14
+ const maxLen = Math.max(...lines.map(l => Styler_1.Styler.len(l)));
15
+ const boxWidth = Math.max(maxLen + 6, 40);
16
+ const boxHeight = lines.length + 4;
17
+ const x = Math.floor((width - boxWidth) / 2);
18
+ const y = Math.floor((height - boxHeight) / 2);
19
+ // Manually clear popup area for opacity
20
+ for (let i = 0; i < boxHeight; i++) {
21
+ Screen_1.Screen.write(x, y + i, " ".repeat(boxWidth));
22
+ }
23
+ Box_1.Box.render([
24
+ ...lines
25
+ ], {
26
+ x, y,
27
+ width: boxWidth,
28
+ height: boxHeight,
29
+ title: options.title || "ALERT",
30
+ borderColor: options.color || 'red',
31
+ style: 'double',
32
+ padding: 1
33
+ });
34
+ Screen_1.Screen.write(x + 2, y + boxHeight - 1, Styler_1.Styler.style("[Press Enter]", 'dim'));
35
+ };
36
+ const POPUP_Z_INDEX = 9999;
37
+ Screen_1.Screen.mount(renderPopup, POPUP_Z_INDEX);
38
+ const handler = (key) => {
39
+ if (key.name === 'return' || key.name === 'enter' || key.name === 'escape') {
40
+ Screen_1.Screen.unmount(renderPopup);
41
+ Input_1.Input.offKey(handler);
42
+ return true; // Stop propagation
43
+ }
44
+ return true; // Consume other keys while popup is open?
45
+ // Usually alerts are modal, so yes, consume everything.
46
+ };
47
+ Input_1.Input.onKey(handler);
48
+ }
49
+ }
50
+ exports.Popup = Popup;
@@ -0,0 +1,28 @@
1
+ import { ColorName } from '../core/Styler';
2
+ interface ProgressBarOptions {
3
+ width?: number;
4
+ total?: number;
5
+ completeChar?: string;
6
+ incompleteChar?: string;
7
+ format?: string;
8
+ color?: ColorName;
9
+ x?: number;
10
+ y?: number;
11
+ }
12
+ export declare class ProgressBar {
13
+ private current;
14
+ private total;
15
+ private width;
16
+ private chars;
17
+ private format;
18
+ private color;
19
+ private x;
20
+ private y;
21
+ constructor(options?: ProgressBarOptions);
22
+ update(current: number, tokens?: Record<string, string>, pos?: {
23
+ x: number;
24
+ y: number;
25
+ }): void;
26
+ finish(): void;
27
+ }
28
+ export {};
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProgressBar = void 0;
4
+ const Styler_1 = require("../core/Styler");
5
+ const Screen_1 = require("../core/Screen");
6
+ class ProgressBar {
7
+ constructor(options = {}) {
8
+ this.current = 0;
9
+ this.total = options.total || 100;
10
+ this.width = options.width || 40;
11
+ this.chars = {
12
+ complete: options.completeChar || '█',
13
+ incomplete: options.incompleteChar || '░'
14
+ };
15
+ this.format = options.format || "[:bar] :percent";
16
+ this.color = options.color || 'green';
17
+ this.x = options.x || 0;
18
+ this.y = options.y || 0;
19
+ }
20
+ // Allow overriding position in update
21
+ update(current, tokens = {}, pos) {
22
+ this.current = current;
23
+ // Use provided pos or stored pos
24
+ const drawX = pos?.x ?? this.x;
25
+ const drawY = pos?.y ?? this.y;
26
+ const ratio = Math.min(Math.max(current / this.total, 0), 1);
27
+ const percent = Math.floor(ratio * 100);
28
+ const completeLen = Math.round(this.width * ratio);
29
+ const incompleteLen = this.width - completeLen;
30
+ const bar = Styler_1.Styler.style(this.chars.complete.repeat(completeLen), this.color) +
31
+ Styler_1.Styler.style(this.chars.incomplete.repeat(incompleteLen), 'dim');
32
+ let str = this.format
33
+ .replace(':bar', bar)
34
+ .replace(':percent', Styler_1.Styler.style(`${percent}%`.padStart(4), 'bold'));
35
+ // Replace custom tokens (e.g., :msg)
36
+ for (const [key, val] of Object.entries(tokens)) {
37
+ str = str.replace(`:${key}`, val);
38
+ }
39
+ // Use Screen.write instead of direct stdout
40
+ Screen_1.Screen.write(drawX, drawY, str);
41
+ }
42
+ finish() {
43
+ // No-op in buffered mode, or maybe clear?
44
+ // Or keep last state.
45
+ }
46
+ }
47
+ exports.ProgressBar = ProgressBar;
@@ -0,0 +1,22 @@
1
+ import { Focusable } from '../core/Focus';
2
+ import * as readline from 'readline';
3
+ export interface RadioGroupOptions {
4
+ id: string;
5
+ label?: string;
6
+ items: string[];
7
+ selectedIndex?: number;
8
+ x: number;
9
+ y: number;
10
+ direction?: 'horizontal' | 'vertical';
11
+ }
12
+ export declare class RadioGroup implements Focusable {
13
+ id: string;
14
+ selectedIndex: number;
15
+ private options;
16
+ private isFocused;
17
+ constructor(options: RadioGroupOptions);
18
+ focus(): void;
19
+ blur(): void;
20
+ handleKey(key: readline.Key): boolean;
21
+ render(): void;
22
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RadioGroup = void 0;
4
+ const Styler_1 = require("../core/Styler");
5
+ const Screen_1 = require("../core/Screen");
6
+ class RadioGroup {
7
+ constructor(options) {
8
+ this.isFocused = false;
9
+ this.id = options.id;
10
+ this.options = { direction: 'vertical', ...options };
11
+ this.selectedIndex = options.selectedIndex || 0;
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
+ const count = this.options.items.length;
25
+ if (key.name === 'up' || key.name === 'left') {
26
+ this.selectedIndex = (this.selectedIndex - 1 + count) % count;
27
+ this.render();
28
+ return true;
29
+ }
30
+ else if (key.name === 'down' || key.name === 'right') {
31
+ this.selectedIndex = (this.selectedIndex + 1) % count;
32
+ this.render();
33
+ return true;
34
+ }
35
+ return false;
36
+ }
37
+ render() {
38
+ const { x, y, label, items, direction } = this.options;
39
+ let currentX = x;
40
+ let currentY = y;
41
+ if (label) {
42
+ Screen_1.Screen.write(currentX, currentY, Styler_1.Styler.style(label + ':', 'bold'));
43
+ if (direction === 'vertical')
44
+ currentY++;
45
+ else
46
+ currentX += label.length + 2;
47
+ }
48
+ items.forEach((item, index) => {
49
+ const isSelected = index === this.selectedIndex;
50
+ const symbol = isSelected ? '(*)' : '( )';
51
+ const symbolColor = isSelected ? 'green' : 'dim';
52
+ const textColor = this.isFocused && isSelected ? 'brightCyan' : (this.isFocused ? 'white' : 'gray');
53
+ const content = Styler_1.Styler.style(symbol, symbolColor) + ' ' + Styler_1.Styler.style(item, textColor);
54
+ Screen_1.Screen.write(currentX, currentY, content);
55
+ if (direction === 'vertical') {
56
+ currentY++;
57
+ }
58
+ else {
59
+ currentX += item.length + 6; // spacing
60
+ }
61
+ });
62
+ }
63
+ }
64
+ exports.RadioGroup = RadioGroup;
@@ -0,0 +1,24 @@
1
+ import { Focusable } from '../core/Focus';
2
+ import * as readline from 'readline';
3
+ export interface SelectOptions {
4
+ id: string;
5
+ label?: string;
6
+ items: string[];
7
+ x: number;
8
+ y: number;
9
+ width?: number;
10
+ onSelect?: (index: number, item: string) => void;
11
+ }
12
+ export declare class Select implements Focusable {
13
+ id: string;
14
+ private options;
15
+ private selectedIndex;
16
+ private isFocused;
17
+ private isOpen;
18
+ constructor(options: SelectOptions);
19
+ focus(): void;
20
+ blur(): void;
21
+ handleKey(key: readline.Key): boolean;
22
+ private clearDropdownArea;
23
+ render(): void;
24
+ }
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Select = void 0;
4
+ const Styler_1 = require("../core/Styler");
5
+ const Screen_1 = require("../core/Screen");
6
+ class Select {
7
+ constructor(options) {
8
+ this.selectedIndex = 0;
9
+ this.isFocused = false;
10
+ this.isOpen = false;
11
+ this.id = options.id;
12
+ this.options = { width: 20, ...options };
13
+ }
14
+ focus() {
15
+ this.isFocused = true;
16
+ this.render();
17
+ }
18
+ blur() {
19
+ this.isFocused = false;
20
+ if (this.isOpen) {
21
+ this.isOpen = false;
22
+ this.clearDropdownArea(); // Clear artifacts
23
+ }
24
+ this.render();
25
+ }
26
+ handleKey(key) {
27
+ if (!this.isFocused)
28
+ return false;
29
+ if (this.isOpen) {
30
+ // Dropdown navigation
31
+ if (key.name === 'up') {
32
+ this.selectedIndex = (this.selectedIndex - 1 + this.options.items.length) % this.options.items.length;
33
+ this.render();
34
+ return true;
35
+ }
36
+ else if (key.name === 'down') {
37
+ this.selectedIndex = (this.selectedIndex + 1) % this.options.items.length;
38
+ this.render();
39
+ return true;
40
+ }
41
+ else if (key.name === 'return' || key.name === 'enter' || key.name === 'escape') {
42
+ this.isOpen = false;
43
+ this.clearDropdownArea(); // Clear artifacts
44
+ if (key.name !== 'escape' && this.options.onSelect) {
45
+ this.options.onSelect(this.selectedIndex, this.options.items[this.selectedIndex]);
46
+ }
47
+ this.render();
48
+ return true;
49
+ }
50
+ }
51
+ else {
52
+ // Closed state navigation
53
+ if (key.name === 'return' || key.name === 'enter' || key.name === 'space') {
54
+ this.isOpen = true;
55
+ this.render();
56
+ return true;
57
+ }
58
+ // Optional: Allow arrows to cycle through items without opening?
59
+ // Usually selects allow this.
60
+ if (key.name === 'down') {
61
+ this.selectedIndex = (this.selectedIndex + 1) % this.options.items.length;
62
+ if (this.options.onSelect)
63
+ this.options.onSelect(this.selectedIndex, this.options.items[this.selectedIndex]);
64
+ this.render();
65
+ return true;
66
+ }
67
+ if (key.name === 'up') {
68
+ this.selectedIndex = (this.selectedIndex - 1 + this.options.items.length) % this.options.items.length;
69
+ if (this.options.onSelect)
70
+ this.options.onSelect(this.selectedIndex, this.options.items[this.selectedIndex]);
71
+ this.render();
72
+ return true;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+ clearDropdownArea() {
78
+ const { x, y, width, items, label } = this.options;
79
+ const w = width || 20;
80
+ const labelText = label ? `${label}: ` : '';
81
+ const inputX = x + labelText.length;
82
+ // Clear the exact area where dropdown items were drawn
83
+ for (let i = 0; i < items.length; i++) {
84
+ const itemY = y + 1 + i;
85
+ // Write spaces to clear. We use ' ' * w.
86
+ // This might wipe underlying content, but it removes the ghost text.
87
+ Screen_1.Screen.write(inputX, itemY, ' '.repeat(w));
88
+ }
89
+ }
90
+ render() {
91
+ const { x, y, width, label, items } = this.options;
92
+ const w = width || 20;
93
+ const labelText = label ? `${label}: ` : '';
94
+ const labelLen = labelText.length;
95
+ if (label) {
96
+ const labelStyle = this.isFocused ? 'brightCyan' : 'white';
97
+ // If not focused, make it dim using modifier
98
+ if (this.isFocused) {
99
+ Screen_1.Screen.write(x, y, Styler_1.Styler.style(labelText, 'brightCyan', 'bold'));
100
+ }
101
+ else {
102
+ Screen_1.Screen.write(x, y, Styler_1.Styler.style(labelText, 'white', 'dim'));
103
+ }
104
+ }
105
+ const inputX = x + labelLen;
106
+ const selectedItem = items[this.selectedIndex] || "";
107
+ const arrow = this.isOpen ? '▲' : '▼';
108
+ // Draw the main box
109
+ const content = ` ${selectedItem} `.padEnd(w - 2) + arrow + ' ';
110
+ const style = this.isFocused ? 'bgBlue' : 'bgWhite';
111
+ const fg = this.isFocused ? 'white' : 'black';
112
+ Screen_1.Screen.write(inputX, y, Styler_1.Styler.style(content, style, fg));
113
+ // Draw Dropdown if open
114
+ if (this.isOpen) {
115
+ for (let i = 0; i < items.length; i++) {
116
+ const itemY = y + 1 + i;
117
+ const isSelected = i === this.selectedIndex;
118
+ const itemContent = ` ${items[i]} `.padEnd(w);
119
+ const itemStyle = isSelected ? 'bgCyan' : 'bgWhite';
120
+ const itemFg = isSelected ? 'black' : 'black';
121
+ Screen_1.Screen.write(inputX, itemY, Styler_1.Styler.style(itemContent, itemStyle, itemFg));
122
+ }
123
+ }
124
+ }
125
+ }
126
+ exports.Select = Select;
@@ -0,0 +1,24 @@
1
+ import { Focusable } from '../core/Focus';
2
+ import * as readline from 'readline';
3
+ export interface TabOptions {
4
+ id: string;
5
+ titles: string[];
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ initialTab?: number;
11
+ onTabChange?: (index: number) => void;
12
+ }
13
+ export declare class TabManager implements Focusable {
14
+ id: string;
15
+ private options;
16
+ private activeIndex;
17
+ private isFocused;
18
+ constructor(options: TabOptions);
19
+ focus(): void;
20
+ blur(): void;
21
+ handleKey(key: readline.Key): boolean;
22
+ private triggerChange;
23
+ render(): void;
24
+ }