@opentui/react 0.1.25 → 0.1.27

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
@@ -188,6 +188,58 @@ function App() {
188
188
 
189
189
  **Returns:** An object with `width` and `height` properties representing the current terminal dimensions.
190
190
 
191
+ #### `useTimeline(options?)`
192
+
193
+ Create and manage animations using OpenTUI's timeline system. This hook automatically registers and unregisters the timeline with the animation engine.
194
+
195
+ ```tsx
196
+ import { render, useTimeline } from "@opentui/react"
197
+ import { useEffect, useState } from "react"
198
+
199
+ function App() {
200
+ const [width, setWidth] = useState(0)
201
+
202
+ const timeline = useTimeline({
203
+ duration: 2000,
204
+ loop: false,
205
+ })
206
+
207
+ useEffect(() => {
208
+ timeline.add(
209
+ {
210
+ width,
211
+ },
212
+ {
213
+ width: 50,
214
+ duration: 2000,
215
+ ease: "linear",
216
+ onUpdate: (animation) => {
217
+ setWidth(animation.targets[0].width)
218
+ },
219
+ },
220
+ )
221
+ }, [])
222
+
223
+ return <box style={{ width, backgroundColor: "#6a5acd" }} />
224
+ }
225
+ ```
226
+
227
+ **Parameters:**
228
+
229
+ - `options?`: Optional `TimelineOptions` object with properties:
230
+ - `duration?`: Animation duration in milliseconds (default: 1000)
231
+ - `loop?`: Whether the timeline should loop (default: false)
232
+ - `autoplay?`: Whether to automatically start the timeline (default: true)
233
+ - `onComplete?`: Callback when timeline completes
234
+ - `onPause?`: Callback when timeline is paused
235
+
236
+ **Returns:** A `Timeline` instance with methods:
237
+
238
+ - `add(target, properties, startTime)`: Add animation to timeline
239
+ - `play()`: Start the timeline
240
+ - `pause()`: Pause the timeline
241
+ - `restart()`: Restart the timeline from beginning
242
+
191
243
  ## Components
192
244
 
193
245
  ### Text Component
@@ -504,6 +556,89 @@ function App() {
504
556
  render(<App />)
505
557
  ```
506
558
 
559
+ ### System Monitor Animation
560
+
561
+ ```tsx
562
+ import { TextAttributes } from "@opentui/core"
563
+ import { render, useTimeline } from "@opentui/react"
564
+ import { useEffect, useState } from "react"
565
+
566
+ type Stats = {
567
+ cpu: number
568
+ memory: number
569
+ network: number
570
+ disk: number
571
+ }
572
+
573
+ export const App = () => {
574
+ const [stats, setAnimatedStats] = useState<Stats>({
575
+ cpu: 0,
576
+ memory: 0,
577
+ network: 0,
578
+ disk: 0,
579
+ })
580
+
581
+ const timeline = useTimeline({
582
+ duration: 3000,
583
+ loop: false,
584
+ })
585
+
586
+ useEffect(() => {
587
+ timeline.add(
588
+ stats,
589
+ {
590
+ cpu: 85,
591
+ memory: 70,
592
+ network: 95,
593
+ disk: 60,
594
+ duration: 3000,
595
+ ease: "linear",
596
+ onUpdate: (values) => {
597
+ setAnimatedStats({ ...values.targets[0] })
598
+ },
599
+ },
600
+ 0,
601
+ )
602
+ }, [])
603
+
604
+ const statsMap = [
605
+ { name: "CPU", key: "cpu", color: "#6a5acd" },
606
+ { name: "Memory", key: "memory", color: "#4682b4" },
607
+ { name: "Network", key: "network", color: "#20b2aa" },
608
+ { name: "Disk", key: "disk", color: "#daa520" },
609
+ ]
610
+
611
+ return (
612
+ <box
613
+ title="System Monitor"
614
+ style={{
615
+ margin: 1,
616
+ padding: 1,
617
+ border: true,
618
+ marginLeft: 2,
619
+ marginRight: 2,
620
+ borderStyle: "single",
621
+ borderColor: "#4a4a4a",
622
+ }}
623
+ >
624
+ {statsMap.map((stat) => (
625
+ <box key={stat.key}>
626
+ <box flexDirection="row" justifyContent="space-between">
627
+ <text>{stat.name}</text>
628
+ <text attributes={TextAttributes.DIM}>{Math.round(stats[stat.key as keyof Stats])}%</text>
629
+ </box>
630
+ <box style={{ backgroundColor: "#333333" }}>
631
+ <box style={{ width: `${stats[stat.key as keyof Stats]}%`, height: 1, backgroundColor: stat.color }} />
632
+ </box>
633
+ </box>
634
+ ))}
635
+ </box>
636
+ )
637
+ }
638
+
639
+ render(<App />)
640
+ ```
641
+
507
642
  ### Styled Text Showcase
508
643
 
509
644
  ```tsx
package/index.js CHANGED
@@ -97,12 +97,12 @@ var AppContext = createContext({
97
97
  var useAppContext = () => {
98
98
  return useContext(AppContext);
99
99
  };
100
- // src/hooks/use-keyboard.tsx
100
+ // src/hooks/use-keyboard.ts
101
101
  import { useEffect } from "react";
102
102
 
103
- // src/hooks/use-event.tsx
103
+ // src/hooks/use-event.ts
104
104
  import { useCallback, useLayoutEffect, useRef } from "react";
105
- function useEvent(handler) {
105
+ function useEffectEvent(handler) {
106
106
  const handlerRef = useRef(handler);
107
107
  useLayoutEffect(() => {
108
108
  handlerRef.current = handler;
@@ -113,18 +113,18 @@ function useEvent(handler) {
113
113
  }, []);
114
114
  }
115
115
 
116
- // src/hooks/use-keyboard.tsx
116
+ // src/hooks/use-keyboard.ts
117
117
  var useKeyboard = (handler) => {
118
118
  const { keyHandler } = useAppContext();
119
- const stableHandler = useEvent(handler);
119
+ const stableHandler = useEffectEvent(handler);
120
120
  useEffect(() => {
121
121
  keyHandler?.on("keypress", stableHandler);
122
122
  return () => {
123
123
  keyHandler?.off("keypress", stableHandler);
124
124
  };
125
- }, [keyHandler, stableHandler]);
125
+ }, [keyHandler]);
126
126
  };
127
- // src/hooks/use-renderer.tsx
127
+ // src/hooks/use-renderer.ts
128
128
  var useRenderer = () => {
129
129
  const { renderer } = useAppContext();
130
130
  if (!renderer) {
@@ -132,19 +132,20 @@ var useRenderer = () => {
132
132
  }
133
133
  return renderer;
134
134
  };
135
- // src/hooks/use-resize.tsx
135
+ // src/hooks/use-resize.ts
136
136
  import { useEffect as useEffect2 } from "react";
137
137
  var useOnResize = (callback) => {
138
138
  const renderer = useRenderer();
139
+ const stableCallback = useEffectEvent(callback);
139
140
  useEffect2(() => {
140
- renderer.on("resize", callback);
141
+ renderer.on("resize", stableCallback);
141
142
  return () => {
142
- renderer.off("resize", callback);
143
+ renderer.off("resize", stableCallback);
143
144
  };
144
- }, [renderer, callback]);
145
+ }, [renderer]);
145
146
  return renderer;
146
147
  };
147
- // src/hooks/use-terminal-dimensions.tsx
148
+ // src/hooks/use-terminal-dimensions.ts
148
149
  import { useState } from "react";
149
150
  var useTerminalDimensions = () => {
150
151
  const renderer = useRenderer();
@@ -158,9 +159,26 @@ var useTerminalDimensions = () => {
158
159
  useOnResize(cb);
159
160
  return dimensions;
160
161
  };
162
+ // src/hooks/use-timeline.ts
163
+ import { engine, Timeline } from "@opentui/core";
164
+ import { useEffect as useEffect3 } from "react";
165
+ var useTimeline = (options = {}) => {
166
+ const timeline = new Timeline(options);
167
+ useEffect3(() => {
168
+ if (!options.autoplay) {
169
+ timeline.play();
170
+ }
171
+ engine.register(timeline);
172
+ return () => {
173
+ timeline.pause();
174
+ engine.unregister(timeline);
175
+ };
176
+ }, []);
177
+ return timeline;
178
+ };
161
179
  // src/reconciler/renderer.ts
162
- import { createCliRenderer } from "@opentui/core";
163
- import React from "react";
180
+ import { createCliRenderer, engine as engine2 } from "@opentui/core";
181
+ import React2 from "react";
164
182
 
165
183
  // src/reconciler/reconciler.ts
166
184
  import ReactReconciler from "react-reconciler";
@@ -455,12 +473,43 @@ function _render(element, root) {
455
473
  reconciler.updateContainer(element, container, null, () => {});
456
474
  }
457
475
 
476
+ // src/components/error-boundary.tsx
477
+ import React from "react";
478
+
479
+ // jsx-dev-runtime.js
480
+ import { Fragment, jsxDEV } from "react/jsx-dev-runtime";
481
+
482
+ // src/components/error-boundary.tsx
483
+ class ErrorBoundary extends React.Component {
484
+ constructor(props) {
485
+ super(props);
486
+ this.state = { hasError: false, error: null };
487
+ }
488
+ static getDerivedStateFromError(error) {
489
+ return { hasError: true, error };
490
+ }
491
+ render() {
492
+ if (this.state.hasError && this.state.error) {
493
+ return /* @__PURE__ */ jsxDEV("box", {
494
+ style: { flexDirection: "column", padding: 2 },
495
+ children: /* @__PURE__ */ jsxDEV("text", {
496
+ fg: "red",
497
+ children: this.state.error.stack || this.state.error.message
498
+ }, undefined, false, undefined, this)
499
+ }, undefined, false, undefined, this);
500
+ }
501
+ return this.props.children;
502
+ }
503
+ }
504
+
458
505
  // src/reconciler/renderer.ts
459
506
  async function render(node, rendererConfig = {}) {
460
507
  const renderer = await createCliRenderer(rendererConfig);
461
- _render(React.createElement(AppContext.Provider, { value: { keyHandler: renderer.keyInput, renderer } }, node), renderer.root);
508
+ engine2.attach(renderer);
509
+ _render(React2.createElement(AppContext.Provider, { value: { keyHandler: renderer.keyInput, renderer } }, React2.createElement(ErrorBoundary, null, node)), renderer.root);
462
510
  }
463
511
  export {
512
+ useTimeline,
464
513
  useTerminalDimensions,
465
514
  useRenderer,
466
515
  useOnResize,
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.1.25",
7
+ "version": "0.1.27",
8
8
  "description": "React renderer for building terminal user interfaces using OpenTUI core",
9
9
  "license": "MIT",
10
10
  "repository": {
@@ -35,11 +35,13 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@opentui/core": "0.1.25",
38
+ "@opentui/core": "0.1.27",
39
39
  "react-reconciler": "^0.32.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/bun": "latest",
43
+ "@types/node": "latest",
44
+ "@types/react": "^19.0.0",
43
45
  "@types/react-reconciler": "^0.32.0",
44
46
  "typescript": "^5"
45
47
  },
@@ -0,0 +1,16 @@
1
+ import React from "react";
2
+ export declare class ErrorBoundary extends React.Component<{
3
+ children: React.ReactNode;
4
+ }, {
5
+ hasError: boolean;
6
+ error: Error | null;
7
+ }> {
8
+ constructor(props: {
9
+ children: React.ReactNode;
10
+ });
11
+ static getDerivedStateFromError(error: Error): {
12
+ hasError: boolean;
13
+ error: Error;
14
+ };
15
+ render(): any;
16
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./use-keyboard";
2
+ export * from "./use-renderer";
3
+ export * from "./use-resize";
4
+ export * from "./use-terminal-dimensions";
5
+ export * from "./use-timeline";
@@ -6,4 +6,4 @@
6
6
  * Useful for event handlers that need to be passed to effects with empty dependency arrays
7
7
  * or memoized child components.
8
8
  */
9
- export declare function useEvent<T extends (...args: any[]) => any>(handler: T): T;
9
+ export declare function useEffectEvent<T extends (...args: any[]) => any>(handler: T): T;
@@ -1,2 +1,2 @@
1
- import type { ParsedKey } from "@opentui/core";
2
- export declare const useKeyboard: (handler: (key: ParsedKey) => void) => void;
1
+ import type { KeyEvent } from "@opentui/core";
2
+ export declare const useKeyboard: (handler: (key: KeyEvent) => void) => void;
@@ -0,0 +1,2 @@
1
+ import { Timeline, type TimelineOptions } from "@opentui/core";
2
+ export declare const useTimeline: (options?: TimelineOptions) => Timeline;
package/src/index.d.ts CHANGED
@@ -1,8 +1,5 @@
1
1
  export * from "./components";
2
2
  export * from "./components/app";
3
- export * from "./hooks/use-keyboard";
4
- export * from "./hooks/use-renderer";
5
- export * from "./hooks/use-resize";
6
- export * from "./hooks/use-terminal-dimensions";
3
+ export * from "./hooks";
7
4
  export * from "./reconciler/renderer";
8
5
  export * from "./types/components";
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // src/reconciler/renderer.ts
3
- import { createCliRenderer } from "@opentui/core";
4
- import React from "react";
3
+ import { createCliRenderer, engine } from "@opentui/core";
4
+ import React2 from "react";
5
5
 
6
6
  // src/components/app.tsx
7
7
  import { createContext, useContext } from "react";
@@ -390,10 +390,40 @@ function _render(element, root) {
390
390
  reconciler.updateContainer(element, container, null, () => {});
391
391
  }
392
392
 
393
+ // src/components/error-boundary.tsx
394
+ import React from "react";
395
+
396
+ // jsx-dev-runtime.js
397
+ import { Fragment, jsxDEV } from "react/jsx-dev-runtime";
398
+
399
+ // src/components/error-boundary.tsx
400
+ class ErrorBoundary extends React.Component {
401
+ constructor(props) {
402
+ super(props);
403
+ this.state = { hasError: false, error: null };
404
+ }
405
+ static getDerivedStateFromError(error) {
406
+ return { hasError: true, error };
407
+ }
408
+ render() {
409
+ if (this.state.hasError && this.state.error) {
410
+ return /* @__PURE__ */ jsxDEV("box", {
411
+ style: { flexDirection: "column", padding: 2 },
412
+ children: /* @__PURE__ */ jsxDEV("text", {
413
+ fg: "red",
414
+ children: this.state.error.stack || this.state.error.message
415
+ }, undefined, false, undefined, this)
416
+ }, undefined, false, undefined, this);
417
+ }
418
+ return this.props.children;
419
+ }
420
+ }
421
+
393
422
  // src/reconciler/renderer.ts
394
423
  async function render(node, rendererConfig = {}) {
395
424
  const renderer = await createCliRenderer(rendererConfig);
396
- _render(React.createElement(AppContext.Provider, { value: { keyHandler: renderer.keyInput, renderer } }, node), renderer.root);
425
+ engine.attach(renderer);
426
+ _render(React2.createElement(AppContext.Provider, { value: { keyHandler: renderer.keyInput, renderer } }, React2.createElement(ErrorBoundary, null, node)), renderer.root);
397
427
  }
398
428
  export {
399
429
  render