@opentui/react 0.2.2 → 0.2.4

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
@@ -60,6 +60,10 @@ For optimal TypeScript support, configure your `tsconfig.json`:
60
60
  - [Hooks](#hooks)
61
61
  - [useRenderer()](#userenderer)
62
62
  - [useKeyboard(handler, options?)](#usekeyboardhandler-options)
63
+ - [usePaste(handler)](#usepastehandler)
64
+ - [useFocus(handler)](#usefocushandler)
65
+ - [useBlur(handler)](#useblurhandler)
66
+ - [useSelectionHandler(handler)](#useselectionhandlerhandler)
63
67
  - [useOnResize(callback)](#useonresizecallback)
64
68
  - [useTerminalDimensions()](#useterminaldimensions)
65
69
  - [useTimeline(options?)](#usetimelineoptions)
@@ -239,6 +243,89 @@ function App() {
239
243
  }
240
244
  ```
241
245
 
246
+ #### `usePaste(handler)`
247
+
248
+ Handle terminal paste events (bracketed paste).
249
+
250
+ ```tsx
251
+ import { decodePasteBytes } from "@opentui/core"
252
+ import { usePaste } from "@opentui/react"
253
+
254
+ function App() {
255
+ usePaste((event) => {
256
+ const text = decodePasteBytes(event.bytes)
257
+ console.log("Pasted text:", text)
258
+ })
259
+
260
+ return <text>Paste something into the terminal</text>
261
+ }
262
+ ```
263
+
264
+ **Parameters:**
265
+
266
+ - `handler`: Callback function that receives a `PasteEvent` object with `bytes: Uint8Array` (decode with `decodePasteBytes` from `@opentui/core`)
267
+
268
+ #### `useFocus(handler)`
269
+
270
+ Subscribe to terminal window focus events. Fires when the terminal window gains focus.
271
+
272
+ ```tsx
273
+ import { useFocus } from "@opentui/react"
274
+
275
+ function App() {
276
+ useFocus(() => {
277
+ console.log("Terminal gained focus")
278
+ })
279
+
280
+ return <text>Focus-aware component</text>
281
+ }
282
+ ```
283
+
284
+ **Parameters:**
285
+
286
+ - `handler`: Callback function invoked when the terminal gains focus
287
+
288
+ #### `useBlur(handler)`
289
+
290
+ Subscribe to terminal window blur events. Fires when the terminal window loses focus.
291
+
292
+ ```tsx
293
+ import { useBlur } from "@opentui/react"
294
+
295
+ function App() {
296
+ useBlur(() => {
297
+ console.log("Terminal lost focus")
298
+ })
299
+
300
+ return <text>Blur-aware component</text>
301
+ }
302
+ ```
303
+
304
+ **Parameters:**
305
+
306
+ - `handler`: Callback function invoked when the terminal loses focus
307
+
308
+ #### `useSelectionHandler(handler)`
309
+
310
+ Handle text selection events (e.g., when the user selects text via mouse drag).
311
+
312
+ ```tsx
313
+ import { useSelectionHandler } from "@opentui/react"
314
+
315
+ function App() {
316
+ useSelectionHandler((selection) => {
317
+ const text = selection.getSelectedText()
318
+ console.log("Selected:", text)
319
+ })
320
+
321
+ return <text selectable>Select this text with your mouse</text>
322
+ }
323
+ ```
324
+
325
+ **Parameters:**
326
+
327
+ - `handler`: Callback function that receives a `Selection` object with methods like `getSelectedText()`
328
+
242
329
  #### `useOnResize(callback)`
243
330
 
244
331
  Handle terminal resize events.
@@ -166,7 +166,7 @@ import { TextNodeRenderable as TextNodeRenderable2 } from "@opentui/core";
166
166
  // package.json
167
167
  var package_default = {
168
168
  name: "@opentui/react",
169
- version: "0.2.2",
169
+ version: "0.2.4",
170
170
  description: "React renderer for building terminal user interfaces using OpenTUI core",
171
171
  license: "MIT",
172
172
  repository: {
@@ -215,16 +215,16 @@ var package_default = {
215
215
  "@opentui/keymap": "workspace:*",
216
216
  "@types/bun": "latest",
217
217
  "@types/node": "^24.0.0",
218
- "@types/react": "^19.0.0",
219
- "@types/react-reconciler": "^0.32.0",
218
+ "@types/react": "^19.2.0",
219
+ "@types/react-reconciler": "^0.33.0",
220
220
  "@types/ws": "^8.18.1",
221
- react: ">=19.0.0",
221
+ react: ">=19.2.0",
222
222
  "react-devtools-core": "^7.0.1",
223
223
  typescript: "^5",
224
224
  ws: "^8.18.0"
225
225
  },
226
226
  peerDependencies: {
227
- react: ">=19.0.0",
227
+ react: ">=19.2.0",
228
228
  "react-devtools-core": "^7.0.1",
229
229
  ws: "^8.18.0"
230
230
  },
@@ -238,7 +238,7 @@ var package_default = {
238
238
  },
239
239
  dependencies: {
240
240
  "@opentui/core": "workspace:*",
241
- "react-reconciler": "^0.32.0"
241
+ "react-reconciler": "^0.33.0"
242
242
  }
243
243
  };
244
244
 
@@ -380,6 +380,8 @@ var hostConfig = {
380
380
  supportsMutation: true,
381
381
  supportsPersistence: false,
382
382
  supportsHydration: false,
383
+ supportsMicrotasks: true,
384
+ scheduleMicrotask: queueMicrotask,
383
385
  createInstance(type, props, rootContainerInstance, hostContext) {
384
386
  if (textNodeKeys.includes(type) && !hostContext.isInsideText) {
385
387
  throw new Error(`Component of type "${type}" must be created inside of a text node`);
@@ -435,7 +437,7 @@ var hostConfig = {
435
437
  cancelTimeout: clearTimeout,
436
438
  noTimeout: -1,
437
439
  shouldAttemptEagerTransition() {
438
- return false;
440
+ return true;
439
441
  },
440
442
  finalizeInitialChildren(instance, type, props, rootContainerInstance, hostContext) {
441
443
  setInitialProperties(instance, type, props);
@@ -444,11 +446,9 @@ var hostConfig = {
444
446
  commitMount(instance, type, props, internalInstanceHandle) {},
445
447
  commitUpdate(instance, type, oldProps, newProps, internalInstanceHandle) {
446
448
  updateProperties(instance, type, oldProps, newProps);
447
- instance.requestRender();
448
449
  },
449
450
  commitTextUpdate(textInstance, oldText, newText) {
450
451
  textInstance.children = [newText];
451
- textInstance.requestRender();
452
452
  },
453
453
  appendChildToContainer(container, child) {
454
454
  container.add(child);
@@ -458,19 +458,15 @@ var hostConfig = {
458
458
  },
459
459
  hideInstance(instance) {
460
460
  instance.visible = false;
461
- instance.requestRender();
462
461
  },
463
462
  unhideInstance(instance, props) {
464
463
  instance.visible = true;
465
- instance.requestRender();
466
464
  },
467
465
  hideTextInstance(textInstance) {
468
466
  textInstance.visible = false;
469
- textInstance.requestRender();
470
467
  },
471
468
  unhideTextInstance(textInstance, text) {
472
469
  textInstance.visible = true;
473
- textInstance.requestRender();
474
470
  },
475
471
  clearContainer(container) {
476
472
  const children = container.getChildren();
@@ -489,6 +485,12 @@ var hostConfig = {
489
485
  maySuspendCommit() {
490
486
  return false;
491
487
  },
488
+ maySuspendCommitOnUpdate() {
489
+ return false;
490
+ },
491
+ maySuspendCommitInSyncRender() {
492
+ return false;
493
+ },
492
494
  NotPendingTransition: null,
493
495
  HostTransitionContext: createContext2(null),
494
496
  resetFormInstance() {},
@@ -554,7 +556,7 @@ $ bun add react-devtools-core@7 -d
554
556
  }
555
557
  reconciler.injectIntoDevTools();
556
558
  function _render(element, root) {
557
- const container = reconciler.createContainer(root, ConcurrentRoot, null, false, null, "", console.error, console.error, console.error, console.error, null);
559
+ const container = reconciler.createContainer(root, ConcurrentRoot, null, false, null, "", console.error, console.error, console.error, () => {});
558
560
  reconciler.updateContainer(element, container, null, () => {});
559
561
  return container;
560
562
  }
package/index.js CHANGED
@@ -11,9 +11,9 @@ import {
11
11
  getComponentCatalogue,
12
12
  jsxDEV,
13
13
  useAppContext
14
- } from "./chunk-42jvgte5.js";
14
+ } from "./chunk-wfd0ft37.js";
15
15
  import"./chunk-2mx7fq49.js";
16
- // src/hooks/use-keyboard.ts
16
+ // src/hooks/use-blur.ts
17
17
  import { useEffect } from "react";
18
18
 
19
19
  // src/hooks/use-event.ts
@@ -29,11 +29,44 @@ function useEffectEvent(handler) {
29
29
  }, []);
30
30
  }
31
31
 
32
+ // src/hooks/use-renderer.ts
33
+ var useRenderer = () => {
34
+ const { renderer } = useAppContext();
35
+ if (!renderer) {
36
+ throw new Error("Renderer not found.");
37
+ }
38
+ return renderer;
39
+ };
40
+
41
+ // src/hooks/use-blur.ts
42
+ var useBlur = (handler) => {
43
+ const renderer = useRenderer();
44
+ const stableHandler = useEffectEvent(handler);
45
+ useEffect(() => {
46
+ renderer.on("blur", stableHandler);
47
+ return () => {
48
+ renderer.off("blur", stableHandler);
49
+ };
50
+ }, [renderer]);
51
+ };
52
+ // src/hooks/use-focus.ts
53
+ import { useEffect as useEffect2 } from "react";
54
+ var useFocus = (handler) => {
55
+ const renderer = useRenderer();
56
+ const stableHandler = useEffectEvent(handler);
57
+ useEffect2(() => {
58
+ renderer.on("focus", stableHandler);
59
+ return () => {
60
+ renderer.off("focus", stableHandler);
61
+ };
62
+ }, [renderer]);
63
+ };
32
64
  // src/hooks/use-keyboard.ts
65
+ import { useEffect as useEffect3 } from "react";
33
66
  var useKeyboard = (handler, options = { release: false }) => {
34
67
  const { keyHandler } = useAppContext();
35
68
  const stableHandler = useEffectEvent(handler);
36
- useEffect(() => {
69
+ useEffect3(() => {
37
70
  keyHandler?.on("keypress", stableHandler);
38
71
  if (options?.release) {
39
72
  keyHandler?.on("keyrelease", stableHandler);
@@ -46,20 +79,24 @@ var useKeyboard = (handler, options = { release: false }) => {
46
79
  };
47
80
  }, [keyHandler, options.release]);
48
81
  };
49
- // src/hooks/use-renderer.ts
50
- var useRenderer = () => {
51
- const { renderer } = useAppContext();
52
- if (!renderer) {
53
- throw new Error("Renderer not found.");
54
- }
55
- return renderer;
82
+ // src/hooks/use-paste.ts
83
+ import { useEffect as useEffect4 } from "react";
84
+ var usePaste = (handler) => {
85
+ const { keyHandler } = useAppContext();
86
+ const stableHandler = useEffectEvent(handler);
87
+ useEffect4(() => {
88
+ keyHandler?.on("paste", stableHandler);
89
+ return () => {
90
+ keyHandler?.off("paste", stableHandler);
91
+ };
92
+ }, [keyHandler]);
56
93
  };
57
94
  // src/hooks/use-resize.ts
58
- import { useEffect as useEffect2 } from "react";
95
+ import { useEffect as useEffect5 } from "react";
59
96
  var useOnResize = (callback) => {
60
97
  const renderer = useRenderer();
61
98
  const stableCallback = useEffectEvent(callback);
62
- useEffect2(() => {
99
+ useEffect5(() => {
63
100
  renderer.on("resize", stableCallback);
64
101
  return () => {
65
102
  renderer.off("resize", stableCallback);
@@ -67,6 +104,18 @@ var useOnResize = (callback) => {
67
104
  }, [renderer]);
68
105
  return renderer;
69
106
  };
107
+ // src/hooks/use-selection.ts
108
+ import { useEffect as useEffect6 } from "react";
109
+ var useSelectionHandler = (handler) => {
110
+ const renderer = useRenderer();
111
+ const stableHandler = useEffectEvent(handler);
112
+ useEffect6(() => {
113
+ renderer.on("selection", stableHandler);
114
+ return () => {
115
+ renderer.off("selection", stableHandler);
116
+ };
117
+ }, [renderer]);
118
+ };
70
119
  // src/hooks/use-terminal-dimensions.ts
71
120
  import { useState } from "react";
72
121
  var useTerminalDimensions = () => {
@@ -83,10 +132,10 @@ var useTerminalDimensions = () => {
83
132
  };
84
133
  // src/hooks/use-timeline.ts
85
134
  import { engine, Timeline } from "@opentui/core";
86
- import { useEffect as useEffect3 } from "react";
135
+ import { useEffect as useEffect7 } from "react";
87
136
  var useTimeline = (options = {}) => {
88
137
  const timeline = new Timeline(options);
89
- useEffect3(() => {
138
+ useEffect7(() => {
90
139
  if (!options.autoplay) {
91
140
  timeline.play();
92
141
  }
@@ -102,7 +151,7 @@ var useTimeline = (options = {}) => {
102
151
  import {
103
152
  createSlotRegistry
104
153
  } from "@opentui/core";
105
- import React, { Fragment as Fragment2, useEffect as useEffect4, useMemo, useRef as useRef2, useState as useState2 } from "react";
154
+ import React, { Fragment as Fragment2, useEffect as useEffect8, useMemo, useRef as useRef2, useState as useState2 } from "react";
106
155
  function createReactSlotRegistry(renderer, context, options = {}) {
107
156
  return createSlotRegistry(renderer, "react:slot-registry", context, options);
108
157
  }
@@ -181,12 +230,12 @@ function Slot(props) {
181
230
  const slotName = String(props.name);
182
231
  const renderFailuresByPluginRef = useRef2(new Map);
183
232
  const pendingRenderReportsRef = useRef2(new Map);
184
- useEffect4(() => {
233
+ useEffect8(() => {
185
234
  return registry.subscribe(() => {
186
235
  setVersion((current) => current + 1);
187
236
  });
188
237
  }, [registry]);
189
- useEffect4(() => {
238
+ useEffect8(() => {
190
239
  if (pendingRenderReportsRef.current.size === 0) {
191
240
  return;
192
241
  }
@@ -318,9 +367,13 @@ import { createElement as createElement2 } from "react";
318
367
  export {
319
368
  useTimeline,
320
369
  useTerminalDimensions,
370
+ useSelectionHandler,
321
371
  useRenderer,
372
+ usePaste,
322
373
  useOnResize,
323
374
  useKeyboard,
375
+ useFocus,
376
+ useBlur,
324
377
  useAppContext,
325
378
  getComponentCatalogue,
326
379
  flushSync,
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "type": "module",
7
- "version": "0.2.2",
7
+ "version": "0.2.4",
8
8
  "description": "React renderer for building terminal user interfaces using OpenTUI core",
9
9
  "license": "MIT",
10
10
  "repository": {
@@ -48,23 +48,23 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@opentui/core": "0.2.2",
52
- "react-reconciler": "^0.32.0"
51
+ "@opentui/core": "0.2.4",
52
+ "react-reconciler": "^0.33.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@opentui/keymap": "workspace:*",
56
56
  "@types/bun": "latest",
57
57
  "@types/node": "^24.0.0",
58
- "@types/react": "^19.0.0",
59
- "@types/react-reconciler": "^0.32.0",
58
+ "@types/react": "^19.2.0",
59
+ "@types/react-reconciler": "^0.33.0",
60
60
  "@types/ws": "^8.18.1",
61
- "react": ">=19.0.0",
61
+ "react": ">=19.2.0",
62
62
  "react-devtools-core": "^7.0.1",
63
63
  "typescript": "^5",
64
64
  "ws": "^8.18.0"
65
65
  },
66
66
  "peerDependencies": {
67
- "react": ">=19.0.0",
67
+ "react": ">=19.2.0",
68
68
  "react-devtools-core": "^7.0.1",
69
69
  "ws": "^8.18.0"
70
70
  }
@@ -1,5 +1,9 @@
1
+ export * from "./use-blur.js";
2
+ export * from "./use-focus.js";
1
3
  export * from "./use-keyboard.js";
4
+ export * from "./use-paste.js";
2
5
  export * from "./use-renderer.js";
3
6
  export * from "./use-resize.js";
7
+ export * from "./use-selection.js";
4
8
  export * from "./use-terminal-dimensions.js";
5
9
  export * from "./use-timeline.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Subscribe to terminal window blur events.
3
+ * Fires when the terminal window loses focus.
4
+ *
5
+ * @example
6
+ * useBlur(() => {
7
+ * console.log("Terminal lost focus")
8
+ * })
9
+ */
10
+ export declare const useBlur: (handler: () => void) => void;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Subscribe to terminal window focus events.
3
+ * Fires when the terminal window gains focus.
4
+ *
5
+ * @example
6
+ * useFocus(() => {
7
+ * console.log("Terminal gained focus")
8
+ * })
9
+ */
10
+ export declare const useFocus: (handler: () => void) => void;
@@ -0,0 +1,10 @@
1
+ import type { PasteEvent } from "@opentui/core";
2
+ /**
3
+ * Subscribe to terminal paste events (bracketed paste).
4
+ *
5
+ * @example
6
+ * usePaste((event) => {
7
+ * console.log("Pasted:", event.text)
8
+ * })
9
+ */
10
+ export declare const usePaste: (handler: (event: PasteEvent) => void) => void;
@@ -0,0 +1,12 @@
1
+ import type { Selection } from "@opentui/core";
2
+ /**
3
+ * Subscribe to text selection events.
4
+ * Fires when the user selects text in the terminal (e.g., via mouse drag).
5
+ *
6
+ * @example
7
+ * useSelectionHandler((selection) => {
8
+ * const text = selection.getSelectedText()
9
+ * console.log("Selected:", text)
10
+ * })
11
+ */
12
+ export declare const useSelectionHandler: (handler: (selection: Selection) => void) => void;
@@ -1,9 +1,16 @@
1
1
  import type { HostConfig } from "react-reconciler";
2
2
  import type { Container, HostContext, Instance, Props, PublicInstance, TextInstance, Type } from "../types/host.js";
3
+ type ReconcilerExtensions = {
4
+ maySuspendCommitOnUpdate(type: Type, oldProps: Props, newProps: Props): boolean;
5
+ maySuspendCommitInSyncRender(type: Type, props: Props): boolean;
6
+ rendererPackageName: string;
7
+ rendererVersion: string;
8
+ };
3
9
  export declare const hostConfig: HostConfig<Type, Props, Container, Instance, TextInstance, unknown, // SuspenseInstance
4
10
  unknown, // HydratableInstance
5
11
  unknown, // FormInstance
6
12
  PublicInstance, HostContext, unknown, // ChildSet
7
13
  unknown, // TimeoutHandle
8
14
  unknown, // NoTimeout
9
- unknown>;
15
+ unknown> & ReconcilerExtensions;
16
+ export {};
@@ -3,6 +3,9 @@ import { type ReactNode } from "react";
3
3
  declare const flushSync: {
4
4
  (): void;
5
5
  <R>(fn: () => R): R;
6
+ } & {
7
+ (): void;
8
+ <R>(fn: () => R): R;
6
9
  };
7
10
  declare const createPortal: (children: ReactNode, containerInfo: any, implementation: any, key?: string | null) => import("react-reconciler").ReactPortal;
8
11
  export type Root = {
package/test-utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  createRoot
4
- } from "./chunk-42jvgte5.js";
4
+ } from "./chunk-wfd0ft37.js";
5
5
  import"./chunk-2mx7fq49.js";
6
6
 
7
7
  // src/test-utils.ts