@opentui/solid 0.1.12 → 0.1.14

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/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type CliRendererConfig } from "@opentui/core";
2
2
  import type { JSX } from "./jsx-runtime";
3
- export * from "./src/elements";
4
3
  export declare const render: (node: () => JSX.Element, renderConfig?: CliRendererConfig) => Promise<void>;
4
+ export * from "./src/reconciler";
5
+ export * from "./src/elements";
6
+ export * from "./src/types/elements";
5
7
  export { type JSX };
package/index.js CHANGED
@@ -6,7 +6,6 @@ import { createCliRenderer } from "@opentui/core";
6
6
  import {
7
7
  ASCIIFontRenderable,
8
8
  BoxRenderable,
9
- GroupRenderable,
10
9
  InputRenderable,
11
10
  SelectRenderable,
12
11
  TabSelectRenderable,
@@ -38,7 +37,7 @@ var onResize = (callback) => {
38
37
  };
39
38
  var useTerminalDimensions = () => {
40
39
  const renderer = useRenderer();
41
- const [terminalDimensions, setTerminalDimensions] = createSignal({ width: renderer.terminalWidth, height: renderer.terminalHeight });
40
+ const [terminalDimensions, setTerminalDimensions] = createSignal({ width: renderer.width, height: renderer.height });
42
41
  const callback = (width, height) => {
43
42
  setTerminalDimensions({ width, height });
44
43
  };
@@ -94,15 +93,21 @@ var useTimeline = (timeline, initialValue, targetValue, options, startTime = 0)
94
93
  };
95
94
 
96
95
  // src/elements/index.ts
97
- var elements = {
98
- ascii_font: ASCIIFontRenderable,
96
+ var baseComponents = {
99
97
  box: BoxRenderable,
100
- group: GroupRenderable,
98
+ text: TextRenderable,
101
99
  input: InputRenderable,
102
100
  select: SelectRenderable,
103
- tab_select: TabSelectRenderable,
104
- text: TextRenderable
101
+ ascii_font: ASCIIFontRenderable,
102
+ tab_select: TabSelectRenderable
105
103
  };
104
+ var componentCatalogue = { ...baseComponents };
105
+ function extend(objects) {
106
+ Object.assign(componentCatalogue, objects);
107
+ }
108
+ function getComponentCatalogue() {
109
+ return componentCatalogue;
110
+ }
106
111
 
107
112
  // src/reconciler.ts
108
113
  import {
@@ -116,6 +121,7 @@ import {
116
121
  TabSelectRenderableEvents,
117
122
  TextRenderable as TextRenderable3
118
123
  } from "@opentui/core";
124
+ import { useContext as useContext2 } from "solid-js";
119
125
  import { createRenderer } from "solid-js/universal";
120
126
 
121
127
  // src/elements/text-node.ts
@@ -142,6 +148,9 @@ var log = (...args) => {
142
148
  // src/elements/text-node.ts
143
149
  var GHOST_NODE_TAG = "text-ghost";
144
150
  var ChunkToTextNodeMap = new WeakMap;
151
+ var isTextChunk = (node) => {
152
+ return typeof node === "object" && "__isChunk" in node;
153
+ };
145
154
 
146
155
  class TextNode {
147
156
  id;
@@ -195,6 +204,7 @@ class TextNode {
195
204
  }
196
205
  }
197
206
  textParent.content = styledText;
207
+ textParent.visible = styledText.toString() !== "";
198
208
  this.parent = parent;
199
209
  }
200
210
  remove(parent) {
@@ -253,9 +263,27 @@ class GhostTextRenderable extends TextRenderable2 {
253
263
  }
254
264
 
255
265
  // src/reconciler.ts
256
- import { useContext as useContext2 } from "solid-js";
266
+ var logId = (node) => {
267
+ if (!node)
268
+ return;
269
+ if (isTextChunk(node)) {
270
+ return node.plainText;
271
+ }
272
+ return node.id;
273
+ };
257
274
  function _insertNode(parent, node, anchor) {
258
- log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
275
+ log("Inserting node:", logId(node), "into parent:", logId(parent), "with anchor:", logId(anchor), node instanceof TextNode);
276
+ if (node instanceof StyledText) {
277
+ log("Inserting styled text:", node.toString());
278
+ for (const chunk of node.chunks) {
279
+ _insertNode(parent, _createTextNode(chunk), anchor);
280
+ return;
281
+ }
282
+ }
283
+ if (isTextChunk(node)) {
284
+ _insertNode(parent, _createTextNode(node), anchor);
285
+ return;
286
+ }
259
287
  if (node instanceof TextNode) {
260
288
  return node.insert(parent, anchor);
261
289
  }
@@ -263,6 +291,10 @@ function _insertNode(parent, node, anchor) {
263
291
  return;
264
292
  }
265
293
  if (anchor) {
294
+ if (isTextChunk(anchor)) {
295
+ console.warn("Cannot add non text node with text chunk anchor");
296
+ return;
297
+ }
266
298
  const anchorIndex = parent.getChildren().findIndex((el) => {
267
299
  if (anchor instanceof TextNode) {
268
300
  return el.id === anchor.textParent?.id;
@@ -275,7 +307,20 @@ function _insertNode(parent, node, anchor) {
275
307
  }
276
308
  }
277
309
  function _removeNode(parent, node) {
278
- log("Removing node:", node.id, "from parent:", parent.id);
310
+ log("Removing node:", logId(node), "from parent:", logId(parent));
311
+ if (isTextChunk(node)) {
312
+ const textNode = TextNode.getTextNodeFromChunk(node);
313
+ if (textNode) {
314
+ _removeNode(parent, textNode);
315
+ }
316
+ } else if (node instanceof StyledText) {
317
+ for (const chunk of node.chunks) {
318
+ const textNode = TextNode.getTextNodeFromChunk(chunk);
319
+ if (!textNode)
320
+ continue;
321
+ _removeNode(parent, textNode);
322
+ }
323
+ }
279
324
  if (node instanceof TextNode) {
280
325
  return node.remove(parent);
281
326
  }
@@ -284,6 +329,16 @@ function _removeNode(parent, node) {
284
329
  node.destroyRecursively();
285
330
  }
286
331
  }
332
+ function _createTextNode(value) {
333
+ log("Creating text node:", value);
334
+ const chunk = value && isTextChunk(value) ? value : {
335
+ __isChunk: true,
336
+ text: new TextEncoder().encode(`${value}`),
337
+ plainText: `${value}`
338
+ };
339
+ const textNode = new TextNode(chunk);
340
+ return textNode;
341
+ }
287
342
  var {
288
343
  render: _render,
289
344
  effect,
@@ -292,7 +347,7 @@ var {
292
347
  createElement,
293
348
  createTextNode,
294
349
  insertNode,
295
- insert: solidUniversalInsert,
350
+ insert,
296
351
  spread,
297
352
  setProp,
298
353
  mergeProps,
@@ -305,24 +360,23 @@ var {
305
360
  if (!solidRenderer) {
306
361
  throw new Error("No renderer found");
307
362
  }
363
+ const elements = getComponentCatalogue();
364
+ if (!elements[tagName]) {
365
+ throw new Error(`[Reconciler] Unknown component type: ${tagName}`);
366
+ }
308
367
  const element = new elements[tagName](solidRenderer, { id });
309
368
  log("Element created with id:", id);
310
369
  return element;
311
370
  },
312
- createTextNode(value) {
313
- log("Creating text node:", value);
314
- const chunk = typeof value === "object" && "__isChunk" in value ? value : {
315
- __isChunk: true,
316
- text: new TextEncoder().encode(`${value}`),
317
- plainText: `${value}`
318
- };
319
- const textNode = new TextNode(chunk);
320
- return textNode;
321
- },
371
+ createTextNode: _createTextNode,
322
372
  replaceText(textNode, value) {
323
- log("Replacing text:", value, "in node:", textNode.id);
373
+ log("Replacing text:", value, "in node:", logId(textNode));
324
374
  if (textNode instanceof Renderable2)
325
375
  return;
376
+ if (isTextChunk(textNode)) {
377
+ console.warn("Cannot replace text on text chunk", logId(textNode));
378
+ return;
379
+ }
326
380
  const newChunk = {
327
381
  __isChunk: true,
328
382
  text: new TextEncoder().encode(value),
@@ -331,8 +385,8 @@ var {
331
385
  textNode.replaceText(newChunk);
332
386
  },
333
387
  setProperty(node, name, value, prev) {
334
- if (node instanceof TextNode) {
335
- console.warn("Cannot set property on text node:", node.id);
388
+ if (node instanceof TextNode || isTextChunk(node)) {
389
+ console.warn("Cannot set property on text node:", logId(node));
336
390
  return;
337
391
  }
338
392
  if (name.startsWith("on:")) {
@@ -429,18 +483,25 @@ var {
429
483
  },
430
484
  insertNode: _insertNode,
431
485
  removeNode: _removeNode,
432
- getParentNode(node) {
433
- log("Getting parent of node:", node.id);
486
+ getParentNode(childNode) {
487
+ log("Getting parent of node:", logId(childNode));
488
+ let node = childNode;
489
+ if (isTextChunk(childNode)) {
490
+ const parentTextNode = TextNode.getTextNodeFromChunk(childNode);
491
+ if (!parentTextNode)
492
+ return;
493
+ node = parentTextNode;
494
+ }
434
495
  const parent = node.parent;
435
496
  if (!parent) {
436
- log("No parent found for node:", node.id);
497
+ log("No parent found for node:", logId(node));
437
498
  return;
438
499
  }
439
- log("Parent found:", parent.id, "for node:", node.id);
500
+ log("Parent found:", logId(parent), "for node:", logId(node));
440
501
  return parent;
441
502
  },
442
503
  getFirstChild(node) {
443
- log("Getting first child of node:", node.id);
504
+ log("Getting first child of node:", logId(node));
444
505
  if (node instanceof TextRenderable3) {
445
506
  const chunk = node.content.chunks[0];
446
507
  if (chunk) {
@@ -449,22 +510,26 @@ var {
449
510
  return;
450
511
  }
451
512
  }
452
- if (node instanceof TextNode) {
513
+ if (node instanceof TextNode || isTextChunk(node)) {
453
514
  return;
454
515
  }
455
516
  const firstChild = node.getChildren()[0];
456
517
  if (!firstChild) {
457
- log("No first child found for node:", node.id);
518
+ log("No first child found for node:", logId(node));
458
519
  return;
459
520
  }
460
- log("First child found:", firstChild.id, "for node:", node.id);
521
+ log("First child found:", logId(firstChild), "for node:", logId(node));
461
522
  return firstChild;
462
523
  },
463
524
  getNextSibling(node) {
464
- log("Getting next sibling of node:", node.id);
525
+ log("Getting next sibling of node:", logId(node));
526
+ if (isTextChunk(node)) {
527
+ console.warn("Cannot get next sibling of text chunk");
528
+ return;
529
+ }
465
530
  const parent = node.parent;
466
531
  if (!parent) {
467
- log("No parent found for node:", node.id);
532
+ log("No parent found for node:", logId(node));
468
533
  return;
469
534
  }
470
535
  if (node instanceof TextNode) {
@@ -472,31 +537,31 @@ var {
472
537
  const siblings2 = parent.content.chunks;
473
538
  const index2 = siblings2.indexOf(node.chunk);
474
539
  if (index2 === -1 || index2 === siblings2.length - 1) {
475
- log("No next sibling found for node:", node.id);
540
+ log("No next sibling found for node:", logId(node));
476
541
  return;
477
542
  }
478
543
  const nextSibling2 = siblings2[index2 + 1];
479
544
  if (!nextSibling2) {
480
- log("Next sibling is null for node:", node.id);
545
+ log("Next sibling is null for node:", logId(node));
481
546
  return;
482
547
  }
483
548
  return TextNode.getTextNodeFromChunk(nextSibling2);
484
549
  }
485
- console.warn("Text parent is not a text node:", node.id);
550
+ console.warn("Text parent is not a text node:", logId(node));
486
551
  return;
487
552
  }
488
553
  const siblings = parent.getChildren();
489
554
  const index = siblings.indexOf(node);
490
555
  if (index === -1 || index === siblings.length - 1) {
491
- log("No next sibling found for node:", node.id);
556
+ log("No next sibling found for node:", logId(node));
492
557
  return;
493
558
  }
494
559
  const nextSibling = siblings[index + 1];
495
560
  if (!nextSibling) {
496
- log("Next sibling is null for node:", node.id);
561
+ log("Next sibling is null for node:", logId(node));
497
562
  return;
498
563
  }
499
- log("Next sibling found:", nextSibling.id, "for node:", node.id);
564
+ log("Next sibling found:", logId(nextSibling), "for node:", logId(node));
500
565
  return nextSibling;
501
566
  }
502
567
  });
@@ -519,9 +584,24 @@ export {
519
584
  useSelectionHandler,
520
585
  useRenderer,
521
586
  useKeyHandler,
587
+ use,
588
+ spread,
589
+ setProp,
522
590
  render,
523
591
  onResize,
524
- elements,
592
+ mergeProps,
593
+ memo,
594
+ insertNode,
595
+ insert,
596
+ getComponentCatalogue,
597
+ extend,
598
+ effect,
599
+ createTextNode,
600
+ createElement,
525
601
  createComponentTimeline,
602
+ createComponent,
603
+ componentCatalogue,
604
+ baseComponents,
605
+ _render,
526
606
  RendererContext
527
607
  };
package/jsx-runtime.d.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { Renderable } from "@opentui/core"
2
- import {
3
- ASCIIFontElementProps,
4
- BoxElementProps,
5
- GroupElementProps,
6
- InputElementProps,
7
- SelectElementProps,
8
- TabSelectElementProps,
9
- TextElementProps,
10
- } from "./src/elements/index"
2
+ import type {
3
+ AsciiFontProps,
4
+ BoxProps,
5
+ ExtendedIntrinsicElements,
6
+ InputProps,
7
+ OpenTUIComponents,
8
+ SelectProps,
9
+ TabSelectProps,
10
+ TextProps,
11
+ } from "./src/types/elements"
11
12
 
12
13
  declare namespace JSX {
13
14
  // Replace Node with Renderable
@@ -15,14 +16,13 @@ declare namespace JSX {
15
16
 
16
17
  interface ArrayElement extends Array<Element> {}
17
18
 
18
- interface IntrinsicElements {
19
- ascii_font: ASCIIFontElementProps
20
- box: BoxElementProps
21
- group: GroupElementProps
22
- input: InputElementProps
23
- select: SelectElementProps
24
- tab_select: TabSelectElementProps
25
- text: TextElementProps
19
+ interface IntrinsicElements extends ExtendedIntrinsicElements<OpenTUIComponents> {
20
+ box: BoxProps
21
+ text: TextProps
22
+ input: InputProps
23
+ select: SelectProps
24
+ ascii_font: AsciiFontProps
25
+ tab_select: TabSelectProps
26
26
  }
27
27
 
28
28
  interface ElementChildrenAttribute {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "index.js",
5
5
  "types": "index.d.ts",
6
6
  "type": "module",
7
- "version": "0.1.12",
7
+ "version": "0.1.14",
8
8
  "description": "SolidJS renderer for OpenTUI",
9
9
  "license": "MIT",
10
10
  "repository": {
@@ -18,11 +18,6 @@
18
18
  "import": "./index.js",
19
19
  "require": "./index.js"
20
20
  },
21
- "./reconciler": {
22
- "types": "./src/reconciler.d.ts",
23
- "import": "./src/reconciler.js",
24
- "require": "./src/reconciler.js"
25
- },
26
21
  "./preload": {
27
22
  "import": "./scripts/preload.ts"
28
23
  },
@@ -30,7 +25,7 @@
30
25
  "./jsx-dev-runtime": "./jsx-runtime.d.ts"
31
26
  },
32
27
  "dependencies": {
33
- "@opentui/core": "0.1.12",
28
+ "@opentui/core": "0.1.14",
34
29
  "babel-plugin-module-resolver": "5.0.2",
35
30
  "@babel/core": "7.28.0",
36
31
  "@babel/preset-typescript": "7.27.1",
@@ -29,7 +29,7 @@ const solidTransformPlugin: BunPlugin = {
29
29
  [
30
30
  solid,
31
31
  {
32
- moduleName: "@opentui/solid/reconciler",
32
+ moduleName: "@opentui/solid",
33
33
  generate: "universal",
34
34
  },
35
35
  ],
@@ -1,55 +1,28 @@
1
- import type { ASCIIFontOptions, BoxOptions, InputRenderableOptions, Renderable, RenderableOptions, SelectOption, SelectRenderableOptions, StyledText, TabSelectOption, TabSelectRenderableOptions, TextChunk, TextOptions } from "@opentui/core";
2
- import { ASCIIFontRenderable, BoxRenderable, GroupRenderable, InputRenderable, SelectRenderable, TabSelectRenderable, TextRenderable } from "@opentui/core";
3
- import type { JSX, Ref } from "solid-js";
1
+ import { ASCIIFontRenderable, BoxRenderable, InputRenderable, SelectRenderable, TabSelectRenderable, TextRenderable } from "@opentui/core";
2
+ import type { RenderableConstructor } from "../types/elements";
4
3
  export * from "./hooks";
5
- export declare const elements: {
6
- ascii_font: typeof ASCIIFontRenderable;
4
+ export declare const baseComponents: {
7
5
  box: typeof BoxRenderable;
8
- group: typeof GroupRenderable;
6
+ text: typeof TextRenderable;
9
7
  input: typeof InputRenderable;
10
8
  select: typeof SelectRenderable;
9
+ ascii_font: typeof ASCIIFontRenderable;
11
10
  tab_select: typeof TabSelectRenderable;
12
- text: typeof TextRenderable;
13
- };
14
- export type Element = keyof typeof elements;
15
- type RenderableNonStyleKeys = "buffered";
16
- type ElementProps<T extends RenderableOptions<K>, K extends Renderable = Renderable, NonStyleKeys extends keyof T = RenderableNonStyleKeys> = {
17
- style?: Omit<T, NonStyleKeys | RenderableNonStyleKeys>;
18
- ref?: Ref<K>;
19
- } & T;
20
- type ContainerProps = {
21
- children?: JSX.Element;
22
- };
23
- export type BoxElementProps = ElementProps<BoxOptions, BoxRenderable, "title"> & ContainerProps;
24
- export type BoxStyle = BoxElementProps["style"];
25
- export type GroupElementProps = ElementProps<RenderableOptions, GroupRenderable> & ContainerProps;
26
- export type GroupStyle = GroupElementProps["style"];
27
- export type InputElementProps = ElementProps<InputRenderableOptions, InputRenderable, "value" | "maxLength" | "placeholder"> & {
28
- onInput?: (value: string) => void;
29
- onSubmit?: (value: string) => void;
30
- onChange?: (value: string) => void;
31
- focused?: boolean;
32
- };
33
- export type InputStyle = InputElementProps["style"];
34
- type TabSelectEventCallback = (index: number, option: TabSelectOption) => void;
35
- export type TabSelectElementProps = ElementProps<TabSelectRenderableOptions, TabSelectRenderable, "options" | "showScrollArrows" | "showDescription" | "wrapSelection"> & {
36
- onSelect?: TabSelectEventCallback;
37
- onChange?: TabSelectEventCallback;
38
- focused?: boolean;
39
- };
40
- export type TabSelectStyle = TabSelectElementProps["style"];
41
- type SelectEventCallback = (index: number, option: SelectOption) => void;
42
- export type SelectElementProps = ElementProps<SelectRenderableOptions, SelectRenderable, "options" | "showScrollIndicator" | "wrapSelection" | "fastScrollStep"> & {
43
- onSelect?: SelectEventCallback;
44
- onChange?: SelectEventCallback;
45
- focused?: boolean;
46
- };
47
- export type SelectStyle = SelectElementProps["style"];
48
- type TextChildTypes = (string & {}) | number | boolean | null | undefined;
49
- type TextProps = {
50
- children: TextChildTypes | StyledText | TextChunk | Array<TextChildTypes | TextChunk>;
51
11
  };
52
- export type ASCIIFontElementProps = ElementProps<ASCIIFontOptions, ASCIIFontRenderable, "text" | "selectable"> & {};
53
- export type ASCIIFontStyle = ASCIIFontElementProps["style"];
54
- export type TextElementProps = ElementProps<TextOptions, TextRenderable, "content" | "selectable"> & TextProps;
55
- export type TextStyle = TextElementProps["style"];
12
+ type ComponentCatalogue = Record<string, RenderableConstructor>;
13
+ export declare const componentCatalogue: ComponentCatalogue;
14
+ /**
15
+ * Extend the component catalogue with new renderable components
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * // Extend with an object of components
20
+ * extend({
21
+ * consoleButton: ConsoleButtonRenderable,
22
+ * customBox: CustomBoxRenderable
23
+ * })
24
+ * ```
25
+ */
26
+ export declare function extend<T extends ComponentCatalogue>(objects: T): void;
27
+ export declare function getComponentCatalogue(): ComponentCatalogue;
28
+ export type { ExtendedComponentProps, ExtendedIntrinsicElements, RenderableConstructor } from "../types/elements";
@@ -1,5 +1,6 @@
1
1
  import { Renderable, TextRenderable, type RenderContext, type TextChunk, type TextOptions } from "@opentui/core";
2
2
  import { type DomNode } from "../reconciler";
3
+ export declare const isTextChunk: (node: any) => node is TextChunk;
3
4
  /**
4
5
  * Represents a text node in the SolidJS reconciler.
5
6
  */
@@ -1,5 +1,4 @@
1
- import { Renderable } from "@opentui/core";
1
+ import { Renderable, type TextChunk } from "@opentui/core";
2
2
  import { TextNode } from "./elements/text-node";
3
- export type DomNode = Renderable | TextNode;
4
- export declare const _render: (code: () => DomNode, node: DomNode) => () => void, effect: <T>(fn: (prev?: T) => T, init?: T) => void, memo: <T>(fn: () => T, equal: boolean) => () => T, createComponent: <T>(Comp: (props: T) => DomNode, props: T) => DomNode, createElement: (tag: string) => DomNode, createTextNode: (value: string) => DomNode, insertNode: (parent: DomNode, node: DomNode, anchor?: DomNode | undefined) => void, solidUniversalInsert: <T>(parent: any, accessor: T | (() => T), marker?: any | null, initial?: any) => DomNode, spread: <T>(node: any, accessor: (() => T) | T, skipChildren?: boolean) => void, setProp: <T>(node: DomNode, name: string, value: T, prev?: T | undefined) => T, mergeProps: (...sources: unknown[]) => unknown, use: <A, T>(fn: (element: DomNode, arg: A) => T, element: DomNode, arg: A) => T;
5
- export declare const insert: typeof solidUniversalInsert;
3
+ export type DomNode = Renderable | TextNode | TextChunk;
4
+ export declare const _render: (code: () => DomNode, node: DomNode) => () => void, effect: <T>(fn: (prev?: T) => T, init?: T) => void, memo: <T>(fn: () => T, equal: boolean) => () => T, createComponent: <T>(Comp: (props: T) => DomNode, props: T) => DomNode, createElement: (tag: string) => DomNode, createTextNode: (value: string) => DomNode, insertNode: (parent: DomNode, node: DomNode, anchor?: DomNode | undefined) => void, insert: <T>(parent: any, accessor: T | (() => T), marker?: any | null, initial?: any) => DomNode, spread: <T>(node: any, accessor: (() => T) | T, skipChildren?: boolean) => void, setProp: <T>(node: DomNode, name: string, value: T, prev?: T | undefined) => T, mergeProps: (...sources: unknown[]) => unknown, use: <A, T>(fn: (element: DomNode, arg: A) => T, element: DomNode, arg: A) => T;
@@ -0,0 +1,64 @@
1
+ import type { ASCIIFontOptions, ASCIIFontRenderable, BoxOptions, BoxRenderable, InputRenderable, InputRenderableOptions, Renderable, RenderableOptions, RenderContext, SelectOption, SelectRenderable, SelectRenderableOptions, StyledText, TabSelectOption, TabSelectRenderable, TabSelectRenderableOptions, TextChunk, TextOptions, TextRenderable } from "@opentui/core";
2
+ import type { JSX, Ref } from "solid-js";
3
+ /** Properties that should not be included in the style prop */
4
+ export type NonStyledProps = "id" | "buffered" | "live" | "enableLayout" | "selectable" | "renderAfter" | "renderBefore" | `on${string}`;
5
+ /** Solid-specific props for all components */
6
+ export type ElementProps<TRenderable = unknown> = {
7
+ ref?: Ref<TRenderable>;
8
+ };
9
+ /** Base type for any renderable constructor */
10
+ export type RenderableConstructor<TRenderable extends Renderable = Renderable> = new (ctx: RenderContext, options: any) => TRenderable;
11
+ /** Extract the options type from a renderable constructor */
12
+ type ExtractRenderableOptions<TConstructor> = TConstructor extends new (ctx: RenderContext, options: infer TOptions) => any ? TOptions : never;
13
+ /** Extract the renderable type from a constructor */
14
+ type ExtractRenderable<TConstructor> = TConstructor extends new (ctx: RenderContext, options: any) => infer TRenderable ? TRenderable : never;
15
+ /** Determine which properties should be excluded from styling for different renderable types */
16
+ export type GetNonStyledProperties<TConstructor> = TConstructor extends RenderableConstructor<TextRenderable> ? NonStyledProps | "content" : TConstructor extends RenderableConstructor<BoxRenderable> ? NonStyledProps | "title" : TConstructor extends RenderableConstructor<ASCIIFontRenderable> ? NonStyledProps | "text" | "selectable" : TConstructor extends RenderableConstructor<InputRenderable> ? NonStyledProps | "placeholder" | "value" : NonStyledProps;
17
+ /** Base props for container components that accept children */
18
+ type ContainerProps<TOptions> = TOptions & {
19
+ children?: JSX.Element;
20
+ };
21
+ /** Smart component props that automatically determine excluded properties */
22
+ type ComponentProps<TOptions extends RenderableOptions<TRenderable>, TRenderable extends Renderable> = TOptions & {
23
+ style?: Partial<Omit<TOptions, GetNonStyledProperties<RenderableConstructor<TRenderable>>>>;
24
+ } & ElementProps<TRenderable>;
25
+ /** Valid text content types for Text component children */
26
+ type TextChildren = (string & {}) | number | boolean | null | undefined;
27
+ export type TextProps = ComponentProps<TextOptions, TextRenderable> & {
28
+ children?: TextChildren | StyledText | TextChunk | Array<TextChildren | StyledText | TextChunk>;
29
+ };
30
+ export type BoxProps = ComponentProps<ContainerProps<BoxOptions>, BoxRenderable>;
31
+ export type InputProps = ComponentProps<InputRenderableOptions, InputRenderable> & {
32
+ focused?: boolean;
33
+ onInput?: (value: string) => void;
34
+ onChange?: (value: string) => void;
35
+ onSubmit?: (value: string) => void;
36
+ };
37
+ export type SelectProps = ComponentProps<SelectRenderableOptions, SelectRenderable> & {
38
+ focused?: boolean;
39
+ onChange?: (index: number, option: SelectOption | null) => void;
40
+ onSelect?: (index: number, option: SelectOption | null) => void;
41
+ };
42
+ export type AsciiFontProps = ComponentProps<ASCIIFontOptions, ASCIIFontRenderable>;
43
+ export type TabSelectProps = ComponentProps<TabSelectRenderableOptions, TabSelectRenderable> & {
44
+ focused?: boolean;
45
+ onChange?: (index: number, option: TabSelectOption | null) => void;
46
+ onSelect?: (index: number, option: TabSelectOption | null) => void;
47
+ };
48
+ /** Convert renderable constructor to component props with proper style exclusions */
49
+ export type ExtendedComponentProps<TConstructor extends RenderableConstructor, TOptions = ExtractRenderableOptions<TConstructor>> = TOptions & {
50
+ children?: JSX.Element;
51
+ style?: Partial<Omit<TOptions, GetNonStyledProperties<TConstructor>>>;
52
+ } & ElementProps<ExtractRenderable<TConstructor>>;
53
+ /** Helper type to create JSX element properties from a component catalogue */
54
+ export type ExtendedIntrinsicElements<TComponentCatalogue extends Record<string, RenderableConstructor>> = {
55
+ [TComponentName in keyof TComponentCatalogue]: ExtendedComponentProps<TComponentCatalogue[TComponentName]>;
56
+ };
57
+ /**
58
+ * Global augmentation interface for extended components
59
+ * This will be augmented by user code using module augmentation
60
+ */
61
+ export interface OpenTUIComponents {
62
+ [componentName: string]: RenderableConstructor;
63
+ }
64
+ export {};
package/src/reconciler.js DELETED
@@ -1,481 +0,0 @@
1
- // @bun
2
- // src/reconciler.ts
3
- import {
4
- InputRenderable as InputRenderable2,
5
- InputRenderableEvents,
6
- Renderable as Renderable2,
7
- SelectRenderable as SelectRenderable2,
8
- SelectRenderableEvents,
9
- StyledText,
10
- TabSelectRenderable as TabSelectRenderable2,
11
- TabSelectRenderableEvents,
12
- TextRenderable as TextRenderable3
13
- } from "@opentui/core";
14
- import { createRenderer } from "solid-js/universal";
15
-
16
- // src/elements/index.ts
17
- import {
18
- ASCIIFontRenderable,
19
- BoxRenderable,
20
- GroupRenderable,
21
- InputRenderable,
22
- SelectRenderable,
23
- TabSelectRenderable,
24
- TextRenderable
25
- } from "@opentui/core";
26
-
27
- // src/elements/hooks.ts
28
- import {
29
- getKeyHandler,
30
- Timeline
31
- } from "@opentui/core";
32
- import { createContext, createSignal, onCleanup, onMount, useContext } from "solid-js";
33
- var RendererContext = createContext();
34
-
35
- // src/elements/index.ts
36
- var elements = {
37
- ascii_font: ASCIIFontRenderable,
38
- box: BoxRenderable,
39
- group: GroupRenderable,
40
- input: InputRenderable,
41
- select: SelectRenderable,
42
- tab_select: TabSelectRenderable,
43
- text: TextRenderable
44
- };
45
-
46
- // src/elements/text-node.ts
47
- import { Renderable, TextRenderable as TextRenderable2 } from "@opentui/core";
48
-
49
- // src/utils/id-counter.ts
50
- var idCounter = new Map;
51
- function getNextId(elementType) {
52
- if (!idCounter.has(elementType)) {
53
- idCounter.set(elementType, 0);
54
- }
55
- const value = idCounter.get(elementType) + 1;
56
- idCounter.set(elementType, value);
57
- return `${elementType}-${value}`;
58
- }
59
-
60
- // src/utils/log.ts
61
- var log = (...args) => {
62
- if (process.env.DEBUG) {
63
- console.log("[Reconciler]", ...args);
64
- }
65
- };
66
-
67
- // src/elements/text-node.ts
68
- var GHOST_NODE_TAG = "text-ghost";
69
- var ChunkToTextNodeMap = new WeakMap;
70
-
71
- class TextNode {
72
- id;
73
- chunk;
74
- parent;
75
- textParent;
76
- constructor(chunk) {
77
- this.id = getNextId("text-node");
78
- this.chunk = chunk;
79
- ChunkToTextNodeMap.set(chunk, this);
80
- }
81
- replaceText(newChunk) {
82
- const textParent = this.textParent;
83
- if (!textParent) {
84
- log("No parent found for text node:", this.id);
85
- return;
86
- }
87
- textParent.content = textParent.content.replace(newChunk, this.chunk);
88
- this.chunk = newChunk;
89
- ChunkToTextNodeMap.set(newChunk, this);
90
- }
91
- static getTextNodeFromChunk(chunk) {
92
- return ChunkToTextNodeMap.get(chunk);
93
- }
94
- insert(parent, anchor) {
95
- if (!(parent instanceof Renderable)) {
96
- log("Attaching text node to parent text node, impossible");
97
- return;
98
- }
99
- let textParent;
100
- if (!(parent instanceof TextRenderable2)) {
101
- textParent = this.getOrCreateTextGhostNode(parent, anchor);
102
- } else {
103
- textParent = parent;
104
- }
105
- this.textParent = textParent;
106
- let styledText = textParent.content;
107
- if (anchor && anchor instanceof TextNode) {
108
- const anchorIndex = styledText.chunks.indexOf(anchor.chunk);
109
- if (anchorIndex === -1) {
110
- log("anchor not found");
111
- return;
112
- }
113
- styledText = styledText.insert(this.chunk, anchorIndex);
114
- } else {
115
- const firstChunk = textParent.content.chunks[0];
116
- if (firstChunk && !ChunkToTextNodeMap.has(firstChunk)) {
117
- styledText = styledText.replace(this.chunk, firstChunk);
118
- } else {
119
- styledText = styledText.insert(this.chunk);
120
- }
121
- }
122
- textParent.content = styledText;
123
- this.parent = parent;
124
- }
125
- remove(parent) {
126
- if (!(parent instanceof Renderable)) {
127
- ChunkToTextNodeMap.delete(this.chunk);
128
- return;
129
- }
130
- if (parent === this.textParent && parent instanceof TextRenderable2) {
131
- ChunkToTextNodeMap.delete(this.chunk);
132
- parent.content = parent.content.remove(this.chunk);
133
- return;
134
- }
135
- if (this.textParent) {
136
- ChunkToTextNodeMap.delete(this.chunk);
137
- let styledText = this.textParent.content;
138
- styledText = styledText.remove(this.chunk);
139
- if (styledText.chunks.length > 0) {
140
- this.textParent.content = styledText;
141
- } else {
142
- this.parent?.remove(this.textParent.id);
143
- this.textParent.destroyRecursively();
144
- }
145
- }
146
- }
147
- getOrCreateTextGhostNode(parent, anchor) {
148
- if (anchor instanceof TextNode && anchor.textParent) {
149
- return anchor.textParent;
150
- }
151
- const children = parent.getChildren();
152
- if (anchor instanceof Renderable) {
153
- const anchorIndex = children.findIndex((el) => el.id === anchor.id);
154
- const beforeAnchor = children[anchorIndex - 1];
155
- if (beforeAnchor instanceof GhostTextRenderable) {
156
- return beforeAnchor;
157
- }
158
- }
159
- const lastChild = children.at(-1);
160
- if (lastChild instanceof GhostTextRenderable) {
161
- return lastChild;
162
- }
163
- const ghostNode = new GhostTextRenderable(parent.ctx, {
164
- id: getNextId(GHOST_NODE_TAG)
165
- });
166
- insertNode(parent, ghostNode, anchor);
167
- return ghostNode;
168
- }
169
- }
170
-
171
- class GhostTextRenderable extends TextRenderable2 {
172
- constructor(ctx, options) {
173
- super(ctx, options);
174
- }
175
- static isGhostNode(node) {
176
- return node instanceof GhostTextRenderable;
177
- }
178
- }
179
-
180
- // src/reconciler.ts
181
- import { useContext as useContext2 } from "solid-js";
182
- function _insertNode(parent, node, anchor) {
183
- log("Inserting node:", node.id, "into parent:", parent.id, "with anchor:", anchor?.id);
184
- if (node instanceof TextNode) {
185
- return node.insert(parent, anchor);
186
- }
187
- if (!(parent instanceof Renderable2)) {
188
- return;
189
- }
190
- if (anchor) {
191
- const anchorIndex = parent.getChildren().findIndex((el) => {
192
- if (anchor instanceof TextNode) {
193
- return el.id === anchor.textParent?.id;
194
- }
195
- return el.id === anchor.id;
196
- });
197
- parent.add(node, anchorIndex);
198
- } else {
199
- parent.add(node);
200
- }
201
- }
202
- function _removeNode(parent, node) {
203
- log("Removing node:", node.id, "from parent:", parent.id);
204
- if (node instanceof TextNode) {
205
- return node.remove(parent);
206
- }
207
- if (parent instanceof Renderable2 && node instanceof Renderable2) {
208
- parent.remove(node.id);
209
- node.destroyRecursively();
210
- }
211
- }
212
- var {
213
- render: _render,
214
- effect,
215
- memo,
216
- createComponent,
217
- createElement,
218
- createTextNode,
219
- insertNode,
220
- insert: solidUniversalInsert,
221
- spread,
222
- setProp,
223
- mergeProps,
224
- use
225
- } = createRenderer({
226
- createElement(tagName) {
227
- log("Creating element:", tagName);
228
- const id = getNextId(tagName);
229
- const solidRenderer = useContext2(RendererContext);
230
- if (!solidRenderer) {
231
- throw new Error("No renderer found");
232
- }
233
- const element = new elements[tagName](solidRenderer, { id });
234
- log("Element created with id:", id);
235
- return element;
236
- },
237
- createTextNode(value) {
238
- log("Creating text node:", value);
239
- const chunk = typeof value === "object" && "__isChunk" in value ? value : {
240
- __isChunk: true,
241
- text: new TextEncoder().encode(`${value}`),
242
- plainText: `${value}`
243
- };
244
- const textNode = new TextNode(chunk);
245
- return textNode;
246
- },
247
- replaceText(textNode, value) {
248
- log("Replacing text:", value, "in node:", textNode.id);
249
- if (textNode instanceof Renderable2)
250
- return;
251
- const newChunk = {
252
- __isChunk: true,
253
- text: new TextEncoder().encode(value),
254
- plainText: value
255
- };
256
- textNode.replaceText(newChunk);
257
- },
258
- setProperty(node, name, value, prev) {
259
- if (node instanceof TextNode) {
260
- console.warn("Cannot set property on text node:", node.id);
261
- return;
262
- }
263
- if (name.startsWith("on:")) {
264
- const eventName = name.slice(3);
265
- if (value) {
266
- node.on(eventName, value);
267
- }
268
- if (prev) {
269
- node.off(eventName, prev);
270
- }
271
- return;
272
- }
273
- switch (name) {
274
- case "focused":
275
- if (value) {
276
- node.focus();
277
- } else {
278
- node.blur();
279
- }
280
- break;
281
- case "onChange":
282
- let event = undefined;
283
- if (node instanceof SelectRenderable2) {
284
- event = SelectRenderableEvents.SELECTION_CHANGED;
285
- } else if (node instanceof TabSelectRenderable2) {
286
- event = TabSelectRenderableEvents.SELECTION_CHANGED;
287
- } else if (node instanceof InputRenderable2) {
288
- event = InputRenderableEvents.CHANGE;
289
- }
290
- if (!event)
291
- break;
292
- if (value) {
293
- node.on(event, value);
294
- }
295
- if (prev) {
296
- node.off(event, prev);
297
- }
298
- break;
299
- case "onInput":
300
- if (node instanceof InputRenderable2) {
301
- if (value) {
302
- node.on(InputRenderableEvents.INPUT, value);
303
- }
304
- if (prev) {
305
- node.off(InputRenderableEvents.INPUT, prev);
306
- }
307
- }
308
- break;
309
- case "onSubmit":
310
- if (node instanceof InputRenderable2) {
311
- if (value) {
312
- node.on(InputRenderableEvents.ENTER, value);
313
- }
314
- if (prev) {
315
- node.off(InputRenderableEvents.ENTER, prev);
316
- }
317
- }
318
- break;
319
- case "onSelect":
320
- if (node instanceof SelectRenderable2) {
321
- if (value) {
322
- node.on(SelectRenderableEvents.ITEM_SELECTED, value);
323
- }
324
- if (prev) {
325
- node.off(SelectRenderableEvents.ITEM_SELECTED, prev);
326
- }
327
- } else if (node instanceof TabSelectRenderable2) {
328
- if (value) {
329
- node.on(TabSelectRenderableEvents.ITEM_SELECTED, value);
330
- }
331
- if (prev) {
332
- node.off(TabSelectRenderableEvents.ITEM_SELECTED, prev);
333
- }
334
- }
335
- break;
336
- case "style":
337
- for (const prop in value) {
338
- const propVal = value[prop];
339
- if (prev !== undefined && propVal === prev[prop])
340
- continue;
341
- node[prop] = propVal;
342
- }
343
- break;
344
- case "text":
345
- case "content":
346
- node[name] = typeof value === "string" ? value : Array.isArray(value) ? value.join("") : `${value}`;
347
- break;
348
- default:
349
- node[name] = value;
350
- }
351
- },
352
- isTextNode(node) {
353
- return node instanceof TextNode;
354
- },
355
- insertNode: _insertNode,
356
- removeNode: _removeNode,
357
- getParentNode(node) {
358
- log("Getting parent of node:", node.id);
359
- const parent = node.parent;
360
- if (!parent) {
361
- log("No parent found for node:", node.id);
362
- return;
363
- }
364
- log("Parent found:", parent.id, "for node:", node.id);
365
- return parent;
366
- },
367
- getFirstChild(node) {
368
- log("Getting first child of node:", node.id);
369
- if (node instanceof TextRenderable3) {
370
- const chunk = node.content.chunks[0];
371
- if (chunk) {
372
- return TextNode.getTextNodeFromChunk(chunk);
373
- } else {
374
- return;
375
- }
376
- }
377
- if (node instanceof TextNode) {
378
- return;
379
- }
380
- const firstChild = node.getChildren()[0];
381
- if (!firstChild) {
382
- log("No first child found for node:", node.id);
383
- return;
384
- }
385
- log("First child found:", firstChild.id, "for node:", node.id);
386
- return firstChild;
387
- },
388
- getNextSibling(node) {
389
- log("Getting next sibling of node:", node.id);
390
- const parent = node.parent;
391
- if (!parent) {
392
- log("No parent found for node:", node.id);
393
- return;
394
- }
395
- if (node instanceof TextNode) {
396
- if (parent instanceof TextRenderable3) {
397
- const siblings2 = parent.content.chunks;
398
- const index2 = siblings2.indexOf(node.chunk);
399
- if (index2 === -1 || index2 === siblings2.length - 1) {
400
- log("No next sibling found for node:", node.id);
401
- return;
402
- }
403
- const nextSibling2 = siblings2[index2 + 1];
404
- if (!nextSibling2) {
405
- log("Next sibling is null for node:", node.id);
406
- return;
407
- }
408
- return TextNode.getTextNodeFromChunk(nextSibling2);
409
- }
410
- console.warn("Text parent is not a text node:", node.id);
411
- return;
412
- }
413
- const siblings = parent.getChildren();
414
- const index = siblings.indexOf(node);
415
- if (index === -1 || index === siblings.length - 1) {
416
- log("No next sibling found for node:", node.id);
417
- return;
418
- }
419
- const nextSibling = siblings[index + 1];
420
- if (!nextSibling) {
421
- log("Next sibling is null for node:", node.id);
422
- return;
423
- }
424
- log("Next sibling found:", nextSibling.id, "for node:", node.id);
425
- return nextSibling;
426
- }
427
- });
428
- var insertStyledText = (parent, value, current, marker) => {
429
- while (typeof current === "function")
430
- current = current();
431
- if (value === current)
432
- return current;
433
- if (current) {
434
- if (typeof current === "object" && "__isChunk" in current) {
435
- const node = TextNode.getTextNodeFromChunk(current);
436
- if (node) {
437
- _removeNode(parent, node);
438
- }
439
- } else if (current instanceof StyledText) {
440
- for (const chunk of current.chunks) {
441
- const chunkNode = TextNode.getTextNodeFromChunk(chunk);
442
- if (!chunkNode)
443
- continue;
444
- _removeNode(parent, chunkNode);
445
- }
446
- }
447
- }
448
- if (value instanceof StyledText) {
449
- log("Inserting styled text:", value.toString());
450
- for (const chunk of value.chunks) {
451
- insertNode(parent, createTextNode(chunk), marker);
452
- }
453
- return value;
454
- } else if (value && typeof value === "object" && "__isChunk" in value) {
455
- insertNode(parent, createTextNode(value), marker);
456
- return value;
457
- }
458
- return solidUniversalInsert(parent, value, marker, current);
459
- };
460
- var insert = (parent, accessor, marker, initial) => {
461
- if (marker !== undefined && !initial)
462
- initial = [];
463
- if (typeof accessor !== "function")
464
- return insertStyledText(parent, accessor, initial, marker);
465
- effect((current) => insertStyledText(parent, accessor(), current, marker), initial);
466
- };
467
- export {
468
- use,
469
- spread,
470
- solidUniversalInsert,
471
- setProp,
472
- mergeProps,
473
- memo,
474
- insertNode,
475
- insert,
476
- effect,
477
- createTextNode,
478
- createElement,
479
- createComponent,
480
- _render
481
- };