@opentui/react 0.0.0-20250908-4906ddad → 0.0.0-20250915-f5db043a
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 +137 -66
- package/index.js +86 -65
- package/jsx-namespace.d.ts +10 -0
- package/package.json +2 -2
- package/src/components/index.d.ts +8 -0
- package/src/components/text.d.ts +24 -0
- package/src/reconciler/reconciler.d.ts +1 -1
- package/src/reconciler/renderer.js +86 -65
- package/src/types/components.d.ts +9 -5
- package/src/types/host.d.ts +6 -4
package/README.md
CHANGED
|
@@ -4,6 +4,14 @@ A React renderer for building terminal user interfaces using [OpenTUI core](http
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
Quick start with [bun](https://bun.sh) and [create-tui](https://github.com/msmps/create-tui):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun create tui --template react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Manual installation:
|
|
14
|
+
|
|
7
15
|
```bash
|
|
8
16
|
bun install @opentui/react @opentui/core react
|
|
9
17
|
```
|
|
@@ -14,14 +22,7 @@ bun install @opentui/react @opentui/core react
|
|
|
14
22
|
import { render } from "@opentui/react"
|
|
15
23
|
|
|
16
24
|
function App() {
|
|
17
|
-
return
|
|
18
|
-
<box>
|
|
19
|
-
<text fg="#00FF00">Hello, Terminal!</text>
|
|
20
|
-
<box title="Welcome" padding={2}>
|
|
21
|
-
<text>Welcome to OpenTUI with React!</text>
|
|
22
|
-
</box>
|
|
23
|
-
</box>
|
|
24
|
-
)
|
|
25
|
+
return <text>Hello, world!</text>
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
render(<App />)
|
|
@@ -56,20 +57,27 @@ OpenTUI React provides several built-in components that map to OpenTUI core rend
|
|
|
56
57
|
- **`<box>`** - Container with borders and layout
|
|
57
58
|
- **`<input>`** - Text input field
|
|
58
59
|
- **`<select>`** - Selection dropdown
|
|
60
|
+
- **`<scrollbox>`** - A scrollable box
|
|
59
61
|
- **`<tab-select>`** - Tab-based selection
|
|
60
62
|
- **`<ascii-font>`** - Display ASCII art text with different font styles
|
|
61
63
|
|
|
64
|
+
Helpers:
|
|
65
|
+
|
|
66
|
+
- **`<span>`, `<strong>`, `<em>`, `<u>`, `<b>`, `<i>`, `<br>`** - Text modifiers (_must be used inside of the text component_)
|
|
67
|
+
|
|
62
68
|
### Styling
|
|
63
69
|
|
|
64
70
|
Components can be styled using props or the `style` prop:
|
|
65
71
|
|
|
66
72
|
```tsx
|
|
67
73
|
// Direct props
|
|
68
|
-
<
|
|
74
|
+
<box backgroundColor="blue" padding={2}>
|
|
75
|
+
<text>Hello, world!</text>
|
|
76
|
+
</box>
|
|
69
77
|
|
|
70
78
|
// Style prop
|
|
71
79
|
<box style={{ backgroundColor: "blue", padding: 2 }}>
|
|
72
|
-
<text>
|
|
80
|
+
<text>Hello, world!</text>
|
|
73
81
|
</box>
|
|
74
82
|
```
|
|
75
83
|
|
|
@@ -102,14 +110,15 @@ Access the OpenTUI renderer instance.
|
|
|
102
110
|
```tsx
|
|
103
111
|
import { useRenderer } from "@opentui/react"
|
|
104
112
|
|
|
105
|
-
function
|
|
113
|
+
function App() {
|
|
106
114
|
const renderer = useRenderer()
|
|
107
115
|
|
|
108
116
|
useEffect(() => {
|
|
109
|
-
renderer.
|
|
117
|
+
renderer.console.show()
|
|
118
|
+
console.log("Hello, from the console!")
|
|
110
119
|
}, [])
|
|
111
120
|
|
|
112
|
-
return <
|
|
121
|
+
return <box />
|
|
113
122
|
}
|
|
114
123
|
```
|
|
115
124
|
|
|
@@ -120,7 +129,7 @@ Handle keyboard events.
|
|
|
120
129
|
```tsx
|
|
121
130
|
import { useKeyboard } from "@opentui/react"
|
|
122
131
|
|
|
123
|
-
function
|
|
132
|
+
function App() {
|
|
124
133
|
useKeyboard((key) => {
|
|
125
134
|
if (key.name === "escape") {
|
|
126
135
|
process.exit(0)
|
|
@@ -139,7 +148,7 @@ Handle terminal resize events.
|
|
|
139
148
|
import { useOnResize, useRenderer } from "@opentui/react"
|
|
140
149
|
import { useEffect } from "react"
|
|
141
150
|
|
|
142
|
-
function
|
|
151
|
+
function App() {
|
|
143
152
|
const renderer = useRenderer()
|
|
144
153
|
|
|
145
154
|
useEffect(() => {
|
|
@@ -161,7 +170,7 @@ Get current terminal dimensions and automatically update when the terminal is re
|
|
|
161
170
|
```tsx
|
|
162
171
|
import { useTerminalDimensions } from "@opentui/react"
|
|
163
172
|
|
|
164
|
-
function
|
|
173
|
+
function App() {
|
|
165
174
|
const { width, height } = useTerminalDimensions()
|
|
166
175
|
|
|
167
176
|
return (
|
|
@@ -186,19 +195,21 @@ function MyComponent() {
|
|
|
186
195
|
Display text with rich formatting.
|
|
187
196
|
|
|
188
197
|
```tsx
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
function TextExample() {
|
|
198
|
+
function App() {
|
|
192
199
|
return (
|
|
193
200
|
<box>
|
|
194
201
|
{/* Simple text */}
|
|
195
202
|
<text>Hello World</text>
|
|
196
203
|
|
|
197
204
|
{/* Rich text with children */}
|
|
198
|
-
<text>
|
|
205
|
+
<text>
|
|
206
|
+
<span fg="red">Red Text</span>
|
|
207
|
+
</text>
|
|
199
208
|
|
|
200
|
-
{/*
|
|
201
|
-
<text>
|
|
209
|
+
{/* Text modifiers */}
|
|
210
|
+
<text>
|
|
211
|
+
<strong>Bold</strong>, <em>Italic</em>, and <u>Underlined</u>
|
|
212
|
+
</text>
|
|
202
213
|
</box>
|
|
203
214
|
)
|
|
204
215
|
}
|
|
@@ -209,22 +220,23 @@ function TextExample() {
|
|
|
209
220
|
Container with borders and layout capabilities.
|
|
210
221
|
|
|
211
222
|
```tsx
|
|
212
|
-
function
|
|
223
|
+
function App() {
|
|
213
224
|
return (
|
|
214
225
|
<box flexDirection="column">
|
|
215
226
|
{/* Basic box */}
|
|
216
|
-
<box>
|
|
227
|
+
<box border>
|
|
217
228
|
<text>Simple box</text>
|
|
218
229
|
</box>
|
|
219
230
|
|
|
220
231
|
{/* Box with title and styling */}
|
|
221
|
-
<box title="Settings" borderStyle="double" padding={2} backgroundColor="blue">
|
|
232
|
+
<box title="Settings" border borderStyle="double" padding={2} backgroundColor="blue">
|
|
222
233
|
<text>Box content</text>
|
|
223
234
|
</box>
|
|
224
235
|
|
|
225
236
|
{/* Styled box */}
|
|
226
237
|
<box
|
|
227
238
|
style={{
|
|
239
|
+
border: true,
|
|
228
240
|
width: 40,
|
|
229
241
|
height: 10,
|
|
230
242
|
margin: 1,
|
|
@@ -246,20 +258,16 @@ Text input field with event handling.
|
|
|
246
258
|
```tsx
|
|
247
259
|
import { useState } from "react"
|
|
248
260
|
|
|
249
|
-
function
|
|
261
|
+
function App() {
|
|
250
262
|
const [value, setValue] = useState("")
|
|
251
|
-
const [focused, setFocused] = useState(true)
|
|
252
263
|
|
|
253
264
|
return (
|
|
254
|
-
<box title="Enter your name" style={{ height: 3 }}>
|
|
265
|
+
<box title="Enter your name" style={{ border: true, height: 3 }}>
|
|
255
266
|
<input
|
|
256
267
|
placeholder="Type here..."
|
|
257
|
-
focused
|
|
268
|
+
focused
|
|
258
269
|
onInput={setValue}
|
|
259
270
|
onSubmit={(value) => console.log("Submitted:", value)}
|
|
260
|
-
style={{
|
|
261
|
-
focusedBackgroundColor: "#333333",
|
|
262
|
-
}}
|
|
263
271
|
/>
|
|
264
272
|
</box>
|
|
265
273
|
)
|
|
@@ -274,7 +282,7 @@ Dropdown selection component.
|
|
|
274
282
|
import type { SelectOption } from "@opentui/core"
|
|
275
283
|
import { useState } from "react"
|
|
276
284
|
|
|
277
|
-
function
|
|
285
|
+
function App() {
|
|
278
286
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
279
287
|
|
|
280
288
|
const options: SelectOption[] = [
|
|
@@ -284,7 +292,7 @@ function SelectExample() {
|
|
|
284
292
|
]
|
|
285
293
|
|
|
286
294
|
return (
|
|
287
|
-
<box style={{ height: 24 }}>
|
|
295
|
+
<box style={{ border: true, height: 24 }}>
|
|
288
296
|
<select
|
|
289
297
|
style={{ height: 22 }}
|
|
290
298
|
options={options}
|
|
@@ -299,6 +307,50 @@ function SelectExample() {
|
|
|
299
307
|
}
|
|
300
308
|
```
|
|
301
309
|
|
|
310
|
+
### Scrollbox Component
|
|
311
|
+
|
|
312
|
+
A scrollable box.
|
|
313
|
+
|
|
314
|
+
```tsx
|
|
315
|
+
function App() {
|
|
316
|
+
return (
|
|
317
|
+
<scrollbox
|
|
318
|
+
style={{
|
|
319
|
+
rootOptions: {
|
|
320
|
+
backgroundColor: "#24283b",
|
|
321
|
+
},
|
|
322
|
+
wrapperOptions: {
|
|
323
|
+
backgroundColor: "#1f2335",
|
|
324
|
+
},
|
|
325
|
+
viewportOptions: {
|
|
326
|
+
backgroundColor: "#1a1b26",
|
|
327
|
+
},
|
|
328
|
+
contentOptions: {
|
|
329
|
+
backgroundColor: "#16161e",
|
|
330
|
+
},
|
|
331
|
+
scrollbarOptions: {
|
|
332
|
+
showArrows: true,
|
|
333
|
+
trackOptions: {
|
|
334
|
+
foregroundColor: "#7aa2f7",
|
|
335
|
+
backgroundColor: "#414868",
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
}}
|
|
339
|
+
focused
|
|
340
|
+
>
|
|
341
|
+
{Array.from({ length: 1000 }).map((_, i) => (
|
|
342
|
+
<box
|
|
343
|
+
key={i}
|
|
344
|
+
style={{ width: "100%", padding: 1, marginBottom: 1, backgroundColor: i % 2 === 0 ? "#292e42" : "#2f3449" }}
|
|
345
|
+
>
|
|
346
|
+
<text content={`Box ${i}`} />
|
|
347
|
+
</box>
|
|
348
|
+
))}
|
|
349
|
+
</scrollbox>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
302
354
|
### ASCII Font Component
|
|
303
355
|
|
|
304
356
|
Display ASCII art text with different font styles.
|
|
@@ -307,7 +359,7 @@ Display ASCII art text with different font styles.
|
|
|
307
359
|
import { measureText } from "@opentui/core"
|
|
308
360
|
import { useState } from "react"
|
|
309
361
|
|
|
310
|
-
function
|
|
362
|
+
function App() {
|
|
311
363
|
const text = "ASCII"
|
|
312
364
|
const [font, setFont] = useState<"block" | "shade" | "slick" | "tiny">("tiny")
|
|
313
365
|
|
|
@@ -317,10 +369,11 @@ function ASCIIFontExample() {
|
|
|
317
369
|
})
|
|
318
370
|
|
|
319
371
|
return (
|
|
320
|
-
<box style={{ paddingLeft: 1, paddingRight: 1 }}>
|
|
372
|
+
<box style={{ border: true, paddingLeft: 1, paddingRight: 1 }}>
|
|
321
373
|
<box
|
|
322
374
|
style={{
|
|
323
375
|
height: 8,
|
|
376
|
+
border: true,
|
|
324
377
|
marginBottom: 1,
|
|
325
378
|
}}
|
|
326
379
|
>
|
|
@@ -365,10 +418,10 @@ function ASCIIFontExample() {
|
|
|
365
418
|
### Login Form
|
|
366
419
|
|
|
367
420
|
```tsx
|
|
368
|
-
import { useState, useCallback } from "react"
|
|
369
421
|
import { render, useKeyboard } from "@opentui/react"
|
|
422
|
+
import { useCallback, useState } from "react"
|
|
370
423
|
|
|
371
|
-
function
|
|
424
|
+
function App() {
|
|
372
425
|
const [username, setUsername] = useState("")
|
|
373
426
|
const [password, setPassword] = useState("")
|
|
374
427
|
const [focused, setFocused] = useState<"username" | "password">("username")
|
|
@@ -389,10 +442,10 @@ function LoginForm() {
|
|
|
389
442
|
}, [username, password])
|
|
390
443
|
|
|
391
444
|
return (
|
|
392
|
-
<box style={{ padding: 2, flexDirection: "column" }}>
|
|
445
|
+
<box style={{ border: true, padding: 2, flexDirection: "column", gap: 1 }}>
|
|
393
446
|
<text fg="#FFFF00">Login Form</text>
|
|
394
447
|
|
|
395
|
-
<box title="Username" style={{ width: 40, height: 3
|
|
448
|
+
<box title="Username" style={{ border: true, width: 40, height: 3 }}>
|
|
396
449
|
<input
|
|
397
450
|
placeholder="Enter username..."
|
|
398
451
|
onInput={setUsername}
|
|
@@ -401,7 +454,7 @@ function LoginForm() {
|
|
|
401
454
|
/>
|
|
402
455
|
</box>
|
|
403
456
|
|
|
404
|
-
<box title="Password" style={{ width: 40, height: 3
|
|
457
|
+
<box title="Password" style={{ border: true, width: 40, height: 3 }}>
|
|
405
458
|
<input
|
|
406
459
|
placeholder="Enter password..."
|
|
407
460
|
onInput={setPassword}
|
|
@@ -421,16 +474,16 @@ function LoginForm() {
|
|
|
421
474
|
)
|
|
422
475
|
}
|
|
423
476
|
|
|
424
|
-
render(<
|
|
477
|
+
render(<App />)
|
|
425
478
|
```
|
|
426
479
|
|
|
427
480
|
### Counter with Timer
|
|
428
481
|
|
|
429
482
|
```tsx
|
|
430
|
-
import { useState, useEffect } from "react"
|
|
431
483
|
import { render } from "@opentui/react"
|
|
484
|
+
import { useEffect, useState } from "react"
|
|
432
485
|
|
|
433
|
-
function
|
|
486
|
+
function App() {
|
|
434
487
|
const [count, setCount] = useState(0)
|
|
435
488
|
|
|
436
489
|
useEffect(() => {
|
|
@@ -448,48 +501,66 @@ function Counter() {
|
|
|
448
501
|
)
|
|
449
502
|
}
|
|
450
503
|
|
|
451
|
-
render(<
|
|
504
|
+
render(<App />)
|
|
452
505
|
```
|
|
453
506
|
|
|
454
507
|
### Styled Text Showcase
|
|
455
508
|
|
|
456
509
|
```tsx
|
|
457
|
-
import { blue, bold, red, t, underline } from "@opentui/core"
|
|
458
510
|
import { render } from "@opentui/react"
|
|
459
511
|
|
|
460
|
-
function
|
|
512
|
+
function App() {
|
|
461
513
|
return (
|
|
462
|
-
|
|
514
|
+
<>
|
|
463
515
|
<text>Simple text</text>
|
|
464
|
-
<text>
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
<text>
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
516
|
+
<text>
|
|
517
|
+
<strong>Bold text</strong>
|
|
518
|
+
</text>
|
|
519
|
+
<text>
|
|
520
|
+
<u>Underlined text</u>
|
|
521
|
+
</text>
|
|
522
|
+
<text>
|
|
523
|
+
<span fg="red">Red text</span>
|
|
524
|
+
</text>
|
|
525
|
+
<text>
|
|
526
|
+
<span fg="blue">Blue text</span>
|
|
527
|
+
</text>
|
|
528
|
+
<text>
|
|
529
|
+
<strong fg="red">Bold red text</strong>
|
|
530
|
+
</text>
|
|
531
|
+
<text>
|
|
532
|
+
<strong>Bold</strong> and <span fg="blue">blue</span> combined
|
|
533
|
+
</text>
|
|
534
|
+
</>
|
|
471
535
|
)
|
|
472
536
|
}
|
|
473
537
|
|
|
474
|
-
render(<
|
|
538
|
+
render(<App />)
|
|
475
539
|
```
|
|
476
540
|
|
|
477
541
|
## Component Extension
|
|
478
542
|
|
|
479
|
-
You can create custom components by extending
|
|
543
|
+
You can create custom components by extending OpenTUIs base renderables:
|
|
480
544
|
|
|
481
545
|
```tsx
|
|
482
|
-
import { BoxRenderable, OptimizedBuffer, RGBA } from "@opentui/core"
|
|
546
|
+
import { BoxRenderable, OptimizedBuffer, RGBA, type BoxOptions, type RenderContext } from "@opentui/core"
|
|
483
547
|
import { extend, render } from "@opentui/react"
|
|
484
548
|
|
|
485
549
|
// Create custom component class
|
|
486
550
|
class ButtonRenderable extends BoxRenderable {
|
|
487
551
|
private _label: string = "Button"
|
|
488
552
|
|
|
489
|
-
constructor(
|
|
490
|
-
super(
|
|
491
|
-
|
|
492
|
-
|
|
553
|
+
constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {
|
|
554
|
+
super(ctx, {
|
|
555
|
+
border: true,
|
|
556
|
+
borderStyle: "single",
|
|
557
|
+
minHeight: 3,
|
|
558
|
+
...options,
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
if (options.label) {
|
|
562
|
+
this._label = options.label
|
|
563
|
+
}
|
|
493
564
|
}
|
|
494
565
|
|
|
495
566
|
protected renderSelf(buffer: OptimizedBuffer): void {
|
|
@@ -510,19 +581,19 @@ class ButtonRenderable extends BoxRenderable {
|
|
|
510
581
|
// Add TypeScript support
|
|
511
582
|
declare module "@opentui/react" {
|
|
512
583
|
interface OpenTUIComponents {
|
|
513
|
-
|
|
584
|
+
consoleButton: typeof ButtonRenderable
|
|
514
585
|
}
|
|
515
586
|
}
|
|
516
587
|
|
|
517
588
|
// Register the component
|
|
518
|
-
extend({
|
|
589
|
+
extend({ consoleButton: ButtonRenderable })
|
|
519
590
|
|
|
520
591
|
// Use in JSX
|
|
521
592
|
function App() {
|
|
522
593
|
return (
|
|
523
594
|
<box>
|
|
524
|
-
<
|
|
525
|
-
<
|
|
595
|
+
<consoleButton label="Click me!" style={{ backgroundColor: "blue" }} />
|
|
596
|
+
<consoleButton label="Another button" style={{ backgroundColor: "green" }} />
|
|
526
597
|
</box>
|
|
527
598
|
)
|
|
528
599
|
}
|
package/index.js
CHANGED
|
@@ -9,6 +9,62 @@ import {
|
|
|
9
9
|
TabSelectRenderable,
|
|
10
10
|
TextRenderable
|
|
11
11
|
} from "@opentui/core";
|
|
12
|
+
|
|
13
|
+
// src/components/text.ts
|
|
14
|
+
import { TextAttributes, TextNodeRenderable } from "@opentui/core";
|
|
15
|
+
var textNodeKeys = ["span", "b", "strong", "i", "em", "u", "br"];
|
|
16
|
+
|
|
17
|
+
class SpanRenderable extends TextNodeRenderable {
|
|
18
|
+
ctx;
|
|
19
|
+
constructor(ctx, options) {
|
|
20
|
+
super(options);
|
|
21
|
+
this.ctx = ctx;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class TextModifierRenderable extends SpanRenderable {
|
|
26
|
+
constructor(options, modifier) {
|
|
27
|
+
super(null, options);
|
|
28
|
+
if (modifier === "b" || modifier === "strong") {
|
|
29
|
+
this.attributes = (this.attributes || 0) | TextAttributes.BOLD;
|
|
30
|
+
} else if (modifier === "i" || modifier === "em") {
|
|
31
|
+
this.attributes = (this.attributes || 0) | TextAttributes.ITALIC;
|
|
32
|
+
} else if (modifier === "u") {
|
|
33
|
+
this.attributes = (this.attributes || 0) | TextAttributes.UNDERLINE;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class BoldSpanRenderable extends TextModifierRenderable {
|
|
39
|
+
constructor(_ctx, options) {
|
|
40
|
+
super(options, "b");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class ItalicSpanRenderable extends TextModifierRenderable {
|
|
45
|
+
constructor(_ctx, options) {
|
|
46
|
+
super(options, "i");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class UnderlineSpanRenderable extends TextModifierRenderable {
|
|
51
|
+
constructor(_ctx, options) {
|
|
52
|
+
super(options, "u");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class LineBreakRenderable extends SpanRenderable {
|
|
57
|
+
constructor(_ctx, options) {
|
|
58
|
+
super(null, options);
|
|
59
|
+
this.add();
|
|
60
|
+
}
|
|
61
|
+
add() {
|
|
62
|
+
return super.add(`
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/components/index.ts
|
|
12
68
|
var baseComponents = {
|
|
13
69
|
box: BoxRenderable,
|
|
14
70
|
text: TextRenderable,
|
|
@@ -16,7 +72,14 @@ var baseComponents = {
|
|
|
16
72
|
select: SelectRenderable,
|
|
17
73
|
scrollbox: ScrollBoxRenderable,
|
|
18
74
|
"ascii-font": ASCIIFontRenderable,
|
|
19
|
-
"tab-select": TabSelectRenderable
|
|
75
|
+
"tab-select": TabSelectRenderable,
|
|
76
|
+
span: SpanRenderable,
|
|
77
|
+
br: LineBreakRenderable,
|
|
78
|
+
b: BoldSpanRenderable,
|
|
79
|
+
strong: BoldSpanRenderable,
|
|
80
|
+
i: ItalicSpanRenderable,
|
|
81
|
+
em: ItalicSpanRenderable,
|
|
82
|
+
u: UnderlineSpanRenderable
|
|
20
83
|
};
|
|
21
84
|
var componentCatalogue = { ...baseComponents };
|
|
22
85
|
function extend(objects) {
|
|
@@ -104,6 +167,7 @@ import ReactReconciler from "react-reconciler";
|
|
|
104
167
|
import { ConcurrentRoot } from "react-reconciler/constants";
|
|
105
168
|
|
|
106
169
|
// src/reconciler/host-config.ts
|
|
170
|
+
import { TextNodeRenderable as TextNodeRenderable2 } from "@opentui/core";
|
|
107
171
|
import { createContext as createContext2 } from "react";
|
|
108
172
|
import { DefaultEventPriority, NoEventPriority } from "react-reconciler/constants";
|
|
109
173
|
|
|
@@ -122,13 +186,11 @@ function getNextId(type) {
|
|
|
122
186
|
import {
|
|
123
187
|
InputRenderable as InputRenderable2,
|
|
124
188
|
InputRenderableEvents,
|
|
189
|
+
isRenderable,
|
|
125
190
|
SelectRenderable as SelectRenderable2,
|
|
126
191
|
SelectRenderableEvents,
|
|
127
|
-
StyledText,
|
|
128
192
|
TabSelectRenderable as TabSelectRenderable2,
|
|
129
|
-
TabSelectRenderableEvents
|
|
130
|
-
TextRenderable as TextRenderable2,
|
|
131
|
-
stringToStyledText
|
|
193
|
+
TabSelectRenderableEvents
|
|
132
194
|
} from "@opentui/core";
|
|
133
195
|
function initEventListeners(instance, eventName, listener, previousListener) {
|
|
134
196
|
if (previousListener) {
|
|
@@ -138,46 +200,6 @@ function initEventListeners(instance, eventName, listener, previousListener) {
|
|
|
138
200
|
instance.on(eventName, listener);
|
|
139
201
|
}
|
|
140
202
|
}
|
|
141
|
-
function handleTextChildren(textInstance, children) {
|
|
142
|
-
if (children == null) {
|
|
143
|
-
textInstance.content = stringToStyledText("");
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (Array.isArray(children)) {
|
|
147
|
-
const chunks = [];
|
|
148
|
-
for (const child of children) {
|
|
149
|
-
if (typeof child === "string") {
|
|
150
|
-
chunks.push({
|
|
151
|
-
__isChunk: true,
|
|
152
|
-
text: new TextEncoder().encode(child),
|
|
153
|
-
plainText: child
|
|
154
|
-
});
|
|
155
|
-
} else if (child && typeof child === "object" && "__isChunk" in child) {
|
|
156
|
-
chunks.push(child);
|
|
157
|
-
} else if (child instanceof StyledText) {
|
|
158
|
-
chunks.push(...child.chunks);
|
|
159
|
-
} else if (child != null) {
|
|
160
|
-
const stringValue = String(child);
|
|
161
|
-
chunks.push({
|
|
162
|
-
__isChunk: true,
|
|
163
|
-
text: new TextEncoder().encode(stringValue),
|
|
164
|
-
plainText: stringValue
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
textInstance.content = new StyledText(chunks);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
if (typeof children === "string") {
|
|
172
|
-
textInstance.content = stringToStyledText(children);
|
|
173
|
-
} else if (children && typeof children === "object" && "__isChunk" in children) {
|
|
174
|
-
textInstance.content = new StyledText([children]);
|
|
175
|
-
} else if (children instanceof StyledText) {
|
|
176
|
-
textInstance.content = children;
|
|
177
|
-
} else {
|
|
178
|
-
textInstance.content = stringToStyledText(String(children));
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
203
|
function setStyle(instance, styles, oldStyles) {
|
|
182
204
|
if (styles && typeof styles === "object") {
|
|
183
205
|
if (oldStyles != null) {
|
|
@@ -226,19 +248,18 @@ function setProperty(instance, type, propKey, propValue, oldPropValue) {
|
|
|
226
248
|
}
|
|
227
249
|
break;
|
|
228
250
|
case "focused":
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
if (isRenderable(instance)) {
|
|
252
|
+
if (!!propValue) {
|
|
253
|
+
instance.focus();
|
|
254
|
+
} else {
|
|
255
|
+
instance.blur();
|
|
256
|
+
}
|
|
233
257
|
}
|
|
234
258
|
break;
|
|
235
259
|
case "style":
|
|
236
260
|
setStyle(instance, propValue, oldPropValue);
|
|
237
261
|
break;
|
|
238
262
|
case "children":
|
|
239
|
-
if (type === "text" && instance instanceof TextRenderable2) {
|
|
240
|
-
handleTextChildren(instance, propValue);
|
|
241
|
-
}
|
|
242
263
|
break;
|
|
243
264
|
default:
|
|
244
265
|
instance[propKey] = propValue;
|
|
@@ -279,10 +300,13 @@ var hostConfig = {
|
|
|
279
300
|
supportsPersistence: false,
|
|
280
301
|
supportsHydration: false,
|
|
281
302
|
createInstance(type, props, rootContainerInstance, hostContext) {
|
|
303
|
+
if (textNodeKeys.includes(type) && !hostContext.isInsideText) {
|
|
304
|
+
throw new Error(`Component of type "${type}" must be created inside of a text node`);
|
|
305
|
+
}
|
|
282
306
|
const id = getNextId(type);
|
|
283
307
|
const components = getComponentCatalogue();
|
|
284
308
|
if (!components[type]) {
|
|
285
|
-
throw new Error(`
|
|
309
|
+
throw new Error(`Unknown component type: ${type}`);
|
|
286
310
|
}
|
|
287
311
|
return new components[type](rootContainerInstance.ctx, {
|
|
288
312
|
id,
|
|
@@ -311,23 +335,20 @@ var hostConfig = {
|
|
|
311
335
|
containerInfo.requestRender();
|
|
312
336
|
},
|
|
313
337
|
getRootHostContext(rootContainerInstance) {
|
|
314
|
-
return {};
|
|
338
|
+
return { isInsideText: false };
|
|
315
339
|
},
|
|
316
340
|
getChildHostContext(parentHostContext, type, rootContainerInstance) {
|
|
317
|
-
|
|
341
|
+
const isInsideText = ["text", ...textNodeKeys].includes(type);
|
|
342
|
+
return { ...parentHostContext, isInsideText };
|
|
318
343
|
},
|
|
319
344
|
shouldSetTextContent(type, props) {
|
|
320
|
-
if (type === "text") {
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
345
|
return false;
|
|
324
346
|
},
|
|
325
347
|
createTextInstance(text, rootContainerInstance, hostContext) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
});
|
|
348
|
+
if (!hostContext.isInsideText) {
|
|
349
|
+
throw new Error("Text must be created inside of a text node");
|
|
350
|
+
}
|
|
351
|
+
return TextNodeRenderable2.fromString(text);
|
|
331
352
|
},
|
|
332
353
|
scheduleTimeout: setTimeout,
|
|
333
354
|
cancelTimeout: clearTimeout,
|
|
@@ -345,7 +366,7 @@ var hostConfig = {
|
|
|
345
366
|
instance.requestRender();
|
|
346
367
|
},
|
|
347
368
|
commitTextUpdate(textInstance, oldText, newText) {
|
|
348
|
-
textInstance.
|
|
369
|
+
textInstance.children = [newText];
|
|
349
370
|
textInstance.requestRender();
|
|
350
371
|
},
|
|
351
372
|
appendChildToContainer(container, child) {
|
|
@@ -408,7 +429,7 @@ var hostConfig = {
|
|
|
408
429
|
},
|
|
409
430
|
detachDeletedInstance(instance) {
|
|
410
431
|
if (!instance.parent) {
|
|
411
|
-
instance.
|
|
432
|
+
instance.destroyRecursively();
|
|
412
433
|
}
|
|
413
434
|
},
|
|
414
435
|
getPublicInstance(instance) {
|
package/jsx-namespace.d.ts
CHANGED
|
@@ -4,9 +4,11 @@ import type {
|
|
|
4
4
|
BoxProps,
|
|
5
5
|
ExtendedIntrinsicElements,
|
|
6
6
|
InputProps,
|
|
7
|
+
LineBreakProps,
|
|
7
8
|
OpenTUIComponents,
|
|
8
9
|
ScrollBoxProps,
|
|
9
10
|
SelectProps,
|
|
11
|
+
SpanProps,
|
|
10
12
|
TabSelectProps,
|
|
11
13
|
TextProps,
|
|
12
14
|
} from "./src/types/components"
|
|
@@ -29,10 +31,18 @@ export namespace JSX {
|
|
|
29
31
|
interface IntrinsicElements extends React.JSX.IntrinsicElements, ExtendedIntrinsicElements<OpenTUIComponents> {
|
|
30
32
|
box: BoxProps
|
|
31
33
|
text: TextProps
|
|
34
|
+
span: SpanProps
|
|
32
35
|
input: InputProps
|
|
33
36
|
select: SelectProps
|
|
34
37
|
scrollbox: ScrollBoxProps
|
|
35
38
|
"ascii-font": AsciiFontProps
|
|
36
39
|
"tab-select": TabSelectProps
|
|
40
|
+
// Text modifiers
|
|
41
|
+
b: SpanProps
|
|
42
|
+
i: SpanProps
|
|
43
|
+
u: SpanProps
|
|
44
|
+
strong: SpanProps
|
|
45
|
+
em: SpanProps
|
|
46
|
+
br: LineBreakProps
|
|
37
47
|
}
|
|
38
48
|
}
|
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.0.0-
|
|
7
|
+
"version": "0.0.0-20250915-f5db043a",
|
|
8
8
|
"description": "React renderer for building terminal user interfaces using OpenTUI core",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@opentui/core": "0.0.0-
|
|
38
|
+
"@opentui/core": "0.0.0-20250915-f5db043a",
|
|
39
39
|
"react-reconciler": "^0.32.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ASCIIFontRenderable, BoxRenderable, InputRenderable, ScrollBoxRenderable, SelectRenderable, TabSelectRenderable, TextRenderable } from "@opentui/core";
|
|
2
2
|
import type { RenderableConstructor } from "../types/components";
|
|
3
|
+
import { BoldSpanRenderable, ItalicSpanRenderable, LineBreakRenderable, SpanRenderable, UnderlineSpanRenderable } from "./text";
|
|
3
4
|
export declare const baseComponents: {
|
|
4
5
|
box: typeof BoxRenderable;
|
|
5
6
|
text: typeof TextRenderable;
|
|
@@ -8,6 +9,13 @@ export declare const baseComponents: {
|
|
|
8
9
|
scrollbox: typeof ScrollBoxRenderable;
|
|
9
10
|
"ascii-font": typeof ASCIIFontRenderable;
|
|
10
11
|
"tab-select": typeof TabSelectRenderable;
|
|
12
|
+
span: typeof SpanRenderable;
|
|
13
|
+
br: typeof LineBreakRenderable;
|
|
14
|
+
b: typeof BoldSpanRenderable;
|
|
15
|
+
strong: typeof BoldSpanRenderable;
|
|
16
|
+
i: typeof ItalicSpanRenderable;
|
|
17
|
+
em: typeof ItalicSpanRenderable;
|
|
18
|
+
u: typeof UnderlineSpanRenderable;
|
|
11
19
|
};
|
|
12
20
|
type ComponentCatalogue = Record<string, RenderableConstructor>;
|
|
13
21
|
export declare const componentCatalogue: ComponentCatalogue;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TextNodeRenderable, type RenderContext, type TextNodeOptions } from "@opentui/core";
|
|
2
|
+
export declare const textNodeKeys: readonly ["span", "b", "strong", "i", "em", "u", "br"];
|
|
3
|
+
export type TextNodeKey = (typeof textNodeKeys)[number];
|
|
4
|
+
export declare class SpanRenderable extends TextNodeRenderable {
|
|
5
|
+
private readonly ctx;
|
|
6
|
+
constructor(ctx: RenderContext | null, options: TextNodeOptions);
|
|
7
|
+
}
|
|
8
|
+
declare class TextModifierRenderable extends SpanRenderable {
|
|
9
|
+
constructor(options: TextNodeOptions, modifier?: TextNodeKey);
|
|
10
|
+
}
|
|
11
|
+
export declare class BoldSpanRenderable extends TextModifierRenderable {
|
|
12
|
+
constructor(_ctx: RenderContext | null, options: TextNodeOptions);
|
|
13
|
+
}
|
|
14
|
+
export declare class ItalicSpanRenderable extends TextModifierRenderable {
|
|
15
|
+
constructor(_ctx: RenderContext | null, options: TextNodeOptions);
|
|
16
|
+
}
|
|
17
|
+
export declare class UnderlineSpanRenderable extends TextModifierRenderable {
|
|
18
|
+
constructor(_ctx: RenderContext | null, options: TextNodeOptions);
|
|
19
|
+
}
|
|
20
|
+
export declare class LineBreakRenderable extends SpanRenderable {
|
|
21
|
+
constructor(_ctx: RenderContext | null, options: TextNodeOptions);
|
|
22
|
+
add(): number;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RootRenderable } from "@opentui/core";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import ReactReconciler from "react-reconciler";
|
|
4
|
-
export declare const reconciler: ReactReconciler.Reconciler<RootRenderable, import("@opentui/core").
|
|
4
|
+
export declare const reconciler: ReactReconciler.Reconciler<RootRenderable, import("@opentui/core").BaseRenderable, import("@opentui/core").TextNodeRenderable, unknown, unknown, import("@opentui/core").BaseRenderable>;
|
|
5
5
|
export declare function _render(element: React.ReactNode, root: RootRenderable): void;
|
|
@@ -15,6 +15,7 @@ import ReactReconciler from "react-reconciler";
|
|
|
15
15
|
import { ConcurrentRoot } from "react-reconciler/constants";
|
|
16
16
|
|
|
17
17
|
// src/reconciler/host-config.ts
|
|
18
|
+
import { TextNodeRenderable as TextNodeRenderable2 } from "@opentui/core";
|
|
18
19
|
import { createContext as createContext2 } from "react";
|
|
19
20
|
import { DefaultEventPriority, NoEventPriority } from "react-reconciler/constants";
|
|
20
21
|
|
|
@@ -28,6 +29,62 @@ import {
|
|
|
28
29
|
TabSelectRenderable,
|
|
29
30
|
TextRenderable
|
|
30
31
|
} from "@opentui/core";
|
|
32
|
+
|
|
33
|
+
// src/components/text.ts
|
|
34
|
+
import { TextAttributes, TextNodeRenderable } from "@opentui/core";
|
|
35
|
+
var textNodeKeys = ["span", "b", "strong", "i", "em", "u", "br"];
|
|
36
|
+
|
|
37
|
+
class SpanRenderable extends TextNodeRenderable {
|
|
38
|
+
ctx;
|
|
39
|
+
constructor(ctx, options) {
|
|
40
|
+
super(options);
|
|
41
|
+
this.ctx = ctx;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class TextModifierRenderable extends SpanRenderable {
|
|
46
|
+
constructor(options, modifier) {
|
|
47
|
+
super(null, options);
|
|
48
|
+
if (modifier === "b" || modifier === "strong") {
|
|
49
|
+
this.attributes = (this.attributes || 0) | TextAttributes.BOLD;
|
|
50
|
+
} else if (modifier === "i" || modifier === "em") {
|
|
51
|
+
this.attributes = (this.attributes || 0) | TextAttributes.ITALIC;
|
|
52
|
+
} else if (modifier === "u") {
|
|
53
|
+
this.attributes = (this.attributes || 0) | TextAttributes.UNDERLINE;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class BoldSpanRenderable extends TextModifierRenderable {
|
|
59
|
+
constructor(_ctx, options) {
|
|
60
|
+
super(options, "b");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class ItalicSpanRenderable extends TextModifierRenderable {
|
|
65
|
+
constructor(_ctx, options) {
|
|
66
|
+
super(options, "i");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class UnderlineSpanRenderable extends TextModifierRenderable {
|
|
71
|
+
constructor(_ctx, options) {
|
|
72
|
+
super(options, "u");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
class LineBreakRenderable extends SpanRenderable {
|
|
77
|
+
constructor(_ctx, options) {
|
|
78
|
+
super(null, options);
|
|
79
|
+
this.add();
|
|
80
|
+
}
|
|
81
|
+
add() {
|
|
82
|
+
return super.add(`
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/components/index.ts
|
|
31
88
|
var baseComponents = {
|
|
32
89
|
box: BoxRenderable,
|
|
33
90
|
text: TextRenderable,
|
|
@@ -35,7 +92,14 @@ var baseComponents = {
|
|
|
35
92
|
select: SelectRenderable,
|
|
36
93
|
scrollbox: ScrollBoxRenderable,
|
|
37
94
|
"ascii-font": ASCIIFontRenderable,
|
|
38
|
-
"tab-select": TabSelectRenderable
|
|
95
|
+
"tab-select": TabSelectRenderable,
|
|
96
|
+
span: SpanRenderable,
|
|
97
|
+
br: LineBreakRenderable,
|
|
98
|
+
b: BoldSpanRenderable,
|
|
99
|
+
strong: BoldSpanRenderable,
|
|
100
|
+
i: ItalicSpanRenderable,
|
|
101
|
+
em: ItalicSpanRenderable,
|
|
102
|
+
u: UnderlineSpanRenderable
|
|
39
103
|
};
|
|
40
104
|
var componentCatalogue = { ...baseComponents };
|
|
41
105
|
function getComponentCatalogue() {
|
|
@@ -57,13 +121,11 @@ function getNextId(type) {
|
|
|
57
121
|
import {
|
|
58
122
|
InputRenderable as InputRenderable2,
|
|
59
123
|
InputRenderableEvents,
|
|
124
|
+
isRenderable,
|
|
60
125
|
SelectRenderable as SelectRenderable2,
|
|
61
126
|
SelectRenderableEvents,
|
|
62
|
-
StyledText,
|
|
63
127
|
TabSelectRenderable as TabSelectRenderable2,
|
|
64
|
-
TabSelectRenderableEvents
|
|
65
|
-
TextRenderable as TextRenderable2,
|
|
66
|
-
stringToStyledText
|
|
128
|
+
TabSelectRenderableEvents
|
|
67
129
|
} from "@opentui/core";
|
|
68
130
|
function initEventListeners(instance, eventName, listener, previousListener) {
|
|
69
131
|
if (previousListener) {
|
|
@@ -73,46 +135,6 @@ function initEventListeners(instance, eventName, listener, previousListener) {
|
|
|
73
135
|
instance.on(eventName, listener);
|
|
74
136
|
}
|
|
75
137
|
}
|
|
76
|
-
function handleTextChildren(textInstance, children) {
|
|
77
|
-
if (children == null) {
|
|
78
|
-
textInstance.content = stringToStyledText("");
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (Array.isArray(children)) {
|
|
82
|
-
const chunks = [];
|
|
83
|
-
for (const child of children) {
|
|
84
|
-
if (typeof child === "string") {
|
|
85
|
-
chunks.push({
|
|
86
|
-
__isChunk: true,
|
|
87
|
-
text: new TextEncoder().encode(child),
|
|
88
|
-
plainText: child
|
|
89
|
-
});
|
|
90
|
-
} else if (child && typeof child === "object" && "__isChunk" in child) {
|
|
91
|
-
chunks.push(child);
|
|
92
|
-
} else if (child instanceof StyledText) {
|
|
93
|
-
chunks.push(...child.chunks);
|
|
94
|
-
} else if (child != null) {
|
|
95
|
-
const stringValue = String(child);
|
|
96
|
-
chunks.push({
|
|
97
|
-
__isChunk: true,
|
|
98
|
-
text: new TextEncoder().encode(stringValue),
|
|
99
|
-
plainText: stringValue
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
textInstance.content = new StyledText(chunks);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (typeof children === "string") {
|
|
107
|
-
textInstance.content = stringToStyledText(children);
|
|
108
|
-
} else if (children && typeof children === "object" && "__isChunk" in children) {
|
|
109
|
-
textInstance.content = new StyledText([children]);
|
|
110
|
-
} else if (children instanceof StyledText) {
|
|
111
|
-
textInstance.content = children;
|
|
112
|
-
} else {
|
|
113
|
-
textInstance.content = stringToStyledText(String(children));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
138
|
function setStyle(instance, styles, oldStyles) {
|
|
117
139
|
if (styles && typeof styles === "object") {
|
|
118
140
|
if (oldStyles != null) {
|
|
@@ -161,19 +183,18 @@ function setProperty(instance, type, propKey, propValue, oldPropValue) {
|
|
|
161
183
|
}
|
|
162
184
|
break;
|
|
163
185
|
case "focused":
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
if (isRenderable(instance)) {
|
|
187
|
+
if (!!propValue) {
|
|
188
|
+
instance.focus();
|
|
189
|
+
} else {
|
|
190
|
+
instance.blur();
|
|
191
|
+
}
|
|
168
192
|
}
|
|
169
193
|
break;
|
|
170
194
|
case "style":
|
|
171
195
|
setStyle(instance, propValue, oldPropValue);
|
|
172
196
|
break;
|
|
173
197
|
case "children":
|
|
174
|
-
if (type === "text" && instance instanceof TextRenderable2) {
|
|
175
|
-
handleTextChildren(instance, propValue);
|
|
176
|
-
}
|
|
177
198
|
break;
|
|
178
199
|
default:
|
|
179
200
|
instance[propKey] = propValue;
|
|
@@ -214,10 +235,13 @@ var hostConfig = {
|
|
|
214
235
|
supportsPersistence: false,
|
|
215
236
|
supportsHydration: false,
|
|
216
237
|
createInstance(type, props, rootContainerInstance, hostContext) {
|
|
238
|
+
if (textNodeKeys.includes(type) && !hostContext.isInsideText) {
|
|
239
|
+
throw new Error(`Component of type "${type}" must be created inside of a text node`);
|
|
240
|
+
}
|
|
217
241
|
const id = getNextId(type);
|
|
218
242
|
const components = getComponentCatalogue();
|
|
219
243
|
if (!components[type]) {
|
|
220
|
-
throw new Error(`
|
|
244
|
+
throw new Error(`Unknown component type: ${type}`);
|
|
221
245
|
}
|
|
222
246
|
return new components[type](rootContainerInstance.ctx, {
|
|
223
247
|
id,
|
|
@@ -246,23 +270,20 @@ var hostConfig = {
|
|
|
246
270
|
containerInfo.requestRender();
|
|
247
271
|
},
|
|
248
272
|
getRootHostContext(rootContainerInstance) {
|
|
249
|
-
return {};
|
|
273
|
+
return { isInsideText: false };
|
|
250
274
|
},
|
|
251
275
|
getChildHostContext(parentHostContext, type, rootContainerInstance) {
|
|
252
|
-
|
|
276
|
+
const isInsideText = ["text", ...textNodeKeys].includes(type);
|
|
277
|
+
return { ...parentHostContext, isInsideText };
|
|
253
278
|
},
|
|
254
279
|
shouldSetTextContent(type, props) {
|
|
255
|
-
if (type === "text") {
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
280
|
return false;
|
|
259
281
|
},
|
|
260
282
|
createTextInstance(text, rootContainerInstance, hostContext) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
});
|
|
283
|
+
if (!hostContext.isInsideText) {
|
|
284
|
+
throw new Error("Text must be created inside of a text node");
|
|
285
|
+
}
|
|
286
|
+
return TextNodeRenderable2.fromString(text);
|
|
266
287
|
},
|
|
267
288
|
scheduleTimeout: setTimeout,
|
|
268
289
|
cancelTimeout: clearTimeout,
|
|
@@ -280,7 +301,7 @@ var hostConfig = {
|
|
|
280
301
|
instance.requestRender();
|
|
281
302
|
},
|
|
282
303
|
commitTextUpdate(textInstance, oldText, newText) {
|
|
283
|
-
textInstance.
|
|
304
|
+
textInstance.children = [newText];
|
|
284
305
|
textInstance.requestRender();
|
|
285
306
|
},
|
|
286
307
|
appendChildToContainer(container, child) {
|
|
@@ -343,7 +364,7 @@ var hostConfig = {
|
|
|
343
364
|
},
|
|
344
365
|
detachDeletedInstance(instance) {
|
|
345
366
|
if (!instance.parent) {
|
|
346
|
-
instance.
|
|
367
|
+
instance.destroyRecursively();
|
|
347
368
|
}
|
|
348
369
|
},
|
|
349
370
|
getPublicInstance(instance) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ASCIIFontOptions, ASCIIFontRenderable, BoxOptions, BoxRenderable, InputRenderable, InputRenderableOptions,
|
|
1
|
+
import type { ASCIIFontOptions, ASCIIFontRenderable, BaseRenderable, BoxOptions, BoxRenderable, InputRenderable, InputRenderableOptions, RenderableOptions, RenderContext, ScrollBoxOptions, ScrollBoxRenderable, SelectOption, SelectRenderable, SelectRenderableOptions, TabSelectOption, TabSelectRenderable, TabSelectRenderableOptions, TextNodeOptions, TextNodeRenderable, TextOptions, TextRenderable } from "@opentui/core";
|
|
2
2
|
import type React from "react";
|
|
3
3
|
/** Properties that should not be included in the style prop */
|
|
4
4
|
export type NonStyledProps = "id" | "buffered" | "live" | "enableLayout" | "selectable" | "renderAfter" | "renderBefore" | `on${string}`;
|
|
@@ -8,7 +8,7 @@ export type ReactProps<TRenderable = unknown> = {
|
|
|
8
8
|
ref?: React.Ref<TRenderable>;
|
|
9
9
|
};
|
|
10
10
|
/** Base type for any renderable constructor */
|
|
11
|
-
export type RenderableConstructor<TRenderable extends
|
|
11
|
+
export type RenderableConstructor<TRenderable extends BaseRenderable = BaseRenderable> = new (ctx: RenderContext, options: any) => TRenderable;
|
|
12
12
|
/** Extract the options type from a renderable constructor */
|
|
13
13
|
type ExtractRenderableOptions<TConstructor> = TConstructor extends new (ctx: RenderContext, options: infer TOptions) => any ? TOptions : never;
|
|
14
14
|
/** Extract the renderable type from a constructor */
|
|
@@ -20,14 +20,18 @@ type ContainerProps<TOptions> = TOptions & {
|
|
|
20
20
|
children?: React.ReactNode;
|
|
21
21
|
};
|
|
22
22
|
/** Smart component props that automatically determine excluded properties */
|
|
23
|
-
type ComponentProps<TOptions extends RenderableOptions<TRenderable>, TRenderable extends
|
|
23
|
+
type ComponentProps<TOptions extends RenderableOptions<TRenderable>, TRenderable extends BaseRenderable> = TOptions & {
|
|
24
24
|
style?: Partial<Omit<TOptions, GetNonStyledProperties<RenderableConstructor<TRenderable>>>>;
|
|
25
25
|
} & ReactProps<TRenderable>;
|
|
26
26
|
/** Valid text content types for Text component children */
|
|
27
|
-
type TextChildren = string | number | boolean | null | undefined;
|
|
27
|
+
type TextChildren = string | number | boolean | null | undefined | React.ReactNode;
|
|
28
28
|
export type TextProps = ComponentProps<TextOptions, TextRenderable> & {
|
|
29
|
-
children?: TextChildren
|
|
29
|
+
children?: TextChildren;
|
|
30
30
|
};
|
|
31
|
+
export type SpanProps = ComponentProps<TextNodeOptions, TextNodeRenderable> & {
|
|
32
|
+
children?: TextChildren;
|
|
33
|
+
};
|
|
34
|
+
export type LineBreakProps = Pick<SpanProps, "id">;
|
|
31
35
|
export type BoxProps = ComponentProps<ContainerProps<BoxOptions>, BoxRenderable>;
|
|
32
36
|
export type InputProps = ComponentProps<InputRenderableOptions, InputRenderable> & {
|
|
33
37
|
focused?: boolean;
|
package/src/types/host.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BaseRenderable, RootRenderable, TextNodeRenderable } from "@opentui/core";
|
|
2
2
|
import { baseComponents } from "../components";
|
|
3
3
|
export type Type = keyof typeof baseComponents;
|
|
4
4
|
export type Props = Record<string, any>;
|
|
5
5
|
export type Container = RootRenderable;
|
|
6
|
-
export type Instance =
|
|
7
|
-
export type TextInstance =
|
|
6
|
+
export type Instance = BaseRenderable;
|
|
7
|
+
export type TextInstance = TextNodeRenderable;
|
|
8
8
|
export type PublicInstance = Instance;
|
|
9
|
-
export type HostContext = Record<string, any
|
|
9
|
+
export type HostContext = Record<string, any> & {
|
|
10
|
+
isInsideText?: boolean;
|
|
11
|
+
};
|