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