@rlabs-inc/tui 0.3.0 → 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 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: `Count: ${count.value}` })
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: () => todo.text }) // Props can be reactive too!
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.0",
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 = {}
@@ -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
 
@@ -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)
@@ -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.) */
@@ -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