@flyingrobots/bijou-tui 4.4.0 → 5.0.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.
Files changed (149) hide show
  1. package/README.md +102 -600
  2. package/dist/app-frame-actions.d.ts.map +1 -1
  3. package/dist/app-frame-actions.js +6 -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 +53 -0
  7. package/dist/app-frame-i18n.js.map +1 -1
  8. package/dist/app-frame-layers.d.ts +11 -1
  9. package/dist/app-frame-layers.d.ts.map +1 -1
  10. package/dist/app-frame-layers.js +19 -0
  11. package/dist/app-frame-layers.js.map +1 -1
  12. package/dist/app-frame-overlays.d.ts +82 -0
  13. package/dist/app-frame-overlays.d.ts.map +1 -0
  14. package/dist/app-frame-overlays.js +480 -0
  15. package/dist/app-frame-overlays.js.map +1 -0
  16. package/dist/app-frame-render.d.ts +8 -8
  17. package/dist/app-frame-render.d.ts.map +1 -1
  18. package/dist/app-frame-render.js +84 -25
  19. package/dist/app-frame-render.js.map +1 -1
  20. package/dist/app-frame-types.d.ts +122 -5
  21. package/dist/app-frame-types.d.ts.map +1 -1
  22. package/dist/app-frame-types.js +17 -1
  23. package/dist/app-frame-types.js.map +1 -1
  24. package/dist/app-frame-utils.d.ts.map +1 -1
  25. package/dist/app-frame-utils.js +6 -5
  26. package/dist/app-frame-utils.js.map +1 -1
  27. package/dist/app-frame.d.ts +43 -83
  28. package/dist/app-frame.d.ts.map +1 -1
  29. package/dist/app-frame.js +499 -575
  30. package/dist/app-frame.js.map +1 -1
  31. package/dist/browsable-list.d.ts +12 -0
  32. package/dist/browsable-list.d.ts.map +1 -1
  33. package/dist/browsable-list.js +17 -6
  34. package/dist/browsable-list.js.map +1 -1
  35. package/dist/canvas.d.ts.map +1 -1
  36. package/dist/canvas.js +27 -7
  37. package/dist/canvas.js.map +1 -1
  38. package/dist/collection-surface.d.ts +8 -0
  39. package/dist/collection-surface.d.ts.map +1 -1
  40. package/dist/collection-surface.js +72 -8
  41. package/dist/collection-surface.js.map +1 -1
  42. package/dist/command-palette.d.ts +5 -3
  43. package/dist/command-palette.d.ts.map +1 -1
  44. package/dist/command-palette.js +5 -3
  45. package/dist/command-palette.js.map +1 -1
  46. package/dist/css/text-style.d.ts +2 -0
  47. package/dist/css/text-style.d.ts.map +1 -1
  48. package/dist/css/text-style.js +18 -8
  49. package/dist/css/text-style.js.map +1 -1
  50. package/dist/debug-overlay.d.ts +19 -0
  51. package/dist/debug-overlay.d.ts.map +1 -0
  52. package/dist/debug-overlay.js +25 -0
  53. package/dist/debug-overlay.js.map +1 -0
  54. package/dist/design-language.d.ts.map +1 -1
  55. package/dist/design-language.js +4 -5
  56. package/dist/design-language.js.map +1 -1
  57. package/dist/driver.d.ts +102 -0
  58. package/dist/driver.d.ts.map +1 -1
  59. package/dist/driver.js +259 -19
  60. package/dist/driver.js.map +1 -1
  61. package/dist/file-picker.d.ts.map +1 -1
  62. package/dist/file-picker.js +2 -2
  63. package/dist/file-picker.js.map +1 -1
  64. package/dist/flex.d.ts.map +1 -1
  65. package/dist/flex.js +6 -6
  66. package/dist/flex.js.map +1 -1
  67. package/dist/focus-area.d.ts +9 -1
  68. package/dist/focus-area.d.ts.map +1 -1
  69. package/dist/focus-area.js +69 -40
  70. package/dist/focus-area.js.map +1 -1
  71. package/dist/grid.d.ts +2 -2
  72. package/dist/grid.d.ts.map +1 -1
  73. package/dist/grid.js +11 -148
  74. package/dist/grid.js.map +1 -1
  75. package/dist/help.d.ts +2 -0
  76. package/dist/help.d.ts.map +1 -1
  77. package/dist/help.js +6 -4
  78. package/dist/help.js.map +1 -1
  79. package/dist/icon-presentation.d.ts +10 -0
  80. package/dist/icon-presentation.d.ts.map +1 -0
  81. package/dist/icon-presentation.js +12 -0
  82. package/dist/icon-presentation.js.map +1 -0
  83. package/dist/index.d.ts +14 -8
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +7 -5
  86. package/dist/index.js.map +1 -1
  87. package/dist/layout-preset.d.ts +1 -1
  88. package/dist/layout-preset.d.ts.map +1 -1
  89. package/dist/motion/reconciler.d.ts +6 -2
  90. package/dist/motion/reconciler.d.ts.map +1 -1
  91. package/dist/motion/reconciler.js +40 -10
  92. package/dist/motion/reconciler.js.map +1 -1
  93. package/dist/motion/types.d.ts +3 -1
  94. package/dist/motion/types.d.ts.map +1 -1
  95. package/dist/navigable-table.d.ts +9 -10
  96. package/dist/navigable-table.d.ts.map +1 -1
  97. package/dist/navigable-table.js +33 -14
  98. package/dist/navigable-table.js.map +1 -1
  99. package/dist/notification.d.ts +15 -0
  100. package/dist/notification.d.ts.map +1 -1
  101. package/dist/notification.js +98 -29
  102. package/dist/notification.js.map +1 -1
  103. package/dist/overlay.d.ts.map +1 -1
  104. package/dist/overlay.js +50 -19
  105. package/dist/overlay.js.map +1 -1
  106. package/dist/pager.d.ts +3 -1
  107. package/dist/pager.d.ts.map +1 -1
  108. package/dist/pager.js +4 -0
  109. package/dist/pager.js.map +1 -1
  110. package/dist/pipeline/middleware/grayscale.d.ts.map +1 -1
  111. package/dist/pipeline/middleware/grayscale.js +5 -5
  112. package/dist/pipeline/middleware/grayscale.js.map +1 -1
  113. package/dist/pipeline/middleware/motion.js +2 -2
  114. package/dist/pipeline/middleware/motion.js.map +1 -1
  115. package/dist/pipeline/middleware/surface-shaders.d.ts +37 -0
  116. package/dist/pipeline/middleware/surface-shaders.d.ts.map +1 -0
  117. package/dist/pipeline/middleware/surface-shaders.js +164 -0
  118. package/dist/pipeline/middleware/surface-shaders.js.map +1 -0
  119. package/dist/pipeline/pipeline.d.ts +29 -1
  120. package/dist/pipeline/pipeline.d.ts.map +1 -1
  121. package/dist/pipeline/pipeline.js +86 -23
  122. package/dist/pipeline/pipeline.js.map +1 -1
  123. package/dist/runtime.d.ts +19 -0
  124. package/dist/runtime.d.ts.map +1 -1
  125. package/dist/runtime.js +183 -30
  126. package/dist/runtime.js.map +1 -1
  127. package/dist/shell-quit.d.ts +1 -1
  128. package/dist/shell-quit.d.ts.map +1 -1
  129. package/dist/shell-quit.js +14 -3
  130. package/dist/shell-quit.js.map +1 -1
  131. package/dist/split-pane.d.ts +2 -2
  132. package/dist/split-pane.d.ts.map +1 -1
  133. package/dist/split-pane.js +28 -49
  134. package/dist/split-pane.js.map +1 -1
  135. package/dist/subapp/mount.d.ts +17 -0
  136. package/dist/subapp/mount.d.ts.map +1 -1
  137. package/dist/subapp/mount.js +13 -0
  138. package/dist/subapp/mount.js.map +1 -1
  139. package/dist/surface-layout.d.ts +8 -3
  140. package/dist/surface-layout.d.ts.map +1 -1
  141. package/dist/surface-layout.js +27 -12
  142. package/dist/surface-layout.js.map +1 -1
  143. package/dist/view-output.js +2 -2
  144. package/dist/view-output.js.map +1 -1
  145. package/dist/viewport.d.ts +13 -1
  146. package/dist/viewport.d.ts.map +1 -1
  147. package/dist/viewport.js +33 -11
  148. package/dist/viewport.js.map +1 -1
  149. package/package.json +3 -3
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 `createSubAppAdapter()`, `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
- 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';
23
+ import { startApp } from '@flyingrobots/bijou-node';
24
+ import { createSubAppAdapter, mount, type App } from '@flyingrobots/bijou-tui';
25
+ import { createSurface } from '@flyingrobots/bijou';
38
26
 
39
- initDefaultContext();
27
+ type ChildMsg = { type: 'tick' };
28
+ type ParentModel = {
29
+ left: { count: number };
30
+ right: { count: number };
31
+ };
32
+ type ParentMsg = { type: 'left'; msg: ChildMsg } | { type: 'right'; msg: ChildMsg };
40
33
 
41
- // Minimal child apps
42
- const childApp: App<{ count: number }, any> = {
34
+ const childApp: App<{ count: number }, ChildMsg> = {
43
35
  init: () => [{ count: 0 }, []],
44
36
  update: (msg, model) => [model, []],
45
37
  view: (model) => {
@@ -49,642 +41,152 @@ const childApp: App<{ count: number }, any> = {
49
41
  }
50
42
  };
51
43
 
52
- interface Model {
53
- left: { count: number };
54
- right: { count: number };
55
- }
44
+ const mapLeft = createSubAppAdapter<ParentMsg, ChildMsg>({
45
+ tick: (msg) => ({ type: 'left', msg }),
46
+ });
47
+
48
+ const mapRight = createSubAppAdapter<ParentMsg, ChildMsg>({
49
+ tick: (msg) => ({ type: 'right', msg }),
50
+ });
56
51
 
57
- // Parent App mounting two independent Sub-Apps
58
- const app: App<Model, any> = {
52
+ const app: App<ParentModel, ParentMsg> = {
59
53
  init: () => [{ left: { count: 0 }, right: { count: 0 } }, []],
60
54
  update: (msg, model) => [model, []],
61
55
  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 });
56
+ const [left] = mount(childApp, { model: model.left, onMsg: mapLeft });
57
+ const [right] = mount(childApp, { model: model.right, onMsg: mapRight });
65
58
 
66
- // Composite them onto the main screen
67
59
  const screen = createSurface(80, 24);
68
- screen.blit(leftSurface, 0, 0);
69
- screen.blit(rightSurface, 40, 0);
60
+ screen.blit(left, 0, 0);
61
+ screen.blit(right, 40, 0);
70
62
  return screen;
71
63
  }
72
64
  };
73
65
 
74
- run(app);
75
- ```
76
-
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);
66
+ await startApp(app);
108
67
  ```
109
68
 
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.
69
+ For Node hosts, prefer `startApp()` for the first-app path. Reach for
70
+ `run(app, { ctx })` when the host owns context creation explicitly.
118
71
 
119
- ## Command Model
72
+ When you need to mix small string fragments with surface-returning primitives,
73
+ keep composition on the surface side: use `contentSurface()` directly or pass
74
+ strings into `vstackSurface()` / `hstackSurface()`. Raw strings are still not a
75
+ valid `view()` return type.
120
76
 
121
- `Cmd<M>` may resolve synchronously or asynchronously to:
77
+ ## Strategy: Choosing Component Families
122
78
 
123
- - a final message
124
- - `QUIT`
125
- - a cleanup handle or cleanup function for a long-lived effect
126
- - `void`
79
+ Select the family based on the interaction semantic.
127
80
 
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.
81
+ ### Overlays and Interruption
82
+ - **`drawer()`**: Supplemental detail while maintaining main context.
83
+ - **`modal()`**: Required decision that blocks background activity.
84
+ - **`toast()`**: Transient notification for a single event.
85
+ - **`tooltip()`**: Micro-explanation for a local target.
86
+ - **`debugOverlay()`**: Development-only perf HUD composited onto any app surface.
129
87
 
130
- ## Features Breakdown
88
+ ### Collection Interaction
89
+ - **`navigableTable()`**: Keyboard-driven traversal and cell inspection.
90
+ - **`browsableList()`**: Description-led traversal in one dimension.
91
+ - **`commandPalette()`**: Action discovery and navigation.
131
92
 
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.
93
+ ### Shell and Workspace Layout
94
+ - **`createFramedApp()`**: Batteries-included workspace with tabs, panes, and help.
95
+ - **`splitPane()`**: Dynamic primary/secondary context comparison.
96
+ - **`grid()`**: Stable regions with simultaneous visibility.
97
+ - **`viewport()`**: The canonical scroll mask for rich composition.
139
98
 
140
- ## Choosing Component Families
99
+ For framed shells, Node hosts can still prefer `startApp(app)`: the hosted
100
+ Node bootstrap delegates to self-running framed apps automatically. Use the
101
+ explicit runner path when you want to stay inside `@flyingrobots/bijou-tui`
102
+ or when the host owns `ctx` directly:
141
103
 
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
104
+ ```typescript
105
+ import { createFramedApp, runFramedApp } from '@flyingrobots/bijou-tui';
152
106
 
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.
107
+ const app = createFramedApp({ pages: [page] });
158
108
 
159
- ### Shell and workspace layout
109
+ await app.run({ ctx });
110
+ // or: await runFramedApp({ pages: [page] }, { ctx });
111
+ ```
160
112
 
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.
113
+ This path keeps the shell batteries included: mouse input defaults to `true`,
114
+ the shared runtime loop still does the heavy lifting, and frame timing/budget
115
+ telemetry stays attached to the frame model for shell-owned UI. For Node-hosted
116
+ apps, `startApp(app)` remains the default bootstrap.
170
117
 
171
118
  ## Animation
172
119
 
173
120
  ### Spring Physics
174
-
175
121
  ```typescript
176
122
  import { animate, SPRING_PRESETS } from '@flyingrobots/bijou-tui';
177
123
 
178
- // Physics-based (default) — runs until the spring settles
179
124
  const cmd = animate({
180
125
  from: 0,
181
126
  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,
127
+ spring: 'wobbly',
200
128
  onFrame: (v) => ({ type: 'scroll', y: v }),
201
129
  });
202
130
  ```
203
131
 
204
- ### Timeline
205
-
206
- GSAP-style orchestration — pure state machine, no timers:
207
-
132
+ ### Timeline Orchestration
208
133
  ```typescript
209
134
  import { timeline } from '@flyingrobots/bijou-tui';
210
135
 
211
136
  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')
137
+ .add('slideIn', { type: 'tween', from: -100, to: 0, duration: 300 })
214
138
  .label('settled')
215
- .add('bounce', { from: 0, to: 10, spring: 'wobbly' }, 'settled')
216
- .call('onReady', 'settled+=50')
139
+ .add('bounce', { from: 0, to: 10, spring: 'wobbly' }, 'settled')
217
140
  .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
141
  ```
226
142
 
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.
143
+ ## Post-Process Shaders
232
144
 
233
145
  ```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
- ```
243
-
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
146
+ import { run, surfaceShaderFilter, scanlines, vignette } from '@flyingrobots/bijou-tui';
291
147
 
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),
148
+ await run(app, {
149
+ configurePipeline(pipeline) {
150
+ pipeline.use('PostProcess', surfaceShaderFilter(
151
+ scanlines({ dimFactor: 0.82 }),
152
+ vignette({ edgeFactor: 0.78 }),
153
+ ));
366
154
  },
367
155
  });
368
156
  ```
369
157
 
370
- Prefer `gridSurface()` when the regions are already structured `Surface` views. Keep `grid()` for explicit text composition or lowering paths.
158
+ Use `surfaceShaderFilter(...)` to compose built-in post-process passes like
159
+ `scanlines()`, `flicker()`, `noise()`, and `vignette()` over the packed target
160
+ surface before diff/output.
371
161
 
372
- ## Resize Handling
162
+ ## Testing
373
163
 
374
- Terminal resize events are dispatched automatically as `ResizeMsg`:
164
+ Use `testRuntime()` when you want an inspectable harness instead of a
165
+ one-shot script result:
375
166
 
376
167
  ```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
- ```
168
+ import { testRuntime } from '@flyingrobots/bijou-tui';
392
169
 
393
- ## Event Bus
170
+ const harness = await testRuntime(app, { ctx });
171
+ await harness.press('q');
394
172
 
395
- The runtime uses an `EventBus` internally. You can also create your own for custom event sources:
173
+ expect(harness.frame).toBeDefined();
174
+ expect(harness.messages).toHaveLength(1);
175
+ expect(harness.commands.every((record) => record.settled)).toBe(true);
396
176
 
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
177
+ await harness.teardown();
406
178
  ```
407
179
 
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:
180
+ Keep `runScript()` for fixture-style interaction playback and GIF/demo
181
+ capture, and use `testRuntime()` when you need direct assertions on
182
+ snapshots, emitted messages, command outcomes, or cleanup disposal.
413
183
 
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
184
+ ## Documentation
668
185
 
669
- Apache-2.0
186
+ - **[GUIDE.md](./GUIDE.md)**: Productive-fast path for building apps.
187
+ - **[ADVANCED_GUIDE.md](./ADVANCED_GUIDE.md)**: Shell doctrine, shaders, and motion internals.
188
+ - **[Render Pipeline Guide](../../docs/guides/render-pipeline.md)**: Stage order, `RenderState`, and `configurePipeline()` truth.
189
+ - **[Design System](../../docs/design-system/README.md)**: Semantic guidance and patterns.
670
190
 
671
191
  ---
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
- ```
192
+ Built with 💎 by [FLYING ROBOTS](https://github.com/flyingrobots)