@flyingrobots/bijou-tui 0.1.0 → 0.2.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 +221 -30
- package/dist/animate.d.ts +60 -0
- package/dist/animate.d.ts.map +1 -0
- package/dist/animate.js +98 -0
- package/dist/animate.js.map +1 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +2 -2
- package/dist/commands.js.map +1 -1
- package/dist/eventbus.d.ts +69 -0
- package/dist/eventbus.d.ts.map +1 -0
- package/dist/eventbus.js +120 -0
- package/dist/eventbus.js.map +1 -0
- package/dist/flex.d.ts +64 -0
- package/dist/flex.d.ts.map +1 -0
- package/dist/flex.js +261 -0
- package/dist/flex.js.map +1 -0
- package/dist/help.d.ts +58 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +104 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -1
- package/dist/index.js.map +1 -1
- package/dist/inputstack.d.ts +79 -0
- package/dist/inputstack.d.ts.map +1 -0
- package/dist/inputstack.js +81 -0
- package/dist/inputstack.js.map +1 -0
- package/dist/keybindings.d.ts +83 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +184 -0
- package/dist/keybindings.js.map +1 -0
- package/dist/layout.d.ts +0 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +3 -4
- package/dist/layout.js.map +1 -1
- package/dist/runtime.d.ts +3 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +24 -37
- package/dist/runtime.js.map +1 -1
- package/dist/screen.d.ts +10 -4
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +13 -11
- package/dist/screen.js.map +1 -1
- package/dist/spring.d.ts +139 -0
- package/dist/spring.d.ts.map +1 -0
- package/dist/spring.js +106 -0
- package/dist/spring.js.map +1 -0
- package/dist/timeline.d.ts +127 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +298 -0
- package/dist/timeline.js.map +1 -0
- package/dist/types.d.ts +11 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewport.d.ts +64 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +162 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
# @flyingrobots/bijou-tui
|
|
2
2
|
|
|
3
|
-
TEA runtime for terminal UIs — model/update/view with
|
|
3
|
+
TEA runtime for terminal UIs — model/update/view with physics-based animation, flexbox layout, declarative keybindings, and a centralized event bus.
|
|
4
4
|
|
|
5
|
-
Inspired by [Bubble Tea](https://github.com/charmbracelet/bubbletea) (Go)
|
|
5
|
+
Inspired by [Bubble Tea](https://github.com/charmbracelet/bubbletea) (Go) and [GSAP](https://gsap.com/) animation.
|
|
6
|
+
|
|
7
|
+
## What's New in 0.2.0?
|
|
8
|
+
|
|
9
|
+
- **Industrial-Grade Renderer** — Flicker-free, scroll-safe rendering loop with `WRAP_DISABLE` and `CLEAR_LINE_TO_END` support.
|
|
10
|
+
- **Spring animation engine** — physics-based springs with 6 presets, plus multi-frame emission for 60fps+ fluidity.
|
|
11
|
+
- **`animate()`** — GSAP-style animation commands for TEA, with `onComplete` signals and `immediate: true` for reduced-motion.
|
|
12
|
+
- **`viewport()`** — scrollable content pane with proportional scrollbar and ANSI-aware clipping.
|
|
13
|
+
- **`flex()`** — flexbox layout with grow/basis/min/max, true horizontal centering, and auto-reflow.
|
|
14
|
+
- **`ResizeMsg`** — terminal resize events auto-dispatched by the runtime.
|
|
15
|
+
- **`EventBus`** — centralized typed event emitter unifying keyboard, resize, and multi-message commands.
|
|
16
|
+
- **Keybinding manager** — `createKeyMap()` for declarative key binding with modifiers, named groups, runtime enable/disable.
|
|
17
|
+
- **Help generator** — `helpView()`, `helpShort()`, `helpFor()` auto-generated from registered keybindings.
|
|
18
|
+
- **Input stack** — `createInputStack()` for layered input dispatch with opaque (modal) and passthrough layers.
|
|
19
|
+
|
|
20
|
+
See the [CHANGELOG](https://github.com/flyingrobots/bijou/blob/main/CHANGELOG.md) for the full release history.
|
|
6
21
|
|
|
7
22
|
## Install
|
|
8
23
|
|
|
9
24
|
```bash
|
|
10
|
-
npm install @flyingrobots/bijou @flyingrobots/bijou-tui
|
|
25
|
+
npm install @flyingrobots/bijou @flyingrobots/bijou-node @flyingrobots/bijou-tui
|
|
11
26
|
```
|
|
12
27
|
|
|
13
28
|
## Quick Start
|
|
14
29
|
|
|
15
30
|
```typescript
|
|
31
|
+
import { initDefaultContext } from '@flyingrobots/bijou-node';
|
|
16
32
|
import { run, quit, tick, type App, type KeyMsg } from '@flyingrobots/bijou-tui';
|
|
17
33
|
|
|
34
|
+
initDefaultContext();
|
|
35
|
+
|
|
18
36
|
type Model = { count: number };
|
|
19
37
|
|
|
20
38
|
const app: App<Model> = {
|
|
21
|
-
init: () => [{ count: 0 },
|
|
39
|
+
init: () => [{ count: 0 }, []],
|
|
22
40
|
|
|
23
41
|
update: (msg, model) => {
|
|
24
42
|
if (msg.type === 'key') {
|
|
25
|
-
if (msg.key === 'q') return [model, quit()];
|
|
26
|
-
if (msg.key === '+') return [{ count: model.count + 1 },
|
|
27
|
-
if (msg.key === '-') return [{ count: model.count - 1 },
|
|
28
|
-
}
|
|
29
|
-
if (msg.type === 'tick') {
|
|
30
|
-
return [model, tick(1000)];
|
|
43
|
+
if (msg.key === 'q') return [model, [quit()]];
|
|
44
|
+
if (msg.key === '+') return [{ count: model.count + 1 }, []];
|
|
45
|
+
if (msg.key === '-') return [{ count: model.count - 1 }, []];
|
|
31
46
|
}
|
|
32
|
-
return [model,
|
|
47
|
+
return [model, []];
|
|
33
48
|
},
|
|
34
49
|
|
|
35
50
|
view: (model) => `Count: ${model.count}\n\nPress +/- to change, q to quit`,
|
|
@@ -38,35 +53,211 @@ const app: App<Model> = {
|
|
|
38
53
|
run(app);
|
|
39
54
|
```
|
|
40
55
|
|
|
41
|
-
##
|
|
56
|
+
## Animation
|
|
57
|
+
|
|
58
|
+
### Spring Physics
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { animate, SPRING_PRESETS } from '@flyingrobots/bijou-tui';
|
|
62
|
+
|
|
63
|
+
// Physics-based (default) — runs until the spring settles
|
|
64
|
+
const cmd = animate({
|
|
65
|
+
from: 0,
|
|
66
|
+
to: 100,
|
|
67
|
+
spring: 'wobbly', // or 'default', 'gentle', 'stiff', 'slow', 'molasses'
|
|
68
|
+
onFrame: (v) => ({ type: 'scroll', y: v }),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Duration-based with easing
|
|
72
|
+
const fade = animate({
|
|
73
|
+
type: 'tween',
|
|
74
|
+
from: 0,
|
|
75
|
+
to: 1,
|
|
76
|
+
duration: 300,
|
|
77
|
+
ease: EASINGS.easeOutCubic,
|
|
78
|
+
onFrame: (v) => ({ type: 'fade', opacity: v }),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Skip animation (reduced motion)
|
|
82
|
+
const jump = animate({
|
|
83
|
+
from: 0, to: 100,
|
|
84
|
+
immediate: true,
|
|
85
|
+
onFrame: (v) => ({ type: 'scroll', y: v }),
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Timeline
|
|
42
90
|
|
|
43
|
-
|
|
91
|
+
GSAP-style orchestration — pure state machine, no timers:
|
|
44
92
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- **`Cmd`** — side-effect commands returned from `update`
|
|
48
|
-
- **`QUIT`** — sentinel value to signal app termination
|
|
93
|
+
```typescript
|
|
94
|
+
import { timeline } from '@flyingrobots/bijou-tui';
|
|
49
95
|
|
|
50
|
-
|
|
96
|
+
const tl = timeline()
|
|
97
|
+
.add('slideIn', { type: 'tween', from: -100, to: 0, duration: 300 })
|
|
98
|
+
.add('fadeIn', { type: 'tween', from: 0, to: 1, duration: 200 }, '-=100')
|
|
99
|
+
.label('settled')
|
|
100
|
+
.add('bounce', { from: 0, to: 10, spring: 'wobbly' }, 'settled')
|
|
101
|
+
.call('onReady', 'settled+=50')
|
|
102
|
+
.build();
|
|
51
103
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
104
|
+
// Drive from TEA update:
|
|
105
|
+
let tlState = tl.init();
|
|
106
|
+
// on each frame:
|
|
107
|
+
tlState = tl.step(tlState, 1/60);
|
|
108
|
+
const { slideIn, fadeIn, bounce } = tl.values(tlState);
|
|
109
|
+
const fired = tl.firedCallbacks(prev, tlState); // ['onReady']
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Position syntax: `'<'` (parallel), `'+=N'` (gap), `'-=N'` (overlap), `'<+=N'` (offset from previous start), absolute ms, `'label'`, `'label+=N'`.
|
|
55
113
|
|
|
56
|
-
|
|
114
|
+
## Layout
|
|
57
115
|
|
|
58
|
-
|
|
59
|
-
- **`clearAndHome()`** — clear screen and move cursor to top-left
|
|
60
|
-
- **`renderFrame(content)`** — efficient frame rendering
|
|
116
|
+
### Flexbox
|
|
61
117
|
|
|
62
|
-
|
|
118
|
+
```typescript
|
|
119
|
+
import { flex } from '@flyingrobots/bijou-tui';
|
|
63
120
|
|
|
64
|
-
|
|
65
|
-
|
|
121
|
+
// Sidebar + main content, responsive to terminal width
|
|
122
|
+
flex({ direction: 'row', width: cols, height: rows, gap: 1 },
|
|
123
|
+
{ basis: 20, content: sidebarText },
|
|
124
|
+
{ flex: 1, content: (w, h) => renderMain(w, h) },
|
|
125
|
+
);
|
|
66
126
|
|
|
67
|
-
|
|
127
|
+
// Header + body + footer
|
|
128
|
+
flex({ direction: 'column', width: cols, height: rows },
|
|
129
|
+
{ basis: 1, content: headerLine },
|
|
130
|
+
{ flex: 1, content: (w, h) => renderBody(w, h) },
|
|
131
|
+
{ basis: 1, content: statusLine },
|
|
132
|
+
);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Children can be **render functions** `(width, height) => string` — they receive their allocated space and reflow automatically when the terminal resizes.
|
|
136
|
+
|
|
137
|
+
### Viewport
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { viewport, createScrollState, scrollBy, pageDown } from '@flyingrobots/bijou-tui';
|
|
141
|
+
|
|
142
|
+
let scroll = createScrollState(content, viewportHeight);
|
|
143
|
+
|
|
144
|
+
// Render visible window with scrollbar
|
|
145
|
+
const view = viewport({ width: 60, height: 20, content, scrollY: scroll.y });
|
|
146
|
+
|
|
147
|
+
// Handle scroll keys
|
|
148
|
+
scroll = scrollBy(scroll, 1); // down one line
|
|
149
|
+
scroll = pageDown(scroll); // down one page
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Basic Layout
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { vstack, hstack } from '@flyingrobots/bijou-tui';
|
|
156
|
+
|
|
157
|
+
vstack(header, content, footer); // vertical stack
|
|
158
|
+
hstack(2, leftPanel, rightPanel); // side-by-side with gap
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Resize Handling
|
|
162
|
+
|
|
163
|
+
Terminal resize events are dispatched automatically as `ResizeMsg`:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
update(msg, model) {
|
|
167
|
+
if (msg.type === 'resize') {
|
|
168
|
+
return [{ ...model, cols: msg.columns, rows: msg.rows }, []];
|
|
169
|
+
}
|
|
170
|
+
// ...
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
view(model) {
|
|
174
|
+
return flex(
|
|
175
|
+
{ direction: 'row', width: model.cols, height: model.rows },
|
|
176
|
+
{ basis: 20, content: sidebar },
|
|
177
|
+
{ flex: 1, content: (w, h) => mainContent(w, h) },
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Event Bus
|
|
183
|
+
|
|
184
|
+
The runtime uses an `EventBus` internally. You can also create your own for custom event sources:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { createEventBus } from '@flyingrobots/bijou-tui';
|
|
188
|
+
|
|
189
|
+
const bus = createEventBus<MyMsg>();
|
|
190
|
+
bus.connectIO(ctx.io); // keyboard + resize
|
|
191
|
+
bus.on((msg) => { /* ... */ }); // single subscription
|
|
192
|
+
bus.emit(customMsg); // synthetic events
|
|
193
|
+
bus.runCmd(someCommand); // command results re-emitted
|
|
194
|
+
bus.dispose(); // clean shutdown
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full event flow and [GUIDE.md](./GUIDE.md) for detailed usage patterns.
|
|
198
|
+
|
|
199
|
+
## Keybinding Manager
|
|
200
|
+
|
|
201
|
+
Declarative key binding with modifier support, named groups, and runtime enable/disable:
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import { createKeyMap, type KeyMsg } from '@flyingrobots/bijou-tui';
|
|
205
|
+
|
|
206
|
+
type Msg = { type: 'quit' } | { type: 'help' } | { type: 'move'; dir: string };
|
|
207
|
+
|
|
208
|
+
const kb = createKeyMap<Msg>()
|
|
209
|
+
.bind('q', 'Quit', { type: 'quit' })
|
|
210
|
+
.bind('?', 'Help', { type: 'help' })
|
|
211
|
+
.bind('ctrl+c', 'Force quit', { type: 'quit' })
|
|
212
|
+
.group('Navigation', (g) => g
|
|
213
|
+
.bind('j', 'Down', { type: 'move', dir: 'down' })
|
|
214
|
+
.bind('k', 'Up', { type: 'move', dir: 'up' })
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// In TEA update:
|
|
218
|
+
const action = kb.handle(keyMsg);
|
|
219
|
+
if (action !== undefined) return [model, [/* ... */]];
|
|
220
|
+
|
|
221
|
+
// Runtime enable/disable
|
|
222
|
+
kb.disableGroup('Navigation');
|
|
223
|
+
kb.enable('Quit');
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Help Generation
|
|
227
|
+
|
|
228
|
+
Auto-generate help text from registered bindings:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { helpView, helpShort, helpFor } from '@flyingrobots/bijou-tui';
|
|
232
|
+
|
|
233
|
+
helpView(kb); // full grouped multi-line help
|
|
234
|
+
helpShort(kb); // "q Quit • ? Help • Ctrl+c Force quit • j Down • k Up"
|
|
235
|
+
helpFor(kb, 'Nav'); // only Navigation group
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Input Stack
|
|
239
|
+
|
|
240
|
+
Layered input dispatch for modal UIs — push/pop handlers with opaque or passthrough behavior:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { createInputStack, type KeyMsg } from '@flyingrobots/bijou-tui';
|
|
244
|
+
|
|
245
|
+
const stack = createInputStack<KeyMsg, Msg>();
|
|
246
|
+
|
|
247
|
+
// Base layer — global keys, lets unmatched events fall through
|
|
248
|
+
stack.push(appKeys, { passthrough: true });
|
|
249
|
+
|
|
250
|
+
// Modal opens — captures all input (opaque by default)
|
|
251
|
+
const modalId = stack.push(modalKeys);
|
|
252
|
+
|
|
253
|
+
// Dispatch returns first matched action, top-down
|
|
254
|
+
const action = stack.dispatch(keyMsg);
|
|
255
|
+
|
|
256
|
+
// Modal closes
|
|
257
|
+
stack.remove(modalId);
|
|
258
|
+
```
|
|
68
259
|
|
|
69
|
-
|
|
260
|
+
`KeyMap` implements `InputHandler`, so it plugs directly into the input stack.
|
|
70
261
|
|
|
71
262
|
## Related Packages
|
|
72
263
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEA-integrated animation commands.
|
|
3
|
+
*
|
|
4
|
+
* GSAP-style API: `animate()` returns a `Cmd` that fires `onFrame`
|
|
5
|
+
* messages as the animation progresses, fitting naturally into the
|
|
6
|
+
* TEA update cycle.
|
|
7
|
+
*
|
|
8
|
+
* Two animation modes:
|
|
9
|
+
* - **spring**: Physics-based (default). No fixed duration — runs until
|
|
10
|
+
* the spring settles. Use for organic, responsive motion.
|
|
11
|
+
* - **tween**: Duration-based with easing curves. Use for predictable,
|
|
12
|
+
* timed transitions.
|
|
13
|
+
*
|
|
14
|
+
* Both modes support `immediate: true` to skip animation and jump to
|
|
15
|
+
* the target value in a single frame.
|
|
16
|
+
*/
|
|
17
|
+
import type { Cmd } from './types.js';
|
|
18
|
+
import { type SpringConfig, type SpringPreset, type EasingFn } from './spring.js';
|
|
19
|
+
interface AnimateBase<M> {
|
|
20
|
+
/** Starting value. */
|
|
21
|
+
readonly from: number;
|
|
22
|
+
/** Target value. */
|
|
23
|
+
readonly to: number;
|
|
24
|
+
/** Frames per second. Default: 60. */
|
|
25
|
+
readonly fps?: number;
|
|
26
|
+
/** Skip animation — jump to target in one frame. Default: false. */
|
|
27
|
+
readonly immediate?: boolean;
|
|
28
|
+
/** Called each frame with the interpolated value. Return a message for TEA. */
|
|
29
|
+
readonly onFrame: (value: number) => M;
|
|
30
|
+
/** Optional message to emit when the animation is fully complete. */
|
|
31
|
+
readonly onComplete?: () => M;
|
|
32
|
+
}
|
|
33
|
+
export interface SpringAnimateOptions<M> extends AnimateBase<M> {
|
|
34
|
+
readonly type?: 'spring';
|
|
35
|
+
/** Spring config — preset name or custom values. */
|
|
36
|
+
readonly spring?: Partial<SpringConfig> | SpringPreset;
|
|
37
|
+
}
|
|
38
|
+
export interface TweenAnimateOptions<M> extends AnimateBase<M> {
|
|
39
|
+
readonly type: 'tween';
|
|
40
|
+
/** Duration in milliseconds. */
|
|
41
|
+
readonly duration: number;
|
|
42
|
+
/** Easing function. Default: easeOutCubic. */
|
|
43
|
+
readonly ease?: EasingFn;
|
|
44
|
+
}
|
|
45
|
+
export type AnimateOptions<M> = SpringAnimateOptions<M> | TweenAnimateOptions<M>;
|
|
46
|
+
/**
|
|
47
|
+
* Create a TEA command that drives an animation.
|
|
48
|
+
*
|
|
49
|
+
* Spring mode (default):
|
|
50
|
+
* ```ts
|
|
51
|
+
* animate({ from: 0, to: 100, spring: 'wobbly', onFrame: (v) => ({ type: 'scroll', y: v }) })
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function animate<M>(options: AnimateOptions<M>): Cmd<M>;
|
|
55
|
+
/**
|
|
56
|
+
* Run animations in sequence. Each animation completes before the next starts.
|
|
57
|
+
*/
|
|
58
|
+
export declare function sequence<M>(...cmds: Cmd<M>[]): Cmd<M>;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=animate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animate.d.ts","sourceRoot":"","sources":["../src/animate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,QAAQ,EAQd,MAAM,aAAa,CAAC;AAMrB,UAAU,WAAW,CAAC,CAAC;IACrB,sBAAsB;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB,CAAC,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,CAAC;IAC7D,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;IACzB,oDAAoD;IACpD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC;CACxD;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC,CAAE,SAAQ,WAAW,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,gCAAgC;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,oBAAoB,CAAC,CAAC,CAAC,GAAG,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAMjF;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAiB7D;AAsED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAMrD"}
|
package/dist/animate.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEA-integrated animation commands.
|
|
3
|
+
*
|
|
4
|
+
* GSAP-style API: `animate()` returns a `Cmd` that fires `onFrame`
|
|
5
|
+
* messages as the animation progresses, fitting naturally into the
|
|
6
|
+
* TEA update cycle.
|
|
7
|
+
*
|
|
8
|
+
* Two animation modes:
|
|
9
|
+
* - **spring**: Physics-based (default). No fixed duration — runs until
|
|
10
|
+
* the spring settles. Use for organic, responsive motion.
|
|
11
|
+
* - **tween**: Duration-based with easing curves. Use for predictable,
|
|
12
|
+
* timed transitions.
|
|
13
|
+
*
|
|
14
|
+
* Both modes support `immediate: true` to skip animation and jump to
|
|
15
|
+
* the target value in a single frame.
|
|
16
|
+
*/
|
|
17
|
+
import { springStep, createSpringState, resolveSpringConfig, tweenStep, createTweenState, resolveTweenConfig, EASINGS, } from './spring.js';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// animate() — the main API
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Create a TEA command that drives an animation.
|
|
23
|
+
*
|
|
24
|
+
* Spring mode (default):
|
|
25
|
+
* ```ts
|
|
26
|
+
* animate({ from: 0, to: 100, spring: 'wobbly', onFrame: (v) => ({ type: 'scroll', y: v }) })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function animate(options) {
|
|
30
|
+
const { from, to, fps = 60, immediate = false, onFrame, onComplete } = options;
|
|
31
|
+
// Immediate mode — single frame, no physics
|
|
32
|
+
if (immediate) {
|
|
33
|
+
return async (emit) => {
|
|
34
|
+
emit(onFrame(to));
|
|
35
|
+
if (onComplete)
|
|
36
|
+
emit(onComplete());
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (options.type === 'tween') {
|
|
40
|
+
return createTweenCmd(from, to, options.duration, options.ease ?? EASINGS.easeOutCubic, fps, onFrame, onComplete);
|
|
41
|
+
}
|
|
42
|
+
const config = resolveSpringConfig(options.spring);
|
|
43
|
+
return createSpringCmd(from, to, config, fps, onFrame, onComplete);
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Internal: spring command
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
function createSpringCmd(from, to, config, fps, onFrame, onComplete) {
|
|
49
|
+
return (emit) => new Promise((resolve) => {
|
|
50
|
+
let state = createSpringState(from);
|
|
51
|
+
const dt = 1 / fps;
|
|
52
|
+
const intervalMs = Math.round(1000 / fps);
|
|
53
|
+
const id = setInterval(() => {
|
|
54
|
+
state = springStep(state, to, config, dt);
|
|
55
|
+
emit(onFrame(state.value));
|
|
56
|
+
if (state.done) {
|
|
57
|
+
clearInterval(id);
|
|
58
|
+
if (onComplete)
|
|
59
|
+
emit(onComplete());
|
|
60
|
+
resolve();
|
|
61
|
+
}
|
|
62
|
+
}, intervalMs);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Internal: tween command
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function createTweenCmd(from, to, duration, ease, fps, onFrame, onComplete) {
|
|
69
|
+
const config = resolveTweenConfig({ from, to, duration, ease });
|
|
70
|
+
return (emit) => new Promise((resolve) => {
|
|
71
|
+
let state = createTweenState(from);
|
|
72
|
+
const intervalMs = Math.round(1000 / fps);
|
|
73
|
+
const id = setInterval(() => {
|
|
74
|
+
state = tweenStep(state, config, intervalMs);
|
|
75
|
+
emit(onFrame(state.value));
|
|
76
|
+
if (state.done) {
|
|
77
|
+
clearInterval(id);
|
|
78
|
+
if (onComplete)
|
|
79
|
+
emit(onComplete());
|
|
80
|
+
resolve();
|
|
81
|
+
}
|
|
82
|
+
}, intervalMs);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// sequence() — chain animations like a GSAP timeline
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* Run animations in sequence. Each animation completes before the next starts.
|
|
90
|
+
*/
|
|
91
|
+
export function sequence(...cmds) {
|
|
92
|
+
return async (emit) => {
|
|
93
|
+
for (const cmd of cmds) {
|
|
94
|
+
await cmd(emit);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=animate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animate.js","sourceRoot":"","sources":["../src/animate.ts"],"names":[],"mappings":"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"}
|
package/dist/commands.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,GAAG,EAAmB,MAAM,YAAY,CAAC;AAE7D,gDAAgD;AAChD,wBAAgB,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAEhC;AAED,gEAAgE;AAChE,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,GAAG,EAAmB,MAAM,YAAY,CAAC;AAE7D,gDAAgD;AAChD,wBAAgB,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAEhC;AAED,gEAAgE;AAChE,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAKlD;AAED,0DAA0D;AAC1D,wBAAgB,KAAK,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAEpD"}
|
package/dist/commands.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { QUIT } from './types.js';
|
|
2
2
|
/** Command that signals the runtime to quit. */
|
|
3
3
|
export function quit() {
|
|
4
|
-
return () =>
|
|
4
|
+
return async (_emit) => QUIT;
|
|
5
5
|
}
|
|
6
6
|
/** Command that delivers a message after a delay (one-shot). */
|
|
7
7
|
export function tick(ms, msg) {
|
|
8
|
-
return () => new Promise((resolve) => {
|
|
8
|
+
return (_emit) => new Promise((resolve) => {
|
|
9
9
|
setTimeout(() => resolve(msg), ms);
|
|
10
10
|
});
|
|
11
11
|
}
|
package/dist/commands.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAA6B,MAAM,YAAY,CAAC;AAE7D,gDAAgD;AAChD,MAAM,UAAU,IAAI;IAClB,OAAO,
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAA6B,MAAM,YAAY,CAAC;AAE7D,gDAAgD;AAChD,MAAM,UAAU,IAAI;IAClB,OAAO,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,IAAkB,CAAC;AAC7C,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,IAAI,CAAI,EAAU,EAAE,GAAM;IACxC,OAAO,CAAC,KAAK,EAAE,EAAE,CACf,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,EAAE;QACzB,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACP,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,KAAK,CAAI,GAAG,IAAc;IACxC,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized event bus for TUI applications.
|
|
3
|
+
*
|
|
4
|
+
* Unifies all input sources (keyboard, resize, commands, custom) into
|
|
5
|
+
* a single typed event stream. The TEA runtime subscribes to the bus
|
|
6
|
+
* instead of manually wiring callbacks.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const bus = createEventBus<Msg>();
|
|
10
|
+
*
|
|
11
|
+
* // Connect I/O sources (keyboard + resize)
|
|
12
|
+
* bus.connectIO(ctx.io);
|
|
13
|
+
*
|
|
14
|
+
* // Subscribe to all events
|
|
15
|
+
* bus.on((msg) => {
|
|
16
|
+
* const [model, cmds] = app.update(msg, model);
|
|
17
|
+
* cmds.forEach(cmd => bus.runCmd(cmd));
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* // Emit custom events
|
|
21
|
+
* bus.emit({ type: 'tick' });
|
|
22
|
+
*
|
|
23
|
+
* // In tests — emit directly, no I/O needed
|
|
24
|
+
* bus.emit({ type: 'key', key: 'a', ctrl: false, alt: false, shift: false });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import type { IOPort } from '@flyingrobots/bijou';
|
|
28
|
+
import type { Cmd, KeyMsg, ResizeMsg } from './types.js';
|
|
29
|
+
/** Any message the bus can carry — built-in or app-defined. */
|
|
30
|
+
export type BusMsg<M> = KeyMsg | ResizeMsg | M;
|
|
31
|
+
export interface EventBus<M> {
|
|
32
|
+
/**
|
|
33
|
+
* Subscribe to all events. Returns a dispose function.
|
|
34
|
+
* Multiple subscribers are supported — all receive every event.
|
|
35
|
+
*/
|
|
36
|
+
on(handler: (msg: BusMsg<M>) => void): Disposable;
|
|
37
|
+
/** Emit a message to all subscribers. */
|
|
38
|
+
emit(msg: BusMsg<M>): void;
|
|
39
|
+
/**
|
|
40
|
+
* Connect keyboard and resize sources from an IOPort.
|
|
41
|
+
* Raw stdin bytes are parsed into KeyMsg automatically.
|
|
42
|
+
* Returns a dispose function that disconnects both.
|
|
43
|
+
*/
|
|
44
|
+
connectIO(io: IOPort): Disposable;
|
|
45
|
+
/**
|
|
46
|
+
* Run a command. The command receives the bus's `emit` function to
|
|
47
|
+
* dispatch intermediate messages during execution. When it resolves:
|
|
48
|
+
* - QUIT signal → fires onQuit handlers
|
|
49
|
+
* - Message → emitted to all subscribers
|
|
50
|
+
* - void/undefined → ignored
|
|
51
|
+
*
|
|
52
|
+
* Rejected commands are logged to stderr via `console.error`.
|
|
53
|
+
*/
|
|
54
|
+
runCmd(cmd: Cmd<M>): void;
|
|
55
|
+
/**
|
|
56
|
+
* Register a quit handler. Called when a command resolves to QUIT.
|
|
57
|
+
* Separate from `on()` so the runtime can handle shutdown without
|
|
58
|
+
* the app needing to filter for it.
|
|
59
|
+
*/
|
|
60
|
+
onQuit(handler: () => void): Disposable;
|
|
61
|
+
/** Disconnect all sources and remove all subscribers. */
|
|
62
|
+
dispose(): void;
|
|
63
|
+
}
|
|
64
|
+
interface Disposable {
|
|
65
|
+
dispose(): void;
|
|
66
|
+
}
|
|
67
|
+
export declare function createEventBus<M>(): EventBus<M>;
|
|
68
|
+
export {};
|
|
69
|
+
//# sourceMappingURL=eventbus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eventbus.d.ts","sourceRoot":"","sources":["../src/eventbus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAClD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAQzD,+DAA+D;AAC/D,MAAM,MAAM,MAAM,CAAC,CAAC,IAAI,MAAM,GAAG,SAAS,GAAG,CAAC,CAAC;AAE/C,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB;;;OAGG;IACH,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,UAAU,CAAC;IAElD,yCAAyC;IACzC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,CAAC;IAElC;;;;;;;;OAQG;IACH,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAE1B;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,UAAU,CAAC;IAExC,yDAAyD;IACzD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,UAAU,UAAU;IAClB,OAAO,IAAI,IAAI,CAAC;CACjB;AAMD,wBAAgB,cAAc,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CA0F/C"}
|