@rlabs-inc/tui 0.3.1 → 0.3.2
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 +7 -4
- package/package.json +2 -2
- package/src/api/mount.ts +15 -1
- package/src/primitives/box.ts +8 -1
- package/src/primitives/text.ts +30 -2
- package/src/primitives/types.ts +10 -3
- package/src/state/keyboard.ts +3 -2
package/README.md
CHANGED
|
@@ -12,7 +12,6 @@ A blazing-fast, fine-grained reactive terminal UI framework with complete flexbo
|
|
|
12
12
|
| Updates per second | 41,000+ |
|
|
13
13
|
| Layout (10K components) | 0.66ms |
|
|
14
14
|
| Memory per component | ~500 bytes |
|
|
15
|
-
| Maximum components tested | 1,000,000 |
|
|
16
15
|
|
|
17
16
|
## Quick Start
|
|
18
17
|
|
|
@@ -21,15 +20,18 @@ bun add tui @rlabs-inc/signals
|
|
|
21
20
|
```
|
|
22
21
|
|
|
23
22
|
```typescript
|
|
24
|
-
import { mount, box, text } from 'tui'
|
|
23
|
+
import { mount, box, text, derived } from 'tui'
|
|
25
24
|
import { signal } from '@rlabs-inc/signals'
|
|
26
25
|
|
|
27
26
|
// Create reactive state
|
|
28
27
|
const count = signal(0)
|
|
28
|
+
const doubled = derived(() => count.value * 2)
|
|
29
29
|
|
|
30
30
|
// Build your UI
|
|
31
31
|
box({ width: 40, height: 10, children: () => {
|
|
32
|
-
text({ content:
|
|
32
|
+
text({ content: count }) // signal directly
|
|
33
|
+
text({ content: doubled }) // derived directly
|
|
34
|
+
text({ content: () => `Count: ${count.value}` }) // () => for formatting
|
|
33
35
|
}})
|
|
34
36
|
|
|
35
37
|
// Mount to terminal
|
|
@@ -56,6 +58,7 @@ setInterval(() => count.value++, 1000)
|
|
|
56
58
|
- Effects for side effects
|
|
57
59
|
- Bindings for prop connections
|
|
58
60
|
- Zero reconciliation - reactivity IS the update mechanism
|
|
61
|
+
- **Clean syntax**: Pass signals/deriveds directly, use `() =>` only for inline computations
|
|
59
62
|
|
|
60
63
|
### State Modules
|
|
61
64
|
- **keyboard** - Key events, shortcuts, input buffering
|
|
@@ -155,7 +158,7 @@ box({
|
|
|
155
158
|
(todo) => box({ // Render function per item
|
|
156
159
|
id: `todo-${todo.id}`, // Stable ID for reconciliation
|
|
157
160
|
children: () => {
|
|
158
|
-
text({ content:
|
|
161
|
+
text({ content: todo.text }) // Static value from item
|
|
159
162
|
}
|
|
160
163
|
}),
|
|
161
164
|
{ key: (todo) => todo.id } // Key function for efficient updates
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rlabs-inc/tui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "The Terminal UI Framework for TypeScript/Bun - Blazing-fast, fine-grained reactive terminal UI with complete flexbox layout",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"src"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
-
"dev": "bun run examples/hello.ts",
|
|
18
|
+
"dev": "bun run examples/showcase/01-hello-counter.ts",
|
|
19
19
|
"test": "bun test",
|
|
20
20
|
"typecheck": "tsc --noEmit"
|
|
21
21
|
},
|
package/src/api/mount.ts
CHANGED
|
@@ -46,8 +46,22 @@ import { globalKeys } from '../state/global-keys'
|
|
|
46
46
|
*
|
|
47
47
|
* @param root - Function that creates the component tree
|
|
48
48
|
* @param options - Mount options (mode, mouse, keyboard)
|
|
49
|
-
* @returns Cleanup function to unmount
|
|
49
|
+
* @returns Cleanup function to unmount (or AppendMountResult for append mode)
|
|
50
50
|
*/
|
|
51
|
+
// Overloads for proper return type inference
|
|
52
|
+
export async function mount(
|
|
53
|
+
root: () => void,
|
|
54
|
+
options: MountOptions & { mode: 'append' }
|
|
55
|
+
): Promise<AppendMountResult>
|
|
56
|
+
export async function mount(
|
|
57
|
+
root: () => void,
|
|
58
|
+
options?: MountOptions & { mode?: 'fullscreen' | 'inline' }
|
|
59
|
+
): Promise<() => Promise<void>>
|
|
60
|
+
export async function mount(
|
|
61
|
+
root: () => void,
|
|
62
|
+
options?: MountOptions
|
|
63
|
+
): Promise<(() => Promise<void>) | AppendMountResult>
|
|
64
|
+
// Implementation
|
|
51
65
|
export async function mount(
|
|
52
66
|
root: () => void,
|
|
53
67
|
options: MountOptions = {}
|
package/src/primitives/box.ts
CHANGED
|
@@ -116,15 +116,22 @@ function alignSelfToNum(align: string | undefined): number {
|
|
|
116
116
|
/**
|
|
117
117
|
* Create a slot source for enum props - returns getter for reactive, value for static.
|
|
118
118
|
* For use with slotArray.setSource()
|
|
119
|
+
* Handles: static values, signals/bindings ({ value: T }), and getter functions (() => T)
|
|
119
120
|
*/
|
|
120
121
|
function enumSource<T extends string>(
|
|
121
|
-
prop: T | { value: T } | undefined,
|
|
122
|
+
prop: T | { value: T } | (() => T) | undefined,
|
|
122
123
|
converter: (val: T | undefined) => number
|
|
123
124
|
): number | (() => number) {
|
|
125
|
+
// Handle getter function (inline derived)
|
|
126
|
+
if (typeof prop === 'function') {
|
|
127
|
+
return () => converter(prop())
|
|
128
|
+
}
|
|
129
|
+
// Handle object with .value (signal/binding/derived)
|
|
124
130
|
if (prop !== undefined && typeof prop === 'object' && prop !== null && 'value' in prop) {
|
|
125
131
|
const reactiveSource = prop as { value: T }
|
|
126
132
|
return () => converter(reactiveSource.value)
|
|
127
133
|
}
|
|
134
|
+
// Static value
|
|
128
135
|
return converter(prop as T | undefined)
|
|
129
136
|
}
|
|
130
137
|
|
package/src/primitives/text.ts
CHANGED
|
@@ -76,18 +76,45 @@ function alignSelfToNum(alignSelf: string | undefined): number {
|
|
|
76
76
|
|
|
77
77
|
// bindEnumProp removed - using enumSource instead
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Convert content prop (string | number) to string source for setSource.
|
|
81
|
+
* Handles: static values, signals, and getters.
|
|
82
|
+
*/
|
|
83
|
+
function contentToStringSource(
|
|
84
|
+
content: TextProps['content']
|
|
85
|
+
): string | (() => string) {
|
|
86
|
+
// Getter function - wrap to convert
|
|
87
|
+
if (typeof content === 'function') {
|
|
88
|
+
return () => String(content())
|
|
89
|
+
}
|
|
90
|
+
// Signal/binding/derived with .value
|
|
91
|
+
if (content !== null && typeof content === 'object' && 'value' in content) {
|
|
92
|
+
const reactive = content as { value: string | number }
|
|
93
|
+
return () => String(reactive.value)
|
|
94
|
+
}
|
|
95
|
+
// Static value
|
|
96
|
+
return String(content)
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
/**
|
|
80
100
|
* Create a slot source for enum props - returns getter for reactive, value for static.
|
|
81
101
|
* For use with slotArray.setSource()
|
|
102
|
+
* Handles: static values, signals/bindings ({ value: T }), and getter functions (() => T)
|
|
82
103
|
*/
|
|
83
104
|
function enumSource<T extends string>(
|
|
84
|
-
prop: T | { value: T } | undefined,
|
|
105
|
+
prop: T | { value: T } | (() => T) | undefined,
|
|
85
106
|
converter: (val: T | undefined) => number
|
|
86
107
|
): number | (() => number) {
|
|
108
|
+
// Handle getter function (inline derived)
|
|
109
|
+
if (typeof prop === 'function') {
|
|
110
|
+
return () => converter(prop())
|
|
111
|
+
}
|
|
112
|
+
// Handle object with .value (signal/binding/derived)
|
|
87
113
|
if (prop !== undefined && typeof prop === 'object' && prop !== null && 'value' in prop) {
|
|
88
114
|
const reactiveSource = prop as { value: T }
|
|
89
115
|
return () => converter(reactiveSource.value)
|
|
90
116
|
}
|
|
117
|
+
// Static value
|
|
91
118
|
return converter(prop as T | undefined)
|
|
92
119
|
}
|
|
93
120
|
|
|
@@ -123,8 +150,9 @@ export function text(props: TextProps): Cleanup {
|
|
|
123
150
|
// ==========================================================================
|
|
124
151
|
// TEXT CONTENT - Always needed (this is a text component!)
|
|
125
152
|
// Uses setSource() for stable slot tracking (fixes bind() replacement bug)
|
|
153
|
+
// Converts numbers to strings automatically
|
|
126
154
|
// ==========================================================================
|
|
127
|
-
textArrays.textContent.setSource(index, props.content)
|
|
155
|
+
textArrays.textContent.setSource(index, contentToStringSource(props.content))
|
|
128
156
|
|
|
129
157
|
// Text styling - only set if passed
|
|
130
158
|
if (props.attrs !== undefined) textArrays.textAttrs.setSource(index, props.attrs)
|
package/src/primitives/types.ts
CHANGED
|
@@ -16,8 +16,15 @@ import type { Variant } from '../state/theme'
|
|
|
16
16
|
/**
|
|
17
17
|
* A prop value that can be static or reactive.
|
|
18
18
|
* Components will sync reactive values automatically.
|
|
19
|
+
*
|
|
20
|
+
* Accepts:
|
|
21
|
+
* - Static value: `42`, `'hello'`
|
|
22
|
+
* - Signal: `mySignal`
|
|
23
|
+
* - Derived: `myDerived`
|
|
24
|
+
* - Binding: `bind(source)`
|
|
25
|
+
* - Getter (inline derived): `() => count.value * 2`
|
|
19
26
|
*/
|
|
20
|
-
export type Reactive<T> = T | WritableSignal<T> | Binding<T> | ReadonlyBinding<T>
|
|
27
|
+
export type Reactive<T> = T | WritableSignal<T> | Binding<T> | ReadonlyBinding<T> | (() => T)
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* Make specific props reactive while keeping others static.
|
|
@@ -138,8 +145,8 @@ export interface BoxProps extends StyleProps, BorderProps, DimensionProps, Spaci
|
|
|
138
145
|
// =============================================================================
|
|
139
146
|
|
|
140
147
|
export interface TextProps extends StyleProps, DimensionProps, SpacingProps, LayoutProps {
|
|
141
|
-
/** Text content */
|
|
142
|
-
content: Reactive<string>
|
|
148
|
+
/** Text content (strings and numbers auto-converted) */
|
|
149
|
+
content: Reactive<string | number>
|
|
143
150
|
/** Text alignment: 'left' | 'center' | 'right' */
|
|
144
151
|
align?: Reactive<'left' | 'center' | 'right'>
|
|
145
152
|
/** Text attributes (bold, italic, etc.) */
|
package/src/state/keyboard.ts
CHANGED
|
@@ -52,7 +52,7 @@ export const lastKey = derived(() => lastEvent.value?.key ?? '')
|
|
|
52
52
|
// =============================================================================
|
|
53
53
|
|
|
54
54
|
const globalHandlers = new Set<KeyHandler>()
|
|
55
|
-
const keyHandlers = new Map<string, Set<() => void | boolean
|
|
55
|
+
const keyHandlers = new Map<string, Set<() => void | boolean | Promise<void>>>()
|
|
56
56
|
const focusedHandlers = new Map<number, Set<KeyHandler>>()
|
|
57
57
|
|
|
58
58
|
// =============================================================================
|
|
@@ -118,8 +118,9 @@ export function on(handler: KeyHandler): () => void {
|
|
|
118
118
|
* Subscribe to specific key(s).
|
|
119
119
|
* Handler receives no arguments - check lastEvent if needed.
|
|
120
120
|
* Return true to consume the event.
|
|
121
|
+
* Async handlers are supported (e.g., for cleanup functions).
|
|
121
122
|
*/
|
|
122
|
-
export function onKey(key: string | string[], handler: () => void | boolean): () => void {
|
|
123
|
+
export function onKey(key: string | string[], handler: () => void | boolean | Promise<void>): () => void {
|
|
123
124
|
const keys = Array.isArray(key) ? key : [key]
|
|
124
125
|
const unsubscribers: (() => void)[] = []
|
|
125
126
|
|