@opentui/react 0.0.0-20250908-4906ddad → 0.0.0-20250912-12c969f4

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
@@ -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,6 +57,7 @@ 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
 
@@ -65,11 +67,13 @@ Components can be styled using props or the `style` prop:
65
67
 
66
68
  ```tsx
67
69
  // Direct props
68
- <text fg="#FF0000">Hello</text>
70
+ <box backgroundColor="blue" padding={2}>
71
+ <text>Hello, world!</text>
72
+ </box>
69
73
 
70
74
  // Style prop
71
75
  <box style={{ backgroundColor: "blue", padding: 2 }}>
72
- <text>Styled content</text>
76
+ <text>Hello, world!</text>
73
77
  </box>
74
78
  ```
75
79
 
@@ -102,14 +106,15 @@ Access the OpenTUI renderer instance.
102
106
  ```tsx
103
107
  import { useRenderer } from "@opentui/react"
104
108
 
105
- function MyComponent() {
109
+ function App() {
106
110
  const renderer = useRenderer()
107
111
 
108
112
  useEffect(() => {
109
- renderer.toggleDebugOverlay()
113
+ renderer.console.show()
114
+ console.log("Hello, from the console!")
110
115
  }, [])
111
116
 
112
- return <text>Debug available</text>
117
+ return <box />
113
118
  }
114
119
  ```
115
120
 
@@ -120,7 +125,7 @@ Handle keyboard events.
120
125
  ```tsx
121
126
  import { useKeyboard } from "@opentui/react"
122
127
 
123
- function MyComponent() {
128
+ function App() {
124
129
  useKeyboard((key) => {
125
130
  if (key.name === "escape") {
126
131
  process.exit(0)
@@ -139,7 +144,7 @@ Handle terminal resize events.
139
144
  import { useOnResize, useRenderer } from "@opentui/react"
140
145
  import { useEffect } from "react"
141
146
 
142
- function MyComponent() {
147
+ function App() {
143
148
  const renderer = useRenderer()
144
149
 
145
150
  useEffect(() => {
@@ -161,7 +166,7 @@ Get current terminal dimensions and automatically update when the terminal is re
161
166
  ```tsx
162
167
  import { useTerminalDimensions } from "@opentui/react"
163
168
 
164
- function MyComponent() {
169
+ function App() {
165
170
  const { width, height } = useTerminalDimensions()
166
171
 
167
172
  return (
@@ -188,7 +193,7 @@ Display text with rich formatting.
188
193
  ```tsx
189
194
  import { bold, fg, t } from "@opentui/core"
190
195
 
191
- function TextExample() {
196
+ function App() {
192
197
  return (
193
198
  <box>
194
199
  {/* Simple text */}
@@ -209,22 +214,23 @@ function TextExample() {
209
214
  Container with borders and layout capabilities.
210
215
 
211
216
  ```tsx
212
- function BoxExample() {
217
+ function App() {
213
218
  return (
214
219
  <box flexDirection="column">
215
220
  {/* Basic box */}
216
- <box>
221
+ <box border>
217
222
  <text>Simple box</text>
218
223
  </box>
219
224
 
220
225
  {/* Box with title and styling */}
221
- <box title="Settings" borderStyle="double" padding={2} backgroundColor="blue">
226
+ <box title="Settings" border borderStyle="double" padding={2} backgroundColor="blue">
222
227
  <text>Box content</text>
223
228
  </box>
224
229
 
225
230
  {/* Styled box */}
226
231
  <box
227
232
  style={{
233
+ border: true,
228
234
  width: 40,
229
235
  height: 10,
230
236
  margin: 1,
@@ -246,20 +252,16 @@ Text input field with event handling.
246
252
  ```tsx
247
253
  import { useState } from "react"
248
254
 
249
- function InputExample() {
255
+ function App() {
250
256
  const [value, setValue] = useState("")
251
- const [focused, setFocused] = useState(true)
252
257
 
253
258
  return (
254
- <box title="Enter your name" style={{ height: 3 }}>
259
+ <box title="Enter your name" style={{ border: true, height: 3 }}>
255
260
  <input
256
261
  placeholder="Type here..."
257
- focused={focused}
262
+ focused
258
263
  onInput={setValue}
259
264
  onSubmit={(value) => console.log("Submitted:", value)}
260
- style={{
261
- focusedBackgroundColor: "#333333",
262
- }}
263
265
  />
264
266
  </box>
265
267
  )
@@ -274,7 +276,7 @@ Dropdown selection component.
274
276
  import type { SelectOption } from "@opentui/core"
275
277
  import { useState } from "react"
276
278
 
277
- function SelectExample() {
279
+ function App() {
278
280
  const [selectedIndex, setSelectedIndex] = useState(0)
279
281
 
280
282
  const options: SelectOption[] = [
@@ -284,7 +286,7 @@ function SelectExample() {
284
286
  ]
285
287
 
286
288
  return (
287
- <box style={{ height: 24 }}>
289
+ <box style={{ border: true, height: 24 }}>
288
290
  <select
289
291
  style={{ height: 22 }}
290
292
  options={options}
@@ -299,6 +301,50 @@ function SelectExample() {
299
301
  }
300
302
  ```
301
303
 
304
+ ### Scrollbox Component
305
+
306
+ A scrollable box.
307
+
308
+ ```tsx
309
+ function App() {
310
+ return (
311
+ <scrollbox
312
+ style={{
313
+ rootOptions: {
314
+ backgroundColor: "#24283b",
315
+ },
316
+ wrapperOptions: {
317
+ backgroundColor: "#1f2335",
318
+ },
319
+ viewportOptions: {
320
+ backgroundColor: "#1a1b26",
321
+ },
322
+ contentOptions: {
323
+ backgroundColor: "#16161e",
324
+ },
325
+ scrollbarOptions: {
326
+ showArrows: true,
327
+ trackOptions: {
328
+ foregroundColor: "#7aa2f7",
329
+ backgroundColor: "#414868",
330
+ },
331
+ },
332
+ }}
333
+ focused
334
+ >
335
+ {Array.from({ length: 1000 }).map((_, i) => (
336
+ <box
337
+ key={i}
338
+ style={{ width: "100%", padding: 1, marginBottom: 1, backgroundColor: i % 2 === 0 ? "#292e42" : "#2f3449" }}
339
+ >
340
+ <text content={`Box ${i}`} />
341
+ </box>
342
+ ))}
343
+ </scrollbox>
344
+ )
345
+ }
346
+ ```
347
+
302
348
  ### ASCII Font Component
303
349
 
304
350
  Display ASCII art text with different font styles.
@@ -307,7 +353,7 @@ Display ASCII art text with different font styles.
307
353
  import { measureText } from "@opentui/core"
308
354
  import { useState } from "react"
309
355
 
310
- function ASCIIFontExample() {
356
+ function App() {
311
357
  const text = "ASCII"
312
358
  const [font, setFont] = useState<"block" | "shade" | "slick" | "tiny">("tiny")
313
359
 
@@ -317,10 +363,11 @@ function ASCIIFontExample() {
317
363
  })
318
364
 
319
365
  return (
320
- <box style={{ paddingLeft: 1, paddingRight: 1 }}>
366
+ <box style={{ border: true, paddingLeft: 1, paddingRight: 1 }}>
321
367
  <box
322
368
  style={{
323
369
  height: 8,
370
+ border: true,
324
371
  marginBottom: 1,
325
372
  }}
326
373
  >
@@ -365,10 +412,10 @@ function ASCIIFontExample() {
365
412
  ### Login Form
366
413
 
367
414
  ```tsx
368
- import { useState, useCallback } from "react"
369
415
  import { render, useKeyboard } from "@opentui/react"
416
+ import { useCallback, useState } from "react"
370
417
 
371
- function LoginForm() {
418
+ function App() {
372
419
  const [username, setUsername] = useState("")
373
420
  const [password, setPassword] = useState("")
374
421
  const [focused, setFocused] = useState<"username" | "password">("username")
@@ -389,10 +436,10 @@ function LoginForm() {
389
436
  }, [username, password])
390
437
 
391
438
  return (
392
- <box style={{ padding: 2, flexDirection: "column" }}>
439
+ <box style={{ border: true, padding: 2, flexDirection: "column", gap: 1 }}>
393
440
  <text fg="#FFFF00">Login Form</text>
394
441
 
395
- <box title="Username" style={{ width: 40, height: 3, marginTop: 1 }}>
442
+ <box title="Username" style={{ border: true, width: 40, height: 3 }}>
396
443
  <input
397
444
  placeholder="Enter username..."
398
445
  onInput={setUsername}
@@ -401,7 +448,7 @@ function LoginForm() {
401
448
  />
402
449
  </box>
403
450
 
404
- <box title="Password" style={{ width: 40, height: 3, marginTop: 1 }}>
451
+ <box title="Password" style={{ border: true, width: 40, height: 3 }}>
405
452
  <input
406
453
  placeholder="Enter password..."
407
454
  onInput={setPassword}
@@ -421,16 +468,16 @@ function LoginForm() {
421
468
  )
422
469
  }
423
470
 
424
- render(<LoginForm />)
471
+ render(<App />)
425
472
  ```
426
473
 
427
474
  ### Counter with Timer
428
475
 
429
476
  ```tsx
430
- import { useState, useEffect } from "react"
431
477
  import { render } from "@opentui/react"
478
+ import { useEffect, useState } from "react"
432
479
 
433
- function Counter() {
480
+ function App() {
434
481
  const [count, setCount] = useState(0)
435
482
 
436
483
  useEffect(() => {
@@ -448,7 +495,7 @@ function Counter() {
448
495
  )
449
496
  }
450
497
 
451
- render(<Counter />)
498
+ render(<App />)
452
499
  ```
453
500
 
454
501
  ### Styled Text Showcase
@@ -457,7 +504,7 @@ render(<Counter />)
457
504
  import { blue, bold, red, t, underline } from "@opentui/core"
458
505
  import { render } from "@opentui/react"
459
506
 
460
- function StyledTextShowcase() {
507
+ function App() {
461
508
  return (
462
509
  <box style={{ flexDirection: "column" }}>
463
510
  <text>Simple text</text>
@@ -471,25 +518,32 @@ function StyledTextShowcase() {
471
518
  )
472
519
  }
473
520
 
474
- render(<StyledTextShowcase />)
521
+ render(<App />)
475
522
  ```
476
523
 
477
524
  ## Component Extension
478
525
 
479
- You can create custom components by extending OpenTUI's base renderables:
526
+ You can create custom components by extending OpenTUIs base renderables:
480
527
 
481
528
  ```tsx
482
- import { BoxRenderable, OptimizedBuffer, RGBA } from "@opentui/core"
529
+ import { BoxRenderable, OptimizedBuffer, RGBA, type BoxOptions, type RenderContext } from "@opentui/core"
483
530
  import { extend, render } from "@opentui/react"
484
531
 
485
532
  // Create custom component class
486
533
  class ButtonRenderable extends BoxRenderable {
487
534
  private _label: string = "Button"
488
535
 
489
- constructor(id: string, options: any) {
490
- super(id, options)
491
- this.borderStyle = "single"
492
- this.padding = 1
536
+ constructor(ctx: RenderContext, options: BoxOptions & { label?: string }) {
537
+ super(ctx, {
538
+ border: true,
539
+ borderStyle: "single",
540
+ minHeight: 3,
541
+ ...options,
542
+ })
543
+
544
+ if (options.label) {
545
+ this._label = options.label
546
+ }
493
547
  }
494
548
 
495
549
  protected renderSelf(buffer: OptimizedBuffer): void {
@@ -510,19 +564,19 @@ class ButtonRenderable extends BoxRenderable {
510
564
  // Add TypeScript support
511
565
  declare module "@opentui/react" {
512
566
  interface OpenTUIComponents {
513
- button: typeof ButtonRenderable
567
+ consoleButton: typeof ButtonRenderable
514
568
  }
515
569
  }
516
570
 
517
571
  // Register the component
518
- extend({ button: ButtonRenderable })
572
+ extend({ consoleButton: ButtonRenderable })
519
573
 
520
574
  // Use in JSX
521
575
  function App() {
522
576
  return (
523
577
  <box>
524
- <button label="Click me!" style={{ backgroundColor: "blue" }} />
525
- <button label="Another button" style={{ backgroundColor: "green" }} />
578
+ <consoleButton label="Click me!" style={{ backgroundColor: "blue" }} />
579
+ <consoleButton label="Another button" style={{ backgroundColor: "green" }} />
526
580
  </box>
527
581
  )
528
582
  }
package/index.js CHANGED
@@ -9,6 +9,51 @@ 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"];
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(options) {
40
+ super(options, "b");
41
+ }
42
+ }
43
+
44
+ class ItalicSpanRenderable extends TextModifierRenderable {
45
+ constructor(options) {
46
+ super(options, "i");
47
+ }
48
+ }
49
+
50
+ class UnderlineSpanRenderable extends TextModifierRenderable {
51
+ constructor(options) {
52
+ super(options, "u");
53
+ }
54
+ }
55
+
56
+ // src/components/index.ts
12
57
  var baseComponents = {
13
58
  box: BoxRenderable,
14
59
  text: TextRenderable,
@@ -16,7 +61,13 @@ var baseComponents = {
16
61
  select: SelectRenderable,
17
62
  scrollbox: ScrollBoxRenderable,
18
63
  "ascii-font": ASCIIFontRenderable,
19
- "tab-select": TabSelectRenderable
64
+ "tab-select": TabSelectRenderable,
65
+ span: SpanRenderable,
66
+ b: BoldSpanRenderable,
67
+ strong: BoldSpanRenderable,
68
+ i: ItalicSpanRenderable,
69
+ em: ItalicSpanRenderable,
70
+ u: UnderlineSpanRenderable
20
71
  };
21
72
  var componentCatalogue = { ...baseComponents };
22
73
  function extend(objects) {
@@ -104,6 +155,7 @@ import ReactReconciler from "react-reconciler";
104
155
  import { ConcurrentRoot } from "react-reconciler/constants";
105
156
 
106
157
  // src/reconciler/host-config.ts
158
+ import { TextNodeRenderable as TextNodeRenderable2 } from "@opentui/core";
107
159
  import { createContext as createContext2 } from "react";
108
160
  import { DefaultEventPriority, NoEventPriority } from "react-reconciler/constants";
109
161
 
@@ -122,13 +174,11 @@ function getNextId(type) {
122
174
  import {
123
175
  InputRenderable as InputRenderable2,
124
176
  InputRenderableEvents,
177
+ isRenderable,
125
178
  SelectRenderable as SelectRenderable2,
126
179
  SelectRenderableEvents,
127
- StyledText,
128
180
  TabSelectRenderable as TabSelectRenderable2,
129
- TabSelectRenderableEvents,
130
- TextRenderable as TextRenderable2,
131
- stringToStyledText
181
+ TabSelectRenderableEvents
132
182
  } from "@opentui/core";
133
183
  function initEventListeners(instance, eventName, listener, previousListener) {
134
184
  if (previousListener) {
@@ -138,46 +188,6 @@ function initEventListeners(instance, eventName, listener, previousListener) {
138
188
  instance.on(eventName, listener);
139
189
  }
140
190
  }
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
191
  function setStyle(instance, styles, oldStyles) {
182
192
  if (styles && typeof styles === "object") {
183
193
  if (oldStyles != null) {
@@ -226,19 +236,18 @@ function setProperty(instance, type, propKey, propValue, oldPropValue) {
226
236
  }
227
237
  break;
228
238
  case "focused":
229
- if (!!propValue) {
230
- instance.focus();
231
- } else {
232
- instance.blur();
239
+ if (isRenderable(instance)) {
240
+ if (!!propValue) {
241
+ instance.focus();
242
+ } else {
243
+ instance.blur();
244
+ }
233
245
  }
234
246
  break;
235
247
  case "style":
236
248
  setStyle(instance, propValue, oldPropValue);
237
249
  break;
238
250
  case "children":
239
- if (type === "text" && instance instanceof TextRenderable2) {
240
- handleTextChildren(instance, propValue);
241
- }
242
251
  break;
243
252
  default:
244
253
  instance[propKey] = propValue;
@@ -279,10 +288,13 @@ var hostConfig = {
279
288
  supportsPersistence: false,
280
289
  supportsHydration: false,
281
290
  createInstance(type, props, rootContainerInstance, hostContext) {
291
+ if (textNodeKeys.includes(type) && !hostContext.isInsideText) {
292
+ throw new Error(`Component of type "${type}" must be created inside of a text node`);
293
+ }
282
294
  const id = getNextId(type);
283
295
  const components = getComponentCatalogue();
284
296
  if (!components[type]) {
285
- throw new Error(`[Reconciler] Unknown component type: ${type}`);
297
+ throw new Error(`Unknown component type: ${type}`);
286
298
  }
287
299
  return new components[type](rootContainerInstance.ctx, {
288
300
  id,
@@ -311,23 +323,20 @@ var hostConfig = {
311
323
  containerInfo.requestRender();
312
324
  },
313
325
  getRootHostContext(rootContainerInstance) {
314
- return {};
326
+ return { isInsideText: false };
315
327
  },
316
328
  getChildHostContext(parentHostContext, type, rootContainerInstance) {
317
- return parentHostContext;
329
+ const isInsideText = ["text", "span", "b", "strong", "i", "em", "u"].includes(type);
330
+ return { ...parentHostContext, isInsideText };
318
331
  },
319
332
  shouldSetTextContent(type, props) {
320
- if (type === "text") {
321
- return true;
322
- }
323
333
  return false;
324
334
  },
325
335
  createTextInstance(text, rootContainerInstance, hostContext) {
326
- const components = getComponentCatalogue();
327
- return new components["text"](rootContainerInstance.ctx, {
328
- id: getNextId("text"),
329
- content: text
330
- });
336
+ if (!hostContext.isInsideText) {
337
+ throw new Error("Text must be created inside of a text node");
338
+ }
339
+ return TextNodeRenderable2.fromString(text);
331
340
  },
332
341
  scheduleTimeout: setTimeout,
333
342
  cancelTimeout: clearTimeout,
@@ -345,7 +354,7 @@ var hostConfig = {
345
354
  instance.requestRender();
346
355
  },
347
356
  commitTextUpdate(textInstance, oldText, newText) {
348
- textInstance.content = newText;
357
+ textInstance.children = [newText];
349
358
  textInstance.requestRender();
350
359
  },
351
360
  appendChildToContainer(container, child) {
@@ -408,7 +417,7 @@ var hostConfig = {
408
417
  },
409
418
  detachDeletedInstance(instance) {
410
419
  if (!instance.parent) {
411
- instance.destroy();
420
+ instance.destroyRecursively();
412
421
  }
413
422
  },
414
423
  getPublicInstance(instance) {
@@ -7,6 +7,7 @@ import type {
7
7
  OpenTUIComponents,
8
8
  ScrollBoxProps,
9
9
  SelectProps,
10
+ SpanProps,
10
11
  TabSelectProps,
11
12
  TextProps,
12
13
  } from "./src/types/components"
@@ -29,10 +30,17 @@ export namespace JSX {
29
30
  interface IntrinsicElements extends React.JSX.IntrinsicElements, ExtendedIntrinsicElements<OpenTUIComponents> {
30
31
  box: BoxProps
31
32
  text: TextProps
33
+ span: SpanProps
32
34
  input: InputProps
33
35
  select: SelectProps
34
36
  scrollbox: ScrollBoxProps
35
37
  "ascii-font": AsciiFontProps
36
38
  "tab-select": TabSelectProps
39
+ // Text modifiers
40
+ b: SpanProps
41
+ i: SpanProps
42
+ u: SpanProps
43
+ strong: SpanProps
44
+ em: SpanProps
37
45
  }
38
46
  }
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-20250908-4906ddad",
7
+ "version": "0.0.0-20250912-12c969f4",
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-20250908-4906ddad",
38
+ "@opentui/core": "0.0.0-20250912-12c969f4",
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, SpanRenderable, UnderlineSpanRenderable } from "./text";
3
4
  export declare const baseComponents: {
4
5
  box: typeof BoxRenderable;
5
6
  text: typeof TextRenderable;
@@ -8,6 +9,12 @@ 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
+ b: typeof BoldSpanRenderable;
14
+ strong: typeof BoldSpanRenderable;
15
+ i: typeof ItalicSpanRenderable;
16
+ em: typeof ItalicSpanRenderable;
17
+ u: typeof UnderlineSpanRenderable;
11
18
  };
12
19
  type ComponentCatalogue = Record<string, RenderableConstructor>;
13
20
  export declare const componentCatalogue: ComponentCatalogue;
@@ -0,0 +1,20 @@
1
+ import { TextNodeRenderable, type RenderContext, type TextNodeOptions } from "@opentui/core";
2
+ export declare const textNodeKeys: readonly ["span", "b", "strong", "i", "em", "u"];
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: any, modifier?: TextNodeKey);
10
+ }
11
+ export declare class BoldSpanRenderable extends TextModifierRenderable {
12
+ constructor(options: any);
13
+ }
14
+ export declare class ItalicSpanRenderable extends TextModifierRenderable {
15
+ constructor(options: any);
16
+ }
17
+ export declare class UnderlineSpanRenderable extends TextModifierRenderable {
18
+ constructor(options: any);
19
+ }
20
+ 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").Renderable, import("@opentui/core").TextRenderable, unknown, unknown, import("@opentui/core").Renderable>;
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,51 @@ 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"];
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(options) {
60
+ super(options, "b");
61
+ }
62
+ }
63
+
64
+ class ItalicSpanRenderable extends TextModifierRenderable {
65
+ constructor(options) {
66
+ super(options, "i");
67
+ }
68
+ }
69
+
70
+ class UnderlineSpanRenderable extends TextModifierRenderable {
71
+ constructor(options) {
72
+ super(options, "u");
73
+ }
74
+ }
75
+
76
+ // src/components/index.ts
31
77
  var baseComponents = {
32
78
  box: BoxRenderable,
33
79
  text: TextRenderable,
@@ -35,7 +81,13 @@ var baseComponents = {
35
81
  select: SelectRenderable,
36
82
  scrollbox: ScrollBoxRenderable,
37
83
  "ascii-font": ASCIIFontRenderable,
38
- "tab-select": TabSelectRenderable
84
+ "tab-select": TabSelectRenderable,
85
+ span: SpanRenderable,
86
+ b: BoldSpanRenderable,
87
+ strong: BoldSpanRenderable,
88
+ i: ItalicSpanRenderable,
89
+ em: ItalicSpanRenderable,
90
+ u: UnderlineSpanRenderable
39
91
  };
40
92
  var componentCatalogue = { ...baseComponents };
41
93
  function getComponentCatalogue() {
@@ -57,13 +109,11 @@ function getNextId(type) {
57
109
  import {
58
110
  InputRenderable as InputRenderable2,
59
111
  InputRenderableEvents,
112
+ isRenderable,
60
113
  SelectRenderable as SelectRenderable2,
61
114
  SelectRenderableEvents,
62
- StyledText,
63
115
  TabSelectRenderable as TabSelectRenderable2,
64
- TabSelectRenderableEvents,
65
- TextRenderable as TextRenderable2,
66
- stringToStyledText
116
+ TabSelectRenderableEvents
67
117
  } from "@opentui/core";
68
118
  function initEventListeners(instance, eventName, listener, previousListener) {
69
119
  if (previousListener) {
@@ -73,46 +123,6 @@ function initEventListeners(instance, eventName, listener, previousListener) {
73
123
  instance.on(eventName, listener);
74
124
  }
75
125
  }
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
126
  function setStyle(instance, styles, oldStyles) {
117
127
  if (styles && typeof styles === "object") {
118
128
  if (oldStyles != null) {
@@ -161,19 +171,18 @@ function setProperty(instance, type, propKey, propValue, oldPropValue) {
161
171
  }
162
172
  break;
163
173
  case "focused":
164
- if (!!propValue) {
165
- instance.focus();
166
- } else {
167
- instance.blur();
174
+ if (isRenderable(instance)) {
175
+ if (!!propValue) {
176
+ instance.focus();
177
+ } else {
178
+ instance.blur();
179
+ }
168
180
  }
169
181
  break;
170
182
  case "style":
171
183
  setStyle(instance, propValue, oldPropValue);
172
184
  break;
173
185
  case "children":
174
- if (type === "text" && instance instanceof TextRenderable2) {
175
- handleTextChildren(instance, propValue);
176
- }
177
186
  break;
178
187
  default:
179
188
  instance[propKey] = propValue;
@@ -214,10 +223,13 @@ var hostConfig = {
214
223
  supportsPersistence: false,
215
224
  supportsHydration: false,
216
225
  createInstance(type, props, rootContainerInstance, hostContext) {
226
+ if (textNodeKeys.includes(type) && !hostContext.isInsideText) {
227
+ throw new Error(`Component of type "${type}" must be created inside of a text node`);
228
+ }
217
229
  const id = getNextId(type);
218
230
  const components = getComponentCatalogue();
219
231
  if (!components[type]) {
220
- throw new Error(`[Reconciler] Unknown component type: ${type}`);
232
+ throw new Error(`Unknown component type: ${type}`);
221
233
  }
222
234
  return new components[type](rootContainerInstance.ctx, {
223
235
  id,
@@ -246,23 +258,20 @@ var hostConfig = {
246
258
  containerInfo.requestRender();
247
259
  },
248
260
  getRootHostContext(rootContainerInstance) {
249
- return {};
261
+ return { isInsideText: false };
250
262
  },
251
263
  getChildHostContext(parentHostContext, type, rootContainerInstance) {
252
- return parentHostContext;
264
+ const isInsideText = ["text", "span", "b", "strong", "i", "em", "u"].includes(type);
265
+ return { ...parentHostContext, isInsideText };
253
266
  },
254
267
  shouldSetTextContent(type, props) {
255
- if (type === "text") {
256
- return true;
257
- }
258
268
  return false;
259
269
  },
260
270
  createTextInstance(text, rootContainerInstance, hostContext) {
261
- const components = getComponentCatalogue();
262
- return new components["text"](rootContainerInstance.ctx, {
263
- id: getNextId("text"),
264
- content: text
265
- });
271
+ if (!hostContext.isInsideText) {
272
+ throw new Error("Text must be created inside of a text node");
273
+ }
274
+ return TextNodeRenderable2.fromString(text);
266
275
  },
267
276
  scheduleTimeout: setTimeout,
268
277
  cancelTimeout: clearTimeout,
@@ -280,7 +289,7 @@ var hostConfig = {
280
289
  instance.requestRender();
281
290
  },
282
291
  commitTextUpdate(textInstance, oldText, newText) {
283
- textInstance.content = newText;
292
+ textInstance.children = [newText];
284
293
  textInstance.requestRender();
285
294
  },
286
295
  appendChildToContainer(container, child) {
@@ -343,7 +352,7 @@ var hostConfig = {
343
352
  },
344
353
  detachDeletedInstance(instance) {
345
354
  if (!instance.parent) {
346
- instance.destroy();
355
+ instance.destroyRecursively();
347
356
  }
348
357
  },
349
358
  getPublicInstance(instance) {
@@ -1,4 +1,4 @@
1
- import type { ASCIIFontOptions, ASCIIFontRenderable, BoxOptions, BoxRenderable, InputRenderable, InputRenderableOptions, Renderable, RenderableOptions, RenderContext, ScrollBoxOptions, ScrollBoxRenderable, SelectOption, SelectRenderable, SelectRenderableOptions, StyledText, TabSelectOption, TabSelectRenderable, TabSelectRenderableOptions, TextChunk, TextOptions, TextRenderable } from "@opentui/core";
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 Renderable = Renderable> = new (ctx: RenderContext, options: any) => TRenderable;
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,13 +20,16 @@ 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 Renderable> = TOptions & {
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 | StyledText | TextChunk | Array<TextChildren | StyledText | TextChunk>;
29
+ children?: TextChildren;
30
+ };
31
+ export type SpanProps = ComponentProps<TextNodeOptions, TextNodeRenderable> & {
32
+ children?: TextChildren;
30
33
  };
31
34
  export type BoxProps = ComponentProps<ContainerProps<BoxOptions>, BoxRenderable>;
32
35
  export type InputProps = ComponentProps<InputRenderableOptions, InputRenderable> & {
@@ -1,9 +1,11 @@
1
- import type { Renderable, RootRenderable, TextRenderable } from "@opentui/core";
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 = Renderable;
7
- export type TextInstance = TextRenderable;
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
+ };