@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 +135 -0
- package/index.js +64 -15
- package/package.json +4 -2
- package/src/components/error-boundary.d.ts +16 -0
- package/src/hooks/index.d.ts +5 -0
- package/src/hooks/use-event.d.ts +1 -1
- package/src/hooks/use-keyboard.d.ts +2 -2
- package/src/hooks/use-timeline.d.ts +2 -0
- package/src/index.d.ts +1 -4
- package/src/reconciler/renderer.js +33 -3
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.
|
|
100
|
+
// src/hooks/use-keyboard.ts
|
|
101
101
|
import { useEffect } from "react";
|
|
102
102
|
|
|
103
|
-
// src/hooks/use-event.
|
|
103
|
+
// src/hooks/use-event.ts
|
|
104
104
|
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
105
|
-
function
|
|
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.
|
|
116
|
+
// src/hooks/use-keyboard.ts
|
|
117
117
|
var useKeyboard = (handler) => {
|
|
118
118
|
const { keyHandler } = useAppContext();
|
|
119
|
-
const stableHandler =
|
|
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
|
|
125
|
+
}, [keyHandler]);
|
|
126
126
|
};
|
|
127
|
-
// src/hooks/use-renderer.
|
|
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.
|
|
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",
|
|
141
|
+
renderer.on("resize", stableCallback);
|
|
141
142
|
return () => {
|
|
142
|
-
renderer.off("resize",
|
|
143
|
+
renderer.off("resize", stableCallback);
|
|
143
144
|
};
|
|
144
|
-
}, [renderer
|
|
145
|
+
}, [renderer]);
|
|
145
146
|
return renderer;
|
|
146
147
|
};
|
|
147
|
-
// src/hooks/use-terminal-dimensions.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
+
}
|
package/src/hooks/use-event.d.ts
CHANGED
|
@@ -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
|
|
9
|
+
export declare function useEffectEvent<T extends (...args: any[]) => any>(handler: T): T;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare const useKeyboard: (handler: (key:
|
|
1
|
+
import type { KeyEvent } from "@opentui/core";
|
|
2
|
+
export declare const useKeyboard: (handler: (key: KeyEvent) => void) => void;
|
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
|
|
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
|
|
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
|
-
|
|
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
|