voonex 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -6,10 +6,11 @@
6
6
 
7
7
  - **Zero Dependencies**: Lightweight and easy to audit.
8
8
  - **Double Buffering & Diffing**: Efficient rendering that eliminates flickering and minimizes I/O.
9
+ - **Reactive Signals**: Modern state management inspired by SolidJS. State changes automatically trigger updates.
10
+ - **Component Lifecycle**: Class-based components with `mount`, `unmount`, and lifecycle hooks.
9
11
  - **Auto Layout**: Flexbox-like layout engine for responsive designs.
10
12
  - **Layer Management**: Z-index support for Modals, Tooltips, and Popups.
11
13
  - **Component System**: Built-in widgets like `Box`, `Menu`, `ProgressBar`, `Input`, `Table`, and more.
12
- - **Reactive Rendering**: Automated screen updates via `Screen.mount()` and `Screen.scheduleRender()`.
13
14
  - **Focus Management**: Built-in keyboard navigation and focus delegation.
14
15
  - **Styling Engine**: Simple yet powerful API for ANSI colors and text modifiers.
15
16
  - **TypeScript Support**: Written in TypeScript with full type definitions included.
@@ -24,41 +25,48 @@ npm install voonex
24
25
 
25
26
  ## Quick Start
26
27
 
27
- Here is a minimal example showing how to initialize the screen and display a simple box.
28
+ Here is a minimal example showing how to create a reactive counter app.
28
29
 
29
30
  ```typescript
30
- import { Screen, Box, Styler, Input } from 'voonex';
31
-
32
- // 1. Enter the alternate screen buffer
33
- Screen.enter();
31
+ import { Screen, Component, createSignal, Input } from 'voonex';
32
+
33
+ // 1. Create a Component
34
+ class CounterApp extends Component {
35
+ // Define reactive state
36
+ private count = createSignal(0);
37
+
38
+ // Getters/Setters for convenience
39
+ get value() { return this.count[0](); }
40
+ set value(v) { this.count[1](v); }
41
+
42
+ constructor() {
43
+ super();
44
+
45
+ // Handle input to increment
46
+ Input.onKey(key => {
47
+ if (key.name === 'up') this.value = this.value + 1;
48
+ if (key.name === 'down') this.value = this.value - 1;
49
+ if (key.name === 'q') {
50
+ Screen.leave();
51
+ process.exit(0);
52
+ }
53
+ });
54
+ }
34
55
 
35
- // 2. Define your render function
36
- function render() {
37
- // Render a Box at (5, 5) with a title
38
- Box.render([
39
- "Welcome to Voonex!",
40
- "Press 'q' to exit."
41
- ], {
42
- title: "Hello World",
43
- x: 5,
44
- y: 5,
45
- padding: 1,
46
- style: 'double',
47
- borderColor: 'cyan'
48
- });
56
+ // 2. Implement render()
57
+ // It runs automatically whenever 'this.value' changes!
58
+ render() {
59
+ Screen.write(5, 5, `Count: ${this.value} `);
60
+ Screen.write(5, 7, "Press Up/Down to change, Q to quit.");
61
+ }
49
62
  }
50
63
 
51
- // 3. Mount the render function to the screen loop
52
- Screen.mount(render);
64
+ // 3. Setup Screen
65
+ Screen.enter();
53
66
 
54
- // 4. Handle Input
55
- Input.onKey((key) => {
56
- if (key.name === 'q') {
57
- // Leave the screen buffer properly before exiting
58
- Screen.leave();
59
- process.exit(0);
60
- }
61
- });
67
+ // 4. Mount the App
68
+ const app = new CounterApp();
69
+ app.mount();
62
70
  ```
63
71
 
64
72
  Run it with:
@@ -68,32 +76,48 @@ npx ts-node my-app.ts
68
76
 
69
77
  ## Core Concepts
70
78
 
79
+ ### Reactive Signals
80
+ Voonex uses a fine-grained reactivity system. When you update a signal, Voonex automatically schedules a render for the next tick. No manual `render()` calls required.
81
+
82
+ ```typescript
83
+ import { createSignal } from 'voonex';
84
+
85
+ const [count, setCount] = createSignal(0);
86
+
87
+ // Reading the value
88
+ console.log(count());
89
+
90
+ // Updating the value (triggers UI update)
91
+ setCount(5);
92
+ setCount(prev => prev + 1);
93
+ ```
94
+
95
+ ### Components & Lifecycle
96
+ Components extend the `Component` abstract class.
97
+
98
+ - `mount(zIndex?)`: Registers the component to the screen loop.
99
+ - `unmount()`: Removes the component.
100
+ - `render()`: The drawing logic.
101
+
102
+ **Lifecycle Hooks:**
103
+ - `init()`: Called on instantiation.
104
+ - `onMount()`: Called after mounting.
105
+ - `onUnmount()`: Called after unmounting.
106
+ - `destroy()`: Cleanup hook.
107
+
71
108
  ### The Screen
72
109
  The `Screen` class is the heart of Voonex. It manages the terminal buffer, handles resizing, and optimizes rendering using a diffing algorithm.
73
110
 
74
111
  - `Screen.enter()`: Switches to the alternate buffer (like `vim` or `nano`).
75
112
  - `Screen.leave()`: Restores the original terminal state. **Always call this before exiting.**
76
- - `Screen.mount(renderFn, layer?)`: Registers a function to be called during the render cycle.
77
- - `Screen.scheduleRender()`: Triggers a screen update.
78
113
 
79
114
  #### Layer Management
80
115
  Voonex uses a "Painter's Algorithm" with Z-index layers.
81
116
  ```typescript
82
- import { Screen, Layer } from 'voonex';
117
+ import { Layer } from 'voonex';
83
118
 
84
- Screen.mount(drawBackground, Layer.BACKGROUND); // 0
85
- Screen.mount(drawContent, Layer.CONTENT); // 10
86
- Screen.mount(drawPopup, Layer.MODAL); // 100
87
- ```
88
-
89
- ### Input Handling
90
- Voonex provides global input listeners.
91
-
92
- **Keyboard:**
93
- ```typescript
94
- Input.onKey((key) => {
95
- console.log(key.name);
96
- });
119
+ // Components handle this automatically via mount()
120
+ myComponent.mount(Layer.MODAL);
97
121
  ```
98
122
 
99
123
  ### Layout Engine
@@ -114,7 +138,7 @@ const sidebarRect = rects[0];
114
138
  const contentRect = rects[1];
115
139
  ```
116
140
 
117
- ## Components
141
+ ## Built-in Components
118
142
 
119
143
  ### Box
120
144
  A container for text with optional borders, padding, and titles.
@@ -131,8 +155,8 @@ Box.render([
131
155
  });
132
156
  ```
133
157
 
134
- ### Button
135
- Interactive button that supports Enter.
158
+ ### Button (Reactive)
159
+ Interactive button that supports focus and press states.
136
160
 
137
161
  ```typescript
138
162
  const btn = new Button({
@@ -141,6 +165,8 @@ const btn = new Button({
141
165
  x: 10, y: 10,
142
166
  onPress: () => submitForm()
143
167
  });
168
+
169
+ btn.mount(); // Don't forget to mount!
144
170
  ```
145
171
 
146
172
  ### Input Field
@@ -170,6 +196,10 @@ A modal dialog that overlays other content (uses `Layer.MODAL`).
170
196
  await Popup.alert("This is an important message!", { title: "Alert" });
171
197
  ```
172
198
 
199
+
200
+ > Voonex is currently in Beta stage.
201
+
202
+
173
203
  ## License
174
204
 
175
205
  This project is under the **MIT License**.
@@ -1,3 +1,4 @@
1
+ import { Component } from '../core/Component';
1
2
  import { Focusable } from '../core/Focus';
2
3
  import * as readline from 'readline';
3
4
  interface ButtonOptions {
@@ -9,11 +10,16 @@ interface ButtonOptions {
9
10
  style?: 'simple' | 'brackets';
10
11
  onPress: () => void;
11
12
  }
12
- export declare class Button implements Focusable {
13
+ export declare class Button extends Component implements Focusable {
13
14
  id: string;
15
+ parent?: Focusable;
14
16
  private options;
15
- private isFocused;
16
- private isPressed;
17
+ private isFocusedSignal;
18
+ private isPressedSignal;
19
+ private get isFocused();
20
+ private set isFocused(value);
21
+ private get isPressed();
22
+ private set isPressed(value);
17
23
  constructor(options: ButtonOptions);
18
24
  focus(): void;
19
25
  blur(): void;
@@ -3,21 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Button = void 0;
4
4
  const Styler_1 = require("../core/Styler");
5
5
  const Screen_1 = require("../core/Screen");
6
- class Button {
6
+ const Component_1 = require("../core/Component");
7
+ const Signal_1 = require("../core/Signal");
8
+ class Button extends Component_1.Component {
9
+ // Getters/Setters for convenience
10
+ get isFocused() { return this.isFocusedSignal[0](); }
11
+ set isFocused(v) { this.isFocusedSignal[1](v); }
12
+ get isPressed() { return this.isPressedSignal[0](); }
13
+ set isPressed(v) { this.isPressedSignal[1](v); }
7
14
  constructor(options) {
8
- this.isFocused = false;
9
- this.isPressed = false; // For visual feedback
15
+ super();
16
+ // State managed by signals (implicit render on change)
17
+ this.isFocusedSignal = (0, Signal_1.createSignal)(false);
18
+ this.isPressedSignal = (0, Signal_1.createSignal)(false);
10
19
  this.id = options.id;
11
20
  this.options = { style: 'brackets', ...options };
12
21
  }
13
22
  focus() {
14
23
  this.isFocused = true;
15
- this.render(); // Ensure render called on focus
24
+ // No need to call render(), signal handles it
16
25
  }
17
26
  blur() {
18
27
  this.isFocused = false;
19
28
  this.isPressed = false;
20
- this.render();
29
+ // No need to call render(), signal handles it
21
30
  }
22
31
  handleKey(key) {
23
32
  if (!this.isFocused)
@@ -30,16 +39,17 @@ class Button {
30
39
  }
31
40
  triggerPress() {
32
41
  this.isPressed = true;
33
- this.render(); // Show pressed state visually
42
+ // render triggered by signal
34
43
  // Trigger action slightly delayed to show visual feedback
35
44
  setTimeout(() => {
36
45
  this.isPressed = false;
37
- this.render();
38
46
  this.options.onPress();
39
47
  }, 150);
40
48
  }
41
49
  render() {
42
50
  const { x, y, text, width } = this.options;
51
+ // Reading signals here (isFocused, isPressed) creates the dependency
52
+ // although in our simple system, any signal write triggers global render.
43
53
  let label = text;
44
54
  // Simple visual centering if width is provided
45
55
  if (width) {
@@ -61,7 +71,6 @@ class Button {
61
71
  renderedText = ` ${label} `;
62
72
  }
63
73
  // Apply styles
64
- // Note: Styler.style takes varargs, we need to handle types carefully or cast
65
74
  Screen_1.Screen.write(x, y, Styler_1.Styler.style(renderedText, color, 'bold', bgStyle));
66
75
  }
67
76
  else {
@@ -4,3 +4,37 @@ export interface ComponentLifecycle {
4
4
  onMount?(): void;
5
5
  onUnmount?(): void;
6
6
  }
7
+ export declare abstract class Component implements ComponentLifecycle {
8
+ private mounted;
9
+ private boundRender;
10
+ constructor();
11
+ /**
12
+ * Called when the component is instantiated.
13
+ */
14
+ init?(): void;
15
+ /**
16
+ * Called before the component is destroyed.
17
+ */
18
+ destroy?(): void;
19
+ /**
20
+ * Called when the component is mounted to the screen.
21
+ */
22
+ onMount?(): void;
23
+ /**
24
+ * Called when the component is unmounted from the screen.
25
+ */
26
+ onUnmount?(): void;
27
+ /**
28
+ * The render function that draws the component to the screen.
29
+ */
30
+ abstract render(): void;
31
+ /**
32
+ * Mounts the component to the Screen rendering loop.
33
+ * @param zIndex Layer priority (default: Layer.CONTENT)
34
+ */
35
+ mount(zIndex?: number): void;
36
+ /**
37
+ * Unmounts the component from the Screen rendering loop.
38
+ */
39
+ unmount(): void;
40
+ }
@@ -3,3 +3,34 @@
3
3
  // CORE: COMPONENT LIFECYCLE
4
4
  // ==========================================
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Component = void 0;
7
+ const Screen_1 = require("./Screen");
8
+ class Component {
9
+ constructor() {
10
+ this.mounted = false;
11
+ this.boundRender = this.render.bind(this);
12
+ this.init?.();
13
+ }
14
+ /**
15
+ * Mounts the component to the Screen rendering loop.
16
+ * @param zIndex Layer priority (default: Layer.CONTENT)
17
+ */
18
+ mount(zIndex = Screen_1.Layer.CONTENT) {
19
+ if (this.mounted)
20
+ return;
21
+ this.mounted = true;
22
+ Screen_1.Screen.mount(this.boundRender, zIndex);
23
+ this.onMount?.();
24
+ }
25
+ /**
26
+ * Unmounts the component from the Screen rendering loop.
27
+ */
28
+ unmount() {
29
+ if (!this.mounted)
30
+ return;
31
+ this.mounted = false;
32
+ Screen_1.Screen.unmount(this.boundRender);
33
+ this.onUnmount?.();
34
+ }
35
+ }
36
+ exports.Component = Component;
@@ -0,0 +1,22 @@
1
+ export type Accessor<T> = () => T;
2
+ export type Setter<T> = (value: T | ((prev: T) => T)) => void;
3
+ /**
4
+ * Creates a reactive signal.
5
+ * When the value updates, it automatically schedules a screen render.
6
+ * @param initialValue The initial value of the signal.
7
+ */
8
+ export declare function createSignal<T>(initialValue: T): [Accessor<T>, Setter<T>];
9
+ /**
10
+ * Creates a memoized value that caches its result and only re-calculates
11
+ * when the result of the function changes (polled during access).
12
+ *
13
+ * Note: A true dependency graph is out of scope. This implementation
14
+ * re-runs the function on every access but could store the last result
15
+ * if we had a way to know if dependencies changed.
16
+ *
17
+ * Since we don't track dependencies, we can't safely cache unless we know inputs didn't change.
18
+ * BUT, often `createMemo` is used to avoid expensive calcs if called multiple times in one render pass.
19
+ *
20
+ * For now, this is a simple pass-through.
21
+ */
22
+ export declare function createMemo<T>(fn: () => T): Accessor<T>;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSignal = createSignal;
4
+ exports.createMemo = createMemo;
5
+ const Screen_1 = require("./Screen");
6
+ /**
7
+ * Creates a reactive signal.
8
+ * When the value updates, it automatically schedules a screen render.
9
+ * @param initialValue The initial value of the signal.
10
+ */
11
+ function createSignal(initialValue) {
12
+ let value = initialValue;
13
+ const read = () => value;
14
+ const write = (newValue) => {
15
+ const nextValue = newValue instanceof Function ? newValue(value) : newValue;
16
+ if (value !== nextValue) {
17
+ value = nextValue;
18
+ Screen_1.Screen.scheduleRender();
19
+ }
20
+ };
21
+ return [read, write];
22
+ }
23
+ /**
24
+ * Creates a memoized value that caches its result and only re-calculates
25
+ * when the result of the function changes (polled during access).
26
+ *
27
+ * Note: A true dependency graph is out of scope. This implementation
28
+ * re-runs the function on every access but could store the last result
29
+ * if we had a way to know if dependencies changed.
30
+ *
31
+ * Since we don't track dependencies, we can't safely cache unless we know inputs didn't change.
32
+ * BUT, often `createMemo` is used to avoid expensive calcs if called multiple times in one render pass.
33
+ *
34
+ * For now, this is a simple pass-through.
35
+ */
36
+ function createMemo(fn) {
37
+ // Without a dependency graph (tracking which signals are read inside fn),
38
+ // we cannot safely memoize across time because we don't know when to invalidate.
39
+ // We would need a global "context stack" like SolidJS/React.
40
+ // For this architectural step, we provide the API surface.
41
+ return () => fn();
42
+ }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from './core/Layout';
10
10
  export * from './core/Focus';
11
11
  export * from './core/Events';
12
12
  export * from './core/Component';
13
+ export * from './core/Signal';
13
14
  export * from './components/Box';
14
15
  export * from './components/Menu';
15
16
  export * from './components/ProgressBar';
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ __exportStar(require("./core/Layout"), exports);
26
26
  __exportStar(require("./core/Focus"), exports);
27
27
  __exportStar(require("./core/Events"), exports);
28
28
  __exportStar(require("./core/Component"), exports);
29
+ __exportStar(require("./core/Signal"), exports);
29
30
  __exportStar(require("./components/Box"), exports);
30
31
  __exportStar(require("./components/Menu"), exports);
31
32
  __exportStar(require("./components/ProgressBar"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voonex",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A zero-dependency Terminal UI Library for Node.js.",
5
5
  "license": "MIT",
6
6
  "author": "CodeTease",
@@ -17,9 +17,9 @@
17
17
  "prepublishOnly": "npm run build"
18
18
  },
19
19
  "devDependencies": {
20
- "@types/node": "^20.4.2",
21
- "ts-node": "10.9.2",
22
- "typescript": "5.9.3"
20
+ "@types/node": "^22",
21
+ "ts-node": "^10",
22
+ "typescript": "^5"
23
23
  },
24
24
  "files": [
25
25
  "dist",