@flyingrobots/bijou-tui 4.2.0 → 4.4.1

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 (66) hide show
  1. package/README.md +52 -617
  2. package/dist/app-frame-actions.d.ts.map +1 -1
  3. package/dist/app-frame-actions.js +4 -1
  4. package/dist/app-frame-actions.js.map +1 -1
  5. package/dist/app-frame-i18n.d.ts.map +1 -1
  6. package/dist/app-frame-i18n.js +4 -0
  7. package/dist/app-frame-i18n.js.map +1 -1
  8. package/dist/app-frame-render.d.ts +11 -9
  9. package/dist/app-frame-render.d.ts.map +1 -1
  10. package/dist/app-frame-render.js +102 -39
  11. package/dist/app-frame-render.js.map +1 -1
  12. package/dist/app-frame-types.d.ts +3 -0
  13. package/dist/app-frame-types.d.ts.map +1 -1
  14. package/dist/app-frame-types.js.map +1 -1
  15. package/dist/app-frame.d.ts +27 -1
  16. package/dist/app-frame.d.ts.map +1 -1
  17. package/dist/app-frame.js +244 -63
  18. package/dist/app-frame.js.map +1 -1
  19. package/dist/canvas.d.ts.map +1 -1
  20. package/dist/canvas.js +25 -4
  21. package/dist/canvas.js.map +1 -1
  22. package/dist/command-palette.d.ts +5 -3
  23. package/dist/command-palette.d.ts.map +1 -1
  24. package/dist/command-palette.js +5 -3
  25. package/dist/command-palette.js.map +1 -1
  26. package/dist/css/text-style.d.ts +6 -0
  27. package/dist/css/text-style.d.ts.map +1 -1
  28. package/dist/css/text-style.js +59 -15
  29. package/dist/css/text-style.js.map +1 -1
  30. package/dist/flex.d.ts.map +1 -1
  31. package/dist/flex.js +28 -4
  32. package/dist/flex.js.map +1 -1
  33. package/dist/focus-area.d.ts.map +1 -1
  34. package/dist/focus-area.js +18 -3
  35. package/dist/focus-area.js.map +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/notification.d.ts.map +1 -1
  40. package/dist/notification.js +56 -16
  41. package/dist/notification.js.map +1 -1
  42. package/dist/overlay.d.ts.map +1 -1
  43. package/dist/overlay.js +103 -21
  44. package/dist/overlay.js.map +1 -1
  45. package/dist/pipeline/middleware/grayscale.d.ts +3 -0
  46. package/dist/pipeline/middleware/grayscale.d.ts.map +1 -1
  47. package/dist/pipeline/middleware/grayscale.js +63 -10
  48. package/dist/pipeline/middleware/grayscale.js.map +1 -1
  49. package/dist/pipeline/pipeline.d.ts +7 -0
  50. package/dist/pipeline/pipeline.d.ts.map +1 -1
  51. package/dist/pipeline/pipeline.js.map +1 -1
  52. package/dist/runtime.d.ts.map +1 -1
  53. package/dist/runtime.js +33 -1
  54. package/dist/runtime.js.map +1 -1
  55. package/dist/screen.d.ts +5 -1
  56. package/dist/screen.d.ts.map +1 -1
  57. package/dist/screen.js +6 -2
  58. package/dist/screen.js.map +1 -1
  59. package/dist/shell-quit.d.ts +1 -1
  60. package/dist/shell-quit.d.ts.map +1 -1
  61. package/dist/shell-quit.js +14 -3
  62. package/dist/shell-quit.js.map +1 -1
  63. package/dist/transition-shaders.d.ts.map +1 -1
  64. package/dist/transition-shaders.js +6 -1
  65. package/dist/transition-shaders.js.map +1 -1
  66. package/package.json +3 -3
package/README.md CHANGED
@@ -1,40 +1,37 @@
1
- # `@flyingrobots/bijou-tui`
1
+ # @flyingrobots/bijou-tui
2
2
 
3
3
  The high-fidelity TEA runtime for Bijou.
4
4
 
5
- `bijou-tui` provides the application loop, layout primitives, motion, and orchestration needed to build complex interactive terminal apps on top of the Bijou core.
5
+ `@flyingrobots/bijou-tui` provides the application loop, layout primitives, and physics-powered orchestration needed to build complex interactive terminal apps.
6
6
 
7
- ## Package Role
7
+ ## Role
8
8
 
9
- The TUI package is the surface-first fullscreen runtime in the Bijou stack.
9
+ - **The Elm Architecture (TEA)**: A deterministic state-update-view loop for industrial-strength terminal software.
10
+ - **Fractal TEA**: Compose nested sub-apps with `initSubApp()`, `updateSubApp()`, and `mount()`.
11
+ - **Declarative Motion**: Interpolate layout changes smoothly with physics-based springs and tween animations.
12
+ - **Surface-First Pipeline**: Programmable rendering middleware for fragments, diffing, and shader-based transitions.
10
13
 
11
- ### What's Included
12
- - **Pure view contract:** `App.view` and framed pane renderers now speak `ViewOutput` (`Surface | LayoutNode`).
13
- - **Programmable Rendering Pipeline:** The TEA `view` output is now processed through a 5-stage middleware pipeline (`Layout -> Paint -> PostProcess -> Diff -> Output`). Add custom fragment shaders or logging middleware effortlessly.
14
- - **Fractal TEA (Sub-Apps):** Compose nested apps with `initSubApp()`, `updateSubApp()`, `mount()`, and `mapCmds()` instead of flattening everything into one update loop.
15
- - **Bijou CSS (BCSS):** Style supported runtime surface components and frame shell regions with type/class/id selectors, `var()` token lookups, and terminal-aware media queries (`@media (width < 80)`). This is not yet a global cascade across arbitrary layout nodes.
16
- - **Declarative Motion:** Wrap any component in `motion({ key: 'id' }, ...)` and watch it smoothly interpolate layout changes (move, resize) using physics-based springs.
17
- - **Unified Heartbeat:** All animations and physics calculations are now synchronized to a single `PulseMsg`, eliminating timer jitter and saving CPU.
18
-
19
- ## Installation
14
+ ## Install
20
15
 
21
16
  ```bash
22
17
  npm install @flyingrobots/bijou @flyingrobots/bijou-node @flyingrobots/bijou-tui
23
18
  ```
24
19
 
25
- If you are upgrading an existing app, see [`../../docs/MIGRATING_TO_V4.md`](../../docs/MIGRATING_TO_V4.md).
26
-
27
20
  ## Quick Start (Sub-App Composition)
28
21
 
29
22
  ```typescript
30
23
  import { initDefaultContext } from '@flyingrobots/bijou-node';
31
- import { run, mount, mapCmds, type App } from '@flyingrobots/bijou-tui';
32
- import { createSurface, type Surface } from '@flyingrobots/bijou';
24
+ import { run, mount, type App } from '@flyingrobots/bijou-tui';
25
+ import { createSurface } from '@flyingrobots/bijou';
33
26
 
34
27
  initDefaultContext();
35
28
 
36
- // Minimal child apps
37
- const childApp: App<{ count: number }, any> = {
29
+ type ChildModel = { count: number };
30
+ type ChildMsg = { type: 'noop' };
31
+ type Model = { left: ChildModel; right: ChildModel };
32
+ type Msg = { pane: 'left' | 'right'; msg: ChildMsg };
33
+
34
+ const childApp: App<ChildModel, ChildMsg> = {
38
35
  init: () => [{ count: 0 }, []],
39
36
  update: (msg, model) => [model, []],
40
37
  view: (model) => {
@@ -44,24 +41,22 @@ const childApp: App<{ count: number }, any> = {
44
41
  }
45
42
  };
46
43
 
47
- interface Model {
48
- left: { count: number };
49
- right: { count: number };
50
- }
51
-
52
- // Parent App mounting two independent Sub-Apps
53
- const app: App<Model, any> = {
44
+ const app: App<Model, Msg> = {
54
45
  init: () => [{ left: { count: 0 }, right: { count: 0 } }, []],
55
46
  update: (msg, model) => [model, []],
56
47
  view: (model) => {
57
- // Render the children (they return Surfaces!)
58
- const [leftSurface] = mount(childApp, { model: model.left, onMsg: m => m });
59
- const [rightSurface] = mount(childApp, { model: model.right, onMsg: m => m });
48
+ const [left] = mount(childApp, {
49
+ model: model.left,
50
+ onMsg: (msg) => ({ pane: 'left', msg }),
51
+ });
52
+ const [right] = mount(childApp, {
53
+ model: model.right,
54
+ onMsg: (msg) => ({ pane: 'right', msg }),
55
+ });
60
56
 
61
- // Composite them onto the main screen
62
57
  const screen = createSurface(80, 24);
63
- screen.blit(leftSurface, 0, 0);
64
- screen.blit(rightSurface, 40, 0);
58
+ screen.blit(left, 0, 0);
59
+ screen.blit(right, 40, 0);
65
60
  return screen;
66
61
  }
67
62
  };
@@ -69,617 +64,57 @@ const app: App<Model, any> = {
69
64
  run(app);
70
65
  ```
71
66
 
72
- ## Quick Start (Basic)
73
-
74
- ```typescript
75
- import { initDefaultContext } from '@flyingrobots/bijou-node';
76
- import { stringToSurface } from '@flyingrobots/bijou';
77
- import { run, quit, type App, isKeyMsg } from '@flyingrobots/bijou-tui';
78
-
79
- initDefaultContext();
80
-
81
- type Model = { count: number };
82
-
83
- const app: App<Model> = {
84
- init: () => [{ count: 0 }, []],
85
-
86
- update: (msg, model) => {
87
- if (isKeyMsg(msg)) {
88
- if (msg.key === 'q') return [model, [quit()]];
89
- if (msg.key === '+') return [{ count: model.count + 1 }, []];
90
- if (msg.key === '-') return [{ count: model.count - 1 }, []];
91
- }
92
- return [model, []];
93
- },
94
-
95
- view: (model) => {
96
- const text = `Count: ${model.count}\n\nPress +/- to change, q to quit`;
97
- const lines = text.split('\n');
98
- return stringToSurface(text, Math.max(1, ...lines.map((line) => line.length)), lines.length);
99
- },
100
- };
101
-
102
- run(app);
103
- ```
104
-
105
- ## Runtime Behavior Note
106
-
107
- `run()` behaves differently by output mode:
108
-
109
- - `interactive`: full TEA loop (event bus, key/resize/mouse handling, command-driven updates).
110
- - `pipe` / `static` / `accessible`: render `view(initModel)` once and return immediately.
111
-
112
- In non-interactive modes, there is no normal interactive event loop.
113
-
114
- ## Command Model
115
-
116
- `Cmd<M>` may resolve synchronously or asynchronously to:
117
-
118
- - a final message
119
- - `QUIT`
120
- - a cleanup handle or cleanup function for a long-lived effect
121
- - `void`
67
+ ## Strategy: Choosing Component Families
122
68
 
123
- If a command installs a long-lived effect through `onPulse()` or another runtime-backed capability, return that cleanup so the event bus/runtime can dispose it on shutdown.
69
+ Select the family based on the interaction semantic.
124
70
 
125
- ## Features Breakdown
71
+ ### Overlays and Interruption
72
+ - **`drawer()`**: Supplemental detail while maintaining main context.
73
+ - **`modal()`**: Required decision that blocks background activity.
74
+ - **`toast()`**: Transient notification for a single event.
75
+ - **`tooltip()`**: Micro-explanation for a local target.
126
76
 
127
- - **TEA runtime core**: deterministic model/update/view loop with command-driven side effects.
128
- - **Motion system**: spring physics, tweens, and timeline sequencing for orchestrated terminal animation.
129
- - **Layout engine**: flexbox helpers, stacks, split panes, named-area grids, viewport scrolling, and resize-aware rendering.
130
- - **Input architecture**: keymaps, grouped bindings, generated help views, and layered input stack for modal flows.
131
- - **Overlay composition**: modal, toast, drawer, tooltip, and painter-style compositing primitives (including panel-scoped drawers).
132
- - **App shell**: `createFramedApp()` for tabs/help/chrome/pane-focus boilerplate with optional command palette.
133
- - **Stateful building blocks**: navigable table, browsable list, file picker, focus area, and DAG pane with vim-friendly keymaps.
77
+ ### Collection Interaction
78
+ - **`navigableTable()`**: Keyboard-driven traversal and cell inspection.
79
+ - **`browsableList()`**: Description-led traversal in one dimension.
80
+ - **`commandPalette()`**: Action discovery and navigation.
134
81
 
135
- ## Choosing Component Families
136
-
137
- ### Overlays and interruption
138
-
139
- - Use `toast()` when you are composing a single transient overlay directly.
140
- - Use the notification system when the app needs stacking, placement, actions, routing, or history.
141
- - Use `drawer()` when the user should keep the main surface visible while working in supplemental detail.
142
- - Use `modal()` when background shortcuts and pointer actions should be blocked.
143
- - Use `tooltip()` only for tiny local explanation, not for decisions or scrollable content.
144
- - If the overlay needs embedded component surfaces or multiple real rows, keep it on the structured `Surface` path with `compositeSurface()`.
145
-
146
- ### Collection interaction
147
-
148
- - Use core `table()` or `tableSurface()` for passive comparison.
149
- - Use `navigableTable()` when row/cell focus and keyboard traversal are the real job.
150
- - Use `browsableList()` when the content is one-dimensional and description-led rather than grid-oriented.
151
- - Use `commandPaletteSurface()` when the outcome is an action or navigation target, not a stored form value.
152
- - If users are really choosing persisted values, keep that work in core `select()` / `filter()` / `multiselect()` instead of turning the palette into a value picker.
153
-
154
- ### Shell and workspace layout
155
-
156
- - Use `createFramedApp()` when the app has multiple destinations, overlays, and workspace state that should be standardized.
157
- - Use `splitPane()` when the user benefits from primary-versus-secondary context or side-by-side comparison.
158
- - Use `grid()` when multiple stable regions deserve simultaneous visibility.
159
- - Use `statusBarSurface()` when shell chrome already lives on the structured `Surface` path; keep `statusBar()` for explicit text output.
160
- - Use `helpShortSurface()` or `helpViewSurface()` when shortcut guidance stays inside the rich shell; keep `helpShort()` / `helpView()` for explicit text output.
161
- - Use `commandPaletteSurface()` for action discovery and navigation inside the shell, not as a substitute value picker.
162
- - Use notifications for events and follow-up, not as a replacement for the status rail.
163
- - Keep status rails concise and global; explanatory text belongs in the page, not in shell chrome.
164
- - Mouse is enhancement, not baseline. Overlay layers should consume pointer input before shell chrome or page content, and every click target should mirror an existing keyboard path.
82
+ ### Shell and Workspace Layout
83
+ - **`createFramedApp()`**: Batteries-included workspace with tabs, panes, and help.
84
+ - **`splitPane()`**: Dynamic primary/secondary context comparison.
85
+ - **`grid()`**: Stable regions with simultaneous visibility.
86
+ - **`viewport()`**: The canonical scroll mask for rich composition.
165
87
 
166
88
  ## Animation
167
89
 
168
90
  ### Spring Physics
169
-
170
91
  ```typescript
171
92
  import { animate, SPRING_PRESETS } from '@flyingrobots/bijou-tui';
172
93
 
173
- // Physics-based (default) — runs until the spring settles
174
94
  const cmd = animate({
175
95
  from: 0,
176
96
  to: 100,
177
- spring: 'wobbly', // or 'default', 'gentle', 'stiff', 'slow', 'molasses'
178
- onFrame: (v) => ({ type: 'scroll', y: v }),
179
- });
180
-
181
- // Duration-based with easing
182
- const fade = animate({
183
- type: 'tween',
184
- from: 0,
185
- to: 1,
186
- duration: 300,
187
- ease: EASINGS.easeOutCubic,
188
- onFrame: (v) => ({ type: 'fade', opacity: v }),
189
- });
190
-
191
- // Skip animation (reduced motion)
192
- const jump = animate({
193
- from: 0, to: 100,
194
- immediate: true,
97
+ spring: 'wobbly',
195
98
  onFrame: (v) => ({ type: 'scroll', y: v }),
196
99
  });
197
100
  ```
198
101
 
199
- ### Timeline
200
-
201
- GSAP-style orchestration — pure state machine, no timers:
202
-
102
+ ### Timeline Orchestration
203
103
  ```typescript
204
104
  import { timeline } from '@flyingrobots/bijou-tui';
205
105
 
206
106
  const tl = timeline()
207
- .add('slideIn', { type: 'tween', from: -100, to: 0, duration: 300 })
208
- .add('fadeIn', { type: 'tween', from: 0, to: 1, duration: 200 }, '-=100')
107
+ .add('slideIn', { type: 'tween', from: -100, to: 0, duration: 300 })
209
108
  .label('settled')
210
- .add('bounce', { from: 0, to: 10, spring: 'wobbly' }, 'settled')
211
- .call('onReady', 'settled+=50')
109
+ .add('bounce', { from: 0, to: 10, spring: 'wobbly' }, 'settled')
212
110
  .build();
213
-
214
- // Drive from TEA update:
215
- let tlState = tl.init();
216
- // on each frame:
217
- tlState = tl.step(tlState, 1/60);
218
- const { slideIn, fadeIn, bounce } = tl.values(tlState);
219
- const fired = tl.firedCallbacks(prev, tlState); // ['onReady']
220
111
  ```
221
112
 
222
- Position syntax: `'<'` (parallel), `'+=N'` (gap), `'-=N'` (overlap), `'<+=N'` (offset from previous start), absolute ms, `'label'`, `'label+=N'`.
223
-
224
- ## Transition Shaders
225
-
226
- Custom page transitions are surface-native in v4. Shader functions decide whether each cell shows the previous page or next page, and may optionally provide override data for that cell.
227
-
228
- ```typescript
229
- import { type TransitionShaderFn } from '@flyingrobots/bijou-tui';
230
-
231
- const shimmer: TransitionShaderFn = ({ progress, x, width }) => {
232
- const edge = Math.floor(progress * width);
233
- if (x < edge) return { showNext: true };
234
- if (x === edge) return { showNext: false, overrideChar: '░', overrideRole: 'marker' };
235
- return { showNext: false };
236
- };
237
- ```
238
-
239
- Use `overrideChar` when the base cell styling should stay intact, `overrideCell` when the shader needs full fg/bg/modifier control, and `overrideRole` to tell combinators whether an override is ambient (`'decoration'`) or positional (`'marker'`).
240
-
241
- Use transition shaders to reinforce workspace change, not as default spectacle. If the effect makes the new page harder to read or hides state meaning that should remain explicit in static or accessible modes, it is the wrong transition.
242
-
243
- ## Layout
113
+ ## Documentation
244
114
 
245
- ### Flexbox
246
-
247
- ```typescript
248
- import { flex } from '@flyingrobots/bijou-tui';
249
-
250
- // Sidebar + main content, responsive to terminal width
251
- flex({ direction: 'row', width: cols, height: rows, gap: 1 },
252
- { basis: 20, content: sidebarText },
253
- { flex: 1, content: (w, h) => renderMain(w, h) },
254
- );
255
-
256
- // Header + body + footer
257
- flex({ direction: 'column', width: cols, height: rows },
258
- { basis: 1, content: headerLine },
259
- { flex: 1, content: (w, h) => renderBody(w, h) },
260
- { basis: 1, content: statusLine },
261
- );
262
- ```
263
-
264
- Children can be **render functions** `(width, height) => string` — they receive their allocated space and reflow automatically when the terminal resizes.
265
-
266
- ### Viewport
267
-
268
- ```typescript
269
- import { viewportSurface, createScrollStateForContent, scrollBy, pageDown } from '@flyingrobots/bijou-tui';
270
- import { boxSurface } from '@flyingrobots/bijou';
271
-
272
- const content = boxSurface(longText, { width: 72 });
273
- let scroll = createScrollStateForContent(content, viewportHeight);
274
-
275
- // Mask the content to a visible window with scrollbar
276
- const view = viewportSurface({ width: 60, height: 20, content, scrollY: scroll.y });
277
-
278
- // Handle scroll keys
279
- scroll = scrollBy(scroll, 1); // down one line
280
- scroll = pageDown(scroll); // down one page
281
- ```
282
-
283
- Treat `viewportSurface()` as the canonical scroll mask for rich TUI composition. Keep `viewport()` for explicit text-lowering paths.
284
-
285
- ### Basic Layout
286
-
287
- ```typescript
288
- import {
289
- hstack,
290
- hstackSurface,
291
- place,
292
- placeSurface,
293
- vstack,
294
- vstackSurface,
295
- } from '@flyingrobots/bijou-tui';
296
-
297
- vstack(header, content, footer); // explicit text-lowering path
298
- hstack(2, leftPanel, rightPanel); // explicit text-lowering path
299
- place('Title', { width: 20, height: 3 }); // text placement
300
-
301
- vstackSurface(headerSurface, bodySurface); // structured surface stack
302
- hstackSurface(2, navSurface, mainSurface); // structured horizontal stack
303
- placeSurface(dialogSurface, { // structured placement/alignment
304
- width: cols,
305
- height: rows,
306
- hAlign: 'center',
307
- vAlign: 'middle',
308
- });
309
- ```
310
-
311
- Prefer `vstackSurface()` / `hstackSurface()` / `placeSurface()` when the view is already composed from `Surface` values. Keep `vstack()` / `hstack()` / `place()` for explicit text composition or lowering paths.
312
-
313
- ### Split Pane
314
-
315
- ```typescript
316
- import {
317
- createSplitPaneState, splitPaneSurface, splitPaneResizeBy, splitPaneFocusNext,
318
- } from '@flyingrobots/bijou-tui';
319
-
320
- let state = createSplitPaneState({ ratio: 0.35 });
321
-
322
- // in update:
323
- state = splitPaneResizeBy(state, 2, { total: cols, minA: 16, minB: 16 });
324
- state = splitPaneFocusNext(state);
325
-
326
- // in view:
327
- const output = splitPaneSurface(state, {
328
- direction: 'row',
329
- width: cols,
330
- height: rows,
331
- minA: 16,
332
- minB: 16,
333
- paneA: (w, h) => renderSidebar(w, h),
334
- paneB: (w, h) => renderMain(w, h),
335
- });
336
- ```
337
-
338
- Prefer `splitPaneSurface()` when the panes are already structured `Surface` views. Keep `splitPane()` for explicit text composition or lowering paths.
339
-
340
- ### Grid
341
-
342
- ```typescript
343
- import { gridSurface } from '@flyingrobots/bijou-tui';
344
-
345
- const output = gridSurface({
346
- width: cols,
347
- height: rows,
348
- columns: [24, '1fr'],
349
- rows: [3, '1fr', 8],
350
- areas: [
351
- 'header header',
352
- 'nav main',
353
- 'logs main',
354
- ],
355
- gap: 1,
356
- cells: {
357
- header: (w, h) => renderHeader(w, h),
358
- nav: (w, h) => renderNav(w, h),
359
- logs: (w, h) => renderLogs(w, h),
360
- main: (w, h) => renderMain(w, h),
361
- },
362
- });
363
- ```
364
-
365
- Prefer `gridSurface()` when the regions are already structured `Surface` views. Keep `grid()` for explicit text composition or lowering paths.
366
-
367
- ## Resize Handling
368
-
369
- Terminal resize events are dispatched automatically as `ResizeMsg`:
370
-
371
- ```typescript
372
- update(msg, model) {
373
- if (msg.type === 'resize') {
374
- return [{ ...model, cols: msg.columns, rows: msg.rows }, []];
375
- }
376
- // ...
377
- }
378
-
379
- view(model) {
380
- return flex(
381
- { direction: 'row', width: model.cols, height: model.rows },
382
- { basis: 20, content: sidebar },
383
- { flex: 1, content: (w, h) => mainContent(w, h) },
384
- );
385
- }
386
- ```
387
-
388
- ## Event Bus
389
-
390
- The runtime uses an `EventBus` internally. You can also create your own for custom event sources:
391
-
392
- ```typescript
393
- import { createEventBus } from '@flyingrobots/bijou-tui';
394
-
395
- const bus = createEventBus<MyMsg>();
396
- bus.connectIO(ctx.io); // keyboard + resize
397
- bus.on((msg) => { /* ... */ }); // single subscription
398
- bus.emit(customMsg); // synthetic events
399
- bus.runCmd(someCommand); // final messages re-emitted, cleanup retained
400
- bus.dispose(); // clean shutdown
401
- ```
402
-
403
- See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full event flow and [GUIDE.md](./GUIDE.md) for detailed usage patterns.
404
-
405
- ## Keybinding Manager
406
-
407
- Declarative key binding with modifier support, named groups, and runtime enable/disable:
408
-
409
- ```typescript
410
- import { createKeyMap, type KeyMsg } from '@flyingrobots/bijou-tui';
411
-
412
- type Msg = { type: 'quit' } | { type: 'help' } | { type: 'move'; dir: string };
413
-
414
- const kb = createKeyMap<Msg>()
415
- .bind('q', 'Quit', { type: 'quit' })
416
- .bind('?', 'Help', { type: 'help' })
417
- .bind('ctrl+c', 'Force quit', { type: 'quit' })
418
- .group('Navigation', (g) => g
419
- .bind('j', 'Down', { type: 'move', dir: 'down' })
420
- .bind('k', 'Up', { type: 'move', dir: 'up' })
421
- );
422
-
423
- // In TEA update:
424
- const action = kb.handle(keyMsg);
425
- if (action !== undefined) return [model, [/* ... */]];
426
-
427
- // Runtime enable/disable
428
- kb.disableGroup('Navigation');
429
- kb.enable('Quit');
430
- ```
431
-
432
- ### Help Generation
433
-
434
- Auto-generate help text from registered bindings:
435
-
436
- ```typescript
437
- import {
438
- helpView,
439
- helpViewSurface,
440
- helpShort,
441
- helpShortSurface,
442
- helpFor,
443
- helpForSurface,
444
- } from '@flyingrobots/bijou-tui';
445
-
446
- helpView(kb); // full grouped multi-line help
447
- helpShort(kb); // "q Quit • ? Help • Ctrl+c Force quit • j Down • k Up"
448
- helpFor(kb, 'Nav'); // only Navigation group
449
- helpShortSurface(kb, { width: 48 }); // shell hint that stays on the Surface path
450
- helpViewSurface(kb, { width: 48 }); // grouped help as a Surface
451
- helpForSurface(kb, 'Nav', { width: 48 });
452
- ```
453
-
454
- ### Input Stack
455
-
456
- Layered input dispatch for modal UIs — push/pop handlers with opaque or passthrough behavior:
457
-
458
- ```typescript
459
- import { createInputStack, type KeyMsg } from '@flyingrobots/bijou-tui';
460
-
461
- const stack = createInputStack<KeyMsg, Msg>();
462
-
463
- // Base layer — global keys, lets unmatched events fall through
464
- stack.push(appKeys, { passthrough: true });
465
-
466
- // Modal opens — captures all input (opaque by default)
467
- const modalId = stack.push(modalKeys);
468
-
469
- // Dispatch returns first matched action, top-down
470
- const action = stack.dispatch(keyMsg);
471
-
472
- // Modal closes
473
- stack.remove(modalId);
474
- ```
475
-
476
- `KeyMap` implements `InputHandler`, so it plugs directly into the input stack.
477
-
478
- ## Overlay Compositing
479
-
480
- Paint overlays (modals, toasts) on top of existing content:
481
-
482
- ```typescript
483
- import { compositeSurface, modal, toast } from '@flyingrobots/bijou-tui';
484
-
485
- // Create a centered dialog
486
- const dialog = modal({
487
- title: 'Confirm',
488
- body: 'Delete this item?',
489
- hint: 'y/n',
490
- screenWidth: 80,
491
- screenHeight: 24,
492
- });
493
-
494
- // Create a toast notification
495
- const notification = toast({
496
- message: 'Saved successfully',
497
- variant: 'success', // 'success' | 'error' | 'info'
498
- anchor: 'bottom-right', // 'top-right' | 'bottom-right' | 'bottom-left' | 'top-left'
499
- screenWidth: 80,
500
- screenHeight: 24,
501
- });
502
-
503
- // Paint overlays onto background content
504
- const output = compositeSurface(backgroundSurface, [dialog, notification], { dim: true });
505
- ```
506
-
507
- Each overlay now exposes both `surface` and `content` forms. Prefer `compositeSurface()` when your app is already on the surface-native path. Keep the string-oriented `composite()` path for explicit lowering boundaries, not as the default mental model. The `dim` option fades the background with ANSI dim.
508
-
509
- `modal().body`, `modal().hint`, `drawer().content`, and `tooltip().content` accept either plain strings or structured `Surface` content. Use surfaces when the overlay needs real rows, embedded component surfaces, or richer composition inside the interrupting layer.
510
-
511
- Reach for `toast()` when the app is composing a one-off overlay directly. Reach for the notification system when stacking, actions, routing, or history matter. The notification lab in `examples/notifications` is the canonical higher-level example.
512
-
513
- `drawer()` now supports `left`/`right`/`top`/`bottom` anchors and optional `region` mounting for panel-scoped overlays.
514
- Use `drawer()` when the user should keep the main task visible while consulting or editing supplemental context. Use `modal()` when the user must stop and decide. Use `tooltip()` only for tiny local explanation, not for commands or scrollable content.
515
-
516
- ## App Frame
517
-
518
- `createFramedApp()` wraps page-level TEA logic in a shared shell:
519
-
520
- - tabs + page switching
521
- - pane focus and per-pane scroll isolation
522
- - frame help (`?`) and optional command palette (`ctrl+p` / `:`)
523
- - overlay factory with pane rects for panel-scoped drawers/modals
524
-
525
- Pane renderers return a `Surface` or a `LayoutNode`. The shell normalizes those outputs into the framed scroll/focus path for you.
526
-
527
- Typed shell notes:
528
-
529
- - `createFramedApp<PageModel, Msg>()` returns `FramedApp<PageModel, Msg>`
530
- - page `update()` receives `FramePageMsg<Msg>` so raw `mouse` / `pulse` delivery is explicit
531
- - wrapped shell commands carry `FramedAppMsg<Msg>` instead of collapsing frame/page wrappers into plain `Msg`
532
-
533
- See `examples/release-workbench/main.ts` for the canonical shell demo and `examples/app-frame/main.ts` for a compact focused example.
534
-
535
- Shell role split matters:
536
-
537
- - `statusBarSurface()` communicates concise global state
538
- - `helpShortSurface()` / `helpViewSurface()` teach shortcuts and scope
539
- - `commandPaletteSurface()` handles action discovery and navigation
540
- - notifications surface events and follow-up
541
-
542
- ## Building Blocks
543
-
544
- Reusable stateful components that follow the TEA state + pure transformers + sync render + convenience keymap pattern:
545
-
546
- ### Navigable Table
547
-
548
- ```typescript
549
- import {
550
- createNavigableTableState, navigableTable, navigableTableSurface, navTableFocusNext,
551
- navTableKeyMap, helpShort,
552
- } from '@flyingrobots/bijou-tui';
553
-
554
- const state = createNavigableTableState({ columns, rows, height: 10 });
555
- const textOutput = navigableTable(state, { ctx });
556
- const surfaceOutput = navigableTableSurface(state, { ctx });
557
- const next = navTableFocusNext(state);
558
- ```
559
-
560
- Use `navigableTableSurface()` when the user should actively traverse a table inside a rich TUI surface. Keep `navigableTable()` for explicit text lowering. If the job is still passive comparison, prefer core `table()` or `tableSurface()` and keep the interaction layer simpler.
561
-
562
- ### Browsable List
563
-
564
- ```typescript
565
- import {
566
- createBrowsableListState, browsableList, browsableListSurface, listFocusNext,
567
- browsableListKeyMap,
568
- } from '@flyingrobots/bijou-tui';
569
-
570
- const state = createBrowsableListState({ items, height: 10 });
571
- const textOutput = browsableList(state);
572
- const surfaceOutput = browsableListSurface(state, { width: 40 });
573
- ```
574
-
575
- Use `browsableListSurface()` when the list belongs inside a rich TUI region and should share viewport masking semantics with pagers and focus areas. Keep `browsableList()` for explicit text lowering.
576
-
577
- ### File Picker
578
-
579
- ```typescript
580
- import {
581
- createFilePickerState, filePicker, filePickerSurface, fpFocusNext, fpEnter, fpBack,
582
- filePickerKeyMap,
583
- } from '@flyingrobots/bijou-tui';
584
- import { nodeIO } from '@flyingrobots/bijou-node';
585
-
586
- const io = nodeIO();
587
- const state = createFilePickerState({ cwd: process.cwd(), io, height: 15 });
588
- const textOutput = filePicker(state);
589
- const surfaceOutput = filePickerSurface(state, { width: 60 });
590
- ```
591
-
592
- Use `filePickerSurface()` when the browser lives inside a rich TUI pane and should inherit shared viewport masking semantics. Keep `filePicker()` for explicit text lowering.
593
-
594
- ### Command Palette
595
-
596
- ```typescript
597
- import {
598
- createCommandPaletteState, commandPalette, commandPaletteSurface,
599
- cpFilter, commandPaletteKeyMap,
600
- } from '@flyingrobots/bijou-tui';
601
-
602
- const state = createCommandPaletteState(items, 8);
603
- const textOutput = commandPalette(state, { width: 60 });
604
- const surfaceOutput = commandPaletteSurface(state, { width: 60, ctx });
605
- ```
606
-
607
- Use `commandPaletteSurface()` when the palette is part of a structured shell or overlay and should share viewport masking semantics. Keep `commandPalette()` for explicit text lowering.
608
-
609
- ### Pager
610
-
611
- ```typescript
612
- import {
613
- createPagerStateForSurface,
614
- pagerSurface,
615
- pagerScrollBy,
616
- } from '@flyingrobots/bijou-tui';
617
-
618
- const state = createPagerStateForSurface(contentSurface, { width: 60, height: 20 });
619
- const output = pagerSurface(contentSurface, state);
620
- ```
621
-
622
- ### Focus Area
623
-
624
- ```typescript
625
- import {
626
- createFocusAreaStateForSurface, focusAreaScrollBy, focusAreaSurface,
627
- focusAreaKeyMap,
628
- } from '@flyingrobots/bijou-tui';
629
-
630
- const state = createFocusAreaStateForSurface(contentSurface, {
631
- width: 60,
632
- height: 20,
633
- overflowX: 'scroll',
634
- });
635
- const output = focusAreaSurface(contentSurface, state, { focused: true, ctx });
636
- ```
637
-
638
- If the pane is still intentionally text-composed, `createFocusAreaState()` + `focusArea()` remain the explicit lowering path.
639
-
640
- ### DAG Pane
641
-
642
- Use `dagPane()` when graph inspection is an active task and the user needs keyboard-owned selection, path highlighting, and scroll control. Keep plain `dag()` in `@flyingrobots/bijou` for passive graph explanation, and move to `dagSlice()` or `dagStats()` when a focused fragment or structural summary would be more honest than a full interactive graph.
643
-
644
- ```typescript
645
- import {
646
- createDagPaneState, dagPane, dagPaneSelectChild,
647
- dagPaneSelectParent, dagPaneKeyMap,
648
- } from '@flyingrobots/bijou-tui';
649
-
650
- const state = createDagPaneState({ source: nodes, width: 80, height: 24, ctx });
651
- const output = dagPane(state, { focused: true, ctx });
652
- const next = dagPaneSelectChild(state, ctx); // arrow-key navigation
653
- ```
654
-
655
- All building blocks include `*KeyMap()` factories for preconfigured vim-style keybindings.
656
-
657
- ## Related Packages
658
-
659
- - [`@flyingrobots/bijou`](https://www.npmjs.com/package/@flyingrobots/bijou) — Zero-dependency core with all components and theme engine
660
- - [`@flyingrobots/bijou-node`](https://www.npmjs.com/package/@flyingrobots/bijou-node) — Node.js runtime adapter (chalk, readline, process)
661
-
662
- ## License
663
-
664
- Apache-2.0
115
+ - **[GUIDE.md](./GUIDE.md)**: Productive-fast path for building apps.
116
+ - **[ADVANCED_GUIDE.md](./ADVANCED_GUIDE.md)**: Shell doctrine, shaders, and motion internals.
117
+ - **[Design System](../../docs/design-system/README.md)**: Semantic guidance and patterns.
665
118
 
666
119
  ---
667
-
668
- <p align="center">
669
- Built with 💎 by <a href="https://github.com/flyingrobots">FLYING ROBOTS</a>
670
- </p>
671
-
672
- ```rust
673
- .-:::::'::: .-:. ::-.::::::. :::. .,-:::::/
674
- ;;;'''' ;;; ';;. ;;;;';;;`;;;;, `;;;,;;-'````'
675
- [[[,,== [[[ '[[,[[[' [[[ [[[[[. '[[[[[ [[[[[[/
676
- `$$$"`` $$' c$$" $$$ $$$ "Y$c$$"$$c. "$$
677
- 888 o88oo,.__ ,8P"` 888 888 Y88 `Y8bo,,,o88o
678
- "MM, """"YUMMMmM" MMM MMM YM `'YMUP"YMM
679
- :::::::.. ... :::::::. ... :::::::::::: .::::::.
680
- ;;;;``;;;; .;;;;;;;. ;;;'';;' .;;;;;;;.;;;;;;;;'''';;;` `
681
- [[[,/[[[' ,[[ \[[, [[[__[[\.,[[ \[[, [[ '[==/[[[[,
682
- $$$$$$c $$$, $$$ $$""""Y$$$$$, $$$ $$ ''' $
683
- 888b "88bo,"888,_ _,88P_88o,,od8P"888,_ _,88P 88, 88b dP
684
- MMMM "W" "YMMMMMP" ""YUMMMP" "YMMMMMP" MMM "YMmMY"
685
- ```
120
+ Built with 💎 by [FLYING ROBOTS](https://github.com/flyingrobots)