feat(components): add ScrollBox component (#237)

* feat(runtime): add ScrollBox component

* refactor(components): move ScrollBox into components package

* feat(runtime): add mouse input composable

* refactor(components): delegate ScrollBox mouse input

* docs: describe mouse input composable

* fix(runtime): consume unsupported SGR mouse input

* fix(components): gate ScrollBox input on raw mode support

* fix(runtime): require escape prefix for SGR mouse input

* refactor(components): rename ScrollBox input props to wheel/keyboard

Follow the components boolean-prop convention (bare noun, default false):
enableMouse/enableKeyboard/isActive -> wheel/keyboard, both opt-in. Mouse-
wheel is off by default because enabling terminal mouse tracking suppresses
the terminal's native text selection window-wide. Record the convention in
components-design-principles.md.

* refactor(components): rename ScrollBox linesPerWheel + input tests

- Rename the wheel-step prop wheelLines -> linesPerWheel.
- Drop Home/End keyboard scroll for now — keyboard is PageUp/PageDown only.
- Add tests: keyboard paging, and SGR mouse-mode disable on signal-exit
  (fs.writeSync path, mirroring the bracketed-paste test).

* feat(examples): add ScrollBox streaming demo

A streaming-log demo of <ScrollBox wheel keyboard>: new lines arrive on a
timer and stick to the bottom until you scroll up (wheel / PageUp / PageDown),
then hold position while output keeps arriving. Press q to quit.

---------

Co-authored-by: Yunfei He <i.heyunfei@gmail.com>
This commit is contained in:
Doctor Wu
2026-07-04 11:25:50 +08:00
committed by GitHub
parent 2eb4c4e3c7
commit d8d9296905
34 changed files with 1171 additions and 63 deletions
+20 -18
View File
@@ -129,27 +129,29 @@ createApp(App).mount();
The [`@vue-tui/components`](./packages/components) package adds higher-level components composed from the runtime primitives — published separately from the core.
| Component | Description |
| ------------------------------------ | ------------------------------------------------------------------------------------------ |
| [`<Spinner>`](./packages/components) | Animated loading spinner — built-in `dots`/`line` presets or custom frames, optional label |
| Component | Description |
| -------------------------------------- | ------------------------------------------------------------------------------------------ |
| [`<ScrollBox>`](./packages/components) | Bounded scroll viewport with mouse-wheel scrolling and sticky-bottom behavior |
| [`<Spinner>`](./packages/components) | Animated loading spinner — built-in `dots`/`line` presets or custom frames, optional label |
## Composables (Hooks)
| Composable | Description |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `useInput(handler, opts?)` | Handle keyboard input — receives `(input, key)` with modifier and arrow key detection |
| `usePaste(handler, opts?)` | Handle bracketed paste — receives the pasted `text` as a single event |
| `useFocus(opts?)` | Component-level focus — returns `{ isFocused, focus }` |
| `useFocusManager()` | App-level focus control — `focusNext()`, `focusPrevious()`, `focus(id)` |
| `useApp()` | App lifecycle — `{ exit(error?), waitUntilRenderFlush() }` |
| `useWindowSize()` | Reactive terminal dimensions — `{ columns, rows }` |
| `useStdin()` | Access stdin stream and raw mode control |
| `useStdout()` | Write directly to stdout |
| `useStderr()` | Write directly to stderr |
| `useBoxMetrics(ref)` | Measure a `<Box>` via a template ref — reactive `{ width, height, left, top, hasMeasured }` (or `measureElement(el)` for a one-off `{ width, height }` read) |
| `useCursor()` | Control the terminal cursor — `setCursorPosition(pos)` in output coordinates |
| `useIsScreenReaderEnabled()` | Whether a screen reader is active — returns a boolean for adapting accessible output |
| `useAnimation(opts?)` | Frame-based animation driver — reactive `{ frame, time, delta }` + `reset()` |
| Composable | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `useInput(handler, opts?)` | Handle keyboard input — receives `(input, key)` with modifier and arrow key detection |
| `useMouseInput(handler, opts?)` | Handle terminal mouse input — currently SGR wheel events with ref-counted mouse-mode ownership |
| `usePaste(handler, opts?)` | Handle bracketed paste — receives the pasted `text` as a single event |
| `useFocus(opts?)` | Component-level focus — returns `{ isFocused, focus }` |
| `useFocusManager()` | App-level focus control — `focusNext()`, `focusPrevious()`, `focus(id)` |
| `useApp()` | App lifecycle — `{ exit(error?), waitUntilRenderFlush() }` |
| `useWindowSize()` | Reactive terminal dimensions — `{ columns, rows }` |
| `useStdin()` | Access stdin stream and raw mode control |
| `useStdout()` | Write directly to stdout |
| `useStderr()` | Write directly to stderr |
| `useBoxMetrics(ref)` | Measure a `<Box>` via a template ref — reactive `{ width, height, left, top, hasMeasured }` (or `measureElement(el)` for a one-off `{ width, height }` read) |
| `useCursor()` | Control the terminal cursor — `setCursorPosition(pos)` in output coordinates |
| `useIsScreenReaderEnabled()` | Whether a screen reader is active — returns a boolean for adapting accessible output |
| `useAnimation(opts?)` | Frame-based animation driver — reactive `{ frame, time, delta }` + `reset()` |
## Testing